Repository: CarGuo/gsy_github_app_flutter Branch: master Commit: 5917c1936240 Files: 369 Total size: 1.2 MB Directory structure: gitextract_kfzn5wba/ ├── .fvmrc ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .metadata ├── .vscode/ │ └── settings.json ├── AGENTS.md ├── LICENSE ├── README.md ├── README_EN.md ├── RECORD.md ├── UISCENE_PLUGIN_RISK.md ├── VERSION.md ├── analysis_options.yaml ├── android/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ ├── exported.gradle │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── com/ │ │ │ └── shuyu/ │ │ │ └── gsygithub/ │ │ │ └── gsygithubappflutter/ │ │ │ ├── MainActivity.kt │ │ │ └── UpdateAlbumPlugin.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── launch_background.xml │ │ │ └── normal_background.xml │ │ └── values/ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── gsygithubapp-debug.jks │ └── settings.gradle ├── devtools_options.yaml ├── docs/ │ ├── 00-overview/ │ │ └── project-map.md │ ├── 01-architecture/ │ │ ├── app-layering.md │ │ └── state-management-matrix.md │ ├── 02-features/ │ │ ├── debug.md │ │ ├── dynamic.md │ │ ├── home.md │ │ ├── issue.md │ │ ├── login.md │ │ ├── notify.md │ │ ├── push.md │ │ ├── release.md │ │ ├── repos.md │ │ ├── search.md │ │ ├── trend.md │ │ └── user.md │ ├── 03-runbooks/ │ │ └── local-setup.md │ ├── 04-quality/ │ │ ├── smoke-matrix.md │ │ └── test-strategy.md │ ├── 05-ai/ │ │ ├── agent-guide.md │ │ ├── feature-playbooks/ │ │ │ ├── debug-change.md │ │ │ ├── dynamic-change.md │ │ │ ├── home-change.md │ │ │ ├── issue-change.md │ │ │ ├── notify-change.md │ │ │ ├── push-change.md │ │ │ ├── release-change.md │ │ │ ├── repos-change.md │ │ │ ├── search-change.md │ │ │ ├── trend-change.md │ │ │ └── user-change.md │ │ ├── prompts/ │ │ │ ├── author-handoff.md │ │ │ └── reviewer-system.md │ │ ├── review-harness.md │ │ └── task-playbooks/ │ │ ├── add-api.md │ │ ├── add-page.md │ │ ├── fix-bug.md │ │ └── refactor-state.md │ ├── 06-decisions/ │ │ ├── ADR-0001-状态管理收敛策略.md │ │ ├── ADR-0002-新增功能默认状态方案.md │ │ └── README.md │ ├── CONTRIBUTING_AI.md │ └── README.md ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ └── short_description.txt ├── ios/ │ ├── .gitignore │ ├── Flutter/ │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── ephemeral/ │ │ ├── flutter_lldb_helper.py │ │ └── flutter_lldbinit │ ├── Podfile │ ├── Runner/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── LaunchImage.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── Runner-Bridging-Header.h │ │ └── SceneDelegate.swift │ ├── Runner.xcodeproj/ │ │ ├── project.pbxproj │ │ ├── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Runner.xcscheme │ └── Runner.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcshareddata/ │ └── IDEWorkspaceChecks.plist ├── l10n.yaml ├── lib/ │ ├── app.dart │ ├── common/ │ │ ├── config/ │ │ │ └── config.dart │ │ ├── event/ │ │ │ ├── event_bus.dart │ │ │ ├── http_error_event.dart │ │ │ └── index.dart │ │ ├── local/ │ │ │ └── local_storage.dart │ │ ├── localization/ │ │ │ ├── extension.dart │ │ │ └── l10n/ │ │ │ ├── app_en.arb │ │ │ ├── app_ja.arb │ │ │ ├── app_ko.arb │ │ │ ├── app_localizations.dart │ │ │ ├── app_localizations_en.dart │ │ │ ├── app_localizations_ja.dart │ │ │ ├── app_localizations_ko.dart │ │ │ ├── app_localizations_zh.dart │ │ │ └── app_zh.arb │ │ ├── logger.dart │ │ ├── net/ │ │ │ ├── AGENTS.md │ │ │ ├── address.dart │ │ │ ├── api.dart │ │ │ ├── code.dart │ │ │ ├── graphql/ │ │ │ │ ├── client.dart │ │ │ │ ├── repositories.dart │ │ │ │ └── users.dart │ │ │ ├── interceptors/ │ │ │ │ ├── error_interceptor.dart │ │ │ │ ├── header_interceptor.dart │ │ │ │ ├── log_interceptor.dart │ │ │ │ ├── response_interceptor.dart │ │ │ │ └── token_interceptor.dart │ │ │ ├── result_data.dart │ │ │ ├── transformer.dart │ │ │ ├── transformer.g.dart │ │ │ └── trending/ │ │ │ └── github_trending.dart │ │ ├── repositories/ │ │ │ ├── data_result.dart │ │ │ ├── event_repository.dart │ │ │ ├── issue_repository.dart │ │ │ ├── repos_repository.dart │ │ │ └── user_repository.dart │ │ ├── router/ │ │ │ └── anima_route.dart │ │ ├── style/ │ │ │ └── gsy_style.dart │ │ ├── toast.dart │ │ └── utils/ │ │ ├── code_utils.dart │ │ ├── common_utils.dart │ │ ├── event_utils.dart │ │ ├── html_utils.dart │ │ └── navigator_utils.dart │ ├── db/ │ │ ├── provider/ │ │ │ ├── event/ │ │ │ │ ├── received_event_db_provider.dart │ │ │ │ └── user_event_db_provider.dart │ │ │ ├── issue/ │ │ │ │ ├── issue_comment_db_provider.dart │ │ │ │ └── issue_detail_db_provider.dart │ │ │ ├── repos/ │ │ │ │ ├── read_history_db_provider.dart │ │ │ │ ├── repository_branch_db_provider.dart │ │ │ │ ├── repository_commitInfo_detail_db_provider.dart │ │ │ │ ├── repository_commits_db_provider.dart │ │ │ │ ├── repository_detail_db_provider.dart │ │ │ │ ├── repository_detail_readme_db_provider.dart │ │ │ │ ├── repository_event_db_provider.dart │ │ │ │ ├── repository_fork_db_provider.dart │ │ │ │ ├── repository_issue_db_provider.dart │ │ │ │ ├── repository_pulse_db_provider.dart │ │ │ │ ├── repository_star_db_provider.dart │ │ │ │ ├── repository_watcher_db_provider.dart │ │ │ │ └── trend_repository_db_provider.dart │ │ │ └── user/ │ │ │ ├── org_member_db_provider.dart │ │ │ ├── user_followed_db_provider.dart │ │ │ ├── user_follower_db_provider.dart │ │ │ ├── user_orgs_db_provider.dart │ │ │ ├── user_repos_db_provider.dart │ │ │ ├── user_stared_db_provider.dart │ │ │ └── userinfo_db_provider.dart │ │ ├── sql_manager.dart │ │ └── sql_provider.dart │ ├── env/ │ │ ├── AGENTS.md │ │ ├── config_wrapper.dart │ │ ├── dev.dart │ │ ├── dev.g.dart │ │ ├── env_config.dart │ │ ├── env_config.g.dart │ │ ├── env_json_dev.json │ │ ├── env_json_prod.json │ │ ├── prod.dart │ │ └── prod.g.dart │ ├── main.dart │ ├── main_prod.dart │ ├── model/ │ │ ├── branch.dart │ │ ├── branch.g.dart │ │ ├── commitFile.dart │ │ ├── commitFile.g.dart │ │ ├── commit_comment.dart │ │ ├── commit_comment.g.dart │ │ ├── commit_git_info.dart │ │ ├── commit_git_info.g.dart │ │ ├── commit_git_user.dart │ │ ├── commit_git_user.g.dart │ │ ├── commit_stats.dart │ │ ├── commit_stats.g.dart │ │ ├── commits_comparison.dart │ │ ├── commits_comparison.g.dart │ │ ├── common_list_datatype.dart │ │ ├── download_source.dart │ │ ├── download_source.g.dart │ │ ├── event.dart │ │ ├── event.g.dart │ │ ├── event_payload.dart │ │ ├── event_payload.g.dart │ │ ├── file_model.dart │ │ ├── file_model.g.dart │ │ ├── issue.dart │ │ ├── issue.g.dart │ │ ├── issue_event.dart │ │ ├── issue_event.g.dart │ │ ├── license.dart │ │ ├── license.g.dart │ │ ├── notification.dart │ │ ├── notification.g.dart │ │ ├── notification_subject.dart │ │ ├── notification_subject.g.dart │ │ ├── push_commit.dart │ │ ├── push_commit.g.dart │ │ ├── push_event_commit.dart │ │ ├── push_event_commit.g.dart │ │ ├── release.dart │ │ ├── release.g.dart │ │ ├── release_asset.dart │ │ ├── release_asset.g.dart │ │ ├── repo_commit.dart │ │ ├── repo_commit.g.dart │ │ ├── repository.dart │ │ ├── repository.g.dart │ │ ├── repository_permissions.dart │ │ ├── repository_permissions.g.dart │ │ ├── repository_ql.dart │ │ ├── search_user_ql.dart │ │ ├── template.dart │ │ ├── template.g.dart │ │ ├── trending_repo_model.dart │ │ ├── trending_repo_model.g.dart │ │ ├── user.dart │ │ ├── user.g.dart │ │ ├── user_org.dart │ │ └── user_org.g.dart │ ├── page/ │ │ ├── AGENTS.md │ │ ├── code_detail_page_web.dart │ │ ├── common_list_page.dart │ │ ├── debug/ │ │ │ ├── debug_data_page.dart │ │ │ └── debug_label.dart │ │ ├── dynamic/ │ │ │ ├── dynamic_bloc.dart │ │ │ └── dynamic_page.dart │ │ ├── error_page.dart │ │ ├── gsy_webview.dart │ │ ├── home/ │ │ │ ├── home_page.dart │ │ │ └── widget/ │ │ │ └── home_drawer.dart │ │ ├── honor_list_page.dart │ │ ├── issue/ │ │ │ ├── issue_detail_page.dart │ │ │ ├── issue_edit_dIalog.dart │ │ │ └── widget/ │ │ │ ├── issue_header_item.dart │ │ │ └── issue_item.dart │ │ ├── login/ │ │ │ ├── login_page.dart │ │ │ └── login_webview.dart │ │ ├── my_page.dart │ │ ├── notify/ │ │ │ └── notify_page.dart │ │ ├── photoview_page.dart │ │ ├── push/ │ │ │ ├── push_detail_page.dart │ │ │ └── widget/ │ │ │ ├── push_coed_item.dart │ │ │ └── push_header.dart │ │ ├── release/ │ │ │ ├── release_page.dart │ │ │ └── widget/ │ │ │ └── release_item.dart │ │ ├── repos/ │ │ │ ├── provider/ │ │ │ │ ├── repos_detail_provider.dart │ │ │ │ └── repos_network_provider.dart │ │ │ ├── repository_detail_issue_list_page.dart │ │ │ ├── repository_detail_page.dart │ │ │ ├── repository_detail_readme_page.dart │ │ │ ├── repository_file_list_page.dart │ │ │ ├── repostory_detail_info_page.dart │ │ │ └── widget/ │ │ │ ├── repos_header_item.dart │ │ │ └── repos_item.dart │ │ ├── search/ │ │ │ ├── search_bloc.dart │ │ │ ├── search_page.dart │ │ │ └── widget/ │ │ │ ├── gsy_search_drawer.dart │ │ │ └── gsy_search_input_widget.dart │ │ ├── trend/ │ │ │ ├── trend_page.dart │ │ │ ├── trend_provider.dart │ │ │ ├── trend_provider.g.dart │ │ │ ├── trend_user_page.dart │ │ │ ├── trend_user_provider.dart │ │ │ └── trend_user_provider.g.dart │ │ ├── user/ │ │ │ ├── base_person_provider.dart │ │ │ ├── base_person_provider.g.dart │ │ │ ├── base_person_state.dart │ │ │ ├── person_page.dart │ │ │ └── widget/ │ │ │ ├── user_header.dart │ │ │ └── user_item.dart │ │ ├── user_profile_page.dart │ │ └── welcome_page.dart │ ├── provider/ │ │ ├── app_state_provider.dart │ │ └── app_state_provider.g.dart │ ├── redux/ │ │ ├── gsy_state.dart │ │ ├── login_redux.dart │ │ ├── middleware/ │ │ │ ├── combine_epics.dart │ │ │ ├── epic.dart │ │ │ ├── epic_middleware.dart │ │ │ └── epic_store.dart │ │ └── user_redux.dart │ ├── test/ │ │ ├── demo_app.dart │ │ ├── demo_appbar.dart │ │ ├── demo_bloc_page.dart │ │ ├── demo_db.dart │ │ ├── demo_item.dart │ │ ├── demo_mixins.dart │ │ ├── demo_page.dart │ │ ├── demo_tab_page.dart │ │ ├── demo_text_field_page.dart │ │ ├── demo_user_store.dart │ │ └── demo_widget.dart │ └── widget/ │ ├── anima/ │ │ └── curves_bezier.dart │ ├── animated_background.dart │ ├── diff_scale_text.dart │ ├── flutter_json_widget.dart │ ├── gsy_bottom_action_bar.dart │ ├── gsy_card_item.dart │ ├── gsy_common_option_widget.dart │ ├── gsy_event_item.dart │ ├── gsy_flex_button.dart │ ├── gsy_icon_text.dart │ ├── gsy_input_widget.dart │ ├── gsy_select_item_widget.dart │ ├── gsy_tabbar_widget.dart │ ├── gsy_tabs.dart │ ├── gsy_title_bar.dart │ ├── gsy_user_icon_widget.dart │ ├── markdown/ │ │ ├── gsy_markdown_widget.dart │ │ └── syntax_high_lighter.dart │ ├── menu/ │ │ ├── flutter_radial_menu.dart │ │ └── src/ │ │ ├── arc_progress_indicator.dart │ │ ├── radial_menu.dart │ │ ├── radial_menu_button.dart │ │ ├── radial_menu_center_button.dart │ │ └── radial_menu_item.dart │ ├── mole_widget.dart │ ├── never_overscroll_indicator.dart │ ├── only_share_widget.dart │ ├── particle/ │ │ ├── particle_model.dart │ │ ├── particle_painter.dart │ │ └── particle_widget.dart │ ├── pull/ │ │ ├── custom_bouncing_scroll_physics.dart │ │ ├── gsy_flare_mutli_pull_controller.dart │ │ ├── gsy_flare_pull_controller.dart │ │ ├── gsy_pull_load_widget.dart │ │ ├── gsy_pull_new_load_widget.dart │ │ ├── gsy_refresh_sliver.dart │ │ └── nested/ │ │ ├── gsy_nested_pull_load_widget.dart │ │ ├── gsy_sliver_header_delegate.dart │ │ └── nested_refresh.dart │ └── state/ │ └── gsy_list_state.dart ├── pubspec.yaml ├── static/ │ ├── file/ │ │ ├── Space-Demo.flr │ │ ├── flare_flutter_logo_.flr │ │ ├── launch.riv │ │ ├── loading_world_now.flr │ │ ├── rejection.json │ │ ├── rejection2.json │ │ ├── search.json │ │ └── user.json │ └── font/ │ ├── demo.css │ ├── demo_fontclass.html │ ├── demo_symbol.html │ ├── demo_unicode.html │ ├── iconfont.css │ └── iconfont.js └── tool/ └── ai/ └── build_review_bundle.ps1 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .fvmrc ================================================ { "flutter": "3.38.4" } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms custom: http://img.cdn.guoshuyu.cn/thanks.jpg ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master tags: - '*' pull_request: paths-ignore: - '**/*.md' - '**/*.txt' - '**/*.png' - '**/*.jpg' jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Free Disk Space uses: jlumbroso/free-disk-space@main with: tool-cache: false android: false dotnet: true haskell: true large-packages: true docker-images: true swap-storage: true - uses: actions/checkout@v2 - uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: 21 - uses: subosito/flutter-action@v1 with: flutter-version: '3.38.4' - name: Create config file run: | echo 'class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";}' > lib/common/config/ignoreConfig.dart - run: flutter pub get - run: flutter build apk --release --target-platform=android-arm64 --no-shrink apk: name: Generate APK if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Free Disk Space uses: jlumbroso/free-disk-space@main with: tool-cache: false android: false dotnet: true haskell: true large-packages: true docker-images: true swap-storage: true - name: Checkout uses: actions/checkout@v2 - name: Setup JDK uses: actions/setup-java@v2 with: distribution: 'zulu' java-version: 21 - uses: subosito/flutter-action@v1 with: flutter-version: '3.38.4' - name: Create config file run: | echo 'class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";}' > lib/common/config/ignoreConfig.dart - run: flutter pub get - run: flutter build apk --release --target-platform=android-arm64 --no-shrink - name: Upload APK uses: actions/upload-artifact@v4 with: name: apk path: build/app/outputs/apk/release/app-release.apk release: name: Release APK needs: apk if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - name: Free Disk Space uses: jlumbroso/free-disk-space@main with: tool-cache: false android: false dotnet: true haskell: true large-packages: true docker-images: true swap-storage: true - name: Download APK from build uses: actions/download-artifact@v4 with: name: apk - name: Display structure of downloaded files run: ls -R - name: Create Release id: create_release uses: actions/create-release@v1.1.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} - name: Upload Release APK id: upload_release_asset uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./app-release.apk asset_name: app-release.apk asset_content_type: application/zip ================================================ FILE: .gitignore ================================================ .DS_Store .dart_tool/ .packages .pub/ build/ .flutter-plugins .flutter-plugins-dependencies .gradle/ ignoreConfig.dart flutter_export_environment.sh # Miscellaneous *.class *.log *.pyc *.swp .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .pub-cache/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Exceptions to above rules. !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages # FVM Version Cache .fvm/ ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 18cd7a3601bcffb36fdf2f679f763b5e827c2e8e channel: unknown project_type: app ================================================ FILE: .vscode/settings.json ================================================ { "java.configuration.updateBuildConfiguration": "disabled" } ================================================ FILE: AGENTS.md ================================================ # GSY GitHub App Flutter 协作说明 这是一个 Flutter GitHub 客户端,同时也是带有教学展示性质的示例工程。 不要假设整个仓库是单一架构风格。多种状态管理方案并存是当前设计现实,不是偶然脏代码。 ## 进入仓库后先读 在进行非微小改动前,先读这些文件: 1. `README.md` 2. `docs/README.md` 3. `docs/00-overview/project-map.md` 4. `docs/01-architecture/app-layering.md` 5. `docs/01-architecture/state-management-matrix.md` ## 工作规则 - 改动尽量限制在当前功能域,不要顺手做跨模块重构。 - 非任务明确要求时,不要迁移状态管理框架。 - 优先遵循目标模块现有模式,而不是引入新的全局规范。 - 未经明确允许,不得为了满足需求擅自替换模块既有框架或状态实现,即使替换后看起来更简单。 - `*.g.dart`、多语言生成文件、env 生成文件都视为生成产物;优先重新生成,不要手改。 - 不要提交密钥。`lib/common/config/ignoreConfig.dart` 属于本地或 CI 环境材料。 ## 高风险目录 - `lib/app.dart`:应用根装配、导航、全局报错、Redux 和 Riverpod 混合接线 - `lib/common/net/`:共享网络栈、拦截器、GraphQL/REST 入口 - `lib/common/repositories/`:功能数据访问边界 - `lib/env/`:构建期环境配置和生成文件 - `lib/common/localization/`:ARB 与多语言生成输出 ## 建议修改策略 - 纯 UI 任务:优先只改页面或局部 widget,不碰全局状态 - API 任务:同时检查 `common/net` 与对应 `common/repositories` - 状态任务:沿用该模块当前已有状态方案,除非任务明确要求迁移 - 配置/构建任务:同步更新 `docs/03-runbooks/` 中的操作说明 ## Review 规则 - 中等以上改动默认使用独立 reviewer 上下文 - 每次非微小代码改动后,默认拉起新的 reviewer subagent 或新的干净上下文审查刚刚的修改 - author 在通过一轮新的 reviewer subagent 审查前,不应直接宣告代码任务完成 - reviewer 不应复用 author 的完整上下文历史 - 先看 `docs/05-ai/review-harness.md` - reviewer 提示模板见 `docs/05-ai/prompts/reviewer-system.md` - `tool/ai/build_review_bundle.ps1` 只是可选辅助,不是主流程 ## 本地最小验证 按改动范围选择最小验证集合: - `flutter pub get` - `dart run build_runner build --delete-conflicting-outputs` - `flutter gen-l10n` - `flutter analyze` - `flutter build apk --release --target-platform=android-arm64 --no-shrink` 说明: - 改模型、env、注解生成代码时跑 `build_runner` - 改 ARB 或本地化输入时跑 `flutter gen-l10n` - 改 Android 构建、依赖或运行时关键路径时跑 APK 构建 ## 当前已知约束 - 仓库目前没有提交进来的 `test/` 测试目录 - CI 使用 GitHub Actions,当前偏重构建成功 - 项目同时使用 Redux、Riverpod、Provider、Signals - OAuth 登录相关流程依赖本地 `ignoreConfig.dart` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ![](./logo.png) [![Github Actions](https://github.com/CarGuo/gsy_github_app_flutter/workflows/CI/badge.svg)](https://github.com/CarGuo/gsy_github_app_flutter/actions) [![GitHub stars](https://img.shields.io/github/stars/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/stargazers) [![GitHub forks](https://img.shields.io/github/forks/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/network) [![GitHub issues](https://img.shields.io/github/issues/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/issues) [![GitHub license](https://img.shields.io/github/license/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/blob/master/LICENSE) [![star](https://gitcode.com/ZuoYueLiang/gsy_github_app_flutter/star/badge.svg)](https://gitcode.com/ZuoYueLiang/gsy_github_app_flutter) ### [English Readme](https://github.com/CarGuo/GSYGithubAppFlutter/blob/master/README_EN.md) ## 一款跨平台的开源Github客户端App,提供更丰富的功能,更好体验,旨在更好的日常管理和维护个人Github,提供更好更方便的驾车体验~~Σ( ̄。 ̄ノ)ノ。项目涉及各种常用控件、网络、数据库、设计模式、主题切换、多语言、状态管理(Redux、Riverpod、Provider)等。在开发学习过程中,提供丰富的同款对比: * ### 同款Weex版 ( https://github.com/CarGuo/GSYGithubAppWeex ) * ### 同款ReactNative版 ( https://github.com/CarGuo/GSYGithubApp ) * ### 同款Android Kotlin View版本( https://github.com/CarGuo/GSYGithubAppKotlin ) * ### 同款Android Compose版本( https://github.com/CarGuo/GSYGithubAppCompose ) * ### 简单 Flutter 独立学习项目 ( https://github.com/CarGuo/gsy_flutter_demo ) ## AI 协作与贡献入口 如果你希望用 AI 或更工程化的方式参与这个仓库,建议不要只看本 README,先看下面这些文档入口: - 总导航:`docs/CONTRIBUTING_AI.md` - 文档索引:`docs/README.md` - 项目地图:`docs/00-overview/project-map.md` - 分层边界:`docs/01-architecture/app-layering.md` - 状态管理边界:`docs/01-architecture/state-management-matrix.md` - 手工回归矩阵:`docs/04-quality/smoke-matrix.md` 按任务类型进入: - 修 Bug:`docs/05-ai/task-playbooks/fix-bug.md` - 新增页面:`docs/05-ai/task-playbooks/add-page.md` - 新增接口:`docs/05-ai/task-playbooks/add-api.md` - 状态整理:`docs/05-ai/task-playbooks/refactor-state.md` 按功能域进入: - 仓库详情:`docs/05-ai/feature-playbooks/repos-change.md` - 趋势页:`docs/05-ai/feature-playbooks/trend-change.md` - 通知页:`docs/05-ai/feature-playbooks/notify-change.md` - Issue:`docs/05-ai/feature-playbooks/issue-change.md` - 搜索:`docs/05-ai/feature-playbooks/search-change.md` - 用户页:`docs/05-ai/feature-playbooks/user-change.md` - 首页容器:`docs/05-ai/feature-playbooks/home-change.md` - 动态页:`docs/05-ai/feature-playbooks/dynamic-change.md` - Release:`docs/05-ai/feature-playbooks/release-change.md` - Push 提交详情:`docs/05-ai/feature-playbooks/push-change.md` - 调试页:`docs/05-ai/feature-playbooks/debug-change.md` 长期规则: - 状态管理收敛策略:`docs/06-decisions/ADR-0001-状态管理收敛策略.md` - 新增功能默认状态方案:`docs/06-decisions/ADR-0002-新增功能默认状态方案.md` Review harness: - author / reviewer 分离:`docs/05-ai/review-harness.md` - reviewer prompt:`docs/05-ai/prompts/reviewer-system.md` - review bundle 脚本(可选辅助):`tool/ai/build_review_bundle.ps1` ## 相关文章 | 公众号 | 掘金 | 知乎 | CSDN | 简书 |---------|---------|--------- |---------|---------| | GSYTech | [点我](https://juejin.cn/user/582aca2ba22b9d006b59ae68/posts) | [点我](https://www.zhihu.com/people/carguo) | [点我](https://blog.csdn.net/ZuoYueLiang) | [点我](https://www.jianshu.com/u/6e613846e1ea) - ### [Flutter系列文章专栏](https://juejin.cn/column/6960546078202527774) ---- - ### [Flutter 独立简单学习演示项目](https://github.com/CarGuo/gsy_flutter_demo) - ### [Flutter 完整开发实战详解 Gitbook 预览下载](https://github.com/CarGuo/gsy_flutter_book) - ### [所有运行问题请点击这里](https://github.com/CarGuo/gsy_github_app_flutter/issues/13) * ### GSY老书:[《Flutter开发实战详解》](https://item.jd.com/12883054.html)上架啦:[京东](https://item.jd.com/12883054.html) / [当当](http://product.dangdang.com/28558519.html) / 电子版[京东读书](https://e.jd.com/30624414.html)和[Kindle](https://www.amazon.cn/dp/B08BHQ4TKK/ref=sr_1_5?__mk_zh_CN=亚马逊网站&keywords=flutter&qid=1593498531&s=digital-text&sr=1-5) - ### [如果克隆太慢或者图片看不到,可尝试从码云地址下载](https://gitee.com/CarGuo/GSYGithubAppFlutter) ----- ## 须知 > **因为是偏学习展示项目,所以项目里会有各式各样的模式、库、UI等,请不要介意** > > 0、 全局状态管理目前有多种模式,包括 Provider、Redux、Riverpod 等 > > 1、 TrendPage : 目前采用纯 riverpod 状态管理,演示 > > 2、 Provider:目前在 RepositoryDetailPage 出使用 > > 3、 Redux:目前展示了全局登陆和用户信息等上面使用。 > > 4、 riverpod:目前用于管理全局灰度,多语言。 > > 5、 Repos 等请求展示了 graphQL > > 6、 Redux:目前展示了全局登陆和用户信息等上面使用。 > > 7、 Signals:目前用于 NotifyPage、RepositoryDetailFileListPage 页面内状态管理 > > **列表显示有多个,其中:** > > 1、**gsy_pull_load_widget.dart.dart** > `common_list_page.dart 等使用,搭配 gsy_list_state.dart 使用` > > 2、**gsy_pull_new_load_widget.dart.dart** > `dynamic_page.dart 等使用,搭配 gsy_bloc_list_state.dart 使用` > `有 iOS 和 Android 两种风格下拉风格支持` > > 3、**gsy_nested_pull_load_widget.dart** > `trend_page.dart 等使用,配置sliver 效果` ## 编译运行流程 1、配置好Flutter开发环境(目前Flutter SDK 版本 **3.38**),可参阅 [【搭建环境】](https://flutterchina.club)。 2、clone代码,执行`Packages get`安装第三方包。(因为某些不可抗力原因,国内可能需要设置代理: [代理环境变量](https://flutterchina.club/setup-windows/)) >### 3、重点:你需要自己在lib/common/config/目录下 创建一个`ignoreConfig.dart`文件,然后输入你申请的Github client_id 和 client_secret。 class NetConfig { static const CLIENT_ID = "xxxx"; static const CLIENT_SECRET = "xxxxxxxxxxx"; }   [ 注册 Github APP 传送门](https://github.com/settings/applications/new),当然,前提是你现有一个github账号(~ ̄▽ ̄)~ 。 ### 4、如果使用安全登录(授权登录),那么在上述注册 Github App 的 Authorization callback URL 一栏必须填入 `gsygithubapp://authed`
### 5、运行之前请注意下 >### 1、本地 Flutter SDK 版本 3.38 ; 2、是否执行过 `flutter pub get`;3、 网络等问题参考: [如果出现登陆失败或者请求失败 ](https://github.com/CarGuo/gsy_github_app_flutter/issues/643) ### 下载 #### Apk下载链接: [Apk下载链接1 ](https://github.com/CarGuo/gsy_github_app_flutter/releases) #### Apk下载链接: [Apk下载链接2 ](https://www.openapk.net/gsygithubappflutter/com.shuyu.gsygithub.gsygithubappflutter/) ![openapk](https://www.openapk.net/images/openapk-badge.png) | 类型 | 二维码 | | ----------- | ---------------------------------------- | | **Apk二维码** | ![](./download.png) | | **iOS暂无下载** | | ## 项目结构图 ![](./framework2.png) ### 常见问题 * 如果包同步失败,一般都是因为没设置包代理,可以参考:[环境变量问题](https://github.com/CarGuo/GSYGithubAppFlutter/issues/13) * [如果克隆太慢,可尝试码云地址下载](https://gitee.com/CarGuo/GSYGithubAppFlutter) ### 示例图片 ### 示例图片 ![](./ios.gif) ![](./theme.gif) ### 框架 >当前 Flutter SDK 版本 3.38 ``` 用户交互 → UI层(Widget/Page) → 状态层(Redux/Provider/Riverpod) → 服务层(Repositories) → 网络层(Net) → GitHub API → 数据模型(Model) → 本地存储(DB) → UI更新 ``` ``` ┌─────────────────────────────────────────────────────────────────┐ │ GSY GitHub App │ ├─────────────┬───────────────┬────────────────┬─────────────────┤ │ UI Layer │ State Layer │ Service Layer │ Data Layer │ ├─────────────┼───────────────┼────────────────┼─────────────────┤ │ │ │ │ │ │ ┌─────────┐│ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │ │ Pages ││ │ Redux │ │ │Repositories│ │ │ Models │ │ │ └─────────┘│ └─────────┘ │ └─────────┘ │ └─────────┘ │ │ │ │ │ │ │ ┌─────────┐│ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │ │ Widgets ││ │ Provider│ │ │Network API│ │ │Database │ │ │ └─────────┘│ └─────────┘ │ └─────────┘ │ └─────────┘ │ │ │ │ │ │ │ ┌─────────┐│ ┌─────────┐ │ │ │ │ │Common UI││ │Riverpod │ │ │ │ │ └─────────┘│ └─────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ │ Signals │ │ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ │ └─────────────┴───────────────┴────────────────┴─────────────────┘ ``` ``` lib/ ├── main.dart # 应用入口点 ├── main_prod.dart # 生产环境入口点 ├── app.dart # 应用配置与路由 ├── common/ # 公共功能模块 │ ├── config/ # 应用配置 │ ├── event/ # 事件总线 │ ├── local/ # 本地化 │ ├── localization/ # 多语言支持 │ ├── net/ # 网络请求 │ ├── repositories/ # 数据仓库 │ ├── router/ # 路由配置 │ ├── style/ # 样式配置 │ └── utils/ # 工具类 ├── db/ # 数据库相关 │ ├── provider/ # 数据库提供者 │ ├── sql_manager.dart # SQL管理器 │ └── sql_provider.dart # SQL提供者 ├── env/ # 环境配置 ├── model/ # 数据模型 ├── page/ # 页面 │ ├── debug/ # 调试页面 │ ├── dynamic/ # 动态页面 │ ├── home/ # 主页 │ ├── issue/ # Issue相关页面 │ ├── login/ # 登录页面 │ ├── push/ # 推送相关页面 │ ├── release/ # 发布相关页面 │ ├── repos/ # 仓库相关页面 │ ├── search/ # 搜索页面 │ ├── trend/ # 趋势页面 │ └── user/ # 用户相关页面 ├── provider/ # Provider状态管理 ├── redux/ # Redux状态管理 │ ├── middleware/ # Redux中间件 │ ├── gsy_state.dart # Redux状态定义 │ ├── login_redux.dart # 登录状态管理 │ └── user_redux.dart # 用户状态管理 ├── test/ # 测试相关 └── widget/ # 自定义组件 ├── anima/ # 动画组件 ├── markdown/ # Markdown渲染组件 ├── menu/ # 菜单组件 ├── particle/ # 粒子效果组件 ├── pull/ # 下拉刷新组件 └── state/ # 状态相关组件 ``` riverpod 页面内状态管理: ``` ┌───────────────────────────────────────────────────────────────────────────┐ │ Page Architecture Overview │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ Global State │ │ ┌───────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ │ appThemeProvider │ │ appLocalProvider │ │ appGrepProvider │ │ │ │ (Theme Data) │ │ (Localization) │ │ (Grayscale Mode) │ │ │ └───────────────────┘ └────────────────────┘ └────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────┘ │ ┌────────────────┴────────────────┐ ▼ ▼ ┌─────────────────────────────────┐ ┌─────────────────────────────────────┐ │ TrendPage (Riverpod) │ │ NotifyPage (Signals) │ ├─────────────────────────────────┤ ├─────────────────────────────────────┤ │ │ │ │ │┌─────────────────────────────┐ │ │┌───────────────────────────────────┐│ ││ Riverpod Providers │ │ ││ Signals State ││ ││┌───────────────────────────┐│ │ ││┌─────────────────────────────────┐││ │││ trendFirstProvider ││ │ │││ notifySignal (List) │││ │││ trendSecondProvider ││ │ │││ notifyIndexSignal (int) │││ ││└───────────────────────────┘│ │ │││ signalPage (int) │││ │└─────────────────────────────┘ │ ││└─────────────────────────────────┘││ │ │ │└───────────────────────────────────┘│ │┌─────────────────────────────┐ │ │┌───────────────────────────────────┐│ ││ Local State (StatefulWidget)│ │ ││ SignalsMixin Processing ││ ││ - UI Controls │ │ ││ - createEffect() for reactions ││ ││ - Filter Parameters │ │ ││ - Manages data loading ││ │└─────────────────────────────┘ │ ││ - Updates UI based on signals ││ │ │ │└───────────────────────────────────┘│ └─────────────────────────────────┘ └─────────────────────────────────────┘ │ │ └────────────────┬────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ Data Layer │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ ReposRepository / UserRepository │ │ │ │ ┌────────────────────────┐ ┌─────────────────────────────────┐ │ │ │ │ │ Network Request │───┬──▶│ Database Providers │ │ │ │ │ │ - API calls │ │ │ - Data caching │ │ │ │ │ └────────────────────────┘ │ └─────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ └──▶│ Data Models │ │ │ │ │ │ - Structure definitions │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ UI Components │ │ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ │ View Models │ │ List Items │ │ Interactive UI │ │ │ │ - Data Formatting │ │ - Item Rendering │ │ - User Actions │ │ │ └────────────────────┘ └────────────────────┘ └────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────┘ ``` provider 页面内状态管理: ``` +-----------------------------------------------------+ | App User Interface | +-----------------------------------------------------+ | v +-----------------------------------------------------+ | RepositoryDetailPage (StatefulWidget) | | with SingleTickerProviderStateMixin | +-----------------------------------------------------+ | v +-----------------------------------------------------+ | MultiProvider | +-----------------------------------------------------+ | | v v +------------------+ +-----------------------+ | ReposNetWork |<----------| ReposDetailProvider | | Provider | | | +---------+--------+ +-----------------------+ | | | | v v +-----------------------------------------------------+ | Repository Data Services | | (ReposRepository, IssueRepository) | +-----------------------------------------------------+ | v +-----------------------------------------------------+ | Four Tab Pages (Consumers) | +-----------------------------------------------------+ | | | | v v v v +----------+ +----------+ +----------+ +----------+ | Info | | Readme | | Issues | | Files | | Page | | Page | | Page | | Page | +----------+ +----------+ +----------+ +----------+ | | | | | | | | v v v v +-----------------------------------------------------+ | GlobalKeys for Tab Access | | (infoListKey, readmeKey, issueListKey, fileListKey) | +-----------------------------------------------------+ ``` ![](p1.png) ![](p2.png) ![](p3.png) ![](p4.png) > 更多可见:https://codewiki.google/github.com/carguo/gsy_github_app_flutter ## Star History Chart [![Star History Chart](https://api.star-history.com/svg?repos=CarGuo/gsy_github_app_flutter&type=Date)](https://star-history.com/#CarGuo/gsy_github_app_flutter&Date) ![](http://img.cdn.guoshuyu.cn/thanks.jpg) ![](http://img.cdn.guoshuyu.cn/wechat_qq.png) ### LICENSE ``` CarGuo/GSYGithubAppFlutter is licensed under the Apache License 2.0 A permissive license whose main conditions require preservation of copyright and license notices. Contributors provide an express grant of patent rights. Licensed works, modifications, and larger works may be distributed under different terms and without source code. ``` ================================================ FILE: README_EN.md ================================================ ![](./logo.png) [![Github Actions](https://github.com/CarGuo/gsy_github_app_flutter/workflows/CI/badge.svg)](https://github.com/CarGuo/gsy_github_app_flutter/actions) [![GitHub stars](https://img.shields.io/github/stars/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/stargazers) [![GitHub forks](https://img.shields.io/github/forks/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/network) [![GitHub issues](https://img.shields.io/github/issues/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/issues) [![GitHub license](https://img.shields.io/github/license/CarGuo/GSYGithubAppFlutter.svg)](https://github.com/CarGuo/GSYGithubAppFlutter/blob/master/LICENSE) [![star](https://gitcode.com/ZuoYueLiang/gsy_github_app_flutter/star/badge.svg)](https://gitcode.com/ZuoYueLiang/gsy_github_app_flutter) ### [Chinese Readme](https://github.com/CarGuo/GSYGithubAppFlutter/blob/master/README.md) ## A cross-platform open source Github client App, offering richer features and better experience. Designed for better daily management and maintenance of your personal Github account, providing a more convenient driving experience~~Σ( ̄。 ̄ノ)ノ. The project involves various common widgets, networking, databases, design patterns, theme switching, multi-language support, state management (Redux, Riverpod, Provider), and more. During the development and learning process, it provides rich comparisons with equivalent implementations: * ### Same Weex version ( https://github.com/CarGuo/GSYGithubAppWeex ) * ### Same ReactNative version ( https://github.com/CarGuo/GSYGithubApp ) * ### Same Android Kotlin View version ( https://github.com/CarGuo/GSYGithubAppKotlin ) * ### Same Android Compose version ( https://github.com/CarGuo/GSYGithubAppCompose ) * ### Simple Flutter standalone learning project ( https://github.com/CarGuo/gsy_flutter_demo ) ## Related Articles - ## [Flutter Series Articles Column](https://juejin.cn/column/6960546078202527774) ---- - ## [Flutter Simple Learning Demo Project](https://github.com/CarGuo/gsy_flutter_demo) - ## [Flutter Complete Development Practical Detailed Gitbook Preview Download](https://github.com/CarGuo/gsy_flutter_book) - ## [For all running issues please click here](https://github.com/CarGuo/gsy_github_app_flutter/issues/13) * ### GSY's old book: [《Flutter Development in Action》](https://item.jd.com/12883054.html) is available: [JD.com](https://item.jd.com/12883054.html) / [Dangdang](http://product.dangdang.com/28558519.html) / E-book [JD Reading](https://e.jd.com/30624414.html) and [Kindle](https://www.amazon.cn/dp/B08BHQ4TKK/ref=sr_1_5?__mk_zh_CN=亚马逊网站&keywords=flutter&qid=1593498531&s=digital-text&sr=1-5) - ### [If cloning is too slow or if images don't display, you can try downloading from the Gitee address](https://gitee.com/CarGuo/GSYGithubAppFlutter) ----- ## Important Notes > **Since this is primarily a learning and demonstration project, it includes various patterns, libraries, UIs, etc. Please don't mind the diversity** > > 0. Global state management currently has multiple modes, including Provider, Redux, Riverpod, etc. > > 1. TrendPage: Currently uses pure riverpod state management for demonstration > > 2. Provider: Currently used in RepositoryDetailPage > > 3. Redux: Currently demonstrated for global login and user information. > > 4. riverpod: Currently used to manage global grayscale and multi-language. > > 5. Repos and other requests demonstrate graphQL > > 6. Redux: Currently demonstrated for global login and user information. > > 7. Signals: Currently used for in-page state management in NotifyPage, RepositoryDetailFileListPage > > **There are multiple list displays, including:** > > 1. **gsy_pull_load_widget.dart.dart** > `Used in common_list_page.dart, etc., paired with gsy_list_state.dart` > > 2. **gsy_pull_new_load_widget.dart.dart** > `Used in dynamic_page.dart, etc., paired with gsy_bloc_list_state.dart` > `Supports both iOS and Android pull-to-refresh styles` > > 3. **gsy_nested_pull_load_widget.dart** > `Used in trend_page.dart, etc., configured with sliver effect` ## Compilation and Running Process 1. Set up the Flutter development environment (current Flutter SDK version **3.38**), see [Setting up the environment](https://flutterchina.club). 2. Clone the code, run `Packages get` to install third-party packages. (Due to certain reasons beyond control, you may need to set up a proxy in China: [Proxy environment variables](https://flutterchina.club/setup-windows/)) >### 3. Important: You need to create an `ignoreConfig.dart` file in the lib/common/config/ directory yourself, and then enter your registered Github client_id and client_secret. class NetConfig { static const CLIENT_ID = "xxxx"; static const CLIENT_SECRET = "xxxxxxxxxxx"; } [ Register Github APP link](https://github.com/settings/applications/new), of course, the prerequisite is that you already have a github account (~ ̄▽ ̄)~. ### 4. If using secure login (authorization login), then in the above Github App registration, the Authorization callback URL field must be filled with `gsygithubapp://authed`
### 5. Please note before running >### 1. Local Flutter SDK version 3.38; 2. Have you executed `flutter pub get`; 3. For network and other issues, refer to: [If login fails or requests fail](https://github.com/CarGuo/gsy_github_app_flutter/issues/643) ### Download #### APK download link: [APK download link 1](https://github.com/CarGuo/gsy_github_app_flutter/releases) #### APK download link: [APK download link 2](https://www.openapk.net/gsygithubappflutter/com.shuyu.gsygithub.gsygithubappflutter/) ![openapk](https://www.openapk.net/images/openapk-badge.png) | Type | QR Code | | ----------- | ---------------------------------------- | | **APK QR Code** | ![](./download.png) | | **iOS download not available** | | ## Project Structure Diagram ![](./framework2.png) ### Common Issues * If package synchronization fails, it's usually because the package proxy is not set. You can refer to: [Environment variable issues](https://github.com/CarGuo/GSYGithubAppFlutter/issues/13) * [If cloning is too slow, you can try downloading from the Gitee address](https://gitee.com/CarGuo/GSYGithubAppFlutter) ### Example Images ![](./ios.gif) ![](./theme.gif) ### Framework >Current Flutter SDK version 3.38 ``` User Interaction → UI Layer(Widget/Page) → State Layer(Redux/Provider/Riverpod) → Service Layer(Repositories) → Network Layer(Net) → GitHub API → Data Model(Model) → Local Storage(DB) → UI Update ``` ``` ┌─────────────────────────────────────────────────────────────────┐ │ GSY GitHub App │ ├─────────────┬───────────────┬────────────────┬─────────────────┤ │ UI Layer │ State Layer │ Service Layer │ Data Layer │ ├─────────────┼───────────────┼────────────────┼─────────────────┤ │ │ │ │ │ │ ┌─────────┐│ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │ │ Pages ││ │ Redux │ │ │Repositories│ │ │ Models │ │ │ └─────────┘│ └─────────┘ │ └─────────┘ │ └─────────┘ │ │ │ │ │ │ │ ┌─────────┐│ ┌─────────┐ │ ┌─────────┐ │ ┌─────────┐ │ │ │ Widgets ││ │ Provider│ │ │Network API│ │ │Database │ │ │ └─────────┘│ └─────────┘ │ └─────────┘ │ └─────────┘ │ │ │ │ │ │ │ ┌─────────┐│ ┌─────────┐ │ │ │ │ │Common UI││ │Riverpod │ │ │ │ │ └─────────┘│ └─────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ │ │ Signals │ │ │ │ │ │ └─────────┘ │ │ │ │ │ │ │ │ └─────────────┴───────────────┴────────────────┴─────────────────┘ ``` ``` lib/ ├── main.dart # Application entry point ├── main_prod.dart # Production environment entry point ├── app.dart # Application configuration and routing ├── common/ # Common functionality modules │ ├── config/ # Application configuration │ ├── event/ # Event bus │ ├── local/ # Localization │ ├── localization/ # Multi-language support │ ├── net/ # Network requests │ ├── repositories/ # Data repositories │ ├── router/ # Routing configuration │ ├── style/ # Style configuration │ └── utils/ # Utility classes ├── db/ # Database related │ ├── provider/ # Database providers │ ├── sql_manager.dart # SQL manager │ └── sql_provider.dart # SQL provider ├── env/ # Environment configuration ├── model/ # Data models ├── page/ # Pages │ ├── debug/ # Debug pages │ ├── dynamic/ # Dynamic pages │ ├── home/ # Home page │ ├── issue/ # Issue related pages │ ├── login/ # Login page │ ├── push/ # Push related pages │ ├── release/ # Release related pages │ ├── repos/ # Repository related pages │ ├── search/ # Search page │ ├── trend/ # Trend page │ └── user/ # User related pages ├── provider/ # Provider state management ├── redux/ # Redux state management │ ├── middleware/ # Redux middleware │ ├── gsy_state.dart # Redux state definition │ ├── login_redux.dart # Login state management │ └── user_redux.dart # User state management ├── test/ # Test related └── widget/ # Custom widgets ├── anima/ # Animation widgets ├── markdown/ # Markdown rendering widgets ├── menu/ # Menu widgets ├── particle/ # Particle effect widgets ├── pull/ # Pull-to-refresh widgets └── state/ # State related widgets ``` Riverpod page state management: ``` ┌───────────────────────────────────────────────────────────────────────────┐ │ Page Architecture Overview │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ Global State │ │ ┌───────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ │ appThemeProvider │ │ appLocalProvider │ │ appGrepProvider │ │ │ │ (Theme Data) │ │ (Localization) │ │ (Grayscale Mode) │ │ │ └───────────────────┘ └────────────────────┘ └────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────┘ │ ┌────────────────┴────────────────┐ ▼ ▼ ┌─────────────────────────────────┐ ┌─────────────────────────────────────┐ │ TrendPage (Riverpod) │ │ NotifyPage (Signals) │ ├─────────────────────────────────┤ ├─────────────────────────────────────┤ │ │ │ │ │┌─────────────────────────────┐ │ │┌───────────────────────────────────┐│ ││ Riverpod Providers │ │ ││ Signals State ││ ││┌───────────────────────────┐│ │ ││┌─────────────────────────────────┐││ │││ trendFirstProvider ││ │ │││ notifySignal (List) │││ │││ trendSecondProvider ││ │ │││ notifyIndexSignal (int) │││ ││└───────────────────────────┘│ │ │││ signalPage (int) │││ │└─────────────────────────────┘ │ ││└─────────────────────────────────┘││ │ │ │└───────────────────────────────────┘│ │┌─────────────────────────────┐ │ │┌───────────────────────────────────┐│ ││ Local State (StatefulWidget)│ │ ││ SignalsMixin Processing ││ ││ - UI Controls │ │ ││ - createEffect() for reactions ││ ││ - Filter Parameters │ │ ││ - Manages data loading ││ │└─────────────────────────────┘ │ ││ - Updates UI based on signals ││ │ │ │└───────────────────────────────────┘│ └─────────────────────────────────┘ └─────────────────────────────────────┘ │ │ └────────────────┬────────────────┘ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ Data Layer │ │ ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ ReposRepository / UserRepository │ │ │ │ ┌────────────────────────┐ ┌─────────────────────────────────┐ │ │ │ │ │ Network Request │───┬──▶│ Database Providers │ │ │ │ │ │ - API calls │ │ │ - Data caching │ │ │ │ │ └────────────────────────┘ │ └─────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ └──▶│ Data Models │ │ │ │ │ │ - Structure definitions │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ UI Components │ │ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │ │ View Models │ │ List Items │ │ Interactive UI │ │ │ │ - Data Formatting │ │ - Item Rendering │ │ - User Actions │ │ │ └────────────────────┘ └────────────────────┘ └────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────┘ ``` Provider page state management: ``` +-----------------------------------------------------+ | App User Interface | +-----------------------------------------------------+ | v +-----------------------------------------------------+ | RepositoryDetailPage (StatefulWidget) | | with SingleTickerProviderStateMixin | +-----------------------------------------------------+ | v +-----------------------------------------------------+ | MultiProvider | +-----------------------------------------------------+ | | v v +------------------+ +-----------------------+ | ReposNetWork |<----------| ReposDetailProvider | | Provider | | | +---------+--------+ +-----------------------+ | | | | v v +-----------------------------------------------------+ | Repository Data Services | | (ReposRepository, IssueRepository) | +-----------------------------------------------------+ | v +-----------------------------------------------------+ | Four Tab Pages (Consumers) | +-----------------------------------------------------+ | | | | v v v v +----------+ +----------+ +----------+ +----------+ | Info | | Readme | | Issues | | Files | | Page | | Page | | Page | | Page | +----------+ +----------+ +----------+ +----------+ | | | | | | | | v v v v +-----------------------------------------------------+ | GlobalKeys for Tab Access | | (infoListKey, readmeKey, issueListKey, fileListKey) | +-----------------------------------------------------+ ``` ![](p1.png) ![](p2.png) ![](p3.png) ![](p4.png) ## Star History Chart [![Star History Chart](https://api.star-history.com/svg?repos=CarGuo/gsy_github_app_flutter&type=Date)](https://star-history.com/#CarGuo/gsy_github_app_flutter&Date) ### LICENSE ``` CarGuo/GSYGithubAppFlutter is licensed under the Apache License 2.0 A permissive license whose main conditions require preservation of copyright and license notices. Contributors provide an express grant of patent rights. Licensed works, modifications, and larger works may be distributed under different terms and without source code. ``` ​ ================================================ FILE: RECORD.md ================================================ flutter build apk --target-platform android-arm64 -t lib/main_prod.dart --no-sound-null-safety flutter packages pub run build_runner build --delete-conflicting-outputs dart migrate --skip-import-check flutter run --no-sound-null-safety https://flutter.cn/docs/development/tools/devtools/cli http://localhost:9100 sudo gem install -n /usr/local/bin cocoapods -v 1.9.3 ./gradlew :app:dependencies AS 全局匹配中文搜索 ^((?!(\*|//)).)+[\u4e00-\u9fa5] 查看 framework 支持 1.进入到framework目录下 cd /Users/.../xFramework.framework 2.输入命令 lipo -info xFramework -tag:gralloc4 # 如何查看dill文件 我们可以通过dart sdk中的vm package提供的dump_kernel.dart打印出dill的内部结构。 ``` dart bin/dump_kernel.dart /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill.txt 注意bin/dump_kernel.dart需要改成自己dart sdk中的具体路径。 ``` ///配置多渠道 flutter run --dart-define=CHANNEL=GSY --dart-define=LANGUAGE=Dart const CHANNEL = String.fromEnvironment('CHANNEL'); const LANGUAGE = String.fromEnvironment('LANGUAGE'); query getUserDetail($name:String!){ user(login: $name) { login, avatarUrl, company, location, bio, email, bioHTML, websiteUrl, viewerIsFollowing, createdAt, repositories(first: 100) { totalCount, nodes { stargazers { totalCount } } } followers { totalCount } following { totalCount } starredRepositories { totalCount } isViewer, #pinnedItems { # #} organizations(first: 100) { nodes { login, avatarUrl, name } } } } query GetStars($name: String!, $owner: String!, $after: String) { repository(name: $name, owner: $owner) { createdAt stargazers(first: 100, after: $after) { edges { node { id login name avatarUrl __typename } starredAt __typename } pageInfo { startCursor endCursor hasNextPage __typename↵ } totalCount __typename } __typename } } xxd /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill dart dump_kernel.dart /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill /Users/xxxxxxx/workspace/flutter-wrok/flutter_app_test/.dart_tool/flutter_build/bf7ed8e7e7b3e64f28f0af8a89a29ca9/app.dill.txt flutter pub deps 打印依赖 m1 mac pod install fail , gem install ffi -- --enable-libffi-alloc iconv -f gbk -t utf8 1.txt > 1_utf.txt AS 检索匹配中文 ^((?!(\*|//)).)+[\u4e00-\u9fa5] serve --ssl-key *.key --ssl-cert *.crt 获取崩溃日志 /// 清空 adb logcat -d *:W > crash.log /// 输出 win 平台可以 adb logcat -d *:E | Select-String "com.shuyu.gsygithub.gsygithubappflutter" > crash.log mac 可以 adb logcat -d *:E | grep "com.shuyu.gsygithub.gsygithubappflutter" > crash.log 获取更进准 flutter build web --no-web-resources-cdn adb shell setprop log.tag.gralloc4 SILENT "No uri found in code block" clear your working set as it has become corrupt ================================================ FILE: UISCENE_PLUGIN_RISK.md ================================================ # UIScene Plugin Risk Checklist - Project: `gsy_github_app_flutter` - Generated on: 2026-02-13 - Scope: iOS plugins listed in `.flutter-plugins-dependencies` - Method: static source scan for Scene lifecycle compatibility signals (`FlutterSceneLifeCycleDelegate`, `addSceneDelegate`, usage of `UIApplication.keyWindow` / `windows`, and AppDelegate lifecycle hooks). ## High Risk ### 1. `url_launcher_ios 6.3.6` - Risk level: High - Why: - Uses deprecated window lookup via `UIApplication.shared.keyWindow`, which is scene-unaware. - Evidence: - `/Users/guoshuyu/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.6/ios/url_launcher_ios/Sources/url_launcher_ios/URLLauncherPlugin.swift:22` - Impact: - In multi-scene/iPad multi-window mode, in-app Safari presentation may target the wrong scene or fail to present. ### 2. `flutter_inappwebview_ios 1.1.2` - Risk level: High - Why: - Multiple code paths still use `keyWindow` / `UIApplication.shared.windows`, which can be incorrect under multi-scene. - Evidence: - `/Users/guoshuyu/.pub-cache/hosted/pub.dev/flutter_inappwebview_ios-1.1.2/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift:40` - `/Users/guoshuyu/.pub-cache/hosted/pub.dev/flutter_inappwebview_ios-1.1.2/ios/Classes/UIApplication/VisibleViewController.swift:13` - `/Users/guoshuyu/.pub-cache/hosted/pub.dev/flutter_inappwebview_ios-1.1.2/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift:93` - Impact: - Headless WebView attach point, visible view-controller resolution, and web-auth presentation anchor may bind to the wrong window/scene. ## Medium Risk ### 1. `fluttertoast 8.2.10` - Risk level: Medium - Why: - Chooses UI window via `UIApplication.sharedApplication.windows` and key-window iteration. - Evidence: - `/Users/guoshuyu/.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/Classes/FluttertoastPlugin.m:130` - Impact: - Toast may appear on a non-active scene window or behave inconsistently in multi-window mode. ## Low Risk ### 1. `share_plus 12.0.1` - Risk level: Low - Why: - Uses `connectedScenes` and `UIWindowScene.windows` for root view-controller selection on iOS 13+. - Evidence: - `/Users/guoshuyu/.pub-cache/hosted/pub.dev/share_plus-12.0.1/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m:12` - Note: - Keeps a fallback to `keyWindow` for iOS 12 and below (acceptable for non-scene OS versions). ### 2. Other installed iOS plugins in this repo - `connectivity_plus 6.0.5` - `device_info_plus 10.1.2` - `package_info_plus 8.0.2` - `path_provider_foundation 2.5.1` - `permission_handler_apple 9.4.7` - `rive_common 0.4.11` - `shared_preferences_foundation 2.5.6` - `sqflite 2.3.3+1` - `webview_flutter_wkwebview 3.23.5` Risk level: Low (for UIScene lifecycle migration) - Why: - No direct AppDelegate lifecycle hook usage (`addApplicationDelegate` / `openURL` callbacks / `continueUserActivity`) detected in plugin runtime source paths. ## Recommended Actions 1. Upgrade first: - `url_launcher_ios` - `flutter_inappwebview_ios` - `fluttertoast` 2. Run targeted iPad multi-window validation: - `url_launcher` in-app Safari presentation - InAppWebView / headless webview attach - Web auth session presentation anchor - Toast display target window - Share sheet presentation 3. If upgrade is blocked: - Apply local patch to resolve active `UIWindowScene` and active window/VC instead of `keyWindow`/global `windows`. ================================================ FILE: VERSION.md ================================================ ### 请直接看 github release ### 1.1.9 * 修正弹出键盘的时候被挤压问题 * 修复切换主题导致长按输入框弹出异常 * 修复其他小问题。 ### 1.1.8 * 修复反馈输入框遮挡问题。 * 更新部分插件,更新了 sdk 到 1.1.9 ### 1.1.7 * 更新flutter SDK 到 1.1.3 版本,修复TargetSDK 28以上在9.0键盘无法弹出问题。 ### 1.1.6 * flutter升级正式版1.0 ### 1.1.5 * Android 代码详情使用 AndroidView 实现WebView * 升级flutter Sdk * 升级第三方包 ### 1.1.3 * 修复详情tab切换问题。 ### 1.1.2 * 增加滑动返回。 * 修复主页抽屉小屏幕无法滚动。 * 增加部分代码高亮。 * 修复搜索排序按键问题。 * 更新flutter SDK ### 1.1.1 * 更新flutter SDK 0.5.8。 * 修复一些仓库下的readme问题。 ### 1.1.0 * 切换用户切换数据库。 * 多语言。 ### 1.0.9 * 切换主题支持 * 问题修复 ### 1.0.8 * readme图片解析优化 * readme图片增加点击查看 * 组织账号不显示活跃记录Item * 增加用户组织显示 ### 1.0.7 * 增加图片预览 * 修复未读的通知打开提示其他异常 * 增加fork仓库跳转到原仓库 * 增加仓库点击展示 issue 状态信息 * 增加个人状态信息可跳转 * 增加仓库Topic显示 * 通知中心增加侧滑点击已读 ### 1.0.6 * trend修改为redux * 增加本地阅读历史 * drawer 状态栏样式处理 * 增加个人动态提交表。 ### 1.0.5 * 增加本地数据库 * 修复分享问题。 * 修改用户页面样式 * 增加用户加入github时间显示 ### 1.0.4 * 修复启动页变形问题。 * 调整个人页面字体动态大小。 * 增加趋势语言,搜索语言dart选项。 * 增加前后台切换刷新动态。 * 增加触摸隐藏键盘。 * 增加点击检测版本。 * 增加 issue 使用markdown解析 * 增加 issue 输入框的快速输入按键。 * 增加 issue 关闭操作信息。 * 返回文件列表的返回键处理逻辑。 * 修复详情中存在的model转化问题。 ### 1.0.1 (已发布) * 修复loading弹出框黄线问题。 * 调整部分ui。 * 增加Release列表。 * 增加版检测。 * Issue详情页显示问题。 * 返回按键退出问题。 ### 1.0.0 * 第一版完成 ================================================ FILE: analysis_options.yaml ================================================ include: package:flutter_lints/flutter.yaml analyzer: errors: mixin_inherits_from_not_object: ignore plugins: - custom_lint linter: rules: non_constant_identifier_names: false file_names: false constant_identifier_names: false library_private_types_in_public_api: false library_prefixes: false ================================================ FILE: android/.gitignore ================================================ *.iml *.class .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures GeneratedPluginRegistrant.java .cxx/ ================================================ FILE: android/app/build.gradle ================================================ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } apply from: "exported.gradle" android { namespace "com.shuyu.gsygithub.gsygithubappflutter" compileSdkVersion 36 sourceSets { main.java.srcDirs += 'src/main/kotlin' } lint { disable += 'InvalidPackage' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.shuyu.gsygithub.gsygithubappflutter" minSdkVersion flutter.minSdkVersion targetSdkVersion 35 versionCode 69 versionName "7.9.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { debug { storeFile file("../gsygithubapp-debug.jks") storePassword "123456" keyAlias "debug" keyPassword "123456" } release { storeFile file("../gsygithubapp-debug.jks") storePassword "123456" keyAlias "debug" keyPassword "123456" } } buildTypes { debug { signingConfig signingConfigs.debug } release { signingConfig signingConfigs.release } } lint { abortOnError = false } dependenciesInfo { // Disables dependency metadata when building APKs. includeInApk = false // Disables dependency metadata when building Android App Bundles. includeInBundle = false } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } configurations.all { resolutionStrategy { ///为了 https://github.com/pichillilorenzo/flutter_inappwebview/issues/2150 ///force 'androidx.webkit:webkit:1.8.0' ///为了 Failed to transform appcompat-resources-1.7.0.aar ///这玩意导致的 Failed to transform appcompat-resources-1.7.0.aar Cannot invoke "String.length()" because "" is null 居然要升级 AGP 8.6.1 才能适配,有毒 //force "androidx.appcompat:appcompat:1.6.1" } } } flutter { source '../..' } dependencies {} // 在你的模块级别 build.gradle 文件中添加此任务 // 例如: app/build.gradle task findSoFileOrigins { description = "扫描项目依赖的 AAR 文件,找出 .so 文件的来源。" group = "reporting" // 将任务归类到 "reporting" 组下 doLast { // 用于存储 AAR 标识符及其包含的 .so 文件路径 // 键 (Key): AAR 的字符串标识符 (例如:"project :gsyVideoPlayer", "com.example.library:core:1.0.0") // 值 (Value): 一个 Set 集合,包含该 AAR 内所有 .so 文件的路径 (字符串) def aarSoFilesMap = [:] def variants = null if (project.plugins.hasPlugin('com.android.application')) { variants = project.android.applicationVariants } else if (project.plugins.hasPlugin('com.android.library')) { variants = project.android.libraryVariants } else { project.logger.warn("警告: findSoFileOrigins 任务需要 Android 应用插件 (com.android.application) 或库插件 (com.android.library)。") return } if (variants == null || variants.isEmpty()) { project.logger.warn("警告: 未找到任何变体 (variants) 来处理。") return } variants.all { variant -> project.logger.lifecycle("正在扫描变体 '${variant.name}' 中的 AAR 依赖以查找 .so 文件...") // 获取该变体的运行时配置 (runtime configuration) def configuration = variant.getRuntimeConfiguration() try { // 配置一个构件视图 (artifact view) 来精确请求 AAR 类型的构件 def resolvedArtifactsView = configuration.incoming.artifactView { view -> view.attributes { attributes -> // 明确指定我们只对 artifactType 为 'aar' 的构件感兴趣 // AGP 也常用 "android-aar",如果 "aar" 效果不佳,可以尝试替换 attributes.attribute(Attribute.of("artifactType", String.class), "aar") } // lenient(false) 是默认行为。如果设为 true,它会尝试跳过无法解析的构件而不是让整个视图失败。 // 但如果像之前那样,是组件级别的变体选择失败 (如 gsyVideoPlayer),lenient 可能也无法解决。 // view.lenient(false) }.artifacts // 获取 ResolvedArtifactSet project.logger.info("对于变体 '${variant.name}',从配置 '${configuration.name}' 解析到 ${resolvedArtifactsView.artifacts.size()} 个 AAR 类型的构件。") resolvedArtifactsView.each { resolvedArtifactResult -> // resolvedArtifactResult 是 ResolvedArtifactResult 类型的对象 File aarFile = resolvedArtifactResult.file // 获取组件的标识符,这能告诉我们依赖的来源 // 例如:"project :gsyVideoPlayer" 或 "com.google.android.material:material:1.7.0" String aarIdentifier = resolvedArtifactResult.id.componentIdentifier.displayName aarSoFilesMap.putIfAbsent(aarIdentifier, new HashSet()) if (aarFile.exists() && aarFile.name.endsWith('.aar')) { // project.logger.info("正在检查 AAR: ${aarIdentifier} (文件: ${aarFile.name})") try { project.zipTree(aarFile).matching { include '**/*.so' // 匹配 AAR 中的所有 .so 文件 }.each { File soFileInZip -> aarSoFilesMap[aarIdentifier].add(soFileInZip.path) } } catch (Exception e) { project.logger.error("错误: 无法检查 AAR 文件 '${aarIdentifier}' (路径: ${aarFile.absolutePath})。原因: ${e.message}") } } else { if (!aarFile.name.endsWith('.aar')) { project.logger.debug("跳过非 AAR 文件 '${aarFile.name}' (来自: ${aarIdentifier}),其构件类型被解析为 AAR。") } else { project.logger.warn("警告: 来自 '${aarIdentifier}' 的 AAR 文件不存在: ${aarFile.absolutePath}") } } } } catch (Exception e) { // 这个 catch 块会捕获解析构件视图时发生的错误 // 这可能仍然包括之前遇到的 "Could not resolve all artifacts for configuration" 错误, // 如果问题非常根本,即使是特定的构件视图也无法克服。 project.logger.error("错误: 无法为配置 '${configuration.name}' 解析 AAR 类型的构件。" + "这通常表明您的项目设置中存在依赖变体匹配问题," + "特别是对于像 ':gsyVideoPlayer' 这样的项目依赖。 " + "详细信息: ${e.message}", e) // 打印异常堆栈以获取更多信息 project.logger.error("建议: 请检查项目依赖(尤其是本地子项目如 ':gsyVideoPlayer')的构建配置," + "确保它们能正确地发布带有标准 Android 库属性(如组件类别、构建类型,以及适用的 Kotlin 平台类型等)的变体。") // 如果希望任务在此处停止而不是尝试其他变体,可以取消下一行的注释 // throw e } } // 打印结果 if (aarSoFilesMap.isEmpty()) { project.logger.lifecycle("\n在所有已处理变体的可解析 AAR 依赖中均未找到 .so 文件,或者依赖解析失败。") } else { println "\n--- AAR 依赖中的 .so 文件来源 ---" // 按 AAR 标识符排序以获得一致的输出 aarSoFilesMap.sort { it.key }.each { aarId, soFileList -> if (!soFileList.isEmpty()) { println "${aarId}:" // 例如:project :gsyVideoPlayer: 或 com.some.library:core:1.0: soFileList.sort().each { soPath -> // 对 .so 文件路径排序 println " - ${soPath}" // 例如: - jni/armeabi-v7a/libexample.so } } } println "----------------------------------" } project.logger.lifecycle("任务执行完毕。要再次运行此任务,请执行: ./gradlew ${project.name}:${name}") } } ================================================ FILE: android/app/exported.gradle ================================================ /** * 修改 Android 12 因为 exported 的构建问题,主要用于演示 */ android.applicationVariants.all { variant -> variant.outputs.each { output -> def processManifest = output.getProcessManifestProvider().get() processManifest.doLast { task -> def outputDir = task.multiApkManifestOutputDirectory File outputDirectory if (outputDir instanceof File) { outputDirectory = outputDir } else { outputDirectory = outputDir.get().asFile } File manifestOutFile = file("$outputDirectory/AndroidManifest.xml") println("----------- ${manifestOutFile} ----------- ") if (manifestOutFile.exists() && manifestOutFile.canRead() && manifestOutFile.canWrite()) { def manifestFile = manifestOutFile ///这里第二个参数是 false ,所以 namespace 是展开的,所以下面不能用 androidSpace,而是用 nameTag def xml = new XmlParser(false, false).parse(manifestFile) def exportedTag = "android:exported" def nameTag = "android:name" ///指定 space //def androidSpace = new groovy.xml.Namespace('http://schemas.android.com/apk/res/android', 'android') def nodes = xml.application[0].'*'.findAll { //挑选要修改的节点,没有指定的 exported 的才需要增加 //如果 exportedTag 拿不到可以尝试 it.attribute(androidSpace.exported) (it.name() == 'activity' || it.name() == 'receiver' || it.name() == 'service') && it.attribute(exportedTag) == null } ///添加 exported,默认 false nodes.each { def isMain = false it.each { if (it.name() == "intent-filter") { it.each { if (it.name() == "action") { //如果 nameTag 拿不到可以尝试 it.attribute(androidSpace.name) if (it.attributes().get(nameTag) == "android.intent.action.MAIN") { isMain = true println("......................MAIN FOUND......................") } } } } } it.attributes().put(exportedTag, "${isMain}") } PrintWriter pw = new PrintWriter(manifestFile) pw.write(groovy.xml.XmlUtil.serialize(xml)) pw.close() } } } } ================================================ FILE: android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/app/src/main/kotlin/com/shuyu/gsygithub/gsygithubappflutter/MainActivity.kt ================================================ package com.shuyu.gsygithub.gsygithubappflutter import UpdateAlbumPlugin import androidx.annotation.NonNull; import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity: FlutterActivity() { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine) flutterEngine.plugins.add(UpdateAlbumPlugin()) } } ================================================ FILE: android/app/src/main/kotlin/com/shuyu/gsygithub/gsygithubappflutter/UpdateAlbumPlugin.kt ================================================ import android.content.Context import android.content.Intent import android.net.Uri import android.provider.MediaStore import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel class UpdateAlbumPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { /** Channel名称 **/ private var channel: MethodChannel? = null private var context: Context? = null companion object { private val sChannelName = "com.shuyu.gsygithub.gsygithubflutter/UpdateAlbumPlugin" } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel( binding.binaryMessenger, sChannelName) context = binding.applicationContext channel!!.setMethodCallHandler(this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { channel?.setMethodCallHandler(null) channel = null } override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) { when (methodCall.method) { "updateAlbum" -> { val path: String? = methodCall.argument("path") val name: String? = methodCall.argument("name") try { MediaStore.Images.Media.insertImage(context?.contentResolver, path, name, null) } catch (e: Exception) { e.printStackTrace() } // 最后通知图库更新 context?.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://$path"))) } } result.success(null) } } ================================================ FILE: android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: android/app/src/main/res/drawable/normal_background.xml ================================================ ================================================ FILE: android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: android/build.gradle ================================================ allprojects { repositories { google() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } tasks.register("clean", Delete) { delete rootProject.buildDir } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 android.useAndroidX=true #systemProp.http.proxyHost=127.0.0.1 #systemProp.http.proxyPort=7890 #systemProp.https.proxyHost=127.0.0.1 #systemProp.https.proxyPort=7890 ================================================ FILE: android/gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: android/gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: android/settings.gradle ================================================ pluginManagement { def flutterSdkPath = { def properties = new Properties() file("local.properties").withInputStream { properties.load(it) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" return flutterSdkPath }() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { google() mavenCentral() gradlePluginPortal() } } plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.9.1" apply false id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" ================================================ FILE: devtools_options.yaml ================================================ description: This file stores settings for Dart & Flutter DevTools. documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states extensions: ================================================ FILE: docs/00-overview/project-map.md ================================================ # 项目地图 ## 项目定位 `gsy_github_app_flutter` 是一个跨平台 GitHub 客户端。 它既是完整功能应用,也是偏教学展示风格的工程,因此仓库中会刻意保留多种实现方式,而不是追求所有模块都完全统一。 ## 运行主链路 高层数据链路可以概括为: `UI/Page -> 状态层 -> Repository -> 网络/数据库 -> Model -> UI 刷新` 应用入口和全局装配主要集中在: - `lib/main.dart` - `lib/app.dart` 这些位置负责: - 应用启动 - 环境配置装配 - 根导航 - 多语言和主题 - 全局错误处理 - 全局状态容器接线 ## 目录地图 - `lib/main.dart`:应用启动、Zone 异常兜底、环境包装 - `lib/app.dart`:`MaterialApp`、路由、Redux 根 store、Riverpod 容器、HTTP 错误监听 - `lib/common/config/`:应用配置和 OAuth 本地配置 - `lib/common/net/`:API 客户端、拦截器、GraphQL、数据转换 - `lib/common/repositories/`:功能层数据访问边界 - `lib/common/localization/`:本地化扩展、ARB、生成代码 - `lib/db/`:本地数据库 provider 和 SQL 辅助 - `lib/env/`:环境配置及其生成文件 - `lib/model/`:数据模型和序列化生成文件 - `lib/page/`:页面功能目录,如 `repos`、`issue`、`trend`、`notify`、`user` - `lib/provider/`:Provider/Riverpod 相关共享状态 - `lib/redux/`:Redux state、reducer、middleware - `static/`:静态资源 - `.github/workflows/`:GitHub Actions 配置 ## 对协作者很重要的现实 - 项目里同时存在多种状态管理方案 - 同时使用 REST 和 GraphQL - 生成代码是日常开发流程的一部分 - 根 README 主要承担项目介绍和运行说明,不足以替代工程地图 ## AI 最容易犯错的地方 - 误以为整个项目已经统一到某一种状态管理 - 为了解决页面局部问题去改 `lib/app.dart` - 直接手改生成文件而不是回到源输入 - 忘记 `ignoreConfig.dart` 的本地依赖 在修改共享链路前,先看架构文档和对应模块文档。 ================================================ FILE: docs/01-architecture/app-layering.md ================================================ # 应用分层 ## 目的 这个仓库不是严格单一架构实现,而是采用“整体分层明确、局部实现多样”的方式。 协作者不需要强行统一写法,但需要尊重已有边界,避免把局部需求扩散成全局耦合。 ## 1. 入口与应用壳层 - `lib/main.dart` - `lib/app.dart` - `lib/env/` 职责: - 应用启动 - 环境配置装配 - 根导航与多语言/主题 - 全局异常处理 - 全局状态容器接线 原则: - 不要把功能业务逻辑直接塞进这里 - 除非是全局行为问题,否则尽量不要改 `lib/app.dart` ## 2. UI 层 - `lib/page/` - 各功能目录下的局部 widget - `lib/common/` 下复用 UI 组件 职责: - 页面渲染 - 用户交互响应 - 将数据请求和状态变化委托给状态层或 repository 原则: - 优先在功能目录内完成 UI 改动 - 页面不要直接承接太多网络协议细节 ## 3. 状态层 - `lib/redux/` - `lib/provider/` - `lib/app.dart` 中接入的 Riverpod 容器 - 指定页面中的 Signals 职责: - 维护视图状态 - 协调异步加载 - 向 UI 暴露状态变化 原则: - 新改动优先沿用目标模块当前已有状态方案 - 不要在无关任务里做状态管理迁移 ## 4. Repository 层 - `lib/common/repositories/` 职责: - 将功能请求翻译成网络或数据库访问 - 隔离页面/状态层与具体传输实现 原则: - 改接口时优先在 repository 边界收口 - 页面不要绕过 repository 直接铺开网络细节 ## 5. 数据与传输层 - `lib/common/net/` - `lib/db/` - `lib/model/` 职责: - HTTP/GraphQL 访问 - 拦截器与响应转换 - 本地持久化 - 序列化与模型转换 原则: - 不要从页面直接复制网络调用逻辑 - 共用行为优先收敛到共享网络层或 repository ## 生成代码约束 以下内容应视为生成产物: - `lib/model/` 下的 `*.g.dart` - `lib/env/` 下生成文件 - `lib/common/localization/l10n/` 下生成输出 - `riverpod_annotation` 对应的 `*.g.dart` 原则: - 优先修改源输入,再重新生成 - 非必要不要直接手改生成文件 ## 改动入口建议 - 页面展示问题:先看 `lib/page//` - API/模型问题:看 `common/net`、`common/repositories`、`model` - 全局主题/语言/导航问题:看 `lib/app.dart` 和共享 provider/redux - 构建或配置问题:看 `lib/env/`、`pubspec.yaml` 和 runbook ================================================ FILE: docs/01-architecture/state-management-matrix.md ================================================ # 状态管理矩阵 ## 为什么要有这份文档 这个项目刻意保留了多种状态管理方式用于展示和演进。 这对学习有价值,但也意味着协作者和 agent 很容易“按自己的偏好”误改边界。 这份文档的目的是停止猜测。 ## 当前分布 ### Redux - 主要承担应用级共享状态,例如登录态和用户信息 - 根 store 在 `lib/app.dart` 中创建 - reducer 和 middleware 位于 `lib/redux/` ### Riverpod - 用于部分全局共享状态,例如灰度模式、语言、主题 - 也用于部分功能模块,例如趋势页相关数据流 - 根容器同样在 `lib/app.dart` 中接入 ### Provider - 仍存在于部分功能模块,尤其是较早的页面链路 - 典型例子是仓库详情页的跨 tab 状态共享 ### Signals - 用于局部页面状态 - 当前通知页和部分文件列表相关页面会使用 ## 工作策略 - 不要再引入第五种状态管理模式 - 无关任务里不要顺手把模块从一种方案迁到另一种方案 - 新状态优先跟随“最近的既有模式”,而不是个人习惯 - 如果一个需求同时涉及全局状态和页面局部状态,要明确边界,避免双份真相 ## 评审时必问的问题 1. 这份状态是页面局部、功能局部,还是应用全局? 2. 目标模块原来已经在用哪种状态方案? 3. 这次改动是否真的需要碰 `lib/app.dart`? 4. 会不会让 Redux、Riverpod、Provider 或 Signals 之间出现重复状态源? ## 后续方向 如果将来要收敛状态管理种类,应先在 `docs/06-decisions/` 记录决策,再开始迁移。 不要让“慢慢改着改着就变了”成为默认路径。 ================================================ FILE: docs/02-features/debug.md ================================================ # 调试页功能 ## 相关文件 - `lib/page/debug/debug_data_page.dart` - `lib/page/debug/debug_label.dart` - `lib/common/net/interceptors/log_interceptor.dart` - `lib/common/logger.dart` ## 当前实现 调试页用于查看开发过程中的: - HTTP Response - HTTP Request - HTTP Error - Talker 错误日志 并支持复制链接、复制数据、弹出 JSON 查看器。 ## 数据流 调试页本身不发起业务请求,而是消费全局日志和拦截器收集到的数据。 ## 状态管理 - 页面本地 tab 状态 - 数据由全局日志容器和拦截器静态列表提供 ## 高风险点 - 这是辅助调试能力,不应影响线上业务路径 - 改日志结构时要同步看调试页展示 - 大量数据展示和复制逻辑集中在单页内部 ## 修改建议 - 仅在需要增强调试体验时修改 - 不要把正式业务依赖绑到调试页上 - 改日志采集字段时,顺带检查调试页是否还能正确显示 ================================================ FILE: docs/02-features/dynamic.md ================================================ # 动态页功能 ## 相关文件 - `lib/page/dynamic/dynamic_page.dart` - `lib/page/dynamic/dynamic_bloc.dart` - `lib/common/repositories/repos_repository.dart` - `lib/common/utils/event_utils.dart` ## 当前实现 动态页是首页第一个 tab,展示当前用户相关动态流。 页面支持: - 首次加载 - 下拉刷新 - 上拉加载更多 - 生命周期恢复时自动刷新 - 事件点击跳转 ## 数据流 1. 首次进入时先读数据库或本地缓存链路 2. 再触发刷新 3. 页面恢复到前台时,如果已有数据则再次触发刷新 4. 列表项点击后通过 `EventUtils` 分发跳转 ## 状态管理 - 主要通过 `DynamicBloc` 管理列表数据 - 页面本地管理滚动、刷新和忽略点击状态 - 用户名来源于 Redux store ## 高风险点 - 首次加载、手动刷新、生命周期恢复刷新是三条不同入口 - `_ignoring` 会影响页面交互时机 - 数据来源和跳转逻辑分布在 bloc、repository、event utils ## 修改建议 - 列表行为问题优先看 `dynamic_page.dart` - 数据加载与分页问题看 `dynamic_bloc.dart` - 事件跳转问题看 `EventUtils` ================================================ FILE: docs/02-features/home.md ================================================ # 首页容器功能 ## 相关文件 - `lib/page/home/home_page.dart` - `lib/page/home/widget/home_drawer.dart` - `lib/page/dynamic/dynamic_page.dart` - `lib/page/trend/trend_page.dart` - `lib/page/my_page.dart` ## 当前实现 首页不是单一业务页,而是应用主容器,负责: - 三个主 tab 切换 - 搜索入口 - drawer 入口 - 双击 tab 回到顶部 - Android 返回键回桌面 ## 数据流 首页主要负责导航和容器调度,本身不直接承接业务数据。 业务数据由各 tab 页面自己拉取。 ## 状态管理 - 主要是页面容器本地状态 - 通过 `GlobalKey` 驱动各 tab 的 `scrollToTop` ## 高风险点 - 改 tab 结构会影响动态页、趋势页、我的页面联动 - 搜索入口依赖右上角控件位置计算 - 返回键行为是首页特有容器逻辑 ## 修改建议 - 首页问题优先限制在容器和导航层 - 不要把某个 tab 的业务逻辑加回首页 - 改搜索入口时要验证动画起点和跳转 ================================================ FILE: docs/02-features/issue.md ================================================ # Issue 功能 ## 相关文件 - `lib/page/issue/issue_detail_page.dart` - `lib/page/issue/issue_edit_dIalog.dart` - `lib/common/repositories/issue_repository.dart` ## 当前实现 Issue 详情页同时承担: - issue 头部信息展示 - 评论列表展示 - 回复 issue - 编辑 issue - 编辑/删除评论 - open/close issue ## 数据流 1. 页面刷新时先拉 issue 头部信息 2. 再拉评论列表 3. 头部和评论列表分别更新 4. 编辑、回复、删除后通过刷新重新拉取数据 ## 状态管理 - 主要使用页面本地 state - 列表部分依赖 `GSYListState` - 数据入口集中在 `IssueRepository` ## 高风险点 - 头部信息和评论列表是两条并行数据链路 - 编辑、删除、回复之后依赖刷新回流,不是本地直接 patch - issue 状态切换会影响头部按钮和展示状态 ## 修改建议 - 评论展示问题优先改 `issue_detail_page.dart` 和 widget - 数据或协议问题再看 `IssueRepository` - 改编辑能力时要同时验证 reply/edit/delete/open-close 几条链路 ================================================ FILE: docs/02-features/login.md ================================================ # 登录功能 ## 相关文件 - `lib/page/login/login_page.dart` - `lib/page/login/login_webview.dart` - `lib/redux/login_redux.dart` - `lib/common/repositories/user_repository.dart` - `lib/common/net/address.dart` ## 当前实现 登录页同时保留了账号密码入口和 OAuth 入口,但当前用户名密码登录已被直接禁用,页面会提示 `login_deprecated`。 实际可用主链路是 OAuth 登录。 ## 数据流 OAuth 登录链路: 1. `LoginPage` 点击 OAuth 按钮 2. 通过 `Address.getOAuthUrl()` 生成授权地址 3. 跳转到 `login_webview.dart` 4. 登录成功后拿到 `code` 5. 分发 Redux `OAuthAction` 6. `oauthEpic` 调用 `UserRepository.oauth` 7. 登录成功后分发 `LoginSuccessAction` 8. reducer 内跳转首页 ## 状态管理 - 页面输入和交互:页面本地 state - 登录结果与全局用户态:Redux ## 高风险点 - 不要把 OAuth 成功后的全局登录逻辑搬回页面层 - `use_build_context_synchronously` 已在现有代码中被局部忽略,修改时要注意异步后导航安全 - `ignoreConfig.dart` 缺失会直接影响 OAuth 流程 ## 修改建议 - 登录页 UI 小改动:尽量只动 `lib/page/login/` - 登录协议改动:同步检查 `login_redux.dart`、`user_repository.dart`、OAuth 地址构造 - 不要在无关任务中恢复账号密码登录链路,除非需求明确要求 ================================================ FILE: docs/02-features/notify.md ================================================ # 通知功能 ## 相关文件 - `lib/page/notify/notify_page.dart` - `lib/common/repositories/user_repository.dart` - `lib/model/notification.dart` ## 当前实现 通知页负责展示 GitHub 通知消息,并支持: - 未读 / 参与 / 全部 三种筛选 - 下拉刷新 - 上拉加载更多 - 将单条通知标记为已读 - 全部标记为已读 ## 数据流 1. 页面通过 Signals 创建通知列表、筛选索引、页码信号 2. `createEffect` 监听筛选索引和页码变化 3. 变化后触发 `loadData()` 4. `loadData()` 调用 `UserRepository.getNotifyRequest` 5. 根据返回结果刷新列表或追加列表 6. 点开 Issue 类型通知后跳转详情,并在返回时强制刷新 ## 状态管理 - 该页面主要使用 Signals - `notifySignal` 保存列表 - `notifyIndexSignal` 保存筛选状态 - `signalPage` 保存页码 ## 高风险点 - `signalPage = -1` 被用作强制刷新前的中间态,改页码逻辑时不能忽略这个约定 - 列表加载、筛选切换、已读操作都依赖信号联动,改动时要验证多种入口 - 目前只对 `Issue` 类型通知做了明确跳转处理 ## 修改建议 - 页面交互改动优先保持 Signals 结构稳定 - 如果扩展更多通知类型跳转,集中收口在 `_renderEventItem` - 改分页或刷新逻辑时,必须手工验证切 tab、已读、回退刷新三条链路 ================================================ FILE: docs/02-features/push.md ================================================ # Push 提交详情功能 ## 相关文件 - `lib/page/push/push_detail_page.dart` - `lib/page/push/widget/push_header.dart` - `lib/page/push/widget/push_coed_item.dart` - `lib/common/repositories/repos_repository.dart` ## 当前实现 Push 提交详情页负责展示一次 commit 的: - 头部信息 - 文件变更列表 - patch 预览 - 跳回仓库详情入口 ## 数据流 1. 页面刷新时请求 commit 详情 2. 拿到头部信息和文件列表 3. 点击文件项后把 patch 转成 HTML 并跳到代码详情页 ## 状态管理 - 主要是页面本地 state - 列表依赖 `GSYListState` - 数据入口在 `ReposRepository` ## 高风险点 - 头部和文件列表来自同一 commit 详情结果 - patch 需要转换成 HTML 后再展示 - 有的入口需要显示返回仓库首页按钮 ## 修改建议 - UI 展示问题优先改 `push_detail_page.dart` 和 widget - patch 展示问题同时看 `HtmlUtils` - commit 数据问题再看 `ReposRepository` ================================================ FILE: docs/02-features/release.md ================================================ # Release 功能 ## 相关文件 - `lib/page/release/release_page.dart` - `lib/page/release/widget/release_item.dart` - `lib/common/repositories/repos_repository.dart` ## 当前实现 Release 页面支持查看: - Release 列表 - Tag 列表 - 内嵌查看 release HTML 内容 - 外部打开 release 或 tag 页面 ## 数据流 1. 页面根据 tab 选择 release 或 tag 2. 调 `ReposRepository.getRepositoryReleaseRequest` 3. 列表项长按可打开外部链接 4. release 项点击可在应用内查看 HTML 内容 ## 状态管理 - 页面本地 state 保存当前 tab - 列表能力基于 `GSYListState` ## 高风险点 - release 和 tag 共用一个页面,但数据语义不同 - HTML 内容和外部链接两种打开方式都要兼容 - tab 切换时会清空并重刷列表 ## 修改建议 - 展示问题优先改 `release_page.dart` 和 item widget - 数据问题再看 `ReposRepository` - 改 tab 逻辑时要同时验证 release 和 tag 两条链路 ================================================ FILE: docs/02-features/repos.md ================================================ # 仓库详情功能 ## 相关文件 - `lib/page/repos/repository_detail_page.dart` - `lib/page/repos/provider/repos_detail_provider.dart` - `lib/page/repos/provider/repos_network_provider.dart` - `lib/common/repositories/repos_repository.dart` - `lib/common/repositories/issue_repository.dart` ## 当前实现 仓库详情页是一个典型的“功能复杂且跨 tab 共享状态”的模块。 页面包含: - 信息页 - Readme 页 - Issue 列表页 - 文件列表页 它通过 `MultiProvider` 在 tab 间共享 `ReposDetailProvider` 和 `ReposNetWorkProvider`。 ## 数据流 初始化主链路: 1. `RepositoryDetailPage` 创建 `ReposDetailProvider` 2. `initState` 中先拉取分支列表 3. tab 子页面通过共享 provider 请求详情、readme、issue、文件等数据 4. provider 继续委托给 `ReposRepository` 或 `IssueRepository` 分支切换链路: 1. 顶部更多菜单选择 branch 2. 更新 `currentBranch` 3. 主信息、文件列表、readme 分别触发刷新 ## 状态管理 - 当前模块主要使用 Provider - `ReposDetailProvider` 负责共享仓库详情、分支、底部按钮、当前 tab、readme 内容等状态 - `ReposNetWorkProvider` 只是对 repository 调用做一层包装,主要用于演示 provider 依赖 provider ## 高风险点 - 这是跨 tab 共享状态模块,不要把共享状态拆回单页局部变量 - `currentBranch` 会影响多个子页面数据源,改分支逻辑时要检查联动刷新 - 底部按钮依赖仓库详情状态,不要只改显示不改数据更新链路 - 该模块同时触及 repo、issue、readme、file,多点回归风险高 ## 修改建议 - UI 结构改动:先停留在 `lib/page/repos/` - 数据字段改动:同步检查 `ReposDetailProvider` 和 repository 返回模型 - 如果只是改某个 tab,不要顺手重做整个 provider 结构 ================================================ FILE: docs/02-features/search.md ================================================ # 搜索功能 ## 相关文件 - `lib/page/search/search_page.dart` - `lib/page/search/search_bloc.dart` - `lib/page/search/widget/gsy_search_drawer.dart` - `lib/common/repositories/repos_repository.dart` ## 当前实现 搜索页支持: - 搜仓库 - 搜用户 - 排序和过滤 - 搜索动画展开/收起 ## 数据流 1. 页面输入搜索词 2. 选择仓库或用户 tab 3. drawer 选择排序、类型、语言 4. `SearchBLoC.getDataLogic` 调 `ReposRepository.searchRepositoryRequest` 5. 列表根据当前 tab 渲染仓库项或用户项 ## 状态管理 - 页面主要使用本地 state - 查询条件保存在 `SearchBLoC` - 列表能力依赖 `GSYListState` ## 高风险点 - 搜索条件切换会触发清空列表并重新刷新 - 仓库和用户复用同一请求入口,但渲染不同 - 搜索页有自定义圆形展开/收起动画 ## 修改建议 - 纯交互或动画问题优先改 `search_page.dart` - 查询参数问题优先改 `search_bloc.dart` - 接口或返回结构问题再看 `ReposRepository` ================================================ FILE: docs/02-features/trend.md ================================================ # 趋势功能 ## 相关文件 - `lib/page/trend/trend_page.dart` - `lib/page/trend/trend_provider.dart` - `lib/page/trend/trend_user_page.dart` - `lib/common/repositories/repos_repository.dart` ## 当前实现 趋势页展示热门仓库列表,并支持按时间范围和语言筛选。 它现在主要基于 Riverpod provider 获取数据,但页面内部仍保留了一些本地状态和历史兼容写法。 ## 数据流 1. 页面首次进入时设置默认筛选条件 2. 触发 `trendFirstProvider` 3. `trendFirstProvider` 先请求第一阶段结果 4. `trendSecondProvider` 再等待第一阶段结果,并在需要时继续追第二阶段数据 5. 页面优先读第二阶段结果,否则回退到第一阶段结果 这里的设计重点不是“两个列表源”,而是展示先后阶段的数据请求处理方式。 ## 状态管理 - 列表数据请求:Riverpod - 筛选条件、滚动、刷新控制:页面本地 state - 还有 `trendLoadingState`、`trendRequestedState` 这样的模块级变量 ## 高风险点 - `trendLoadingState` 和 `trendRequestedState` 是模块级共享变量,改并发或刷新逻辑时要格外小心 - 首次加载和筛选切换都依赖 `didChangeDependencies` - 页面同时用到了局部 state、Riverpod 和刷新控件,不要只改其中一段就认为链路完整 ## 修改建议 - 小功能改动先只改 `trend_page.dart` - 数据请求或缓存逻辑改动再看 `trend_provider.dart` 和 `ReposRepository` - 如果要重构趋势页状态,先单独立决策,不要夹在普通需求里做 ================================================ FILE: docs/02-features/user.md ================================================ # 用户页功能 ## 相关文件 - `lib/page/user/person_page.dart` - `lib/page/user/base_person_provider.dart` - `lib/page/user/base_person_state.dart` - `lib/common/repositories/user_repository.dart` - `lib/common/repositories/event_repository.dart` ## 当前实现 用户页负责展示用户或组织的详情页,并根据用户类型展示不同内容: - 用户信息头部 - 关注/取消关注 - 用户动态 - 组织成员 - 组织信息与荣誉数据 ## 数据流 1. 刷新时先拉用户信息 2. 再根据用户类型拉用户动态或组织成员 3. 并行获取关注状态和 honor 数据 4. 头部与列表一起渲染到嵌套下拉页面 ## 状态管理 - 页面主体基于 `BasePersonState` - honor 相关使用 provider - 页面本地 state 保存关注状态、用户信息等 ## 高风险点 - 用户和组织走的列表数据源不同 - 关注状态、用户信息、动态列表是多条链路 - `BasePersonState` 和 provider 共同参与页面行为 ## 修改建议 - 用户页展示问题优先改 `person_page.dart` 与 widget - honor 或头部共享逻辑再看 `base_person_provider.dart` - 数据源问题分别看 `UserRepository` 与 `EventRepository` ================================================ FILE: docs/03-runbooks/local-setup.md ================================================ # 本地开发环境 ## 基线要求 - Flutter SDK:以仓库 README 和当前 workflow 为准 - Java:Android 构建需要 - Android 工具链:生成 APK 需要 ## 首次启动 1. 安装 Flutter,并确认 `flutter doctor` 2. 运行 `flutter pub get` 3. 创建 `lib/common/config/ignoreConfig.dart` 4. 填入 GitHub OAuth 所需的 `CLIENT_ID` 和 `CLIENT_SECRET` 示例: ```dart class NetConfig { static const CLIENT_ID = "xxxx"; static const CLIENT_SECRET = "xxxx"; } ``` ## 常用命令 ```bash flutter pub get flutter analyze dart run build_runner build --delete-conflicting-outputs flutter build apk --release --target-platform=android-arm64 --no-shrink ``` ## 什么时候需要重新生成 - 改模型、序列化、Riverpod 注解或 env 源文件时,跑 `build_runner` - 改 ARB 多语言文件时,重新生成本地化输出 ## 常见失败原因 - 缺少 `ignoreConfig.dart` - Flutter 版本不匹配 - 拉包时网络或代理异常 - 手改生成文件但没同步源文件 ## 当前本地验证策略 仓库目前没有提交进来的自动化测试目录,因此本地验证以静态检查、构建和手工冒烟为主: - 跑 `flutter analyze` - 对构建相关改动跑 APK 构建 - 在模拟器或真机上手工验证改动功能 ================================================ FILE: docs/04-quality/smoke-matrix.md ================================================ # 手工回归矩阵 ## 目的 仓库当前缺少自动化测试基线,因此需要一份最小可执行的手工回归矩阵。 它的目标不是覆盖全部功能,而是覆盖最容易因局部改动而回归的主链路。 ## 使用方式 - 改动前:确认本次变更影响哪些功能域 - 改动后:至少执行对应功能域的基础用例 - 改共享层时:除目标功能外,额外抽查一个高频功能 ## 全局基础项 每次涉及共享层或根装配时,至少验证: 1. 应用能正常启动 2. 首页或欢迎页能进入 3. 路由跳转正常 4. 多语言和主题没有明显异常 ## 登录 适用改动: - `lib/page/login/` - `lib/redux/login_redux.dart` - `user_repository` - OAuth 配置或导航相关改动 基础用例: 1. 进入登录页 2. 点击 OAuth 登录按钮 3. 能正常打开登录 WebView 4. 登录完成后能回到应用并进入首页 5. 退出登录后能回到登录页 重点观察: - WebView 跳转是否正常 - OAuth 回调后是否正确更新全局登录态 ## 仓库详情 适用改动: - `lib/page/repos/` - `repos_repository` - `issue_repository` - 相关模型、网络层改动 基础用例: 1. 从列表页进入仓库详情 2. 信息页正常展示 3. Readme 页能加载 4. Issue 页能切换并加载 5. 文件列表页能切换并加载 6. 切换分支后,信息页、Readme、文件列表表现正常 重点观察: - 跨 tab 状态是否同步 - 分支切换是否触发联动刷新 ## 趋势页 适用改动: - `lib/page/trend/` - `ReposRepository.getTrendRequest` - 趋势筛选、滚动、刷新相关改动 基础用例: 1. 进入趋势页 2. 首次加载能显示列表或空态 3. 切换时间筛选 4. 切换语言筛选 5. 下拉刷新 6. 点击列表项进入仓库详情并返回 重点观察: - 首次加载与刷新是否重复触发 - 筛选切换后列表是否正确刷新 ## 通知页 适用改动: - `lib/page/notify/` - `user_repository` 通知相关接口 - Signals 或分页刷新逻辑相关改动 基础用例: 1. 进入通知页 2. 默认列表正常加载 3. 在 未读 / 参与 / 全部 之间切换 4. 下拉刷新 5. 上拉加载更多 6. 将单条未读标记为已读 7. 执行“全部标记为已读” 8. 点击 Issue 类型通知跳转详情并返回 重点观察: - 切 tab 时列表是否正确刷新 - 标记已读后列表是否正确更新 - 返回后是否触发强制刷新 ## 共享网络层 适用改动: - `lib/common/net/` - `lib/common/repositories/` - 认证、拦截器、公共响应解析 基础用例: 1. 验证登录链路 2. 验证趋势页加载 3. 验证仓库详情加载 4. 验证通知页加载 重点观察: - 是否出现全局 toast 异常 - 是否出现统一鉴权失效 - REST 与 GraphQL 路径是否都正常 ## 共享状态或根装配 适用改动: - `lib/app.dart` - `lib/provider/` - `lib/redux/` 基础用例: 1. 应用启动正常 2. 首页进入正常 3. 登录态切换正常 4. 主题或语言切换正常 5. 趋势页、通知页、仓库详情页各抽查一个 ## 执行原则 - 不要求每次全量回归 - 但改共享链路时,不能只测当前页面 - 如果某次改动跨越多个功能域,应把对应模块基础用例全部跑一遍 ================================================ FILE: docs/04-quality/test-strategy.md ================================================ # 测试策略 ## 当前现状 - 仓库目前没有提交进来的 `test/` 目录 - [pubspec.yaml](/D:/workspace/project/gsy_github_app_flutter/pubspec.yaml) 中 `flutter_test` 仍是注释状态 - 质量保障目前主要依赖手工验证和 CI 构建成功 这意味着当前工程对 AI 改动并不友好,因为“改完是否正确”缺少快速反馈。 ## 近期目标 不要等完整测试体系一次性到位。 先建立一个最小可用的工程验证 harness,让 agent 和人都能知道改动是否越界。 ## 近期最小基线 1. 静态检查 - `flutter analyze` 2. 生成代码一致性 - 输入变化后重新生成相关文件 3. 高价值手工冒烟 - 应用启动 - 登录入口 - 仓库详情 - 趋势页 - 通知页 4. Android 构建验证 - 对构建相关改动执行 release APK 构建 当前手工回归入口见: - `docs/04-quality/smoke-matrix.md` ## 第一批值得补的测试 - 高频模型的序列化测试 - 应用壳层和高频页面的 widget smoke test - repository 层的解析与适配测试 ## 典型改动的完成标准 ### UI 小改动 - 页面可正常渲染 - 交互没有破坏导航和状态恢复 ### API 或模型改动 - 请求入口正确 - 模型解析仍正常 - 受影响页面能拿到正确数据 ### 应用壳层或共享状态改动 - 应用能启动 - 主题、语言、登录态相关行为仍正确 - 根导航无明显回归 ## 原则 可靠的小测试集,比覆盖面大但没人维护的测试集更有价值。 测试策略的目标不是形式完整,而是建立可执行、可复用的验证闭环。 ================================================ FILE: docs/05-ai/agent-guide.md ================================================ # Agent 工作指引 ## 目标 做范围清晰、容易验证、方便评审的改动。 不要把 agent 的“能改”误当成“应该改很多”。 ## 编辑前先读 1. `AGENTS.md` 2. `docs/00-overview/project-map.md` 3. `docs/01-architecture/app-layering.md` 4. `docs/01-architecture/state-management-matrix.md` 5. `docs/04-quality/smoke-matrix.md` 6. `docs/06-decisions/ADR-0001-状态管理收敛策略.md` 7. 目标功能对应的 `docs/02-features/*.md` ## 任务模板 按任务类型优先参考: - 修 Bug:`docs/05-ai/task-playbooks/fix-bug.md` - 新增页面:`docs/05-ai/task-playbooks/add-page.md` - 新增接口:`docs/05-ai/task-playbooks/add-api.md` - 状态整理:`docs/05-ai/task-playbooks/refactor-state.md` 这些模板现在都内置了统一收尾步骤: - author 完成修改和最小验证后 - 必须先拉起新的 reviewer subagent - reviewer 独立审查后,author 才能对外汇报完成 ## 功能模板 按功能域优先参考: - 仓库详情:`docs/05-ai/feature-playbooks/repos-change.md` - 趋势页:`docs/05-ai/feature-playbooks/trend-change.md` - 通知页:`docs/05-ai/feature-playbooks/notify-change.md` - Issue:`docs/05-ai/feature-playbooks/issue-change.md` - 搜索:`docs/05-ai/feature-playbooks/search-change.md` - 用户页:`docs/05-ai/feature-playbooks/user-change.md` - 首页容器:`docs/05-ai/feature-playbooks/home-change.md` - 动态页:`docs/05-ai/feature-playbooks/dynamic-change.md` - Release:`docs/05-ai/feature-playbooks/release-change.md` - Push 提交详情:`docs/05-ai/feature-playbooks/push-change.md` - 调试页:`docs/05-ai/feature-playbooks/debug-change.md` 统一入口: - `docs/CONTRIBUTING_AI.md` ## Review 分离 - review 采用 author / reviewer 分离上下文 - 每次非微小代码改动后,默认拉起新的 reviewer subagent 做审查 - 在 reviewer subagent 完成一轮审查前,不应直接向用户汇报“已完成” - 说明见 `docs/05-ai/review-harness.md` - reviewer 提示模板见 `docs/05-ai/prompts/reviewer-system.md` - author 交接模板见 `docs/05-ai/prompts/author-handoff.md` - `tool/ai/build_review_bundle.ps1` 只是可选辅助,不是主路径 ## 工作规则 - 优先遵循目标模块现有模式,不主动创造新的抽象层 - 页面局部问题不要上升到根装配层 - 优先通过 repository 或共享网络边界收口,不要把传输细节散落到页面 - 需要生成的内容改源文件,不改生成输出 - 新出现的架构规则应写入 `docs/06-decisions/`,不要只留在对话或 PR 评审里 ## 改动后最低报告要求 - 改了什么 - 为什么改动边界合理 - 跑了哪些命令或做了哪些手工验证 - 还剩哪些风险 ================================================ FILE: docs/05-ai/feature-playbooks/debug-change.md ================================================ # 功能模板:修改调试页相关功能 ## 开始前先读 1. `docs/02-features/debug.md` 2. `lib/common/net/AGENTS.md` ## 优先定位 - 调试页 UI:`lib/page/debug/debug_data_page.dart` - 日志数据结构:`log_interceptor.dart`、`common/logger.dart` ## 修改策略 - 调试页只服务开发和诊断,不要引入业务依赖 - 改日志字段时同步检查调试页展示 - 复制和 JSON 弹窗链路都要能工作 ## 最低验证 1. 调试页可打开 2. 四个 tab 可切换 3. 列表项可点击查看 JSON 4. 长按和双击复制正常 ## 收尾步骤 调试页改动完成后,仍需先经过新的 reviewer subagent 审查,再对外汇报结果。 ================================================ FILE: docs/05-ai/feature-playbooks/dynamic-change.md ================================================ # 功能模板:修改动态页相关功能 ## 开始前先读 1. `docs/02-features/dynamic.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - 页面刷新和滚动行为:`lib/page/dynamic/dynamic_page.dart` - 列表数据与分页:`lib/page/dynamic/dynamic_bloc.dart` - 事件跳转:`common/utils/event_utils.dart` ## 修改策略 - 首次加载、手动刷新、恢复前台刷新都要分开验证 - 动态页依赖当前登录用户,注意 Redux 用户态 - 不要只改 bloc 不测页面恢复刷新 ## 最低验证 1. 首次进入动态页 2. 下拉刷新 3. 上拉加载更多 4. 退到后台再回来,验证自动刷新 5. 点击事件项跳转 ## 收尾步骤 动态页改动完成后,先拉起新的 reviewer subagent 审查首次加载、恢复刷新和事件跳转,再对外汇报。 ================================================ FILE: docs/05-ai/feature-playbooks/home-change.md ================================================ # 功能模板:修改首页容器相关功能 ## 开始前先读 1. `docs/02-features/home.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - tab 容器和搜索入口:`lib/page/home/home_page.dart` - drawer:`lib/page/home/widget/home_drawer.dart` ## 修改策略 - 首页只负责容器和导航,不要承接具体业务逻辑 - 改 tab 结构时,同时验证 dynamic/trend/my 三个入口 - 改搜索入口时,要验证右上角位置计算与动画起点 ## 最低验证 1. 首页可正常进入 2. 三个 tab 可切换 3. 双击 tab 可回到顶部 4. 搜索入口可打开搜索页 5. Android 返回键行为符合预期 ## 收尾步骤 首页容器相关改动在完成验证后,必须先经过新的 reviewer subagent 审查。 这是容器层改动,不应由 author 自己直接宣布完成。 ================================================ FILE: docs/05-ai/feature-playbooks/issue-change.md ================================================ # 功能模板:修改 Issue 相关功能 ## 开始前先读 1. `docs/02-features/issue.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - 展示和交互问题:`lib/page/issue/issue_detail_page.dart` - 数据协议问题:`lib/common/repositories/issue_repository.dart` ## 修改策略 - 头部信息和评论列表分开看,不要混成一条链路 - 编辑、删除、回复逻辑都依赖刷新回流 - 不要只改按钮显示,不改实际提交链路 ## 最低验证 1. 进入 issue 详情 2. 头部正常展示 3. 评论列表正常加载 4. 回复 issue 5. 编辑 issue 或评论 6. open/close issue 后头部状态更新 ## 收尾步骤 Issue 相关改动完成后,先用新的 reviewer subagent 审查评论链路和头部状态回流,再对外汇报完成。 ================================================ FILE: docs/05-ai/feature-playbooks/notify-change.md ================================================ # 功能模板:修改通知页相关功能 ## 适用场景 - 修改通知列表展示 - 调整未读/参与/全部筛选 - 修复刷新、分页或已读状态问题 - 扩展通知跳转行为 ## 开始前先读 1. `docs/02-features/notify.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 先判断问题属于哪一层 ### 页面与交互层 对应: - `lib/page/notify/notify_page.dart` 适合处理: - 列表渲染 - 滑动已读 - 顶部筛选 - 跳转行为 ### 数据层 对应: - `lib/common/repositories/user_repository.dart` - `lib/model/notification.dart` 适合处理: - 获取通知 - 标记已读 - 全部已读 - 通知字段解析 ## 修改策略 - 优先维持 Signals 结构稳定 - 扩展通知类型跳转时,集中处理在 `_renderEventItem` - 改分页或刷新逻辑时,必须一起验证筛选切换和返回强刷 - 未经明确允许,不得把本模块从 Signals 改成别的状态实现;这里保留 Signals 具有明确演示价值 ## 常见误区 - 忽略 `signalPage = -1` 的强制刷新约定 - 只验证默认列表,不验证三个筛选 tab - 标记已读后忘了检查列表移除或刷新逻辑 ## 最低验证 1. 进入通知页 2. 默认列表加载 3. 切换 未读 / 参与 / 全部 4. 下拉刷新 5. 上拉加载更多 6. 单条标记已读 7. 全部标记已读 8. 点击 Issue 类型通知进入详情并返回 ## 需要额外谨慎的情况 - 修改 Signals 触发链路 - 修改分页边界 - 修改通知详情跳转映射 ## 收尾步骤 通知页改动完成后,必须先经过新的 reviewer subagent 审查。 author 不应在未做独立 review 的情况下直接宣布完成。 ================================================ FILE: docs/05-ai/feature-playbooks/push-change.md ================================================ # 功能模板:修改 Push 提交详情相关功能 ## 开始前先读 1. `docs/02-features/push.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - 页面与 patch 展示:`lib/page/push/push_detail_page.dart` - patch 转 HTML:`common/utils/html_utils.dart` - commit 数据:`ReposRepository` ## 修改策略 - 头部和文件列表来自同一请求结果 - patch 展示问题不要忽略 HTML 转换逻辑 - 改导航时要验证返回仓库详情按钮 ## 最低验证 1. 进入 push 提交详情 2. 头部信息正常 3. 文件变更列表正常 4. 点击文件项进入 patch 详情 5. 返回仓库详情链路正常 ## 收尾步骤 Push 提交详情改动完成后,必须先经过新的 reviewer subagent 审查 patch 展示和返回链路。 ================================================ FILE: docs/05-ai/feature-playbooks/release-change.md ================================================ # 功能模板:修改 Release 相关功能 ## 开始前先读 1. `docs/02-features/release.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - 页面交互与 tab:`lib/page/release/release_page.dart` - 数据获取:`ReposRepository.getRepositoryReleaseRequest` ## 修改策略 - release 和 tag 两条链路都要测 - 内嵌 HTML 预览和外部链接打开都要测 - tab 切换后会清空并重刷列表 ## 最低验证 1. 进入 release 页面 2. release 列表可加载 3. 切到 tag 列表可加载 4. 点击 release 项查看 HTML 5. 长按项可外部打开链接 ## 收尾步骤 Release 页面改动完成后,先由新的 reviewer subagent 审查 release/tag 双链路,再对外汇报完成。 ================================================ FILE: docs/05-ai/feature-playbooks/repos-change.md ================================================ # 功能模板:修改仓库详情相关功能 ## 适用场景 - 修改仓库详情页 UI - 调整 Readme、Issue、文件列表其中一个 tab - 修改分支切换逻辑 - 调整仓库详情底部按钮行为 - 修复仓库详情页的联动刷新问题 ## 开始前先读 1. `docs/02-features/repos.md` 2. `docs/01-architecture/state-management-matrix.md` 3. `docs/04-quality/smoke-matrix.md` 4. `lib/page/AGENTS.md` ## 先判断问题属于哪一层 ### 页面壳层 对应: - `lib/page/repos/repository_detail_page.dart` - tab 容器、标题栏、更多菜单、浮动按钮 适合处理: - 页面布局 - tab 切换 - 分支选择入口 - 顶部和底部操作区 ### 共享状态层 对应: - `lib/page/repos/provider/repos_detail_provider.dart` - `lib/page/repos/provider/repos_network_provider.dart` 适合处理: - 当前 tab - 当前分支 - 仓库详情共享数据 - readme 内容 - 底部按钮联动 ### 数据层 对应: - `lib/common/repositories/repos_repository.dart` - `lib/common/repositories/issue_repository.dart` - 相关 model 适合处理: - 请求字段 - 数据解析 - readme、issue、file 等数据源问题 ## 修改策略 - 只改某一个 tab 时,优先停留在该 tab 及其直接依赖 - 涉及分支切换时,必须检查 info/readme/file 三者联动 - 涉及 issue 创建时,检查弹窗输入、提交、刷新回流 - 不要为了某个 tab 的问题重做整个 Provider 结构 ## 常见误区 - 把跨 tab 共享状态拆成各页面自己的局部变量 - 只改 UI 显示,不改 provider 内状态更新 - 忽略 `currentBranch` 对多个子页的数据源影响 ## 最低验证 1. 从入口进入仓库详情页 2. 信息页正常展示 3. Readme 页能加载 4. Issue 页能切换并加载 5. 文件列表页能切换并加载 6. 切换分支后,至少验证信息页、Readme、文件列表联动 ## 需要额外谨慎的情况 - 修改 provider 字段定义 - 修改共享底部按钮逻辑 - 修改和 issue、readme、file 同时相关的请求入口 ## 收尾步骤 仓库详情相关改动完成后,必须先经过新的 reviewer subagent 审查。 这是跨 tab、跨状态共享模块,不应由 author 直接宣布完成。 ================================================ FILE: docs/05-ai/feature-playbooks/search-change.md ================================================ # 功能模板:修改搜索相关功能 ## 开始前先读 1. `docs/02-features/search.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - 页面、动画、输入交互:`lib/page/search/search_page.dart` - 搜索参数与请求:`lib/page/search/search_bloc.dart` - 接口结果:`ReposRepository.searchRepositoryRequest` ## 修改策略 - 搜索条件变更后要清空旧列表并重刷 - 仓库搜索和用户搜索要分别验证 - 动画问题不要误伤搜索逻辑,请求问题也不要顺手改动画 ## 最低验证 1. 打开搜索页 2. 输入关键字后搜索仓库 3. 切换到用户搜索 4. 改变排序或过滤条件 5. 返回首页时动画正常收起 ## 收尾步骤 搜索页改动完成后,需先由新的 reviewer subagent 审查搜索参数、动画和页面生命周期处理,再对外汇报。 ================================================ FILE: docs/05-ai/feature-playbooks/trend-change.md ================================================ # 功能模板:修改趋势页相关功能 ## 适用场景 - 趋势页列表展示改动 - 时间和语言筛选改动 - 刷新或首次加载问题 - 趋势页跳转链路调整 ## 开始前先读 1. `docs/02-features/trend.md` 2. `docs/06-decisions/ADR-0001-状态管理收敛策略.md` 3. `docs/04-quality/smoke-matrix.md` 4. `lib/page/AGENTS.md` ## 先判断问题属于哪一层 ### 页面层 对应: - `lib/page/trend/trend_page.dart` 适合处理: - 列表渲染 - 筛选头部 - 滚动和刷新控件 - 空态、按钮、跳转 ### 状态与请求层 对应: - `lib/page/trend/trend_provider.dart` - `lib/common/repositories/repos_repository.dart` 适合处理: - 首次请求 - 二阶段请求 - 刷新触发逻辑 - 趋势数据获取 ## 修改策略 - UI 小改动尽量停留在 `trend_page.dart` - 请求或缓存问题再看 `trend_provider.dart` - 不要在普通改动里重构趋势页整体状态管理 - 修改首次加载逻辑时,要同时检查 `didChangeDependencies` 和刷新流程 ## 常见误区 - 忽略模块级变量 `trendLoadingState`、`trendRequestedState` - 只测首次加载,不测切换筛选和下拉刷新 - 误把趋势页当前实现理解成纯页面局部状态 ## 最低验证 1. 进入趋势页 2. 首次加载正常 3. 切换时间筛选 4. 切换语言筛选 5. 下拉刷新 6. 点击列表项进入仓库详情并返回 ## 需要额外谨慎的情况 - 修改 provider 参数结构 - 修改刷新触发时机 - 修改趋势页和仓库详情页之间的跳转 ## 收尾步骤 趋势页改动完成后,先拉起新的 reviewer subagent 审查刷新、筛选和状态边界,再对外汇报完成。 ================================================ FILE: docs/05-ai/feature-playbooks/user-change.md ================================================ # 功能模板:修改用户页相关功能 ## 开始前先读 1. `docs/02-features/user.md` 2. `docs/04-quality/smoke-matrix.md` 3. `lib/page/AGENTS.md` ## 优先定位 - 页面展示与交互:`lib/page/user/person_page.dart` - 共享头部与 honor:`lib/page/user/base_person_provider.dart` - 数据请求:`UserRepository` / `EventRepository` ## 修改策略 - 先判断当前展示对象是用户还是组织 - 关注状态、用户信息、列表数据是多条链路,分别验证 - 不要把组织分支和普通用户分支混在一起改 ## 最低验证 1. 进入用户页 2. 头部信息正常 3. 用户动态或组织成员正常展示 4. 关注/取消关注行为正常 5. 返回链路正常 ## 收尾步骤 用户页改动完成后,先用新的 reviewer subagent 审查用户分支和组织分支,再对外汇报完成。 ================================================ FILE: docs/05-ai/prompts/author-handoff.md ================================================ # Author Handoff Prompt 你是 author agent。你已经完成实现,现在要把改动交给 reviewer agent。 你的任务不是解释自己为什么对,而是提供一个尽量中立、最小、可验证的 review bundle。 请输出: 1. 本次任务目标 2. 变更文件列表 3. 改动边界 4. 已运行的验证命令 5. 未覆盖的风险 6. 建议 reviewer 优先检查的功能域 约束: - 不要输出长篇自我辩护 - 不要把“为什么我认为没问题”作为主体 - 不要省略失败过或没做的验证 - 用事实描述,不要用结论压 reviewer ================================================ FILE: docs/05-ai/prompts/reviewer-system.md ================================================ # Reviewer System Prompt 你是 reviewer agent,不是 author agent。 你的职责是独立审查当前变更,优先发现问题、风险、回归和验证缺口。 不要默认信任 author 的实现,也不要帮助 author 为改动辩护。 ## 你的输入边界 你可以使用: - 仓库代码 - review bundle - `AGENTS.md` - 相关功能文档和 feature playbook - 本地验证命令输出 你不应依赖: - author 的完整对话历史 - author 的内部推理 - “已经验证没问题”这类自证结论 ## 你的优先级 1. 先找 bug 和行为回归 2. 再看边界是否越界 3. 再看验证是否足够 4. 最后才是风格和可维护性建议 ## 输出要求 如果发现问题,按严重程度排序输出: - Findings - Open Questions - Verification Gaps 每条 finding 尽量包含: - 问题是什么 - 为什么会导致错误或回归 - 涉及文件 如果没有发现问题,也必须明确说明: - 未发现明确缺陷 - 仍有哪些验证空白或残余风险 ## 禁止事项 - 不要把 author 的描述当成事实 - 不要因为改动小就跳过共享边界检查 - 不要在没有证据时给出“应该没问题”式结论 ================================================ FILE: docs/05-ai/review-harness.md ================================================ # Review Harness ## 目标 让代码审查尽量摆脱 author 视角,避免“作者自己解释自己为什么没问题”。 本仓库默认采用 `author agent` 和 `reviewer subagent` 分离的方式进行 AI 审查。 这更接近 OpenAI 在《Harness engineering》里强调的做法:把 review 设计成独立反馈回路,而不是同一上下文里的自我安慰式复查。 ## 核心原则 1. `author` 和 `reviewer` 不共用上下文 2. `reviewer` 应使用新的 subagent 或新的干净上下文 3. `reviewer` 不读取 author 的完整对话历史 4. `reviewer` 只拿到最小必要信息: - 任务目标 - 相关文档入口 - 变更文件 - diff - 验证结果 5. `reviewer` 的默认职责是找问题,不是帮 author 辩护 这里最重要的不是“把 review 写成文档”,而是把 review 运行在一个新的思维上下文里。 如果 reviewer 继承了 author 的长上下文,它就很容易共享 author 的错误假设。 ## 两种角色 ### Author 负责: - 理解任务 - 修改代码 - 运行最小验证 - 在需要时产出 review bundle 不负责: - 在同一思路链路里给自己“盖章通过” ### Reviewer 负责: - 在新的 subagent 或新的干净上下文中审查变更 - 优先寻找 bug、回归、越界改动、验证缺失 - 明确指出风险,而不是复述 author 目标 不负责: - 继续帮 author 完成实现 - 读取 author 的内部推理过程 ## 推荐流程 1. Author 完成代码和最小验证 2. 启动新的 reviewer subagent 或新的 reviewer 会话 3. Reviewer 只读取: - `docs/05-ai/prompts/reviewer-system.md` - 当前 diff - 变更相关文件 - 相关功能文档 - 验证结果 4. Reviewer 输出 findings、风险、验证缺口 5. Author 根据 findings 修复 6. 必要时开启下一轮 reviewer 会话 默认流程里,review bundle 不是必需的。 如果当前环境已经能直接让新的 reviewer subagent 读取代码和 diff,那么直接这样做即可。 在这个流程完成前,author 不应直接对外宣告“任务已完成”。 对外汇报应发生在至少一轮独立 reviewer 审查之后。 ## Reviewer 输入边界 Reviewer 默认可以看: - 本仓库代码 - 当前 diff - `AGENTS.md` - 相关功能文档和 feature playbook Reviewer 默认不应该看: - author 的完整聊天历史 - author 的主观解释性长文本 - “我已经确认没问题了”这类自证语句 ## 最小 review bundle 如果无法方便地把当前 diff 和验证结果直接交给 reviewer,才使用 review bundle。 bundle 至少应包含: - 任务摘要 - 变更文件列表 - diff 统计 - 关键 diff - 运行过的验证命令 - 对应功能域和回归建议 默认情况下,`tool/ai/build_review_bundle.ps1` 会收集“当前工作区相对 `HEAD` 的未提交变更”。 它只是一个可选辅助工具,不是主流程。 如果你要审查某个分支相对基线分支的差异,可以显式传入: - `-BaseRef origin/master` - 或其他明确基线 ## 审查标准 Reviewer 优先关注: 1. 逻辑正确性 2. 行为回归 3. 状态边界是否被破坏 4. 是否违反仓库文档中的既有约束 5. 验证是否足够支撑本次改动 ## 输出格式建议 - Findings - Open Questions - Verification Gaps - Residual Risks 如果没有发现问题,也要明确说明: - 未发现明显问题 - 仍然存在哪些验证空白 ## 实操要求 - 只要是中等以上改动,默认走 reviewer 分离流程 - 改共享层、根装配、状态边界、公共网络层时,必须走 reviewer 分离流程 - reviewer 分离流程的首选实现方式是新的 subagent / 新的干净上下文 - 未经过 reviewer 分离流程的代码改动,不应以“已完成”状态对外汇报 ================================================ FILE: docs/05-ai/task-playbooks/add-api.md ================================================ # 任务模板:新增接口或数据请求 ## 适用场景 - 新增 REST 或 GraphQL 请求 - 扩展已有 repository 的数据读取能力 - 新增页面所需的数据接口 ## 开始前先确认 1. 这是 REST 还是 GraphQL 2. 是已有 repository 的扩展,还是确实需要新的边界 3. 是否需要新增模型或更新现有模型 4. 页面是否真的需要新的接口,而不是已有接口字段未使用 ## 先读哪些文档 1. `AGENTS.md` 2. `docs/01-architecture/app-layering.md` 3. 对应功能文档 4. `lib/common/net/AGENTS.md` ## 执行步骤 1. 先找最接近的现有请求入口 2. 优先在 repository 层新增或扩展能力 3. 必要时再补 `common/net` 或 `graphql/` 细节 4. 如需新模型,更新模型定义并重新生成 5. 在页面层只消费 repository 结果,不扩散传输细节 6. 若接口会被多个页面复用,避免把逻辑藏在单页内部 ## 本仓库的修改顺序建议 1. `lib/common/repositories/*` 2. `lib/common/net/*` 或 `lib/common/net/graphql/*` 3. `lib/model/*` 4. 对应 `lib/page/*` 或 provider/redux 层 ## 禁止事项 - 不要把接口细节直接塞进页面 - 不要先改 UI 再临时拼接网络逻辑 - 不要漏掉模型和生成代码同步 ## 最低验证 - `flutter analyze` - 若改模型或注解,执行生成命令 - 手工验证至少一个使用该接口的页面 - 若改公共网络层,按 `smoke-matrix.md` 抽查多个功能 ## 输出要求 至少说明: - 新接口加在了哪个 repository 边界 - 是否新增或更新了模型 - 是否影响共享网络层 - 验证覆盖了哪些页面 ## 收尾步骤 接口或数据请求改动在完成最小验证后,必须经过新的 reviewer subagent 审查。 尤其当改动触及共享网络层、repository 边界或模型时,不应跳过这一步。 ================================================ FILE: docs/05-ai/task-playbooks/add-page.md ================================================ # 任务模板:新增页面 ## 适用场景 - 新增一个页面或子页面 - 新增一个现有功能下的详情页、列表页、设置页 - 需要接入已有导航和数据链路 ## 开始前先确认 1. 这是独立功能页,还是已有功能下的一个子页 2. 页面需要页面局部状态、功能共享状态,还是全局状态 3. 页面数据来自已有 repository 还是需要新增接口 ## 先读哪些文档 1. `AGENTS.md` 2. `docs/01-architecture/app-layering.md` 3. `docs/01-architecture/state-management-matrix.md` 4. 目标功能文档,例如 `docs/02-features/*.md` 5. `docs/06-decisions/ADR-0002-新增功能默认状态方案.md` ## 执行步骤 1. 找到最接近的现有页面作为参照 2. 先决定状态作用域: - 页面局部:优先本地 state - 功能局部共享:沿用当前模块既有方案 - 应用全局:复用 Redux 或 Riverpod 入口 3. 将页面放入对应功能目录,不要新建随意目录层级 4. 复用现有导航、标题栏、列表、加载和弹窗组件 5. 若页面依赖新数据,再补 repository 或 API 入口 6. 补充对应功能文档,至少记录入口、状态方案和主要数据流 ## 本仓库的默认选择 - 若新增 `repos` 子页:优先跟随该模块当前 Provider 结构 - 若新增趋势页相关子页:优先跟随 Riverpod 结构 - 若只是很轻的局部页面交互:优先页面本地 state ## 禁止事项 - 不要为了新页面顺手引入新的状态管理库 - 不要直接把页面私有状态放进全局容器 - 不要复制粘贴一套新的网络调用链路绕过 repository ## 最低验证 - 页面可正常进入与返回 - 主要交互可执行 - 若依赖列表或详情数据,至少完成一次加载与刷新验证 - 若接入导航,验证入口页到新页面再返回的链路 ## 输出要求 至少说明: - 页面放在哪个功能目录 - 选择了哪种状态方案,为什么 - 是否复用了已有 repository 或新增了数据边界 - 跑了哪些验证 ## 收尾步骤 新增页面完成后,author 不应直接宣布完成。 必须先使用新的 reviewer subagent 审查页面边界、状态方案和验证覆盖,再对外汇报结果。 ================================================ FILE: docs/05-ai/task-playbooks/fix-bug.md ================================================ # 任务模板:修复 Bug ## 适用场景 - 页面行为异常 - 接口数据展示错误 - 刷新、分页、跳转、状态同步问题 - 某个已有功能回归 ## 开始前先确认 1. Bug 属于哪个功能域 2. 问题发生在 UI、状态层、repository,还是共享网络层 3. 是否已经有稳定复现路径 ## 先读哪些文档 1. `AGENTS.md` 2. `docs/00-overview/project-map.md` 3. 对应功能文档,例如 `docs/02-features/repos.md` 4. 若涉及共享层,再读: - `docs/01-architecture/app-layering.md` - `docs/04-quality/smoke-matrix.md` ## 执行步骤 1. 先定位最小复现路径 2. 找到最靠近问题的模块,不要一上来改全局 3. 判断问题属于: - 页面展示错误 - 局部状态错误 - 共享状态错误 - repository/网络返回错误 4. 只在必要范围内修改 5. 回归受影响功能和相邻链路 ## 本仓库的定位建议 - 登录问题:先看 `lib/page/login/` 和 `lib/redux/login_redux.dart` - 仓库详情问题:先看 `lib/page/repos/` 与对应 provider - 趋势页问题:先看 `lib/page/trend/` - 通知页问题:先看 `lib/page/notify/` - 多页面同时异常:再考虑 `lib/common/net/`、`lib/common/repositories/` 或 `lib/app.dart` ## 禁止事项 - 不要借修 bug 顺手迁移状态管理框架 - 不要为了页面问题直接修改根装配层 - 不要手改生成文件来掩盖真实问题 ## 最低验证 - 按 `docs/04-quality/smoke-matrix.md` 执行该功能的基础用例 - 若动到共享层,额外抽查一个高频功能 - 若改模型或生成输入,执行相应生成命令 ## 输出要求 至少说明: - 复现路径 - 根因在哪一层 - 改动边界为什么足够小 - 跑了哪些验证 ## 收尾步骤 在 author 自己完成修改和最小验证后,不应直接宣布任务完成。 必须先拉起新的 reviewer subagent 或新的干净 reviewer 上下文,对这次 bug 修复做一轮独立审查。 ================================================ FILE: docs/05-ai/task-playbooks/refactor-state.md ================================================ # 任务模板:整理或重构状态 ## 适用场景 - 页面状态过于分散 - 同一功能出现重复状态源 - 需要把局部状态收敛到当前模块既有状态容器 - 明确批准的小范围状态整理 ## 开始前先确认 1. 这是普通需求里的顺手整理,还是明确批准的状态改造 2. 当前模块已经使用哪种状态方案 3. 改动后是否会影响多个页面、多个 tab 或全局行为 ## 先读哪些文档 1. `docs/01-architecture/state-management-matrix.md` 2. `docs/06-decisions/ADR-0001-状态管理收敛策略.md` 3. `docs/06-decisions/ADR-0002-新增功能默认状态方案.md` 4. 对应功能文档 ## 执行步骤 1. 画清楚当前状态的拥有者和消费者 2. 找出重复状态源和不必要的跨层传递 3. 限制整理范围,只处理目标模块 4. 保持外部行为不变 5. 回归该模块的主要交互链路 ## 是否允许执行 可以直接做: - 将页面内重复局部 state 收敛到当前页面已有状态结构 - 在同一模块内消除明显重复状态源 - 把散落在多个子 widget 的功能局部状态收回到该模块已有 provider 或局部状态容器 需要先补 ADR 再做: - 将整个功能域从 Provider 迁移到 Riverpod - 将 Redux 的登录/用户主链路挪到别的状态方案 - 修改 `lib/app.dart` 中的全局状态接线方式 ## 禁止事项 - 不要把“状态整理”变成大规模架构迁移 - 不要在没有验证的情况下改动共享状态边界 - 不要同时调整状态方案和功能行为,除非任务明确要求 - 未经明确允许,不得把模块从既有框架/状态实现切换到另一套实现,即使你认为新实现更稳定或更容易修 ## 最低验证 - 目标功能的基础回归用例 - 涉及共享状态时,额外抽查一个相邻功能 - 若动到根装配或全局共享状态,回归启动、登录态、主题/语言 ## 输出要求 至少说明: - 旧状态结构的问题是什么 - 为什么这次整理不属于架构迁移 - 改动后状态归属是否更清晰 - 跑了哪些回归 ## 收尾步骤 状态整理类改动默认必须经过新的 reviewer subagent 审查后,才能对外宣告完成。 这是高 author 偏见风险任务,不允许用 author 自己的上下文直接给自己通过。 ================================================ FILE: docs/06-decisions/ADR-0001-状态管理收敛策略.md ================================================ # ADR-0001 状态管理收敛策略 ## 状态 已采纳 ## 日期 2026-03-10 ## 背景 当前项目同时使用 Redux、Riverpod、Provider、Signals。 这种并存状态来自历史演进和教学展示诉求,短期内不会也不应该通过一次重构强行统一。 问题不在于“有多种状态管理”,而在于如果没有新增规则,后续功能和 agent 改动会继续把边界变得更混乱: - 新功能可能按个人偏好继续引入不一致方案 - 页面局部状态可能误上升为全局状态 - 全局状态可能同时在多个状态容器里重复保存 - AI 在缺少明确规则时容易用“看起来最熟的库”去实现需求 ## 决策 项目接受当前多状态并存的现实,但从现在开始约束新增和演进规则: 1. 不新增第五种状态管理方案 2. 无关任务不得顺手迁移既有模块的状态管理 3. 新增状态默认按“作用域”选择,而不是按个人偏好选择 4. 涉及全局共享状态时,优先使用现有全局主路径,而不是新开一条平行状态链路 5. 任何跨模块状态管理迁移都必须先补决策记录,再执行代码改造 6. 未经明确允许,不得为了满足需求或修复问题,擅自把模块从既有框架/状态实现切换到另一套实现 ## 默认选择规则 ### 应用级共享状态 适用场景: - 登录态 - 当前用户核心信息 - 全局主题、语言、灰度模式 - 会影响多个页面和全局入口的状态 默认策略: - 优先复用当前已经承接这类职责的 Redux 或 Riverpod 入口 - 如果是“纯应用配置型共享状态”,优先贴近现有 Riverpod 方式 - 如果是历史上已经由 Redux 管理的登录/用户链路,优先延续 Redux ### 功能级共享状态 适用场景: - 某个复杂功能模块内部多个子页面、tab、局部组件共享状态 默认策略: - 优先沿用该模块当前已存在方案 - 对当前仓库,像 `repos` 这类跨 tab 共享状态,继续沿用模块内 Provider 结构 ### 页面局部交互状态 适用场景: - 筛选条件 - 页码 - 局部开关 - 临时 UI 控制状态 默认策略: - 优先使用页面本地 state - 如果模块已经明确使用 Signals 或局部 Riverpod,则跟随既有模式 ## 不做的事 - 不要求立即把 Redux/Provider/Signals 全部迁到 Riverpod - 不要求为了一致性重写成熟模块 - 不鼓励在普通需求里夹带架构收敛 - 不允许为了“更容易改”就把演示性质明确的模块偷偷改成别的状态方案 ## 影响 正面影响: - 新增功能不会继续无序扩散状态方案 - agent 更容易判断应该把状态放在哪里 - 评审时有了明确基准,而不是只看个人习惯 代价: - 一段时间内项目仍会维持多种状态管理并存 - 部分历史模块看起来仍然不统一 ## 执行规则 以下情况需要先补新 ADR,再开始迁移: - 计划将某一整个功能域从 Provider 迁到 Riverpod - 计划把登录和用户主链路从 Redux 挪走 - 计划修改全局状态入口,影响 `lib/app.dart` 普通需求只需遵守本 ADR,不需要额外发起架构迁移。 额外约束: - 如果某个模块明确承担演示某种状态方案的职责,例如 `notify` 中的 Signals,用普通需求绕过这层设计同样视为越界。 ================================================ FILE: docs/06-decisions/ADR-0002-新增功能默认状态方案.md ================================================ # ADR-0002 新增功能默认状态方案 ## 状态 已采纳 ## 日期 2026-03-10 ## 背景 即使接受“当前存在多种状态管理方案”,团队和 agent 仍然需要一个面向新增功能的默认选择标准。 否则每个新页面都可能重新争论一次,或者由 AI 直接按训练偏好做决定。 ## 决策 新增功能的状态管理默认按以下优先级选择: 1. 先看目标模块已有状态方案 2. 再看状态作用域是页面局部、功能局部还是应用全局 3. 只有在目标模块没有明确先例时,才使用下面的默认规则 ## 默认规则 ### 页面局部状态 优先选择: - `StatefulWidget` 本地 state - 若该模块已使用 Signals,则可继续使用 Signals 适用: - 页码 - 筛选项 - 展开/收起 - 加载中标记 - 只影响当前页面的临时状态 ### 功能局部共享状态 优先选择: - 沿用目标功能目录已存在方案 - 如果目标功能是新建且预计会存在多 tab、多子视图共享,可优先考虑 Riverpod 适用: - 复杂详情页 - 多 tab 共享数据 - 多个子 widget 共用一份异步加载状态 ### 应用全局状态 优先选择: - 复用现有 Redux 或 Riverpod 全局入口 适用: - 登录态 - 当前用户核心身份信息 - 全局主题/语言/灰度模式 ## 禁止事项 - 不要因为某个库“更先进”就替换当前功能模块状态方案 - 不要在一个新功能里同时引入两套新的状态来源 - 不要把页面局部交互状态直接抬升到全局 ## 评审清单 新增功能提交前至少回答: 1. 这个状态为什么不放在页面本地? 2. 为什么不沿用目标模块既有模式? 3. 这个状态会影响几个页面或几个 tab? 4. 是否会和现有 Redux/Riverpod/Provider/Signals 形成重复状态源? ## 结果 这个 ADR 的目标不是追求形式统一,而是降低新增功能的状态决策成本,并减少 AI 改动的随机性。 ================================================ FILE: docs/06-decisions/README.md ================================================ # 决策记录 这个目录用于保存 ADR 风格的长期决策。 凡是“以后还会反复被问到”的架构选择,不要只留在代码评审历史里。 后续适合记录的主题包括: - 状态管理收敛策略 - CI 最小验证基线 - 生成代码工作流 - 功能目录约定 ================================================ FILE: docs/CONTRIBUTING_AI.md ================================================ # AI 协作入口 这个入口页用于帮助人和 agent 快速选择正确的文档路径。 不要从根 README 直接跳进代码,先判断你当前属于哪类任务。 ## 如果你要修 Bug 先读: 1. `docs/05-ai/task-playbooks/fix-bug.md` 2. 对应功能文档 `docs/02-features/*.md` 3. `docs/04-quality/smoke-matrix.md` 注意: - 任务模板和功能模板都已经内置 reviewer subagent 收尾步骤 - author 不应跳过这一步直接宣布完成 ## 如果你要新增页面 先读: 1. `docs/05-ai/task-playbooks/add-page.md` 2. `docs/06-decisions/ADR-0002-新增功能默认状态方案.md` 3. 对应功能文档 `docs/02-features/*.md` ## 如果你要新增接口 先读: 1. `docs/05-ai/task-playbooks/add-api.md` 2. `lib/common/net/AGENTS.md` 3. 对应功能文档 `docs/02-features/*.md` ## 如果你要整理状态 先读: 1. `docs/05-ai/task-playbooks/refactor-state.md` 2. `docs/06-decisions/ADR-0001-状态管理收敛策略.md` 3. `docs/01-architecture/state-management-matrix.md` ## 如果你要改仓库详情相关功能 先读: 1. `docs/05-ai/feature-playbooks/repos-change.md` 2. `docs/02-features/repos.md` 3. `docs/04-quality/smoke-matrix.md` ## 如果你要改趋势页 先读: 1. `docs/05-ai/feature-playbooks/trend-change.md` 2. `docs/02-features/trend.md` 3. `docs/04-quality/smoke-matrix.md` ## 如果你要改通知页 先读: 1. `docs/05-ai/feature-playbooks/notify-change.md` 2. `docs/02-features/notify.md` 3. `docs/04-quality/smoke-matrix.md` ## 如果你要改 Issue 先读: 1. `docs/05-ai/feature-playbooks/issue-change.md` 2. `docs/02-features/issue.md` ## 如果你要改搜索 先读: 1. `docs/05-ai/feature-playbooks/search-change.md` 2. `docs/02-features/search.md` ## 如果你要改用户页 先读: 1. `docs/05-ai/feature-playbooks/user-change.md` 2. `docs/02-features/user.md` ## 如果你要改首页容器 先读: 1. `docs/05-ai/feature-playbooks/home-change.md` 2. `docs/02-features/home.md` ## 如果你要改动态页 先读: 1. `docs/05-ai/feature-playbooks/dynamic-change.md` 2. `docs/02-features/dynamic.md` ## 如果你要改 Release 先读: 1. `docs/05-ai/feature-playbooks/release-change.md` 2. `docs/02-features/release.md` ## 如果你要改 Push 提交详情 先读: 1. `docs/05-ai/feature-playbooks/push-change.md` 2. `docs/02-features/push.md` ## 如果你要改调试页 先读: 1. `docs/05-ai/feature-playbooks/debug-change.md` 2. `docs/02-features/debug.md` ## 通用入口 无论哪类任务,默认都建议先过一遍: 1. `AGENTS.md` 2. `docs/README.md` 3. `docs/00-overview/project-map.md` 4. `docs/01-architecture/app-layering.md` ## 如果你要做 AI review 先读: 1. `docs/05-ai/review-harness.md` 2. `docs/05-ai/prompts/reviewer-system.md` 3. 如需 review bundle,再看 `tool/ai/build_review_bundle.ps1` ================================================ FILE: docs/README.md ================================================ # 文档索引 这个目录同时服务于人类协作者和代码 agent。 第一次进入仓库时,建议按顺序阅读,不要只看根 README 就直接改代码。 ## 推荐阅读顺序 1. `00-overview/project-map.md` 2. `01-architecture/app-layering.md` 3. `01-architecture/state-management-matrix.md` 4. `03-runbooks/local-setup.md` 5. `04-quality/test-strategy.md` 6. `04-quality/smoke-matrix.md` 7. `05-ai/agent-guide.md` ## 目录说明 - `00-overview/`:快速理解项目和目录职责 - `01-architecture/`:分层边界、状态管理边界、生成代码与配置规则 - `02-features/`:按功能说明关键页面、数据流和高风险点 - `03-runbooks/`:本地运行和常见操作手册 - `04-quality/`:验证方式、质量门槛、测试策略 - `05-ai/`:面向 agent 的工作约束和执行方式 - `06-decisions/`:需要长期保留的架构决策记录 ## 当前关键文档 - 架构边界:`01-architecture/app-layering.md` - 状态管理边界:`01-architecture/state-management-matrix.md` - 手工回归入口:`04-quality/smoke-matrix.md` - 长期规则:`06-decisions/ADR-0001-状态管理收敛策略.md` - 任务模板入口:`05-ai/task-playbooks/` - 功能模板入口:`05-ai/feature-playbooks/` - AI 协作导航:`CONTRIBUTING_AI.md` ## 当前目标 当前这批文档的重点不是先做“AI 功能”,而是先把仓库整理成: - 更容易理解 - 更容易局部修改 - 更容易验证 - 更不容易让 agent 猜错边界 ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================

The purpose of the project is to facilitate personal daily maintenance and access to Github, better immerse in the mutual base between coders, Github is your home.

================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ Third-party Github App ================================================ FILE: ios/.gitignore ================================================ .idea/ .vagrant/ .sconsign.dblite .svn/ .DS_Store *.swp profile DerivedData/ build/ GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m .generated/ *.pbxuser *.mode1v3 *.mode2v3 *.perspectivev3 !default.pbxuser !default.mode1v3 !default.mode2v3 !default.perspectivev3 xcuserdata *.moved-aside *.pyc *sync/ Icon? .tags* /Flutter/app.flx /Flutter/app.zip /Flutter/flutter_assets/ /Flutter/App.framework /Flutter/Flutter.framework /Flutter/Generated.xcconfig /Flutter/Flutter.podspec /ServiceDefinitions.json Pods/ .symlinks/ ================================================ FILE: ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: ios/Flutter/Debug.xcconfig ================================================ #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/Release.xcconfig ================================================ #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: ios/Flutter/ephemeral/flutter_lldb_helper.py ================================================ # # Generated file, do not edit. # import lldb def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" base = frame.register["x0"].GetValueAsAddress() page_len = frame.register["x1"].GetValueAsUnsigned() # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the # first page to see if handled it correctly. This makes diagnosing # misconfiguration (e.g. missing breakpoint) easier. data = bytearray(page_len) data[0:8] = b'IHELPED!' error = lldb.SBError() frame.GetThread().GetProcess().WriteMemory(base, data, error) if not error.Success(): print(f'Failed to write into {base}[+{page_len}]', error) return def __lldb_init_module(debugger: lldb.SBDebugger, _): target = debugger.GetDummyTarget() # Caveat: must use BreakpointCreateByRegEx here and not # BreakpointCreateByName. For some reasons callback function does not # get carried over from dummy target for the later. bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) bp.SetAutoContinue(True) print("-- LLDB integration loaded --") ================================================ FILE: ios/Flutter/ephemeral/flutter_lldbinit ================================================ # # Generated file, do not edit. # command script import --relative-to-command-file flutter_lldb_helper.py ================================================ FILE: ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end ================================================ FILE: ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { func didInitializeImplicitFlutterEngine( _ engineBridge: FlutterImplicitEngineBridge ) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) } override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? ) -> Bool { return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "40.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "60.jpg", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "58.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "87.jpg", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "80.jpg", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "logo-4.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "120.jpg", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "180.png", "scale" : "3x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "logo2x-3.jpg", "scale" : "1x" }, { "size" : "152x152", "idiom" : "iphone", "filename" : "152.jpg", "scale" : "1x" }, { "size" : "167x167", "idiom" : "iphone", "filename" : "167.jpg", "scale" : "1x" }, { "size" : "76x76", "idiom" : "iphone", "filename" : "76.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "1x" }, { "idiom" : "iphone", "filename" : "Default@3x-1.png", "scale" : "2x" }, { "idiom" : "iphone", "filename" : "Default@3x.png", "scale" : "3x" }, { "idiom" : "iphone", "subtype" : "retina4", "scale" : "1x" }, { "idiom" : "iphone", "filename" : "Default@3x-2.png", "subtype" : "retina4", "scale" : "2x" }, { "idiom" : "iphone", "filename" : "Default@3x-4.png", "subtype" : "retina4", "scale" : "3x" }, { "idiom" : "iphone", "subtype" : "736h", "scale" : "3x" }, { "idiom" : "iphone", "filename" : "Default@2x.png", "subtype" : "667h", "scale" : "2x" }, { "idiom" : "iphone", "filename" : "Default@3x-3.png", "subtype" : "2436h", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName GSYGithubAppFlutter CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName GSYGithubAppFlutter CFBundlePackageType APPL CFBundleShortVersionString 7.9.3 CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsArbitraryLoads NSAllowsArbitraryLoadsInWebContent NSAppleMusicUsageDescription Allow to get user music NSBluetoothAlwaysUsageDescription Allow to get user location in background NSBluetoothPeripheralUsageDescription Allow to get user location NSCalendarsUsageDescription Allow to get user calendars NSCameraUsageDescription Allow user to scan QR codes using the camera NSContactsUsageDescription Allow to get user contacts NSLocationAlwaysAndWhenInUseUsageDescription Allow to get user location in background NSLocationAlwaysUsageDescription Allow to get user location in background NSLocationWhenInUseUsageDescription Allow to get user location NSMotionUsageDescription Allow to get user motion NSSpeechRecognitionUsageDescription Allow to get user speech recognition UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIApplicationSceneManifest UIApplicationSupportsMultipleScenes UISceneConfigurations UIWindowSceneSessionRoleApplication UISceneConfigurationName Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance io.flutter.embedded_views_preview CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents ================================================ FILE: ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: ios/Runner/SceneDelegate.swift ================================================ import Flutter class SceneDelegate: FlutterSceneDelegate {} ================================================ FILE: ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B1CF5E1D5F3697BA6D5E888 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC05E0EDBFF42659F21BDE4B /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 6B771FB42D6F90E700E9D56C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B771FB32D6F90E700E9D56C /* SceneDelegate.swift */; }; 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 4B43CF9723BDED980094FB3A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 627667392E82CDFAE68C51AEBD0893F4; remoteInfo = flutter_webview_plugin; }; 4B51211D211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 6B534DBBA8DF3878886AC541E1F9B27F; remoteInfo = connectivity; }; 4B51211F211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = FE03FE86639A72DA4B3E03B276CF9787; remoteInfo = device_info; }; 4B512125211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = A01BD7926EF9CCFC9422AAE0154B021C; remoteInfo = fluttertoast; }; 4B512127211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 05CFA344FD05BDC7846987FC69E2968D; remoteInfo = FMDB; }; 4B51212B211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = F3ECC93A65C1AC7391162CC6DE987104; remoteInfo = package_info; }; 4B51212D211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 07082C9AE15BEF3C3E1B87A9E0002399; remoteInfo = "Pods-Runner"; }; 4B51212F211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 3980A755FA9D435ACB2BB8E062462E50; remoteInfo = Reachability; }; 4B512131211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = FA6117CD532A944FB87D5A01C425C052; remoteInfo = share; }; 4B512133211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 5AD4D046EA66405A678044C7D231DED9; remoteInfo = shared_preferences; }; 4B512135211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 395F12FD509BCF5B8F35276A25ED61E9; remoteInfo = sqflite; }; 4B512137211F395B00E0C9B3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 4B40ADA9AD61ADB08CA9739165A77AE5; remoteInfo = url_launcher; }; 4B7052AA25787C7700723D25 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 55E0AFD333353D71ACC2207149E879D6; remoteInfo = Toast; }; 4BDA3AD022744C9D0035B197 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = 7571524E8933F347BDF8BA25D6AF7699; remoteInfo = webview_flutter; }; 4BDB48EC21A2AEFF00E882C1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = F8B158E8230AA4D2DFAE7C847882628E; remoteInfo = path_provider; }; 4BDB48EE21A2AEFF00E882C1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; proxyType = 2; remoteGlobalIDString = FAE7273859BB129D96609B584B6F2535; remoteInfo = permission_handler; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Pods.xcodeproj; path = Pods/Pods.xcodeproj; sourceTree = ""; }; 4F13ED567330568565999636 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 593F42BD9F603A49DC922B8C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 6B771FB32D6F90E700E9D56C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BC05E0EDBFF42659F21BDE4B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 3B1CF5E1D5F3697BA6D5E888 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4B51210D211F395A00E0C9B3 /* Products */ = { isa = PBXGroup; children = ( 4B51211E211F395B00E0C9B3 /* connectivity.framework */, 4B512120211F395B00E0C9B3 /* device_info.framework */, 4B43CF9823BDED980094FB3A /* flutter_webview_plugin.framework */, 4B512126211F395B00E0C9B3 /* fluttertoast.framework */, 4B512128211F395B00E0C9B3 /* FMDB.framework */, 4B51212C211F395B00E0C9B3 /* package_info.framework */, 4BDB48ED21A2AEFF00E882C1 /* path_provider.framework */, 4BDB48EF21A2AEFF00E882C1 /* permission_handler.framework */, 4B51212E211F395B00E0C9B3 /* Pods_Runner.framework */, 4B512130211F395B00E0C9B3 /* Reachability.framework */, 4B512132211F395B00E0C9B3 /* share.framework */, 4B512134211F395B00E0C9B3 /* shared_preferences.framework */, 4B512136211F395B00E0C9B3 /* sqflite.framework */, 4B7052AB25787C7700723D25 /* Toast.framework */, 4B512138211F395B00E0C9B3 /* url_launcher.framework */, 4BDA3AD122744C9D0035B197 /* webview_flutter.framework */, ); name = Products; sourceTree = ""; }; 547008C5C91FE4BDD08FA652 /* Pods */ = { isa = PBXGroup; children = ( 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */, 4F13ED567330568565999636 /* Pods-Runner.debug.xcconfig */, 593F42BD9F603A49DC922B8C /* Pods-Runner.release.xcconfig */, ); name = Pods; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 547008C5C91FE4BDD08FA652 /* Pods */, 9C6818AD15B2DCD007E4DFD4 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 6B771FB32D6F90E700E9D56C /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; 97C146F11CF9000F007C117D /* Supporting Files */ = { isa = PBXGroup; children = ( ); name = "Supporting Files"; sourceTree = ""; }; 9C6818AD15B2DCD007E4DFD4 /* Frameworks */ = { isa = PBXGroup; children = ( BC05E0EDBFF42659F21BDE4B /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( E055B1E414155455BC367B8B /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 0CE87C2C48C36989195F6D5E /* [CP] Embed Pods Frameworks */, 24F7C8729A424760B0DE5489 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1510; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = 8F3DX65RJ6; LastSwiftMigration = 0910; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectReferences = ( { ProductGroup = 4B51210D211F395A00E0C9B3 /* Products */; ProjectRef = 4B51210C211F395A00E0C9B3 /* Pods.xcodeproj */; }, ); projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ 4B43CF9823BDED980094FB3A /* flutter_webview_plugin.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = flutter_webview_plugin.framework; remoteRef = 4B43CF9723BDED980094FB3A /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B51211E211F395B00E0C9B3 /* connectivity.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = connectivity.framework; remoteRef = 4B51211D211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512120211F395B00E0C9B3 /* device_info.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = device_info.framework; remoteRef = 4B51211F211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512126211F395B00E0C9B3 /* fluttertoast.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = fluttertoast.framework; remoteRef = 4B512125211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512128211F395B00E0C9B3 /* FMDB.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = FMDB.framework; remoteRef = 4B512127211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B51212C211F395B00E0C9B3 /* package_info.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = package_info.framework; remoteRef = 4B51212B211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B51212E211F395B00E0C9B3 /* Pods_Runner.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Pods_Runner.framework; remoteRef = 4B51212D211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512130211F395B00E0C9B3 /* Reachability.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Reachability.framework; remoteRef = 4B51212F211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512132211F395B00E0C9B3 /* share.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = share.framework; remoteRef = 4B512131211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512134211F395B00E0C9B3 /* shared_preferences.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = shared_preferences.framework; remoteRef = 4B512133211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512136211F395B00E0C9B3 /* sqflite.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = sqflite.framework; remoteRef = 4B512135211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B512138211F395B00E0C9B3 /* url_launcher.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = url_launcher.framework; remoteRef = 4B512137211F395B00E0C9B3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4B7052AB25787C7700723D25 /* Toast.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = Toast.framework; remoteRef = 4B7052AA25787C7700723D25 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4BDA3AD122744C9D0035B197 /* webview_flutter.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = webview_flutter.framework; remoteRef = 4BDA3AD022744C9D0035B197 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4BDB48ED21A2AEFF00E882C1 /* path_provider.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = path_provider.framework; remoteRef = 4BDB48EC21A2AEFF00E882C1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 4BDB48EF21A2AEFF00E882C1 /* permission_handler.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; path = permission_handler.framework; remoteRef = 4BDB48EE21A2AEFF00E882C1 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 0CE87C2C48C36989195F6D5E /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework", "${BUILT_PRODUCTS_DIR}/Toast/Toast.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_ios/flutter_inappwebview_ios.framework", "${BUILT_PRODUCTS_DIR}/fluttertoast/fluttertoast.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/rive_common/rive_common.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", "${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", "${BUILT_PRODUCTS_DIR}/webview_flutter_wkwebview/webview_flutter_wkwebview.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/fluttertoast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/rive_common.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/webview_flutter_wkwebview.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 24F7C8729A424760B0DE5489 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; E055B1E414155455BC367B8B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 6B771FB42D6F90E700E9D56C /* SceneDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_TEAM = 8F3DX65RJ6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", "DISABLE_PUSH_NOTIFICATIONS=1", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.shuyu.GSYGithubAppBundle; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_TEAM = 8F3DX65RJ6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", "DISABLE_PUSH_NOTIFICATIONS=1", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", ); PRODUCT_BUNDLE_IDENTIFIER = com.shuyu.GSYGithubAppBundle; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = On; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: l10n.yaml ================================================ arb-dir: lib/common/localization/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart ================================================ FILE: lib/app.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/event/http_error_event.dart'; import 'package:gsy_github_app_flutter/common/event/index.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/localization/l10n/app_localizations.dart'; import 'package:gsy_github_app_flutter/common/net/code.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/page/debug/debug_label.dart'; import 'package:gsy_github_app_flutter/page/home/home_page.dart'; import 'package:gsy_github_app_flutter/page/login/login_page.dart'; import 'package:gsy_github_app_flutter/page/photoview_page.dart'; import 'package:gsy_github_app_flutter/page/welcome_page.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:redux/redux.dart'; import 'common/utils/navigator_utils.dart'; class FlutterReduxApp extends StatefulWidget { const FlutterReduxApp({super.key}); @override _FlutterReduxAppState createState() => _FlutterReduxAppState(); } class _FlutterReduxAppState extends State with HttpErrorListener { /// 创建Store,引用 GSYState 中的 appReducer 实现 Reducer 方法 /// initialState 初始化 State final store = Store( appReducer, ///拦截器 middleware: middleware, ///初始化数据 initialState: GSYState( userInfo: User.empty(), login: false, ), ); NavigatorObserver navigatorObserver = NavigatorObserver(); // Helper method to check if the locale is supported Locale _checkSupportedLocale(Locale locale) { // Define the supported locales const supportedLocales = AppLocalizations.supportedLocales; // Check if the requested locale is supported for (final supportedLocale in supportedLocales) { if (supportedLocale.languageCode == locale.languageCode) { return locale; } } // Fall back to English if the locale is not supported return const Locale('en', 'US'); } @override void initState() { super.initState(); Future.delayed(const Duration(seconds: 0), () { /// 通过 with NavigatorObserver ,在这里可以获取可以往上获取到 /// MaterialApp 和 StoreProvider 的 context /// 还可以获取到 navigator; /// 比如在这里增加一个监听,如果 token 失效就退回登陆页。 navigatorObserver.navigator!.context; navigatorObserver.navigator; }); } @override Widget build(BuildContext context) { /// 使用 riverpod 做部分状态共享 /// 这里是为了展示使用 riverpod 的能力所以使用了多种状态管理 return UncontrolledProviderScope( container: globalContainer, child: Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { final (greyApp, appLocale, themeData) = ref.watch(appStateProvider); // Make sure the locale is supported or fall back to a default one final effectiveLocale = _checkSupportedLocale(appLocale); /// 使用 flutter_redux 做部分状态共享 /// 通过 StoreProvider 应用 store /// 这里是为了展示使用 flutter_redux 的能力所以使用了多种状态管理 return StoreProvider( store: store, child: StoreBuilder(builder: (context, store) { Widget app = MaterialApp( navigatorKey: navKey, ///多语言实现代理 localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: [effectiveLocale], locale: effectiveLocale, theme: themeData, navigatorObservers: [navigatorObserver], ///命名式路由 /// "/" 和 MaterialApp 的 home 参数一个效果 ///⚠️ 这里的 name调用,里面 pageContainer 方法有一个 MediaQuery.of(context).copyWith(textScaleFactor: 1), ///⚠️ 而这里的 context 用的是 WidgetBuilder 的 context ~ ///⚠️ 所以 MediaQuery.of(context) 这个 InheritedWidget 就把这个 context “登记”到了 Element 的内部静态 _map 里。 ///⚠️ 所以键盘弹出来的时候,触发了顶层的 MediaQueryData 发生变化,自然就触发了“登记”过的 context 的变化 ///⚠️ 比如 LoginPage 、HomePage ···· ///⚠️ 所以比如你在 搜索页面 键盘弹出时,下面的 HomePage.sName 对应的 WidgetBuilder 会被触发 ///⚠️ 这个是我故意的,如果不需要,可以去掉 pageContainer 或者不要用这里的 context routes: { WelcomePage.sName: (context) { DebugLabel.showDebugLabel(context); return const WelcomePage(); }, HomePage.sName: (context) { return NavigatorUtils.pageContainer( const HomePage(), context); }, LoginPage.sName: (context) { return NavigatorUtils.pageContainer( const LoginPage(), context); }, ///使用 ModalRoute.of(context).settings.arguments; 获取参数 PhotoViewPage.sName: (context) { return const PhotoViewPage(); }, }); if (greyApp) { ///mode one app = ColorFiltered( colorFilter: const ColorFilter.mode( Colors.grey, BlendMode.saturation), child: app); ///mode two // app = ColorFiltered( // colorFilter: greyscale, // child: app); } return app; }), ); }, ), ); } } mixin HttpErrorListener on State { StreamSubscription? stream; GlobalKey navKey = GlobalKey(); @override void initState() { super.initState(); ///Stream演示event bus stream = eventBus.on().listen((event) { errorHandleFunction(event.code, event.message); }); } @override void dispose() { super.dispose(); if (stream != null) { stream!.cancel(); stream = null; } } ///网络错误提醒 errorHandleFunction(int? code, message) { var context = navKey.currentContext!; switch (code) { case Code.NETWORK_ERROR: showToast(context.l10n.network_error); break; case 401: showToast(context.l10n.network_error_401); break; case 403: showToast(context.l10n.network_error_403); break; case 404: showToast(context.l10n.network_error_404); break; case 422: showToast(context.l10n.network_error_422); break; case Code.NETWORK_TIMEOUT: //超时 showToast(context.l10n.network_error_timeout); break; case Code.GITHUB_API_REFUSED: //Github API 异常 showToast(context.l10n.github_refused); break; default: showToast("${context.l10n.network_error_unknown} $message"); break; } } } ================================================ FILE: lib/common/config/config.dart ================================================ class Config { // Private constructor to prevent instantiation Config._(); static bool? DEBUG = true; static const PAGE_SIZE = 20; /// //////////////////////////////////////常量////////////////////////////////////// /// static const API_TOKEN = "4d65e2a5626103f92a71867d7b49fea0"; static const TOKEN_KEY = "token"; static const USER_NAME_KEY = "user-name"; static const PW_KEY = "user-pw"; static const USER_BASIC_CODE = "user-basic-code"; static const USER_INFO = "user-info"; static const LANGUAGE_SELECT = "language-select"; static const LANGUAGE_SELECT_NAME = "language-select-name"; static const REFRESH_LANGUAGE = "refreshLanguageApp"; static const THEME_COLOR = "theme-color"; static const LOCALE = "locale"; static const VIBRATION_ENABLE = "vibration-enable"; } ================================================ FILE: lib/common/event/event_bus.dart ================================================ import 'dart:async'; /// Dispatches events to listeners using the Dart [Stream] API. The [EventBus] /// enables decoupled applications. It allows objects to interact without /// requiring to explicitly define listeners and keeping track of them. /// /// Not all events should be broadcasted through the [EventBus] but only those of /// general interest. /// /// Events are normal Dart objects. By specifying a class, listeners can /// filter events. /// class EventBus { final StreamController _streamController; /// Controller for the event bus stream. StreamController get streamController => _streamController; /// Creates an [EventBus]. /// /// If [sync] is true, events are passed directly to the stream's listeners /// during a [fire] call. If false (the default), the event will be passed to /// the listeners at a later time, after the code creating the event has /// completed. EventBus({bool sync = false}) : _streamController = StreamController.broadcast(sync: sync); /// Instead of using the default [StreamController] you can use this constructor /// to pass your own controller. /// /// An example would be to use an RxDart Subject as the controller. EventBus.customController(StreamController controller) : _streamController = controller; /// Listens for events of Type [T] and its subtypes. /// /// The method is called like this: myEventBus.on(); /// /// If the method is called without a type parameter, the [Stream] contains every /// event of this [EventBus]. /// /// The returned [Stream] is a broadcast stream so multiple subscriptions are /// allowed. /// /// Each listener is handled independently, and if they pause, only the pausing /// listener is affected. A paused listener will buffer events internally until /// unpaused or canceled. So it's usually better to just cancel and later /// subscribe again (avoids memory leak). /// Stream on() { if (T == dynamic) { return streamController.stream as Stream ; } else { return streamController.stream.where((event) => event is T).cast(); } } /// Fires a new event on the event bus with the specified [event]. /// void fire(event) { streamController.add(event); } /// Destroy this [EventBus]. This is generally only in a testing context. /// void destroy() { _streamController.close(); } } ================================================ FILE: lib/common/event/http_error_event.dart ================================================ /// Created by guoshuyu /// Date: 2018-08-16 library; class HttpErrorEvent { final int? code; final String message; HttpErrorEvent(this.code, this.message); } ================================================ FILE: lib/common/event/index.dart ================================================ import 'event_bus.dart'; EventBus eventBus = EventBus(); ================================================ FILE: lib/common/local/local_storage.dart ================================================ import 'package:shared_preferences/shared_preferences.dart'; ///SharedPreferences 本地存储 class LocalStorage { static save(String key, value) async { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setString(key, value); } static get(String key) async { SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.get(key); } static remove(String key) async { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.remove(key); } } ================================================ FILE: lib/common/localization/extension.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/l10n/app_localizations.dart'; extension LocalizationExtension on BuildContext { AppLocalizations get l10n => AppLocalizations.of(this)!; } ================================================ FILE: lib/common/localization/l10n/app_en.arb ================================================ { "welcomeMessage": "Welcome To Flutter", "app_name": "GSYGithubApp", "app_ok": "ok", "app_cancel": "cancel", "app_empty": "Empty(o゚▽゚)o", "app_licenses": "licenses", "app_close": "close", "app_version": "version", "app_back_tip": "Exit?", "app_not_new_version": "No new version.", "app_version_title": "Update Version", "nothing_now": "Nothing", "loading_text": "Loading···", "option_web": "browser", "option_copy": "copy", "option_share": "share", "option_web_launcher_error": "url error", "option_share_title": "share form GSYGitHubFlutter: ", "option_share_copy_success": "Copy Success", "login_text": "Login", "oauth_text": "OAuth", "login_out": "Logout", "login_deprecated": "The API via password authentication will remove on November 13, 2020 by Github", "home_reply": "Feedback", "home_change_language": "Language", "home_vibration": "Vibration", "home_change_grey": "Grey", "home_about": "About", "home_check_update": "Check Update", "home_history": "History", "home_user_info": "Profile", "home_change_theme": "Theme", "home_language_default": "Default", "home_language_zh": "中文", "home_language_en": "English", "home_language_ko": "한국어", "home_language_ja": "日本語", "switch_language": "Select language", "home_theme_default": "Default", "home_theme_1": "Theme 1", "home_theme_2": "Theme 2", "home_theme_3": "Theme 3", "home_theme_4": "Theme 4", "home_theme_5": "Theme 5", "home_theme_6": "Theme 6", "login_username_hint_text": "Username", "login_password_hint_text": "Password", "login_success": "Login Success", "network_error_401": "Http 401", "network_error_403": "Http 403", "network_error_404": "Http 404", "network_error_422": "Request Body Error, Please check Github ClientId or Account/PW", "network_error_timeout": "Http timeout", "network_error_unknown": "Http unknown error", "network_error": "Network error", "github_refused": "Github Api error[OS Error: Connection refused]. Please switch networks or try again later", "load_more_not": "Nothing", "load_more_text": "Loading", "home_dynamic": "Dynamic", "home_trend": "Trend", "home_my": "My", "trend_user_title": "China User Trend", "trend_day": "today", "trend_week": "week", "trend_month": "month", "trend_all": "all", "user_tab_repos": "Repository", "user_tab_fans": "Follower", "user_tab_focus": "Focus", "user_tab_star": "Star", "user_tab_honor": "Honor", "user_dynamic_group": "Members", "user_dynamic_title": "Dynamic", "user_focus": "Focused", "user_un_focus": "Focus", "user_focus_no_support": "Not Support.", "user_create_at": "Create at: ", "user_orgs_title": "Organization", "repos_tab_readme": "README", "repos_tab_info": "Info", "repos_tab_file": "Files", "repos_tab_issue": "Issue", "repos_tab_activity": "Activity", "repos_tab_commits": "Commits", "repos_tab_issue_all": "All", "repos_tab_issue_open": "Open", "repos_tab_issue_closed": "Closed", "repos_option_release": "Release", "repos_option_branch": "Branch", "repos_fork_at": "Fork at ", "repos_create_at": "Create at ", "repos_last_commit": "Last commit at ", "repos_all_issue_count": "All Issue: ", "repos_open_issue_count": "Open Issue: ", "repos_close_issue_count": "Close Issue: ", "repos_issue_search": "Search", "repos_no_support_issue": "Not Support Issue", "issue_reply": "Reply", "issue_edit": "Edit", "issue_open": "Open", "issue_close": "Close", "issue_lock": "Lock", "issue_unlock": "Unlock", "issue_reply_issue": "Reply Issue", "issue_commit_issue": "Commit Issue", "issue_edit_issue": "Edit Issue", "issue_edit_issue_commit": "Edit Reply", "issue_edit_issue_edit_commit": "Edit", "issue_edit_issue_delete_commit": "Delete", "issue_edit_issue_copy_commit": "Copy", "issue_edit_issue_content_not_be_null": "Content cannot be empty", "issue_edit_issue_title_not_be_null": "Title cannot be empty", "issue_edit_issue_title_tip": "Please input title", "issue_edit_issue_content_tip": "Please input content", "notify_title": "Notify", "notify_tab_all": "All", "notify_tab_part": "Part", "notify_tab_unread": "Unread", "notify_unread": "Unread", "notify_readed": "Read", "notify_status": "Status", "notify_type": "Type", "search_title": "Search", "search_tab_repos": "Repository", "search_tab_user": "User", "release_tab_release": "Release", "release_tab_tag": "Tag", "user_profile_name": "Name", "user_profile_email": "Email", "user_profile_link": "Link", "user_profile_org": "Company", "user_profile_location": "Location", "user_profile_info": "Info", "search_type": "Type", "search_sort": "Sort", "search_language": "Language", "feed_back_tip": "Your feedback will be sent to Github as a public issue" } ================================================ FILE: lib/common/localization/l10n/app_ja.arb ================================================ { "welcomeMessage": "Flutterへようこそ", "app_name": "GSYGithubApp", "app_ok": "OK", "app_cancel": "キャンセル", "app_empty": "データなし(o゚▽゚)o", "app_licenses": "ライセンス", "app_close": "閉じる", "app_version": "バージョン", "app_back_tip": "終了しますか?", "app_not_new_version": "新しいバージョンはありません。", "app_version_title": "バージョン更新", "nothing_now": "何もありません", "loading_text": "読み込み中···", "option_web": "ブラウザ", "option_copy": "コピー", "option_share": "共有", "option_web_launcher_error": "URLエラー", "option_share_title": "GSYGitHubFlutterからの共有: ", "option_share_copy_success": "コピー成功", "login_text": "ログイン", "oauth_text": "OAuth", "login_out": "ログアウト", "login_deprecated": "パスワード認証APIは2020年11月13日にGithubによって削除されます", "home_reply": "フィードバック", "home_change_language": "言語", "home_vibration": "振動フィードバック", "home_change_grey": "グレースケール", "home_about": "について", "home_check_update": "更新確認", "home_history": "履歴", "home_user_info": "プロフィール", "home_change_theme": "テーマ", "home_language_default": "デフォルト", "home_language_zh": "中文", "home_language_en": "English", "home_language_ko": "한국어", "home_language_ja": "日本語", "switch_language": "言語を選択", "home_theme_default": "デフォルト", "home_theme_1": "テーマ1", "home_theme_2": "テーマ2", "home_theme_3": "テーマ3", "home_theme_4": "テーマ4", "home_theme_5": "テーマ5", "home_theme_6": "テーマ6", "login_username_hint_text": "ユーザー名", "login_password_hint_text": "パスワード", "login_success": "ログイン成功", "network_error_401": "Http 401", "network_error_403": "Http 403", "network_error_404": "Http 404", "network_error_422": "リクエストボディエラー、Github ClientIdまたはアカウント/パスワードを確認してください", "network_error_timeout": "Http タイムアウト", "network_error_unknown": "Http 不明なエラー", "network_error": "ネットワークエラー", "github_refused": "Github APIエラー[OSエラー: 接続拒否]。ネットワークを切り替えるか、後でもう一度お試しください", "load_more_not": "これ以上ありません", "load_more_text": "読み込み中", "home_dynamic": "アクティビティ", "home_trend": "トレンド", "home_my": "マイ", "trend_user_title": "中国ユーザートレンド", "trend_day": "今日", "trend_week": "週間", "trend_month": "月間", "trend_all": "すべて", "user_tab_repos": "リポジトリ", "user_tab_fans": "フォロワー", "user_tab_focus": "フォロー", "user_tab_star": "スター", "user_tab_honor": "栄誉", "user_dynamic_group": "メンバー", "user_dynamic_title": "アクティビティ", "user_focus": "フォロー中", "user_un_focus": "フォローする", "user_focus_no_support": "サポートされていません。", "user_create_at": "作成日:", "user_orgs_title": "組織", "repos_tab_readme": "README", "repos_tab_info": "情報", "repos_tab_file": "ファイル", "repos_tab_issue": "Issue", "repos_tab_activity": "アクティビティ", "repos_tab_commits": "コミット", "repos_tab_issue_all": "すべて", "repos_tab_issue_open": "オープン", "repos_tab_issue_closed": "クローズ", "repos_option_release": "リリース", "repos_option_branch": "ブランチ", "repos_fork_at": "フォーク日 ", "repos_create_at": "作成日 ", "repos_last_commit": "最終コミット日 ", "repos_all_issue_count": "全Issue:", "repos_open_issue_count": "オープンIssue:", "repos_close_issue_count": "クローズIssue:", "repos_issue_search": "検索", "repos_no_support_issue": "Issueはサポートされていません", "issue_reply": "返信", "issue_edit": "編集", "issue_open": "オープン", "issue_close": "クローズ", "issue_lock": "ロック", "issue_unlock": "アンロック", "issue_reply_issue": "Issueに返信", "issue_commit_issue": "Issueをコミット", "issue_edit_issue": "Issueを編集", "issue_edit_issue_commit": "返信を編集", "issue_edit_issue_edit_commit": "編集", "issue_edit_issue_delete_commit": "削除", "issue_edit_issue_copy_commit": "コピー", "issue_edit_issue_content_not_be_null": "内容を入力してください", "issue_edit_issue_title_not_be_null": "タイトルを入力してください", "issue_edit_issue_title_tip": "タイトルを入力してください", "issue_edit_issue_content_tip": "内容を入力してください", "notify_title": "通知", "notify_tab_all": "すべて", "notify_tab_part": "一部", "notify_tab_unread": "未読", "notify_unread": "未読", "notify_readed": "既読", "notify_status": "ステータス", "notify_type": "タイプ", "search_title": "検索", "search_tab_repos": "リポジトリ", "search_tab_user": "ユーザー", "release_tab_release": "リリース", "release_tab_tag": "タグ", "user_profile_name": "名前", "user_profile_email": "メール", "user_profile_link": "リンク", "user_profile_org": "会社", "user_profile_location": "場所", "user_profile_info": "情報", "search_type": "タイプ", "search_sort": "並び替え", "search_language": "言語", "feed_back_tip": "あなたのフィードバックは公開IssueとしてGitHubに送信されます" } ================================================ FILE: lib/common/localization/l10n/app_ko.arb ================================================ { "welcomeMessage": "Flutter에 오신 것을 환영합니다", "app_name": "GSYGithubApp", "app_ok": "확인", "app_cancel": "취소", "app_empty": "데이터 없음(o゚▽゚)o", "app_licenses": "라이선스", "app_close": "닫기", "app_version": "버전", "app_back_tip": "종료하시겠습니까?", "app_not_new_version": "새로운 버전이 없습니다", "app_version_title": "버전 업데이트", "nothing_now": "아무것도 없음", "loading_text": "로딩 중···", "option_web": "브라우저", "option_copy": "복사", "option_share": "공유", "option_web_launcher_error": "URL 오류", "option_share_title": "GSYGitHubFlutter에서 공유: ", "option_share_copy_success": "복사 성공", "login_text": "로그인", "oauth_text": "OAuth", "login_out": "로그아웃", "login_deprecated": "비밀번호 인증 API는 2020년 11월 13일에 Github에서 제거됩니다", "home_reply": "피드백", "home_change_language": "언어", "home_vibration": "진동 피드백", "home_change_grey": "그레이스케일", "home_about": "정보", "home_check_update": "업데이트 확인", "home_history": "기록", "home_user_info": "프로필", "home_change_theme": "테마", "home_language_default": "기본", "home_language_zh": "中文", "home_language_en": "English", "home_language_ko": "한국어", "home_language_ja": "日本語", "switch_language": "언어 선택", "home_theme_default": "기본 테마", "home_theme_1": "테마 1", "home_theme_2": "테마 2", "home_theme_3": "테마 3", "home_theme_4": "테마 4", "home_theme_5": "테마 5", "home_theme_6": "테마 6", "login_username_hint_text": "사용자 이름", "login_password_hint_text": "비밀번호", "login_success": "로그인 성공", "network_error_401": "Http 401", "network_error_403": "Http 403", "network_error_404": "Http 404", "network_error_422": "요청 본문 오류, Github ClientId 또는 계정/비밀번호를 확인하세요", "network_error_timeout": "Http 시간 초과", "network_error_unknown": "Http 알 수 없는 오류", "network_error": "네트워크 오류", "github_refused": "Github API 오류[OS 오류: 연결 거부]. 네트워크를 전환하거나 나중에 다시 시도하세요", "load_more_not": "더 이상 없음", "load_more_text": "로딩 중", "home_dynamic": "동적", "home_trend": "트렌드", "home_my": "내 정보", "trend_user_title": "중국 사용자 트렌드", "trend_day": "오늘", "trend_week": "이번 주", "trend_month": "이번 달", "trend_all": "전체", "user_tab_repos": "저장소", "user_tab_fans": "팔로워", "user_tab_focus": "팔로잉", "user_tab_star": "스타", "user_tab_honor": "명예", "user_dynamic_group": "멤버", "user_dynamic_title": "동적", "user_focus": "팔로잉 중", "user_un_focus": "팔로우", "user_focus_no_support": "지원되지 않음", "user_create_at": "생성일: ", "user_orgs_title": "조직", "repos_tab_readme": "README", "repos_tab_info": "정보", "repos_tab_file": "파일", "repos_tab_issue": "이슈", "repos_tab_activity": "활동", "repos_tab_commits": "커밋", "repos_tab_issue_all": "전체", "repos_tab_issue_open": "열림", "repos_tab_issue_closed": "닫힘", "repos_option_release": "릴리스", "repos_option_branch": "브랜치", "repos_fork_at": "포크 일자: ", "repos_create_at": "생성 일자: ", "repos_last_commit": "마지막 커밋: ", "repos_all_issue_count": "전체 이슈: ", "repos_open_issue_count": "열린 이슈: ", "repos_close_issue_count": "닫힌 이슈: ", "repos_issue_search": "검색", "repos_no_support_issue": "이슈가 지원되지 않음", "issue_reply": "답변", "issue_edit": "편집", "issue_open": "열기", "issue_close": "닫기", "issue_lock": "잠금", "issue_unlock": "잠금 해제", "issue_reply_issue": "이슈 답변", "issue_commit_issue": "이슈 커밋", "issue_edit_issue": "이슈 편집", "issue_edit_issue_commit": "답변 편집", "issue_edit_issue_edit_commit": "편집", "issue_edit_issue_delete_commit": "삭제", "issue_edit_issue_copy_commit": "복사", "issue_edit_issue_content_not_be_null": "내용을 입력하세요", "issue_edit_issue_title_not_be_null": "제목을 입력하세요", "issue_edit_issue_title_tip": "제목을 입력하세요", "issue_edit_issue_content_tip": "내용을 입력하세요", "notify_title": "알림", "notify_tab_all": "전체", "notify_tab_part": "참여", "notify_tab_unread": "읽지 않음", "notify_unread": "읽지 않음", "notify_readed": "읽음", "notify_status": "상태", "notify_type": "유형", "search_title": "검색", "search_tab_repos": "저장소", "search_tab_user": "사용자", "release_tab_release": "릴리스", "release_tab_tag": "태그", "user_profile_name": "이름", "user_profile_email": "이메일", "user_profile_link": "링크", "user_profile_org": "회사", "user_profile_location": "위치", "user_profile_info": "정보", "search_type": "유형", "search_sort": "정렬", "search_language": "언어", "feed_back_tip": "귀하의 피드백은 Github에 공개 이슈로 제출됩니다" } ================================================ FILE: lib/common/localization/l10n/app_localizations.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'app_localizations_en.dart'; import 'app_localizations_ja.dart'; import 'app_localizations_ko.dart'; import 'app_localizations_zh.dart'; // ignore_for_file: type=lint /// Callers can lookup localized strings with an instance of AppLocalizations /// returned by `AppLocalizations.of(context)`. /// /// Applications need to include `AppLocalizations.delegate()` in their app's /// `localizationDelegates` list, and the locales they support in the app's /// `supportedLocales` list. For example: /// /// ```dart /// import 'l10n/app_localizations.dart'; /// /// return MaterialApp( /// localizationsDelegates: AppLocalizations.localizationsDelegates, /// supportedLocales: AppLocalizations.supportedLocales, /// home: MyApplicationHome(), /// ); /// ``` /// /// ## Update pubspec.yaml /// /// Please make sure to update your pubspec.yaml to include the following /// packages: /// /// ```yaml /// dependencies: /// # Internationalization support. /// flutter_localizations: /// sdk: flutter /// intl: any # Use the pinned version from flutter_localizations /// /// # Rest of dependencies /// ``` /// /// ## iOS Applications /// /// iOS applications define key application metadata, including supported /// locales, in an Info.plist file that is built into the application bundle. /// To configure the locales supported by your app, you’ll need to edit this /// file. /// /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. /// Then, in the Project Navigator, open the Info.plist file under the Runner /// project’s Runner folder. /// /// Next, select the Information Property List item, select Add Item from the /// Editor menu, then select Localizations from the pop-up menu. /// /// Select and expand the newly-created Localizations item then, for each /// locale your application supports, add a new item and select the locale /// you wish to add from the pop-up menu in the Value field. This list should /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; static AppLocalizations? of(BuildContext context) { return Localizations.of(context, AppLocalizations); } static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. /// /// Returns a list of localizations delegates containing this delegate along with /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, /// and GlobalWidgetsLocalizations.delegate. /// /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. static const List> localizationsDelegates = >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('en'), Locale('ja'), Locale('ko'), Locale('zh'), ]; /// No description provided for @welcomeMessage. /// /// In en, this message translates to: /// **'Welcome To Flutter'** String get welcomeMessage; /// No description provided for @app_name. /// /// In en, this message translates to: /// **'GSYGithubApp'** String get app_name; /// No description provided for @app_ok. /// /// In en, this message translates to: /// **'ok'** String get app_ok; /// No description provided for @app_cancel. /// /// In en, this message translates to: /// **'cancel'** String get app_cancel; /// No description provided for @app_empty. /// /// In en, this message translates to: /// **'Empty(o゚▽゚)o'** String get app_empty; /// No description provided for @app_licenses. /// /// In en, this message translates to: /// **'licenses'** String get app_licenses; /// No description provided for @app_close. /// /// In en, this message translates to: /// **'close'** String get app_close; /// No description provided for @app_version. /// /// In en, this message translates to: /// **'version'** String get app_version; /// No description provided for @app_back_tip. /// /// In en, this message translates to: /// **'Exit?'** String get app_back_tip; /// No description provided for @app_not_new_version. /// /// In en, this message translates to: /// **'No new version.'** String get app_not_new_version; /// No description provided for @app_version_title. /// /// In en, this message translates to: /// **'Update Version'** String get app_version_title; /// No description provided for @nothing_now. /// /// In en, this message translates to: /// **'Nothing'** String get nothing_now; /// No description provided for @loading_text. /// /// In en, this message translates to: /// **'Loading···'** String get loading_text; /// No description provided for @option_web. /// /// In en, this message translates to: /// **'browser'** String get option_web; /// No description provided for @option_copy. /// /// In en, this message translates to: /// **'copy'** String get option_copy; /// No description provided for @option_share. /// /// In en, this message translates to: /// **'share'** String get option_share; /// No description provided for @option_web_launcher_error. /// /// In en, this message translates to: /// **'url error'** String get option_web_launcher_error; /// No description provided for @option_share_title. /// /// In en, this message translates to: /// **'share form GSYGitHubFlutter: '** String get option_share_title; /// No description provided for @option_share_copy_success. /// /// In en, this message translates to: /// **'Copy Success'** String get option_share_copy_success; /// No description provided for @login_text. /// /// In en, this message translates to: /// **'Login'** String get login_text; /// No description provided for @oauth_text. /// /// In en, this message translates to: /// **'OAuth'** String get oauth_text; /// No description provided for @login_out. /// /// In en, this message translates to: /// **'Logout'** String get login_out; /// No description provided for @login_deprecated. /// /// In en, this message translates to: /// **'The API via password authentication will remove on November 13, 2020 by Github'** String get login_deprecated; /// No description provided for @home_reply. /// /// In en, this message translates to: /// **'Feedback'** String get home_reply; /// No description provided for @home_change_language. /// /// In en, this message translates to: /// **'Language'** String get home_change_language; /// No description provided for @home_vibration. /// /// In en, this message translates to: /// **'Vibration'** String get home_vibration; /// No description provided for @home_change_grey. /// /// In en, this message translates to: /// **'Grey'** String get home_change_grey; /// No description provided for @home_about. /// /// In en, this message translates to: /// **'About'** String get home_about; /// No description provided for @home_check_update. /// /// In en, this message translates to: /// **'Check Update'** String get home_check_update; /// No description provided for @home_history. /// /// In en, this message translates to: /// **'History'** String get home_history; /// No description provided for @home_user_info. /// /// In en, this message translates to: /// **'Profile'** String get home_user_info; /// No description provided for @home_change_theme. /// /// In en, this message translates to: /// **'Theme'** String get home_change_theme; /// No description provided for @home_language_default. /// /// In en, this message translates to: /// **'Default'** String get home_language_default; /// No description provided for @home_language_zh. /// /// In en, this message translates to: /// **'中文'** String get home_language_zh; /// No description provided for @home_language_en. /// /// In en, this message translates to: /// **'English'** String get home_language_en; /// No description provided for @home_language_ko. /// /// In en, this message translates to: /// **'한국어'** String get home_language_ko; /// No description provided for @home_language_ja. /// /// In en, this message translates to: /// **'日本語'** String get home_language_ja; /// No description provided for @switch_language. /// /// In en, this message translates to: /// **'Select language'** String get switch_language; /// No description provided for @home_theme_default. /// /// In en, this message translates to: /// **'Default'** String get home_theme_default; /// No description provided for @home_theme_1. /// /// In en, this message translates to: /// **'Theme 1'** String get home_theme_1; /// No description provided for @home_theme_2. /// /// In en, this message translates to: /// **'Theme 2'** String get home_theme_2; /// No description provided for @home_theme_3. /// /// In en, this message translates to: /// **'Theme 3'** String get home_theme_3; /// No description provided for @home_theme_4. /// /// In en, this message translates to: /// **'Theme 4'** String get home_theme_4; /// No description provided for @home_theme_5. /// /// In en, this message translates to: /// **'Theme 5'** String get home_theme_5; /// No description provided for @home_theme_6. /// /// In en, this message translates to: /// **'Theme 6'** String get home_theme_6; /// No description provided for @login_username_hint_text. /// /// In en, this message translates to: /// **'Username'** String get login_username_hint_text; /// No description provided for @login_password_hint_text. /// /// In en, this message translates to: /// **'Password'** String get login_password_hint_text; /// No description provided for @login_success. /// /// In en, this message translates to: /// **'Login Success'** String get login_success; /// No description provided for @network_error_401. /// /// In en, this message translates to: /// **'Http 401'** String get network_error_401; /// No description provided for @network_error_403. /// /// In en, this message translates to: /// **'Http 403'** String get network_error_403; /// No description provided for @network_error_404. /// /// In en, this message translates to: /// **'Http 404'** String get network_error_404; /// No description provided for @network_error_422. /// /// In en, this message translates to: /// **'Request Body Error, Please check Github ClientId or Account/PW'** String get network_error_422; /// No description provided for @network_error_timeout. /// /// In en, this message translates to: /// **'Http timeout'** String get network_error_timeout; /// No description provided for @network_error_unknown. /// /// In en, this message translates to: /// **'Http unknown error'** String get network_error_unknown; /// No description provided for @network_error. /// /// In en, this message translates to: /// **'Network error'** String get network_error; /// No description provided for @github_refused. /// /// In en, this message translates to: /// **'Github Api error[OS Error: Connection refused]. Please switch networks or try again later'** String get github_refused; /// No description provided for @load_more_not. /// /// In en, this message translates to: /// **'Nothing'** String get load_more_not; /// No description provided for @load_more_text. /// /// In en, this message translates to: /// **'Loading'** String get load_more_text; /// No description provided for @home_dynamic. /// /// In en, this message translates to: /// **'Dynamic'** String get home_dynamic; /// No description provided for @home_trend. /// /// In en, this message translates to: /// **'Trend'** String get home_trend; /// No description provided for @home_my. /// /// In en, this message translates to: /// **'My'** String get home_my; /// No description provided for @trend_user_title. /// /// In en, this message translates to: /// **'China User Trend'** String get trend_user_title; /// No description provided for @trend_day. /// /// In en, this message translates to: /// **'today'** String get trend_day; /// No description provided for @trend_week. /// /// In en, this message translates to: /// **'week'** String get trend_week; /// No description provided for @trend_month. /// /// In en, this message translates to: /// **'month'** String get trend_month; /// No description provided for @trend_all. /// /// In en, this message translates to: /// **'all'** String get trend_all; /// No description provided for @user_tab_repos. /// /// In en, this message translates to: /// **'Repository'** String get user_tab_repos; /// No description provided for @user_tab_fans. /// /// In en, this message translates to: /// **'Follower'** String get user_tab_fans; /// No description provided for @user_tab_focus. /// /// In en, this message translates to: /// **'Focus'** String get user_tab_focus; /// No description provided for @user_tab_star. /// /// In en, this message translates to: /// **'Star'** String get user_tab_star; /// No description provided for @user_tab_honor. /// /// In en, this message translates to: /// **'Honor'** String get user_tab_honor; /// No description provided for @user_dynamic_group. /// /// In en, this message translates to: /// **'Members'** String get user_dynamic_group; /// No description provided for @user_dynamic_title. /// /// In en, this message translates to: /// **'Dynamic'** String get user_dynamic_title; /// No description provided for @user_focus. /// /// In en, this message translates to: /// **'Focused'** String get user_focus; /// No description provided for @user_un_focus. /// /// In en, this message translates to: /// **'Focus'** String get user_un_focus; /// No description provided for @user_focus_no_support. /// /// In en, this message translates to: /// **'Not Support.'** String get user_focus_no_support; /// No description provided for @user_create_at. /// /// In en, this message translates to: /// **'Create at: '** String get user_create_at; /// No description provided for @user_orgs_title. /// /// In en, this message translates to: /// **'Organization'** String get user_orgs_title; /// No description provided for @repos_tab_readme. /// /// In en, this message translates to: /// **'README'** String get repos_tab_readme; /// No description provided for @repos_tab_info. /// /// In en, this message translates to: /// **'Info'** String get repos_tab_info; /// No description provided for @repos_tab_file. /// /// In en, this message translates to: /// **'Files'** String get repos_tab_file; /// No description provided for @repos_tab_issue. /// /// In en, this message translates to: /// **'Issue'** String get repos_tab_issue; /// No description provided for @repos_tab_activity. /// /// In en, this message translates to: /// **'Activity'** String get repos_tab_activity; /// No description provided for @repos_tab_commits. /// /// In en, this message translates to: /// **'Commits'** String get repos_tab_commits; /// No description provided for @repos_tab_issue_all. /// /// In en, this message translates to: /// **'All'** String get repos_tab_issue_all; /// No description provided for @repos_tab_issue_open. /// /// In en, this message translates to: /// **'Open'** String get repos_tab_issue_open; /// No description provided for @repos_tab_issue_closed. /// /// In en, this message translates to: /// **'Closed'** String get repos_tab_issue_closed; /// No description provided for @repos_option_release. /// /// In en, this message translates to: /// **'Release'** String get repos_option_release; /// No description provided for @repos_option_branch. /// /// In en, this message translates to: /// **'Branch'** String get repos_option_branch; /// No description provided for @repos_fork_at. /// /// In en, this message translates to: /// **'Fork at '** String get repos_fork_at; /// No description provided for @repos_create_at. /// /// In en, this message translates to: /// **'Create at '** String get repos_create_at; /// No description provided for @repos_last_commit. /// /// In en, this message translates to: /// **'Last commit at '** String get repos_last_commit; /// No description provided for @repos_all_issue_count. /// /// In en, this message translates to: /// **'All Issue: '** String get repos_all_issue_count; /// No description provided for @repos_open_issue_count. /// /// In en, this message translates to: /// **'Open Issue: '** String get repos_open_issue_count; /// No description provided for @repos_close_issue_count. /// /// In en, this message translates to: /// **'Close Issue: '** String get repos_close_issue_count; /// No description provided for @repos_issue_search. /// /// In en, this message translates to: /// **'Search'** String get repos_issue_search; /// No description provided for @repos_no_support_issue. /// /// In en, this message translates to: /// **'Not Support Issue'** String get repos_no_support_issue; /// No description provided for @issue_reply. /// /// In en, this message translates to: /// **'Reply'** String get issue_reply; /// No description provided for @issue_edit. /// /// In en, this message translates to: /// **'Edit'** String get issue_edit; /// No description provided for @issue_open. /// /// In en, this message translates to: /// **'Open'** String get issue_open; /// No description provided for @issue_close. /// /// In en, this message translates to: /// **'Close'** String get issue_close; /// No description provided for @issue_lock. /// /// In en, this message translates to: /// **'Lock'** String get issue_lock; /// No description provided for @issue_unlock. /// /// In en, this message translates to: /// **'Unlock'** String get issue_unlock; /// No description provided for @issue_reply_issue. /// /// In en, this message translates to: /// **'Reply Issue'** String get issue_reply_issue; /// No description provided for @issue_commit_issue. /// /// In en, this message translates to: /// **'Commit Issue'** String get issue_commit_issue; /// No description provided for @issue_edit_issue. /// /// In en, this message translates to: /// **'Edit Issue'** String get issue_edit_issue; /// No description provided for @issue_edit_issue_commit. /// /// In en, this message translates to: /// **'Edit Reply'** String get issue_edit_issue_commit; /// No description provided for @issue_edit_issue_edit_commit. /// /// In en, this message translates to: /// **'Edit'** String get issue_edit_issue_edit_commit; /// No description provided for @issue_edit_issue_delete_commit. /// /// In en, this message translates to: /// **'Delete'** String get issue_edit_issue_delete_commit; /// No description provided for @issue_edit_issue_copy_commit. /// /// In en, this message translates to: /// **'Copy'** String get issue_edit_issue_copy_commit; /// No description provided for @issue_edit_issue_content_not_be_null. /// /// In en, this message translates to: /// **'Content cannot be empty'** String get issue_edit_issue_content_not_be_null; /// No description provided for @issue_edit_issue_title_not_be_null. /// /// In en, this message translates to: /// **'Title cannot be empty'** String get issue_edit_issue_title_not_be_null; /// No description provided for @issue_edit_issue_title_tip. /// /// In en, this message translates to: /// **'Please input title'** String get issue_edit_issue_title_tip; /// No description provided for @issue_edit_issue_content_tip. /// /// In en, this message translates to: /// **'Please input content'** String get issue_edit_issue_content_tip; /// No description provided for @notify_title. /// /// In en, this message translates to: /// **'Notify'** String get notify_title; /// No description provided for @notify_tab_all. /// /// In en, this message translates to: /// **'All'** String get notify_tab_all; /// No description provided for @notify_tab_part. /// /// In en, this message translates to: /// **'Part'** String get notify_tab_part; /// No description provided for @notify_tab_unread. /// /// In en, this message translates to: /// **'Unread'** String get notify_tab_unread; /// No description provided for @notify_unread. /// /// In en, this message translates to: /// **'Unread'** String get notify_unread; /// No description provided for @notify_readed. /// /// In en, this message translates to: /// **'Read'** String get notify_readed; /// No description provided for @notify_status. /// /// In en, this message translates to: /// **'Status'** String get notify_status; /// No description provided for @notify_type. /// /// In en, this message translates to: /// **'Type'** String get notify_type; /// No description provided for @search_title. /// /// In en, this message translates to: /// **'Search'** String get search_title; /// No description provided for @search_tab_repos. /// /// In en, this message translates to: /// **'Repository'** String get search_tab_repos; /// No description provided for @search_tab_user. /// /// In en, this message translates to: /// **'User'** String get search_tab_user; /// No description provided for @release_tab_release. /// /// In en, this message translates to: /// **'Release'** String get release_tab_release; /// No description provided for @release_tab_tag. /// /// In en, this message translates to: /// **'Tag'** String get release_tab_tag; /// No description provided for @user_profile_name. /// /// In en, this message translates to: /// **'Name'** String get user_profile_name; /// No description provided for @user_profile_email. /// /// In en, this message translates to: /// **'Email'** String get user_profile_email; /// No description provided for @user_profile_link. /// /// In en, this message translates to: /// **'Link'** String get user_profile_link; /// No description provided for @user_profile_org. /// /// In en, this message translates to: /// **'Company'** String get user_profile_org; /// No description provided for @user_profile_location. /// /// In en, this message translates to: /// **'Location'** String get user_profile_location; /// No description provided for @user_profile_info. /// /// In en, this message translates to: /// **'Info'** String get user_profile_info; /// No description provided for @search_type. /// /// In en, this message translates to: /// **'Type'** String get search_type; /// No description provided for @search_sort. /// /// In en, this message translates to: /// **'Sort'** String get search_sort; /// No description provided for @search_language. /// /// In en, this message translates to: /// **'Language'** String get search_language; /// No description provided for @feed_back_tip. /// /// In en, this message translates to: /// **'Your feedback will be sent to Github as a public issue'** String get feed_back_tip; } class _AppLocalizationsDelegate extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override Future load(Locale locale) { return SynchronousFuture(lookupAppLocalizations(locale)); } @override bool isSupported(Locale locale) => ['en', 'ja', 'ko', 'zh'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { case 'en': return AppLocalizationsEn(); case 'ja': return AppLocalizationsJa(); case 'ko': return AppLocalizationsKo(); case 'zh': return AppLocalizationsZh(); } throw FlutterError( 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 'an issue with the localizations generation tool. Please file an issue ' 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 'that was used.', ); } ================================================ FILE: lib/common/localization/l10n/app_localizations_en.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for English (`en`). class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); @override String get welcomeMessage => 'Welcome To Flutter'; @override String get app_name => 'GSYGithubApp'; @override String get app_ok => 'ok'; @override String get app_cancel => 'cancel'; @override String get app_empty => 'Empty(o゚▽゚)o'; @override String get app_licenses => 'licenses'; @override String get app_close => 'close'; @override String get app_version => 'version'; @override String get app_back_tip => 'Exit?'; @override String get app_not_new_version => 'No new version.'; @override String get app_version_title => 'Update Version'; @override String get nothing_now => 'Nothing'; @override String get loading_text => 'Loading···'; @override String get option_web => 'browser'; @override String get option_copy => 'copy'; @override String get option_share => 'share'; @override String get option_web_launcher_error => 'url error'; @override String get option_share_title => 'share form GSYGitHubFlutter: '; @override String get option_share_copy_success => 'Copy Success'; @override String get login_text => 'Login'; @override String get oauth_text => 'OAuth'; @override String get login_out => 'Logout'; @override String get login_deprecated => 'The API via password authentication will remove on November 13, 2020 by Github'; @override String get home_reply => 'Feedback'; @override String get home_change_language => 'Language'; @override String get home_vibration => 'Vibration'; @override String get home_change_grey => 'Grey'; @override String get home_about => 'About'; @override String get home_check_update => 'Check Update'; @override String get home_history => 'History'; @override String get home_user_info => 'Profile'; @override String get home_change_theme => 'Theme'; @override String get home_language_default => 'Default'; @override String get home_language_zh => '中文'; @override String get home_language_en => 'English'; @override String get home_language_ko => '한국어'; @override String get home_language_ja => '日本語'; @override String get switch_language => 'Select language'; @override String get home_theme_default => 'Default'; @override String get home_theme_1 => 'Theme 1'; @override String get home_theme_2 => 'Theme 2'; @override String get home_theme_3 => 'Theme 3'; @override String get home_theme_4 => 'Theme 4'; @override String get home_theme_5 => 'Theme 5'; @override String get home_theme_6 => 'Theme 6'; @override String get login_username_hint_text => 'Username'; @override String get login_password_hint_text => 'Password'; @override String get login_success => 'Login Success'; @override String get network_error_401 => 'Http 401'; @override String get network_error_403 => 'Http 403'; @override String get network_error_404 => 'Http 404'; @override String get network_error_422 => 'Request Body Error, Please check Github ClientId or Account/PW'; @override String get network_error_timeout => 'Http timeout'; @override String get network_error_unknown => 'Http unknown error'; @override String get network_error => 'Network error'; @override String get github_refused => 'Github Api error[OS Error: Connection refused]. Please switch networks or try again later'; @override String get load_more_not => 'Nothing'; @override String get load_more_text => 'Loading'; @override String get home_dynamic => 'Dynamic'; @override String get home_trend => 'Trend'; @override String get home_my => 'My'; @override String get trend_user_title => 'China User Trend'; @override String get trend_day => 'today'; @override String get trend_week => 'week'; @override String get trend_month => 'month'; @override String get trend_all => 'all'; @override String get user_tab_repos => 'Repository'; @override String get user_tab_fans => 'Follower'; @override String get user_tab_focus => 'Focus'; @override String get user_tab_star => 'Star'; @override String get user_tab_honor => 'Honor'; @override String get user_dynamic_group => 'Members'; @override String get user_dynamic_title => 'Dynamic'; @override String get user_focus => 'Focused'; @override String get user_un_focus => 'Focus'; @override String get user_focus_no_support => 'Not Support.'; @override String get user_create_at => 'Create at: '; @override String get user_orgs_title => 'Organization'; @override String get repos_tab_readme => 'README'; @override String get repos_tab_info => 'Info'; @override String get repos_tab_file => 'Files'; @override String get repos_tab_issue => 'Issue'; @override String get repos_tab_activity => 'Activity'; @override String get repos_tab_commits => 'Commits'; @override String get repos_tab_issue_all => 'All'; @override String get repos_tab_issue_open => 'Open'; @override String get repos_tab_issue_closed => 'Closed'; @override String get repos_option_release => 'Release'; @override String get repos_option_branch => 'Branch'; @override String get repos_fork_at => 'Fork at '; @override String get repos_create_at => 'Create at '; @override String get repos_last_commit => 'Last commit at '; @override String get repos_all_issue_count => 'All Issue: '; @override String get repos_open_issue_count => 'Open Issue: '; @override String get repos_close_issue_count => 'Close Issue: '; @override String get repos_issue_search => 'Search'; @override String get repos_no_support_issue => 'Not Support Issue'; @override String get issue_reply => 'Reply'; @override String get issue_edit => 'Edit'; @override String get issue_open => 'Open'; @override String get issue_close => 'Close'; @override String get issue_lock => 'Lock'; @override String get issue_unlock => 'Unlock'; @override String get issue_reply_issue => 'Reply Issue'; @override String get issue_commit_issue => 'Commit Issue'; @override String get issue_edit_issue => 'Edit Issue'; @override String get issue_edit_issue_commit => 'Edit Reply'; @override String get issue_edit_issue_edit_commit => 'Edit'; @override String get issue_edit_issue_delete_commit => 'Delete'; @override String get issue_edit_issue_copy_commit => 'Copy'; @override String get issue_edit_issue_content_not_be_null => 'Content cannot be empty'; @override String get issue_edit_issue_title_not_be_null => 'Title cannot be empty'; @override String get issue_edit_issue_title_tip => 'Please input title'; @override String get issue_edit_issue_content_tip => 'Please input content'; @override String get notify_title => 'Notify'; @override String get notify_tab_all => 'All'; @override String get notify_tab_part => 'Part'; @override String get notify_tab_unread => 'Unread'; @override String get notify_unread => 'Unread'; @override String get notify_readed => 'Read'; @override String get notify_status => 'Status'; @override String get notify_type => 'Type'; @override String get search_title => 'Search'; @override String get search_tab_repos => 'Repository'; @override String get search_tab_user => 'User'; @override String get release_tab_release => 'Release'; @override String get release_tab_tag => 'Tag'; @override String get user_profile_name => 'Name'; @override String get user_profile_email => 'Email'; @override String get user_profile_link => 'Link'; @override String get user_profile_org => 'Company'; @override String get user_profile_location => 'Location'; @override String get user_profile_info => 'Info'; @override String get search_type => 'Type'; @override String get search_sort => 'Sort'; @override String get search_language => 'Language'; @override String get feed_back_tip => 'Your feedback will be sent to Github as a public issue'; } ================================================ FILE: lib/common/localization/l10n/app_localizations_ja.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for Japanese (`ja`). class AppLocalizationsJa extends AppLocalizations { AppLocalizationsJa([String locale = 'ja']) : super(locale); @override String get welcomeMessage => 'Flutterへようこそ'; @override String get app_name => 'GSYGithubApp'; @override String get app_ok => 'OK'; @override String get app_cancel => 'キャンセル'; @override String get app_empty => 'データなし(o゚▽゚)o'; @override String get app_licenses => 'ライセンス'; @override String get app_close => '閉じる'; @override String get app_version => 'バージョン'; @override String get app_back_tip => '終了しますか?'; @override String get app_not_new_version => '新しいバージョンはありません。'; @override String get app_version_title => 'バージョン更新'; @override String get nothing_now => '何もありません'; @override String get loading_text => '読み込み中···'; @override String get option_web => 'ブラウザ'; @override String get option_copy => 'コピー'; @override String get option_share => '共有'; @override String get option_web_launcher_error => 'URLエラー'; @override String get option_share_title => 'GSYGitHubFlutterからの共有: '; @override String get option_share_copy_success => 'コピー成功'; @override String get login_text => 'ログイン'; @override String get oauth_text => 'OAuth'; @override String get login_out => 'ログアウト'; @override String get login_deprecated => 'パスワード認証APIは2020年11月13日にGithubによって削除されます'; @override String get home_reply => 'フィードバック'; @override String get home_change_language => '言語'; @override String get home_vibration => '振動フィードバック'; @override String get home_change_grey => 'グレースケール'; @override String get home_about => 'について'; @override String get home_check_update => '更新確認'; @override String get home_history => '履歴'; @override String get home_user_info => 'プロフィール'; @override String get home_change_theme => 'テーマ'; @override String get home_language_default => 'デフォルト'; @override String get home_language_zh => '中文'; @override String get home_language_en => 'English'; @override String get home_language_ko => '한국어'; @override String get home_language_ja => '日本語'; @override String get switch_language => '言語を選択'; @override String get home_theme_default => 'デフォルト'; @override String get home_theme_1 => 'テーマ1'; @override String get home_theme_2 => 'テーマ2'; @override String get home_theme_3 => 'テーマ3'; @override String get home_theme_4 => 'テーマ4'; @override String get home_theme_5 => 'テーマ5'; @override String get home_theme_6 => 'テーマ6'; @override String get login_username_hint_text => 'ユーザー名'; @override String get login_password_hint_text => 'パスワード'; @override String get login_success => 'ログイン成功'; @override String get network_error_401 => 'Http 401'; @override String get network_error_403 => 'Http 403'; @override String get network_error_404 => 'Http 404'; @override String get network_error_422 => 'リクエストボディエラー、Github ClientIdまたはアカウント/パスワードを確認してください'; @override String get network_error_timeout => 'Http タイムアウト'; @override String get network_error_unknown => 'Http 不明なエラー'; @override String get network_error => 'ネットワークエラー'; @override String get github_refused => 'Github APIエラー[OSエラー: 接続拒否]。ネットワークを切り替えるか、後でもう一度お試しください'; @override String get load_more_not => 'これ以上ありません'; @override String get load_more_text => '読み込み中'; @override String get home_dynamic => 'アクティビティ'; @override String get home_trend => 'トレンド'; @override String get home_my => 'マイ'; @override String get trend_user_title => '中国ユーザートレンド'; @override String get trend_day => '今日'; @override String get trend_week => '週間'; @override String get trend_month => '月間'; @override String get trend_all => 'すべて'; @override String get user_tab_repos => 'リポジトリ'; @override String get user_tab_fans => 'フォロワー'; @override String get user_tab_focus => 'フォロー'; @override String get user_tab_star => 'スター'; @override String get user_tab_honor => '栄誉'; @override String get user_dynamic_group => 'メンバー'; @override String get user_dynamic_title => 'アクティビティ'; @override String get user_focus => 'フォロー中'; @override String get user_un_focus => 'フォローする'; @override String get user_focus_no_support => 'サポートされていません。'; @override String get user_create_at => '作成日:'; @override String get user_orgs_title => '組織'; @override String get repos_tab_readme => 'README'; @override String get repos_tab_info => '情報'; @override String get repos_tab_file => 'ファイル'; @override String get repos_tab_issue => 'Issue'; @override String get repos_tab_activity => 'アクティビティ'; @override String get repos_tab_commits => 'コミット'; @override String get repos_tab_issue_all => 'すべて'; @override String get repos_tab_issue_open => 'オープン'; @override String get repos_tab_issue_closed => 'クローズ'; @override String get repos_option_release => 'リリース'; @override String get repos_option_branch => 'ブランチ'; @override String get repos_fork_at => 'フォーク日 '; @override String get repos_create_at => '作成日 '; @override String get repos_last_commit => '最終コミット日 '; @override String get repos_all_issue_count => '全Issue:'; @override String get repos_open_issue_count => 'オープンIssue:'; @override String get repos_close_issue_count => 'クローズIssue:'; @override String get repos_issue_search => '検索'; @override String get repos_no_support_issue => 'Issueはサポートされていません'; @override String get issue_reply => '返信'; @override String get issue_edit => '編集'; @override String get issue_open => 'オープン'; @override String get issue_close => 'クローズ'; @override String get issue_lock => 'ロック'; @override String get issue_unlock => 'アンロック'; @override String get issue_reply_issue => 'Issueに返信'; @override String get issue_commit_issue => 'Issueをコミット'; @override String get issue_edit_issue => 'Issueを編集'; @override String get issue_edit_issue_commit => '返信を編集'; @override String get issue_edit_issue_edit_commit => '編集'; @override String get issue_edit_issue_delete_commit => '削除'; @override String get issue_edit_issue_copy_commit => 'コピー'; @override String get issue_edit_issue_content_not_be_null => '内容を入力してください'; @override String get issue_edit_issue_title_not_be_null => 'タイトルを入力してください'; @override String get issue_edit_issue_title_tip => 'タイトルを入力してください'; @override String get issue_edit_issue_content_tip => '内容を入力してください'; @override String get notify_title => '通知'; @override String get notify_tab_all => 'すべて'; @override String get notify_tab_part => '一部'; @override String get notify_tab_unread => '未読'; @override String get notify_unread => '未読'; @override String get notify_readed => '既読'; @override String get notify_status => 'ステータス'; @override String get notify_type => 'タイプ'; @override String get search_title => '検索'; @override String get search_tab_repos => 'リポジトリ'; @override String get search_tab_user => 'ユーザー'; @override String get release_tab_release => 'リリース'; @override String get release_tab_tag => 'タグ'; @override String get user_profile_name => '名前'; @override String get user_profile_email => 'メール'; @override String get user_profile_link => 'リンク'; @override String get user_profile_org => '会社'; @override String get user_profile_location => '場所'; @override String get user_profile_info => '情報'; @override String get search_type => 'タイプ'; @override String get search_sort => '並び替え'; @override String get search_language => '言語'; @override String get feed_back_tip => 'あなたのフィードバックは公開IssueとしてGitHubに送信されます'; } ================================================ FILE: lib/common/localization/l10n/app_localizations_ko.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for Korean (`ko`). class AppLocalizationsKo extends AppLocalizations { AppLocalizationsKo([String locale = 'ko']) : super(locale); @override String get welcomeMessage => 'Flutter에 오신 것을 환영합니다'; @override String get app_name => 'GSYGithubApp'; @override String get app_ok => '확인'; @override String get app_cancel => '취소'; @override String get app_empty => '데이터 없음(o゚▽゚)o'; @override String get app_licenses => '라이선스'; @override String get app_close => '닫기'; @override String get app_version => '버전'; @override String get app_back_tip => '종료하시겠습니까?'; @override String get app_not_new_version => '새로운 버전이 없습니다'; @override String get app_version_title => '버전 업데이트'; @override String get nothing_now => '아무것도 없음'; @override String get loading_text => '로딩 중···'; @override String get option_web => '브라우저'; @override String get option_copy => '복사'; @override String get option_share => '공유'; @override String get option_web_launcher_error => 'URL 오류'; @override String get option_share_title => 'GSYGitHubFlutter에서 공유: '; @override String get option_share_copy_success => '복사 성공'; @override String get login_text => '로그인'; @override String get oauth_text => 'OAuth'; @override String get login_out => '로그아웃'; @override String get login_deprecated => '비밀번호 인증 API는 2020년 11월 13일에 Github에서 제거됩니다'; @override String get home_reply => '피드백'; @override String get home_change_language => '언어'; @override String get home_vibration => '진동 피드백'; @override String get home_change_grey => '그레이스케일'; @override String get home_about => '정보'; @override String get home_check_update => '업데이트 확인'; @override String get home_history => '기록'; @override String get home_user_info => '프로필'; @override String get home_change_theme => '테마'; @override String get home_language_default => '기본'; @override String get home_language_zh => '中文'; @override String get home_language_en => 'English'; @override String get home_language_ko => '한국어'; @override String get home_language_ja => '日本語'; @override String get switch_language => '언어 선택'; @override String get home_theme_default => '기본 테마'; @override String get home_theme_1 => '테마 1'; @override String get home_theme_2 => '테마 2'; @override String get home_theme_3 => '테마 3'; @override String get home_theme_4 => '테마 4'; @override String get home_theme_5 => '테마 5'; @override String get home_theme_6 => '테마 6'; @override String get login_username_hint_text => '사용자 이름'; @override String get login_password_hint_text => '비밀번호'; @override String get login_success => '로그인 성공'; @override String get network_error_401 => 'Http 401'; @override String get network_error_403 => 'Http 403'; @override String get network_error_404 => 'Http 404'; @override String get network_error_422 => '요청 본문 오류, Github ClientId 또는 계정/비밀번호를 확인하세요'; @override String get network_error_timeout => 'Http 시간 초과'; @override String get network_error_unknown => 'Http 알 수 없는 오류'; @override String get network_error => '네트워크 오류'; @override String get github_refused => 'Github API 오류[OS 오류: 연결 거부]. 네트워크를 전환하거나 나중에 다시 시도하세요'; @override String get load_more_not => '더 이상 없음'; @override String get load_more_text => '로딩 중'; @override String get home_dynamic => '동적'; @override String get home_trend => '트렌드'; @override String get home_my => '내 정보'; @override String get trend_user_title => '중국 사용자 트렌드'; @override String get trend_day => '오늘'; @override String get trend_week => '이번 주'; @override String get trend_month => '이번 달'; @override String get trend_all => '전체'; @override String get user_tab_repos => '저장소'; @override String get user_tab_fans => '팔로워'; @override String get user_tab_focus => '팔로잉'; @override String get user_tab_star => '스타'; @override String get user_tab_honor => '명예'; @override String get user_dynamic_group => '멤버'; @override String get user_dynamic_title => '동적'; @override String get user_focus => '팔로잉 중'; @override String get user_un_focus => '팔로우'; @override String get user_focus_no_support => '지원되지 않음'; @override String get user_create_at => '생성일: '; @override String get user_orgs_title => '조직'; @override String get repos_tab_readme => 'README'; @override String get repos_tab_info => '정보'; @override String get repos_tab_file => '파일'; @override String get repos_tab_issue => '이슈'; @override String get repos_tab_activity => '활동'; @override String get repos_tab_commits => '커밋'; @override String get repos_tab_issue_all => '전체'; @override String get repos_tab_issue_open => '열림'; @override String get repos_tab_issue_closed => '닫힘'; @override String get repos_option_release => '릴리스'; @override String get repos_option_branch => '브랜치'; @override String get repos_fork_at => '포크 일자: '; @override String get repos_create_at => '생성 일자: '; @override String get repos_last_commit => '마지막 커밋: '; @override String get repos_all_issue_count => '전체 이슈: '; @override String get repos_open_issue_count => '열린 이슈: '; @override String get repos_close_issue_count => '닫힌 이슈: '; @override String get repos_issue_search => '검색'; @override String get repos_no_support_issue => '이슈가 지원되지 않음'; @override String get issue_reply => '답변'; @override String get issue_edit => '편집'; @override String get issue_open => '열기'; @override String get issue_close => '닫기'; @override String get issue_lock => '잠금'; @override String get issue_unlock => '잠금 해제'; @override String get issue_reply_issue => '이슈 답변'; @override String get issue_commit_issue => '이슈 커밋'; @override String get issue_edit_issue => '이슈 편집'; @override String get issue_edit_issue_commit => '답변 편집'; @override String get issue_edit_issue_edit_commit => '편집'; @override String get issue_edit_issue_delete_commit => '삭제'; @override String get issue_edit_issue_copy_commit => '복사'; @override String get issue_edit_issue_content_not_be_null => '내용을 입력하세요'; @override String get issue_edit_issue_title_not_be_null => '제목을 입력하세요'; @override String get issue_edit_issue_title_tip => '제목을 입력하세요'; @override String get issue_edit_issue_content_tip => '내용을 입력하세요'; @override String get notify_title => '알림'; @override String get notify_tab_all => '전체'; @override String get notify_tab_part => '참여'; @override String get notify_tab_unread => '읽지 않음'; @override String get notify_unread => '읽지 않음'; @override String get notify_readed => '읽음'; @override String get notify_status => '상태'; @override String get notify_type => '유형'; @override String get search_title => '검색'; @override String get search_tab_repos => '저장소'; @override String get search_tab_user => '사용자'; @override String get release_tab_release => '릴리스'; @override String get release_tab_tag => '태그'; @override String get user_profile_name => '이름'; @override String get user_profile_email => '이메일'; @override String get user_profile_link => '링크'; @override String get user_profile_org => '회사'; @override String get user_profile_location => '위치'; @override String get user_profile_info => '정보'; @override String get search_type => '유형'; @override String get search_sort => '정렬'; @override String get search_language => '언어'; @override String get feed_back_tip => '귀하의 피드백은 Github에 공개 이슈로 제출됩니다'; } ================================================ FILE: lib/common/localization/l10n/app_localizations_zh.dart ================================================ // ignore: unused_import import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint /// The translations for Chinese (`zh`). class AppLocalizationsZh extends AppLocalizations { AppLocalizationsZh([String locale = 'zh']) : super(locale); @override String get welcomeMessage => 'Welcome To Flutter'; @override String get app_name => 'GSYGithubApp'; @override String get app_ok => '确定'; @override String get app_cancel => '取消'; @override String get app_empty => '目前什么也没有哟'; @override String get app_licenses => '协议'; @override String get app_close => '关闭'; @override String get app_version => '版本'; @override String get app_back_tip => '确定要退出应用?'; @override String get app_not_new_version => '当前没有新版本'; @override String get app_version_title => '版本更新'; @override String get nothing_now => '目前什么都没有。'; @override String get loading_text => '努力加载中···'; @override String get option_web => '浏览器打开'; @override String get option_copy => '复制链接'; @override String get option_share => '分享'; @override String get option_web_launcher_error => 'url异常'; @override String get option_share_title => '分享自GSYGitHubFlutter: '; @override String get option_share_copy_success => '已经复制到粘贴板'; @override String get login_text => '账号登录'; @override String get oauth_text => '安全登陆'; @override String get login_out => '退出登录'; @override String get login_deprecated => '密码登陆API将在2020年11月13日被Github移除'; @override String get home_reply => '问题反馈'; @override String get home_change_language => '语言切换'; @override String get home_vibration => '震动反馈'; @override String get home_change_grey => '灰度模式'; @override String get home_about => '关于'; @override String get home_check_update => '检测更新'; @override String get home_history => '阅读历史'; @override String get home_user_info => '个人信息'; @override String get home_change_theme => '切换主题'; @override String get home_language_default => '默认'; @override String get home_language_zh => '中文'; @override String get home_language_en => 'English'; @override String get home_language_ko => '한국어'; @override String get home_language_ja => '日本語'; @override String get switch_language => '切换语言'; @override String get home_theme_default => '默认主题'; @override String get home_theme_1 => '主题1'; @override String get home_theme_2 => '主题2'; @override String get home_theme_3 => '主题3'; @override String get home_theme_4 => '主题4'; @override String get home_theme_5 => '主题5'; @override String get home_theme_6 => '主题6'; @override String get login_username_hint_text => '请输入github用户名'; @override String get login_password_hint_text => '请输入密码'; @override String get login_success => '登录成功'; @override String get network_error_401 => '未授权或授权登录失败'; @override String get network_error_403 => '403权限错误'; @override String get network_error_404 => '404错误'; @override String get network_error_422 => '请求实体异常,请确保Github ClientId和账号密码正确'; @override String get network_error_timeout => '请求超时'; @override String get network_error_unknown => '其他异常'; @override String get network_error => '网络错误'; @override String get github_refused => 'Github Api 出现异常[Connection refused],建议换个网络环境或者稍后再试'; @override String get load_more_not => '没有更多数据'; @override String get load_more_text => '正在加载更多'; @override String get home_dynamic => '动态'; @override String get home_trend => '趋势'; @override String get home_my => '我的'; @override String get trend_user_title => '中国用户趋势'; @override String get trend_day => '今日'; @override String get trend_week => '本周'; @override String get trend_month => '本月'; @override String get trend_all => '全部'; @override String get user_tab_repos => '仓库'; @override String get user_tab_fans => '粉丝'; @override String get user_tab_focus => '关注'; @override String get user_tab_star => '星标'; @override String get user_tab_honor => '荣耀'; @override String get user_dynamic_group => '组织成员'; @override String get user_dynamic_title => '个人动态'; @override String get user_focus => '已关注'; @override String get user_un_focus => '关注'; @override String get user_focus_no_support => '不支持关注'; @override String get user_create_at => '创建于:'; @override String get user_orgs_title => '所在组织'; @override String get repos_tab_readme => '详情'; @override String get repos_tab_info => '动态'; @override String get repos_tab_file => '文件'; @override String get repos_tab_issue => 'ISSUE'; @override String get repos_tab_activity => '动态'; @override String get repos_tab_commits => '提交'; @override String get repos_tab_issue_all => '所有'; @override String get repos_tab_issue_open => '打开'; @override String get repos_tab_issue_closed => '关闭'; @override String get repos_option_release => '版本'; @override String get repos_option_branch => '分支'; @override String get repos_fork_at => 'Fork于 '; @override String get repos_create_at => '创建于 '; @override String get repos_last_commit => '最后提交于 '; @override String get repos_all_issue_count => '所有Issue数:'; @override String get repos_open_issue_count => '开启Issue数:'; @override String get repos_close_issue_count => '关闭Issue数:'; @override String get repos_issue_search => '搜索'; @override String get repos_no_support_issue => '该项目未开启Issue'; @override String get issue_reply => '回复'; @override String get issue_edit => '编辑'; @override String get issue_open => '打开'; @override String get issue_close => '关闭'; @override String get issue_lock => '锁定'; @override String get issue_unlock => '解锁'; @override String get issue_reply_issue => '回复Issue'; @override String get issue_commit_issue => '提交Issue'; @override String get issue_edit_issue => '编译Issue'; @override String get issue_edit_issue_commit => '编译回复'; @override String get issue_edit_issue_edit_commit => '编辑'; @override String get issue_edit_issue_delete_commit => '删除'; @override String get issue_edit_issue_copy_commit => '复制'; @override String get issue_edit_issue_content_not_be_null => '内容不能为空'; @override String get issue_edit_issue_title_not_be_null => '标题不能为空'; @override String get issue_edit_issue_title_tip => '请输入标题'; @override String get issue_edit_issue_content_tip => '请输入内容'; @override String get notify_title => '通知'; @override String get notify_tab_all => '所有'; @override String get notify_tab_part => '参与'; @override String get notify_tab_unread => '未读'; @override String get notify_unread => '未读'; @override String get notify_readed => '已读'; @override String get notify_status => '状态'; @override String get notify_type => '类型'; @override String get search_title => '搜索'; @override String get search_tab_repos => '仓库'; @override String get search_tab_user => '用户'; @override String get release_tab_release => '版本'; @override String get release_tab_tag => '标记'; @override String get user_profile_name => '名字'; @override String get user_profile_email => '邮箱'; @override String get user_profile_link => '链接'; @override String get user_profile_org => '公司'; @override String get user_profile_location => '位置'; @override String get user_profile_info => '简介'; @override String get search_type => '类型'; @override String get search_sort => '排序'; @override String get search_language => '语言'; @override String get feed_back_tip => '您的反馈会作为公共Issue提交到Github,您确定要继续吗?'; } ================================================ FILE: lib/common/localization/l10n/app_zh.arb ================================================ { "welcomeMessage": "Welcome To Flutter", "app_name": "GSYGithubApp", "app_ok": "确定", "app_cancel": "取消", "app_empty": "目前什么也没有哟", "app_licenses": "协议", "app_close": "关闭", "app_version": "版本", "app_back_tip": "确定要退出应用?", "app_not_new_version": "当前没有新版本", "app_version_title": "版本更新", "nothing_now": "目前什么都没有。", "loading_text": "努力加载中···", "option_web": "浏览器打开", "option_copy": "复制链接", "option_share": "分享", "option_web_launcher_error": "url异常", "option_share_title": "分享自GSYGitHubFlutter: ", "option_share_copy_success": "已经复制到粘贴板", "login_text": "账号登录", "oauth_text": "安全登陆", "login_out": "退出登录", "login_deprecated": "密码登陆API将在2020年11月13日被Github移除", "home_reply": "问题反馈", "home_change_language": "语言切换", "home_vibration": "震动反馈", "home_change_grey": "灰度模式", "home_about": "关于", "home_check_update": "检测更新", "home_history": "阅读历史", "home_user_info": "个人信息", "home_change_theme": "切换主题", "home_language_default": "默认", "home_language_zh": "中文", "home_language_en": "English", "home_language_ko": "한국어", "home_language_ja": "日本語", "switch_language": "切换语言", "home_theme_default": "默认主题", "home_theme_1": "主题1", "home_theme_2": "主题2", "home_theme_3": "主题3", "home_theme_4": "主题4", "home_theme_5": "主题5", "home_theme_6": "主题6", "login_username_hint_text": "请输入github用户名", "login_password_hint_text": "请输入密码", "login_success": "登录成功", "network_error_401": "未授权或授权登录失败", "network_error_403": "403权限错误", "network_error_404": "404错误", "network_error_422": "请求实体异常,请确保Github ClientId和账号密码正确", "network_error_timeout": "请求超时", "network_error_unknown": "其他异常", "network_error": "网络错误", "github_refused": "Github Api 出现异常[Connection refused],建议换个网络环境或者稍后再试", "load_more_not": "没有更多数据", "load_more_text": "正在加载更多", "home_dynamic": "动态", "home_trend": "趋势", "home_my": "我的", "trend_user_title": "中国用户趋势", "trend_day": "今日", "trend_week": "本周", "trend_month": "本月", "trend_all": "全部", "user_tab_repos": "仓库", "user_tab_fans": "粉丝", "user_tab_focus": "关注", "user_tab_star": "星标", "user_tab_honor": "荣耀", "user_dynamic_group": "组织成员", "user_dynamic_title": "个人动态", "user_focus": "已关注", "user_un_focus": "关注", "user_focus_no_support": "不支持关注", "user_create_at": "创建于:", "user_orgs_title": "所在组织", "repos_tab_readme": "详情", "repos_tab_info": "动态", "repos_tab_file": "文件", "repos_tab_issue": "ISSUE", "repos_tab_activity": "动态", "repos_tab_commits": "提交", "repos_tab_issue_all": "所有", "repos_tab_issue_open": "打开", "repos_tab_issue_closed": "关闭", "repos_option_release": "版本", "repos_option_branch": "分支", "repos_fork_at": "Fork于 ", "repos_create_at": "创建于 ", "repos_last_commit": "最后提交于 ", "repos_all_issue_count": "所有Issue数:", "repos_open_issue_count": "开启Issue数:", "repos_close_issue_count": "关闭Issue数:", "repos_issue_search": "搜索", "repos_no_support_issue": "该项目未开启Issue", "issue_reply": "回复", "issue_edit": "编辑", "issue_open": "打开", "issue_close": "关闭", "issue_lock": "锁定", "issue_unlock": "解锁", "issue_reply_issue": "回复Issue", "issue_commit_issue": "提交Issue", "issue_edit_issue": "编译Issue", "issue_edit_issue_commit": "编译回复", "issue_edit_issue_edit_commit": "编辑", "issue_edit_issue_delete_commit": "删除", "issue_edit_issue_copy_commit": "复制", "issue_edit_issue_content_not_be_null": "内容不能为空", "issue_edit_issue_title_not_be_null": "标题不能为空", "issue_edit_issue_title_tip": "请输入标题", "issue_edit_issue_content_tip": "请输入内容", "notify_title": "通知", "notify_tab_all": "所有", "notify_tab_part": "参与", "notify_tab_unread": "未读", "notify_unread": "未读", "notify_readed": "已读", "notify_status": "状态", "notify_type": "类型", "search_title": "搜索", "search_tab_repos": "仓库", "search_tab_user": "用户", "release_tab_release": "版本", "release_tab_tag": "标记", "user_profile_name": "名字", "user_profile_email": "邮箱", "user_profile_link": "链接", "user_profile_org": "公司", "user_profile_location": "位置", "user_profile_info": "简介", "search_type": "类型", "search_sort": "排序", "search_language": "语言", "feed_back_tip": "您的反馈会作为公共Issue提交到Github,您确定要继续吗?" } ================================================ FILE: lib/common/logger.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:talker_flutter/talker_flutter.dart'; final talker = TalkerFlutter.init( settings: TalkerSettings( /// You can enable/disable all talker processes with this field enabled: true, /// You can enable/disable saving logs data in history useHistory: true, /// Length of history that saving logs data maxHistoryItems: 100, /// You can enable/disable console logs useConsoleLogs: true, ), ); printLog(Object msg) { if (msg is Error) { talker.error("Catch Running Error:", msg); } else if (msg is Exception) { talker.error("Catch Running Exception:", msg); } if (kDebugMode) { print(msg); } } ================================================ FILE: lib/common/net/AGENTS.md ================================================ # 网络层协作说明 这个目录承接全局共享网络能力,是高风险区域。 这里的改动通常会影响多个页面和 repository。 ## 修改前先确认 - 这是协议层问题,还是某个功能模块自己的展示问题? - 改动是否应该优先收敛在 repository,而不是直接改网络底层? - 是否会同时影响 REST、GraphQL、拦截器或响应转换? ## 工作规则 - 不要把页面定制逻辑塞进共享拦截器 - 改公共请求头、token、错误处理时,预期会影响全局 - 改 `graphql/` 时,同时检查对应 repository 和模型 - 改 `transformer`、`interceptors` 时,优先做最小改动 ## 最低验证 - `flutter analyze` - 至少手工验证一个相关功能页面 - 若改动影响认证或公共响应格式,额外验证登录或高频页面加载 ================================================ FILE: lib/common/net/address.dart ================================================ import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/config/ignoreConfig.dart'; ///地址数据 class Address { static const String host = "https://api.github.com/"; static const String hostWeb = "https://github.com/"; static const String graphicHost = 'https://ghchart.rshah.org/'; static const String updateUrl = 'https://github.com/CarGuo/gsy_github_app_flutter/releases'; ///获取授权 post static getAuthorization() { return "${host}authorizations"; } ///搜索 get static search(q, sort, order, type, page, [pageSize = Config.PAGE_SIZE]) { if (type == 'user') { return "${host}search/users?q=$q&page=$page&per_page=$pageSize"; } sort ??= "best%20match"; order ??= "desc"; page ??= 1; pageSize ??= Config.PAGE_SIZE; return "${host}search/repositories?q=$q&sort=$sort&order=$order&page=$page&per_page=$pageSize"; } ///搜索topic tag static searchTopic(topic) { return "${host}search/repositories?q=topic:$topic&sort=stars&order=desc"; } ///用户的仓库 get static userRepos(userName, sort) { sort ??= 'pushed'; return "${host}users/$userName/repos?sort=$sort"; } ///仓库详情 get static getReposDetail(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName"; } ///仓库活动 get static getReposEvent(reposOwner, reposName) { return "${host}networks/$reposOwner/$reposName/events"; } ///仓库Fork get static getReposForks(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/forks"; } ///仓库Star get static getReposStar(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/stargazers"; } ///仓库Watch get static getReposWatcher(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/subscribers"; } ///仓库提交 get static getReposCommits(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/commits"; } ///仓库提交详情 get static getReposCommitsInfo(reposOwner, reposName, sha) { return "${host}repos/$reposOwner/$reposName/commits/$sha"; } ///仓库提交比较 get static getReposCompare(reposOwner, reposName, base, head) { return "${host}repos/$reposOwner/$reposName/compare/$base...$head"; } ///仓库Issue get static getReposIssue(String reposOwner, String reposName, state, sort, direction) { state ??= 'all'; sort ??= 'created'; direction ??= 'desc'; return "${host}repos/$reposOwner/$reposName/issues?state=$state&sort=$sort&direction=$direction"; } ///仓release get static getReposRelease(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/releases"; } ///仓Tag get static getReposTag(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/tags"; } ///仓Contributors get static getReposContributors(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/contributors"; } ///仓库Issue评论 get static getIssueComment(reposOwner, reposName, issueNumber) { return "${host}repos/$reposOwner/$reposName/issues/$issueNumber/comments"; } ///仓库Issue get static getIssueInfo(reposOwner, reposName, issueNumber) { return "${host}repos/$reposOwner/$reposName/issues/$issueNumber"; } ///增加issue评论 post static addIssueComment(reposOwner, reposName, issueNumber) { return "${host}repos/$reposOwner/$reposName/issues/$issueNumber/comments"; } ///编辑issue put static editIssue(reposOwner, reposName, issueNumber) { return "${host}repos/$reposOwner/$reposName/issues/$issueNumber"; } ///锁定issue put static lockIssue(reposOwner, reposName, issueNumber) { return "${host}repos/$reposOwner/$reposName/issues/$issueNumber/lock"; } ///创建issue post static createIssue(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/issues"; } ///搜索issue static repositoryIssueSearch(q) { return "${host}search/issues?q=$q"; } ///编辑评论 patch, delete static editComment(reposOwner, reposName, commentId) { return "${host}repos/$reposOwner/$reposName/issues/comments/$commentId"; } ///自己的star get static myStar(sort) { sort ??= 'updated'; return "${host}users/starred?sort=$sort"; } ///用户的star get static userStar(userName, sort) { sort ??= 'updated'; return "${host}users/$userName/starred?sort=$sort"; } ///关注仓库 put static resolveStarRepos(reposOwner, repos) { return "${host}user/starred/$reposOwner/$repos"; } ///订阅仓库 put static resolveWatcherRepos(reposOwner, repos) { return "${host}user/subscriptions/$reposOwner/$repos"; } ///仓库内容数据 get static reposData(reposOwner, repos) { return "${host}repos/$reposOwner/$repos/contents"; } ///仓库路径下的内容 get static reposDataDir(reposOwner, repos, path, [branch = 'master']) { return "${host}repos/$reposOwner/$repos/contents/$path${(branch == null || branch == "") ? "" : ("?ref=$branch")}"; } ///README 文件地址 get static readmeFile(reposNameFullName, curBranch) { // ignore: prefer_interpolation_to_compose_strings return "${"${host}repos/" + reposNameFullName}/readme${(curBranch == null || curBranch == "" ) ? "" : ("?ref=$curBranch")}"; } ///我的用户信息 GET static getMyUserInfo() { return "${host}user"; } ///用户信息 get static getUserInfo(userName) { return "${host}users/$userName"; } /// get 是否关注 static doFollow(name) { return "${host}user/following/$name"; } ///用户关注 get static getUserFollow(userName) { return "${host}users/$userName/following"; } ///我的关注者 get static getMyFollower() { return "${host}user/followers"; } ///用户的关注者 get static getUserFollower(userName) { return "${host}users/$userName/followers"; } ///create fork post static createFork(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/forks"; } ///branch get static getbranches(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/branches"; } ///fork get static getForker(reposOwner, reposName, sort) { sort ??= 'newest'; return "${host}repos/$reposOwner/$reposName/forks?sort=$sort"; } ///readme get static getReadme(reposOwner, reposName) { return "${host}repos/$reposOwner/$reposName/readme"; } ///用户收到的事件信息 get static getEventReceived(userName) { return "${host}users/$userName/received_events"; } ///用户相关的事件信息 get static getEvent(userName) { return "${host}users/$userName/events"; } ///组织成员 static getMember(orgs) { return "${host}orgs/$orgs/members"; } ///获取用户组织 static getUserOrgs(userName) { return "${host}users/$userName/orgs"; } ///通知 get static getNotifation(all, participating) { if ((all == null && participating == null) || (all == false && participating == false)) { return "${host}notifications"; } all ??= false; participating ??= false; return "${host}notifications?all=$all&participating=$participating"; } ///patch static setNotificationAsRead(threadId) { return "${host}notifications/threads/$threadId"; } ///put static setAllNotificationAsRead() { return "${host}notifications"; } static getOAuthUrl() { return "https://github.com/login/oauth/authorize?client_id" "=${NetConfig.CLIENT_ID}&state=app&" "scope=user,repo,gist,notifications,read:org,workflow&" "redirect_uri=gsygithubapp://authed"; } ///趋势 get static trending(since, languageType) { if (languageType != null) { return "https://github.com/trending/$languageType?since=$since"; } return "https://github.com/trending?since=$since"; } ///趋势 get static trendingApi(since, languageType) { if (languageType != null) { return "https://guoshuyu.cn/github/trend/list?languageType=$languageType&since=$since"; } return "https://guoshuyu.cn/github/trend/list?since=$since"; } ///处理分页参数 static getPageParams(tab, page, [pageSize = Config.PAGE_SIZE]) { if (page != null) { if (pageSize != null) { return "${tab}page=$page&per_page=$pageSize"; } else { return "${tab}page=$page"; } } else { return ""; } } } ================================================ FILE: lib/common/net/api.dart ================================================ import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/common/net/code.dart'; import 'dart:collection'; //import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/error_interceptor.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/header_interceptor.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/log_interceptor.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/response_interceptor.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/token_interceptor.dart'; import 'package:gsy_github_app_flutter/common/net/result_data.dart'; ///http请求 class HttpManager { static const CONTENT_TYPE_JSON = "application/json"; static const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"; late final Dio _dio; late final TokenInterceptors _tokenInterceptors; HttpManager._internal() { _dio = Dio(); // 使用默认配置 _tokenInterceptors = TokenInterceptors(); _dio.interceptors.addAll([ HeaderInterceptors(), _tokenInterceptors, LogsInterceptors(), ErrorInterceptors(), ResponseInterceptors(), ]); } static final HttpManager _instance = HttpManager._internal(); ///发起网络请求 ///[ url] 请求url ///[ params] 请求参数 ///[ header] 外加头 ///[ option] 配置 Future netFetch( url, params, Map? header, Options? option, {noTip = false}) async { Map headers = HashMap(); if (header != null) { headers.addAll(header); } if (option != null) { option.headers = headers; } else { option = Options(method: "get"); option.headers = headers; } resultError(DioException e) { Response? errorResponse; if (e.response != null) { errorResponse = e.response; } else { errorResponse = Response( statusCode: 666, requestOptions: RequestOptions(path: url)); } if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.sendTimeout || e.type == DioExceptionType.receiveTimeout) { errorResponse!.statusCode = Code.NETWORK_TIMEOUT; } return ResultData( Code.errorHandleFunction(errorResponse!.statusCode, e.message, noTip), false, errorResponse.statusCode); } Response response; try { response = await _dio.request(url, data: params, options: option); } on DioException catch (e) { return resultError(e); } if (response.data is DioException) { return resultError(response.data); } return response.data; } ///清除授权 clearAuthorization() { _tokenInterceptors.clearAuthorization(); } ///获取授权token getAuthorization() async { return _tokenInterceptors.getAuthorization(); } /// 提供单例访问 static HttpManager get instance => _instance; } final HttpManager httpManager = HttpManager.instance; // // // initDio() { // DioClient.getInstance(); // initializeNetworkListener(); // } // // class DioClient { // static Dio? _dio; // // DioClient._(); // // static Future getInstance() async { // if (_dio == null) { // await _initialize(); // } // return _dio!; // } // // static Future _initialize() async { // _dio = Dio(BaseOptions( // connectTimeout: const Duration(seconds: 10), // receiveTimeout: const Duration(seconds: 10), // )); // // _dio!.interceptors.add(LogInterceptor( // requestHeader: true, // requestBody: true, // responseHeader: true, // responseBody: true, // )); // } // // static void reset() { // _dio?.close(); // _dio = null; // } // } // // void initializeNetworkListener() { // Connectivity().onConnectivityChanged.listen((result) { // DioClient.reset(); // }); // } ================================================ FILE: lib/common/net/code.dart ================================================ import 'package:gsy_github_app_flutter/common/event/http_error_event.dart'; import 'package:gsy_github_app_flutter/common/event/index.dart'; ///错误编码 class Code { ///网络错误 static const NETWORK_ERROR = -1; ///网络超时 static const NETWORK_TIMEOUT = -2; ///网络返回数据格式化一次 static const NETWORK_JSON_EXCEPTION = -3; ///Github APi Connection refused static const GITHUB_API_REFUSED = -4; static const SUCCESS = 200; static errorHandleFunction(code, message, noTip) { if (noTip) { return message; } if(message != null && message is String && (message.contains("Connection refused") || message.contains("Connection reset"))) { code = GITHUB_API_REFUSED; } eventBus.fire(HttpErrorEvent(code, message)); return message; } } ================================================ FILE: lib/common/net/graphql/client.dart ================================================ import 'package:graphql/client.dart'; import 'package:gsy_github_app_flutter/common/net/graphql/repositories.dart'; import 'package:gsy_github_app_flutter/common/net/graphql/users.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; Future _client(token) async { final HttpLink httpLink = HttpLink( 'https://api.github.com/graphql', ); final AuthLink authLink = AuthLink( getToken: () => '$token', ); final Link link = authLink.concat(httpLink); var path = await CommonUtils.getApplicationDocumentsPath(); final store = await HiveStore.open(path: path); return GraphQLClient( cache: GraphQLCache(store: store), link: link, ); } GraphQLClient? _innerClient; initClient(token) async { _innerClient ??= await _client(token); } releaseClient() { _innerClient = null; } Future? getRepository(String owner, String? name) async { final QueryOptions options = QueryOptions( document: gql(readRepository), variables: { 'owner': owner, 'name': name, }, fetchPolicy: FetchPolicy.noCache); return await _innerClient!.query(options); } Future? getTrendUser(String location, {String? cursor}) async { var variables = cursor == null ? { 'location': "location:$location sort:followers", } : { 'location': "location:$location sort:followers", 'after': cursor, }; final QueryOptions options = QueryOptions( document: gql(cursor == null ? readTrendUser : readTrendUserByCursor), variables: variables, fetchPolicy: FetchPolicy.noCache); return await _innerClient!.query(options); } ================================================ FILE: lib/common/net/graphql/repositories.dart ================================================ const String readRepository = r''' query getRepositoryDetail($owner:String!, $name:String!){ repository(name: $name, owner: $owner) { ...comparisonFields parent { ...comparisonFields } } } fragment comparisonFields on Repository { issuesClosed: issues(states : CLOSED) { totalCount } issuesOpen: issues(states : OPEN) { totalCount } issues { totalCount } nameWithOwner, id, name, owner { login, url, avatarUrl, }, licenseInfo { name } forkCount, stargazers{ totalCount } hasIssuesEnabled, viewerHasStarred, viewerSubscription, hasIssuesEnabled, defaultBranchRef { name }, watchers { totalCount, } isFork languages(first:100) { totalSize, nodes { name, } }, createdAt, pushedAt, pushedAt, sshUrl, url, shortDescriptionHTML, repositoryTopics(first: 100) { totalCount, nodes { topic { name, } } } } '''; ================================================ FILE: lib/common/net/graphql/users.dart ================================================ const String readTrendUser = r''' query getTrendUser($location: String!){ search(type: USER, query: $location, first: 100) { pageInfo { endCursor } user: edges { user: node { ... on User { name, avatarUrl, followers { totalCount }, bio, login, lang: repositories(orderBy: {field: STARGAZERS, direction: DESC}, first:1) { nodes{ name languages(first:1) { nodes { name } } } } } } } } } '''; const String readTrendUserByCursor = r''' query getTrendUser($location: String!, $after: String!){ search(type: USER, query: $location, first: 100, after: $after) { pageInfo { endCursor } user: edges { user: node { ... on User { name, avatarUrl, followers { totalCount }, bio, login, lang: repositories(orderBy: {field: STARGAZERS, direction: DESC}, first:1) { nodes{ name languages(first:1) { nodes { name } } } } } } } } } '''; ================================================ FILE: lib/common/net/interceptors/error_interceptor.dart ================================================ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/common/net/code.dart'; import 'package:gsy_github_app_flutter/common/net/result_data.dart'; ///是否需要弹提示 const NOT_TIP_KEY = "noTip"; /// 错误拦截 /// Created by guoshuyu /// on 2019/3/23. class ErrorInterceptors extends InterceptorsWrapper { @override onRequest(RequestOptions options, handler) async { //没有网络 var connectivityResult = await (Connectivity().checkConnectivity()); if (connectivityResult.isEmpty || connectivityResult[0] == ConnectivityResult.none) { return handler.reject(DioException( requestOptions: options, type: DioExceptionType.unknown, response: Response( requestOptions: options, data: ResultData( Code.errorHandleFunction(Code.NETWORK_ERROR, "", false), false, Code.NETWORK_ERROR)))); } return super.onRequest(options, handler); } } ================================================ FILE: lib/common/net/interceptors/header_interceptor.dart ================================================ import 'package:dio/dio.dart'; /// header拦截器 /// Created by guoshuyu /// on 2019/3/23. class HeaderInterceptors extends InterceptorsWrapper { @override onRequest(RequestOptions options, handler) async { ///超时 options.connectTimeout = const Duration(seconds: 30); options.receiveTimeout = const Duration(seconds: 30); return super.onRequest(options, handler); } } ================================================ FILE: lib/common/net/interceptors/log_interceptor.dart ================================================ // ignore_for_file: type_literal_in_constant_pattern import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; /// Log 拦截器 /// Created by guoshuyu /// on 2019/3/23. class LogsInterceptors extends InterceptorsWrapper { static List sHttpResponses = []; static List sResponsesHttpUrl = []; static List?> sHttpRequest = []; static List sRequestHttpUrl = []; static List?> sHttpError = []; static List sHttpErrorUrl = []; @override onRequest(RequestOptions options, handler) async { if (Config.DEBUG!) { printLog("请求url:${options.path} ${options.method}"); options.headers.forEach((k, v) => options.headers[k] = v ?? ""); printLog('请求头: ${options.headers}'); if (options.data != null) { printLog('请求参数: ${options.data}'); } } try { addLogic(sRequestHttpUrl, options.path); dynamic data; if (options.data is Map) { data = options.data; } else { data = {}; } var map = { "header:": {...options.headers}, }; if (options.method == "POST") { map["data"] = data; } addLogic(sHttpRequest, map); } catch (e) { printLog(e); } return super.onRequest(options, handler); } @override onResponse(Response response, handler) async { if (Config.DEBUG!) { printLog('返回参数: $response'); } switch (response.data.runtimeType) { case Map || List: { try { var data = {}; data["data"] = response.data; addLogic(sResponsesHttpUrl, response.requestOptions.uri.toString()); addLogic(sHttpResponses, data); } catch (e) { printLog(e); } } case String: { try { var data = {}; data["data"] = response.data; addLogic(sResponsesHttpUrl, response.requestOptions.uri.toString()); addLogic(sHttpResponses, data); } catch (e) { printLog(e); } } } return super.onResponse(response, handler); } @override onError(DioException err, handler) async { if (Config.DEBUG!) { printLog('请求异常: $err'); printLog('请求异常信息: ${err.response?.toString() ?? ""}'); } try { addLogic(sHttpErrorUrl, err.requestOptions.path); var errors = {}; errors["error"] = err.message; addLogic(sHttpError, errors); } catch (e) { printLog(e); } return super.onError(err, handler); } static addLogic(List list, data) { if (list.length > 20) { list.removeAt(0); } list.add(data); } } ================================================ FILE: lib/common/net/interceptors/response_interceptor.dart ================================================ import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/net/code.dart'; import 'package:gsy_github_app_flutter/common/net/result_data.dart'; /// Token拦截器 /// Created by guoshuyu /// on 2019/3/23. class ResponseInterceptors extends InterceptorsWrapper { @override onResponse(Response response, handler) async { RequestOptions option = response.requestOptions; dynamic value; try { var header = response.headers[Headers.contentTypeHeader]; if ((header != null && header.toString().contains("text"))) { value = ResultData(response.data, true, Code.SUCCESS); } else if (response.statusCode! >= 200 && response.statusCode! < 300) { value = ResultData(response.data, true, Code.SUCCESS, headers: response.headers); } } catch (e) { printLog(e.toString() + option.path); value = ResultData(response.data, false, response.statusCode, headers: response.headers); } response.data = value; return handler.next(response); } } ================================================ FILE: lib/common/net/interceptors/token_interceptor.dart ================================================ import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/local/local_storage.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/net/graphql/client.dart'; /// Token拦截器 /// Created by guoshuyu /// on 2019/3/23. class TokenInterceptors extends InterceptorsWrapper { String? _token; @override onRequest(RequestOptions options, handler) async { //授权码 if (_token == null) { var authorizationCode = await getAuthorization(); if (authorizationCode != null) { _token = authorizationCode; await initClient(_token); } } if(_token != null) { options.headers["Authorization"] = _token; } return super.onRequest(options, handler); } @override onResponse(Response response, handler) async { try { var responseJson = response.data; if (response.statusCode == 201 && responseJson["token"] != null) { _token = 'token ${responseJson["token"]}'; await LocalStorage.save(Config.TOKEN_KEY, _token); } } catch (e) { printLog(e); } return super.onResponse(response, handler); } ///清除授权 clearAuthorization() { _token = null; LocalStorage.remove(Config.TOKEN_KEY); releaseClient(); } ///获取授权token getAuthorization() async { String? token = await LocalStorage.get(Config.TOKEN_KEY); if (token == null) { String? basic = await LocalStorage.get(Config.USER_BASIC_CODE); if (basic == null) { //提示输入账号密码 } else { //通过 basic 去获取token,获取到设置,返回token return "Basic $basic"; } } else { _token = token; return token; } } } ================================================ FILE: lib/common/net/result_data.dart ================================================ /// 网络结果数据 /// Created by guoshuyu /// Date: 2018-07-16 class ResultData { dynamic data; bool result; int? code; dynamic headers; ResultData(this.data, this.result, this.code, {this.headers}); } ================================================ FILE: lib/common/net/transformer.dart ================================================ import 'package:built_value/iso_8601_date_time_serializer.dart'; import 'package:built_value/serializer.dart'; import 'package:built_value/standard_json_plugin.dart'; import 'package:gsy_github_app_flutter/model/branch.dart'; part 'transformer.g.dart'; @SerializersFor([ Branch, ]) final Serializers serializers = (_$serializers.toBuilder() ..addPlugin(StandardJsonPlugin()) ..add(Iso8601DateTimeSerializer()) ).build(); ================================================ FILE: lib/common/net/transformer.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'transformer.dart'; // ************************************************************************** // BuiltValueGenerator // ************************************************************************** Serializers _$serializers = (Serializers().toBuilder()..add(Branch.serializer)) .build(); // ignore_for_file: deprecated_member_use_from_same_package,type=lint ================================================ FILE: lib/common/net/trending/github_trending.dart ================================================ // ignore_for_file: unnecessary_string_escapes import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/model/trending_repo_model.dart'; import 'package:gsy_github_app_flutter/common/net/api.dart'; import 'package:gsy_github_app_flutter/common/net/code.dart'; import 'package:gsy_github_app_flutter/common/net/result_data.dart'; /// 趋势数据解析 /// Created with guoshuyu /// Date: 2018-07-16 class GitHubTrending { fetchTrending(url) async { var res = await httpManager.netFetch( url, null, null, Options(contentType: "text/plain; charset=utf-8")); if (res != null && res.result && res.data != null) { return ResultData(TrendingUtil.htmlToRepo(res.data), true, Code.SUCCESS); } else { return res; } } } const TAGS = { "meta": { "start": '', "end": 'end' }, "starCount": { "start": '', "end": '' }, "forkCount": { "start": '', "end": '' } }; class TrendingUtil { static htmlToRepo(String responseData) { try { responseData = responseData.replaceAll(RegExp('\n'), ''); // ignore: empty_catches } catch (e) {} var repos = []; var splitWithH3 = responseData.split('', '') + "end"; repo.meta = parseRepoLabelWithTag(repo, metaNoteContent, TAGS["meta"]); repo.starCount = parseRepoLabelWithTag(repo, metaNoteContent, TAGS["starCount"]); repo.forkCount = parseRepoLabelWithTag(repo, metaNoteContent, TAGS["forkCount"]); parseRepoLang(repo, metaNoteContent); parseRepoContributors(repo, metaNoteContent); repos.add(repo); } return repos; } static parseContentWithNote(htmlStr, startFlag, endFlag) { var noteStar = htmlStr.indexOf(startFlag); if (noteStar == -1) { return ''; } else { noteStar += startFlag.length; } var noteEnd = htmlStr.indexOf(endFlag, noteStar); var content = htmlStr.substring(noteStar, noteEnd); return trim(content); } static parseRepoBaseInfo(repo, htmlBaseInfo) { var urlIndex = htmlBaseInfo.indexOf('', urlIndex)); repo.url = url; repo.fullName = url.substring(1, url.length); if (repo.fullName != null && repo.fullName.indexOf('/') != -1) { repo.name = repo.fullName.split('/')[0]; repo.reposName = repo.fullName.split('/')[1]; } String? description = parseContentWithNote( htmlBaseInfo, '

', '

'); if (description != null) { String reg = ".+?"; RegExp tag = RegExp(reg); Iterable tags = tag.allMatches(description); for (Match m in tags) { String match = m .group(0)! .replaceAll(RegExp(""), "") .replaceAll(RegExp(""), ""); description = description?.replaceAll(RegExp(m.group(0)!), match); } } repo.description = description; } static parseRepoLabelWithTag(repo, noteContent, tag) { Object? startFlag; if (TAGS["starCount"] == tag || TAGS["forkCount"] == tag) { startFlag = tag["start"]; } else { startFlag = tag["start"]; } var content = parseContentWithNote(noteContent, startFlag, tag["end"]); if (tag["flag"] != null && content.indexOf(tag["flag"]) != -1 && (content.indexOf(tag["flag"]) + tag["flag"].length <= content.length)) { var metaContent = content.substring( content.indexOf(tag["flag"]) + tag["flag"].length, content.length); return trim(metaContent); } else { return trim(content); } } static parseRepoLang(repo, metaNoteContent) { var content = parseContentWithNote( metaNoteContent, 'programmingLanguage">', ''); repo.language = trim(content); } static parseRepoContributors(TrendingRepoModel repo, htmlContributors) { htmlContributors = parseContentWithNote(htmlContributors, 'Built by', '<\/a>'); var splitWitSemicolon = htmlContributors.split('\"'); if (splitWitSemicolon.length > 1) { repo.contributorsUrl = splitWitSemicolon[1]; } List? contributors = []; for (var i = 0; i < splitWitSemicolon.length; i++) { String url = splitWitSemicolon[i]; if (url.contains('http')) { contributors.add(url); } } repo.contributors = contributors; } static trim(text) { if (text is String) { return text.trim(); } else { return text.toString().trim(); } } } ================================================ FILE: lib/common/repositories/data_result.dart ================================================ class DataResult { Object? data; bool result; Function? next; DataResult(this.data, this.result, {this.next}); } ================================================ FILE: lib/common/repositories/event_repository.dart ================================================ import 'dart:convert'; import 'package:gsy_github_app_flutter/db/provider/event/received_event_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/event/user_event_db_provider.dart'; import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/common/net/address.dart'; import 'package:gsy_github_app_flutter/common/net/api.dart'; class EventRepository { static getEventReceived(String? userName, {page = 1, bool needDb = false}) async { if (userName == null) { return null; } ReceivedEventDbProvider provider = ReceivedEventDbProvider(); next() async { String url = Address.getEventReceived(userName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return null; } if (needDb) { await provider.insert(json.encode(data)); } for (int i = 0; i < data.length; i++) { list.add(Event.fromJson(data[i])); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? dbList = await provider.getEvents(); if (dbList == null || dbList.isEmpty) { return await next(); } DataResult dataResult = DataResult(dbList, true, next: next); return dataResult; } return await next(); } /// 用户行为事件 static getEventRequest(String userName, {page = 0, bool needDb = false}) async { UserEventDbProvider provider = UserEventDbProvider(); next() async { String url = Address.getEvent(userName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(list, true); } if (needDb) { provider.insert(userName, json.encode(data)); } for (int i = 0; i < data.length; i++) { list.add(Event.fromJson(data[i])); } return DataResult(list, true); } else { return null; } } if (needDb) { List? dbList = await provider.getEvents(userName); if (dbList == null || dbList.isEmpty) { return await next(); } DataResult dataResult = DataResult(dbList, true, next: next); return dataResult; } return await next(); } } ================================================ FILE: lib/common/repositories/issue_repository.dart ================================================ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:gsy_github_app_flutter/db/provider/issue/issue_comment_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/issue/issue_detail_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_issue_db_provider.dart'; import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:gsy_github_app_flutter/common/net/address.dart'; import 'package:gsy_github_app_flutter/common/net/api.dart'; /// Issue相关 /// Created by guoshuyu /// Date: 2018-07-19 class IssueRepository { /// 获取仓库issue /// @param page /// @param userName /// @param repository /// @param state issue状态 /// @param sort 排序类型 created updated等 /// @param direction 正序或者倒序 static getRepositoryIssueRequest(String userName, String repository, state, {sort, direction, page = 0, needDb = false}) async { String? fullName = "$userName/$repository"; String dbState = state ?? "*"; RepositoryIssueDbProvider provider = RepositoryIssueDbProvider(); next() async { String url = Address.getReposIssue(userName, repository, state, sort, direction) + Address.getPageParams("&", page); var res = await httpManager.netFetch( url, null, { "Accept": 'application/vnd.github.html,application/vnd.github.VERSION.raw' }, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(Issue.fromJson(data[i])); } if (needDb) { provider.insert(fullName, dbState, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.getData(fullName, dbState); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 搜索仓库issue /// @param q 搜索关键字 /// @param name 用户名 /// @param reposName 仓库名 /// @param page /// @param state 问题状态,all open closed static searchRepositoryRequest(q, name, reposName, state, {page = 1}) async { String? qu; if (state == null || state == 'all') { qu = q + "+repo%3A$name%2F$reposName"; } else { qu = q + "+repo%3A$name%2F$reposName+state%3A$state"; } String url = Address.repositoryIssueSearch(qu) + Address.getPageParams("&", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data["items"]; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(Issue.fromJson(data[i])); } return DataResult(list, true); } else { return DataResult(null, false); } } /// issue的详请 static getIssueInfoRequest(userName, repository, number, {needDb = true}) async { String? fullName = "$userName/$repository"; IssueDetailDbProvider provider = IssueDetailDbProvider(); next() async { String url = Address.getIssueInfo(userName, repository, number); //{"Accept": 'application/vnd.github.html,application/vnd.github.VERSION.raw'} var res = await httpManager.netFetch( url, null, {"Accept": 'application/vnd.github.VERSION.raw'}, null); if (res != null && res.result) { if (needDb) { provider.insert(fullName, number, json.encode(res.data)); } return DataResult(Issue.fromJson(res.data), true); } else { return DataResult(null, false); } } if (needDb) { Issue? issue = await provider.getRepository(fullName, number); if (issue == null) { return await next(); } DataResult dataResult = DataResult(issue, true, next: next); return dataResult; } return await next(); } /// issue的详请列表 static getIssueCommentRequest(userName, repository, number, {page = 0, needDb = false}) async { String? fullName = "$userName/$repository"; IssueCommentDbProvider provider = IssueCommentDbProvider(); next() async { String url = Address.getIssueComment(userName, repository, number) + Address.getPageParams("?", page); //{"Accept": 'application/vnd.github.html,application/vnd.github.VERSION.raw'} var res = await httpManager.netFetch( url, null, {"Accept": 'application/vnd.github.VERSION.raw'}, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } if (needDb) { provider.insert(fullName, number, json.encode(res.data)); } for (int i = 0; i < data.length; i++) { list.add(Issue.fromJson(data[i])); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.getData(fullName, number); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 增加issue的回复 static addIssueCommentRequest(userName, repository, number, comment) async { String url = Address.addIssueComment(userName, repository, number); var res = await httpManager.netFetch( url, {"body": comment}, {"Accept": 'application/vnd.github.VERSION.full+json'}, Options(method: 'POST')); if (res != null && res.result) { return DataResult(res.data, true); } else { return DataResult(null, false); } } /// 编辑issue static editIssueRequest(userName, repository, number, issue) async { String url = Address.editIssue(userName, repository, number); var res = await httpManager.netFetch( url, issue, {"Accept": 'application/vnd.github.VERSION.full+json'}, Options(method: 'PATCH')); if (res != null && res.result) { return DataResult(res.data, true); } else { return DataResult(null, false); } } /// 锁定issue static lockIssueRequest(userName, repository, number, locked) async { String url = Address.lockIssue(userName, repository, number); var res = await httpManager.netFetch( url, null, {"Accept": 'application/vnd.github.VERSION.full+json'}, Options(method: locked ? "DELETE" : 'PUT'), noTip: true); if (res != null && res.result) { return DataResult(res.data, true); } else { return DataResult(null, false); } } /// 创建issue static createIssueRequest(userName, repository, issue) async { String url = Address.createIssue(userName, repository); var res = await httpManager.netFetch( url, issue, {"Accept": 'application/vnd.github.VERSION.full+json'}, Options(method: 'POST')); if (res != null && res.result) { return DataResult(res.data, true); } else { return DataResult(null, false); } } /// 编辑issue回复 static editCommentRequest( userName, repository, number, commentId, comment) async { String url = Address.editComment(userName, repository, commentId); var res = await httpManager.netFetch( url, comment, {"Accept": 'application/vnd.github.VERSION.full+json'}, Options(method: 'PATCH')); if (res != null && res.result) { return DataResult(res.data, true); } else { return DataResult(null, false); } } /// 删除issue回复 static deleteCommentRequest(userName, repository, number, commentId) async { String url = Address.editComment(userName, repository, commentId); var res = await httpManager .netFetch(url, null, null, Options(method: 'DELETE'), noTip: true); if (res != null && res.result) { return DataResult(res.data, true); } else { return DataResult(null, false); } } } ================================================ FILE: lib/common/repositories/repos_repository.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/net/graphql/client.dart'; import 'package:gsy_github_app_flutter/common/net/transformer.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/read_history_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_commits_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_detail_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_detail_readme_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_event_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_fork_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_star_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_watcher_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/trend_repository_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/user/user_repos_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/user/user_stared_db_provider.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/model/branch.dart'; import 'package:gsy_github_app_flutter/model/commits_comparison.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/model/file_model.dart'; import 'package:gsy_github_app_flutter/model/push_commit.dart'; import 'package:gsy_github_app_flutter/model/release.dart'; import 'package:gsy_github_app_flutter/model/repo_commit.dart'; import 'package:gsy_github_app_flutter/model/repository.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:gsy_github_app_flutter/model/trending_repo_model.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/common/net/address.dart'; import 'package:gsy_github_app_flutter/common/net/api.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:pub_semver/pub_semver.dart'; /// Created by guoshuyu /// Date: 2018-07-16 class ReposRepository { /// 趋势数据 /// @param page 分页,趋势数据其实没有分页 /// @param since 数据时长, 本日,本周,本月 /// @param languageType 语言 static getTrendRequest( {since = 'daily', languageType, page = 0, needDb = true}) async { TrendRepositoryDbProvider provider = TrendRepositoryDbProvider(); String languageTypeDb = languageType ?? "*"; next() async { String url = Address.trendingApi(since, languageType); var result = await httpManager.netFetch( url, null, {"api-token": Config.API_TOKEN}, null, noTip: true); if (result != null && result.result && result.data is List) { List list = []; var data = result.data; if (data == null || data.length == 0) { return DataResult(null, false); } if (needDb) { provider.insert("${languageTypeDb}V2", since, json.encode(data)); } for (int i = 0; i < data.length; i++) { TrendingRepoModel model = TrendingRepoModel.fromJson(data[i]); list.add(model); } return DataResult(list, true); } // else { // String url = Address.trending(since, languageType); // var res = await GitHubTrending().fetchTrending(url); // if (res != null && res.result && res.data.length > 0) { // List list = []; // var data = res.data; // if (data == null || data.length == 0) { // return DataResult(null, false); // } // if (needDb) { // provider.insert("${languageTypeDb}V2", since, json.encode(data)); // } // for (int i = 0; i < data.length; i++) { // if(data != null) { // TrendingRepoModel model = data[i]; // list.add(model); // } // } // return DataResult(list, true); // } else { // return DataResult(null, false); // } // } } if (needDb) { List? list = await provider.getData("${languageTypeDb}V2", since); if (list == null || list.isEmpty) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 仓库的详情数据 static getRepositoryDetailRequest(String userName, String reposName, branch, {needDb = true}) async { String? fullName = "$userName/${reposName}v3"; RepositoryDetailDbProvider provider = RepositoryDetailDbProvider(); next() async { var result = await getRepository(userName, reposName); if (result != null && result.data != null) { var data = result.data!["repository"]; if (data == null) { return DataResult(null, false); } var repositoryQL = RepositoryQL.fromMap(data); if (needDb) { provider.insert(fullName, json.encode(data)); } saveHistoryRequest(fullName, DateTime.now(), json.encode(data)); return DataResult(repositoryQL, true); } else { return DataResult(null, false); } } if (needDb) { RepositoryQL? repositoryQL = await provider.getRepository(fullName); if (repositoryQL == null) { return await next(); } DataResult dataResult = DataResult(repositoryQL, true, next: next); return dataResult; } return await next(); } /// 仓库活动事件 static getRepositoryEventRequest(String userName, String reposName, {page = 0, branch = "master", needDb = false}) async { String? fullName = "$userName/$reposName"; RepositoryEventDbProvider provider = RepositoryEventDbProvider(); next() async { String url = Address.getReposEvent(userName, reposName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(Event.fromJson(data[i])); } if (needDb) { provider.insert(fullName, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.getEvents(fullName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 获取用户对当前仓库的star、watcher状态 static getRepositoryStatusRequest(String userName, String reposName) async { String urls = Address.resolveStarRepos(userName, reposName); String urlw = Address.resolveWatcherRepos(userName, reposName); var resS = await httpManager.netFetch(urls, null, null, null, noTip: true); var resW = await httpManager.netFetch(urlw, null, null, null, noTip: true); var data = {"star": resS!.result, "watch": resW!.result}; return DataResult(data, true); } /// 获取仓库的提交列表 static getReposCommitsRequest(String userName, String reposName, {page = 0, branch = "master", needDb = false}) async { String? fullName = "$userName/$reposName"; RepositoryCommitsDbProvider provider = RepositoryCommitsDbProvider(); next() async { String url = Address.getReposCommits(userName, reposName) + Address.getPageParams("?", page) + "&sha=$branch"; var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(RepoCommit.fromJson(data[i])); } if (needDb) { provider.insert(fullName, branch, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.getData(fullName, branch); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// * /// 获取仓库的文件列表 static getReposFileDirRequest(String userName, String reposName, {path = '', branch, text = false, isHtml = false}) async { String url = Address.reposDataDir(userName, reposName, path, branch); var res = await httpManager.netFetch( url, null, //text ? {"Accept": 'application/vnd.github.VERSION.raw'} : {"Accept": 'application/vnd.github.html'}, isHtml ? {"Accept": 'application/vnd.github.html'} : {"Accept": 'application/vnd.github.VERSION.raw'}, Options(contentType: text ? "text" : "json"), ); if (res != null && res.result) { if (text) { return DataResult(res.data, true); } List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } List dirs = []; List files = []; for (int i = 0; i < data.length; i++) { FileModel file = FileModel.fromJson(data[i]); if (file.type == 'file') { files.add(file); } else { dirs.add(file); } } list.addAll(dirs); list.addAll(files); return DataResult(list, true); } else { return DataResult(null, false); } } /// star仓库 static Future doRepositoryStarRequest( String userName, String reposName, star) async { String url = Address.resolveStarRepos(userName, reposName); var res = await httpManager.netFetch( url, null, null, Options(method: !star ? 'PUT' : 'DELETE')); return Future(() { return DataResult(null, res!.result); }); } /// watcher仓库 static doRepositoryWatchRequest( String userName, String reposName, watch) async { String url = Address.resolveWatcherRepos(userName, reposName); var res = await httpManager.netFetch( url, null, null, Options(method: !watch ? 'PUT' : 'DELETE')); return DataResult(null, res!.result); } /// 获取当前仓库所有订阅用户 static getRepositoryWatcherRequest(String userName, String reposName, page, {needDb = false}) async { String? fullName = "$userName/$reposName"; RepositoryWatcherDbProvider provider = RepositoryWatcherDbProvider(); next() async { String url = Address.getReposWatcher(userName, reposName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(User.fromJson(data[i])); } if (needDb) { provider.insert(fullName, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(fullName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 获取当前仓库所有star用户 static getRepositoryStarRequest(String userName, String reposName, page, {needDb = false}) async { String? fullName = "$userName/$reposName"; RepositoryStarDbProvider provider = RepositoryStarDbProvider(); next() async { String url = Address.getReposStar(userName, reposName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(User.fromJson(data[i])); } if (needDb) { provider.insert(fullName, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(fullName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 获取仓库的fork分支 static getRepositoryForksRequest(String userName, String reposName, page, {needDb = false}) async { String? fullName = "$userName/$reposName"; RepositoryForkDbProvider provider = RepositoryForkDbProvider(); next() async { String url = Address.getReposForks(userName, reposName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result && res.data.length > 0) { List list = []; var dataList = res.data; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; list.add(Repository.fromJson(data)); } if (needDb) { provider.insert(fullName, json.encode(dataList)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(fullName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 获取用户所有star static getStarRepositoryRequest(String userName, page, sort, {needDb = false}) async { UserStaredDbProvider provider = UserStaredDbProvider(); next() async { String url = Address.userStar(userName, sort) + Address.getPageParams("&", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result && res.data.length > 0) { List list = []; var dataList = res.data; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; list.add(Repository.fromJson(data)); } if (needDb) { provider.insert(userName, json.encode(dataList)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(userName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 用户的仓库 static getUserRepositoryRequest(String userName, page, sort, {needDb = false}) async { UserReposDbProvider provider = UserReposDbProvider(); next() async { String url = Address.userRepos(userName, sort) + Address.getPageParams("&", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result && res.data.length > 0) { List list = []; var dataList = res.data; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; list.add(Repository.fromJson(data)); } if (needDb) { provider.insert(userName, json.encode(dataList)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(userName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 创建仓库的fork分支 static createForkRequest(String userName, String reposName) async { String url = Address.createFork(userName, reposName); var res = await httpManager.netFetch(url, null, null, Options(method: "POST")); return DataResult(null, res!.result); } /// 获取当前仓库所有分支 static getBranchesRequest(userName, reposName) async { String url = Address.getbranches(userName, reposName); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result && res.data.length > 0) { List list = []; var dataList = res.data; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; ///测试代码 Serializer serializerForType = serializers.serializerForType(Branch) as Serializer; var test = serializers.deserializeWith(serializerForType, data); /// 反序列化 Map result = serializers.serializeWith(serializerForType, test) as Map; printLog("###### $test $result"); list.add(data['name']); } return DataResult(list, true); } else { return DataResult(null, false); } } /// 用户的前100仓库 static getUserRepository100StatusRequest(String userName) async { String url = Address.userRepos(userName, 'pushed') + "&page=1&per_page=100"; var res = await httpManager.netFetch(url, null, null, null); List honorList = []; if (res != null && res.result && res.data.length > 0) { int stared = 0; for (int i = 0; i < res.data.length; i++) { var data = res.data[i]; Repository repository = Repository.fromJson(data); stared += repository.watchersCount!; honorList.add(repository); } //排序 honorList.sort((r1, r2) => r2.watchersCount! - r1.watchersCount!); return DataResult({"stared": stared, "list": honorList}, true); } return DataResult(null, false); } /// 详情的remde数据 static getRepositoryDetailReadmeRequest( String userName, String reposName, branch, {needDb = true}) async { String? fullName = "$userName/$reposName"; RepositoryDetailReadmeDbProvider provider = RepositoryDetailReadmeDbProvider(); next() async { String url = Address.readmeFile('$userName/$reposName', branch); var res = await httpManager.netFetch( url, null, {"Accept": 'application/vnd.github.VERSION.raw'}, Options(contentType: "text/plain; charset=utf-8")); //var res = await httpManager.netFetch(url, null, {"Accept": 'application/vnd.github.html'}, new Options(contentType: ContentType.text)); if (res != null && res.result) { if (needDb) { provider.insert(fullName, branch, res.data); } return DataResult(res.data, true); } return DataResult(null, false); } if (needDb) { String? readme = await provider.getRepositoryReadme(fullName, branch); if (readme == null) { return await next(); } DataResult dataResult = DataResult(readme, true, next: next); return dataResult; } return await next(); } /// 搜索仓库 /// @param q 搜索关键字 /// @param sort 分类排序,beat match、most star等 /// @param order 倒序或者正序 /// @param type 搜索类型,人或者仓库 null \ 'user', /// @param page /// @param pageSize static searchRepositoryRequest( q, language, sort, order, type, page, pageSize) async { if (language != null) { q = q + "%2Blanguage%3A$language"; } String url = Address.search(q, sort, order, type, page, pageSize); var res = await httpManager.netFetch(url, null, null, null); if (type == null) { if (res != null && res.result && res.data["items"] != null) { List list = []; var dataList = res.data["items"]; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; list.add(Repository.fromJson(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } else { if (res != null && res.result && res.data["items"] != null) { List list = []; var data = res.data["items"]; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(User.fromJson(data[i])); } return DataResult(list, true); } else { return DataResult(null, false); } } } /// 获取仓库的单个提交详情 static getReposCommitsInfoRequest( String userName, String reposName, sha) async { String url = Address.getReposCommitsInfo(userName, reposName, sha); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { PushCommit pushCommit = PushCommit.fromJson(res.data); return DataResult(pushCommit, true); } else { return DataResult(null, false); } } /// 获取两个提交之间的比较信息 static getReposCompareRequest( String userName, String reposName, String base, String head) async { String url = Address.getReposCompare(userName, reposName, base, head); var res = await httpManager.netFetch(url, null, null, null, noTip: true); if (res != null && res.result) { CommitsComparison comparison = CommitsComparison.fromJson(res.data); return DataResult(comparison, true); } else { return DataResult(null, false); } } /// 获取仓库的release列表 static getRepositoryReleaseRequest(String userName, String reposName, page, {needHtml = true, release = true}) async { String url = release ? Address.getReposRelease(userName, reposName) + Address.getPageParams("?", page) : Address.getReposTag(userName, reposName) + Address.getPageParams("?", page); var res = await httpManager.netFetch( url, null, { "Accept": 'application/vnd.github.html,application/vnd.github.VERSION.raw' }, null); if (res != null && res.result && res.data.length > 0) { List list = []; var dataList = res.data; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; list.add(Release.fromJson(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } /// 版本更新 static getNewsVersion(BuildContext context, showTip) async { //ios不检查更新 if (Platform.isIOS) { return; } var res = await getRepositoryReleaseRequest( "CarGuo", 'gsy_github_app_flutter', 1, needHtml: false); if (res != null && res.result && res.data.length > 0) { Release release = res.data[0]; String? versionName = release.name; if (versionName != null) { if (Config.DEBUG!) { printLog("versionName $versionName"); } PackageInfo packageInfo = await PackageInfo.fromPlatform(); var appVersion = packageInfo.version; if (Config.DEBUG!) { printLog("appVersion $appVersion"); } Version versionNameNum = Version.parse(versionName); Version currentNum = Version.parse(appVersion); int result = versionNameNum.compareTo(currentNum); if (Config.DEBUG!) { printLog("versionNameNum $versionNameNum currentNum $currentNum"); } if (Config.DEBUG!) { printLog("newsHad $result"); } if (result > 0) { if (!context.mounted) return; CommonUtils.showUpdateDialog( context, "${release.name!}: ${release.body!}"); } else { if (showTip) { if (!context.mounted) return; showToast(context.l10n.app_not_new_version); } } } } } /// 获取issue总数 static getRepositoryIssueStatusRequest( String userName, String repository) async { String url = Address.getReposIssue(userName, repository, null, null, null) + "&per_page=1"; var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result && res.headers != null) { try { StringList? link = res.headers['link']; if (link != null) { var [linkFirst, _] = link; int indexStart = linkFirst.lastIndexOf("page=") + 5; int indexEnd = linkFirst.lastIndexOf(">"); if (indexStart >= 0 && indexEnd >= 0) { String count = linkFirst.substring(indexStart, indexEnd); return DataResult(count, true); } } } catch (e) { printLog(e); } } return DataResult(null, false); } /// 搜索话题 static searchTopicRepositoryRequest(searchTopic, {page = 0}) async { String url = Address.searchTopic(searchTopic) + Address.getPageParams("&", page); var res = await httpManager.netFetch(url, null, null, null); var data = (res!.data != null && res.data["items"] != null) ? res.data["items"] : res.data; if (res.result && data != null && data.length > 0) { List list = []; var dataList = data; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } for (int i = 0; i < dataList.length; i++) { var data = dataList[i]; list.add(Repository.fromJson(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } /// 获取阅读历史 static getHistoryRequest(page) async { ReadHistoryDbProvider provider = ReadHistoryDbProvider(); List? list = await provider.geData(page); if (list == null || list.isEmpty) { return DataResult(null, false); } return DataResult(list, true); } /// 保存阅读历史 static saveHistoryRequest(String? fullName, DateTime dateTime, String data) { ReadHistoryDbProvider provider = ReadHistoryDbProvider(); provider.insert(fullName, dateTime, data); } } ================================================ FILE: lib/common/repositories/user_repository.dart ================================================ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/net/graphql/client.dart'; import 'package:gsy_github_app_flutter/db/provider/user/user_followed_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/user/user_follower_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/user/userinfo_db_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/user/user_orgs_db_provider.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/config/ignoreConfig.dart'; import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/common/local/local_storage.dart'; import 'package:gsy_github_app_flutter/model/notification.dart' as Model; import 'package:gsy_github_app_flutter/model/search_user_ql.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/model/user_org.dart'; import 'package:gsy_github_app_flutter/common/net/address.dart'; import 'package:gsy_github_app_flutter/common/net/api.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/redux/user_redux.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:redux/redux.dart'; class UserRepository { static oauth(code, store) async { httpManager.clearAuthorization(); var res = await httpManager.netFetch( "https://github.com/login/oauth/access_token?" "client_id=${NetConfig.CLIENT_ID}" "&client_secret=${NetConfig.CLIENT_SECRET}" "&code=$code", null, null, Options(method: "POST"), ); dynamic resultData; if (res != null && res.result) { var result = Uri.parse("gsy://oauth?${res.data}"); var token = result.queryParameters["access_token"]!; var token0 = 'token $token'; await LocalStorage.save(Config.TOKEN_KEY, token0); resultData = await getUserInfo(null); if (Config.DEBUG!) { printLog("user result ${resultData.result}"); printLog(resultData.data); printLog(res.data.toString()); } if (resultData.result == true) { store.dispatch(UpdateUserAction(resultData.data)); } } return DataResult(resultData, res!.result); } static login(String userName, String password, store) async { String type = "$userName:$password"; var bytes = utf8.encode(type); var base64Str = base64.encode(bytes); if (Config.DEBUG!) { printLog("base64Str login $base64Str"); } await LocalStorage.save(Config.USER_NAME_KEY, userName); await LocalStorage.save(Config.USER_BASIC_CODE, base64Str); Map requestParams = { "scopes": ['user', 'repo', 'gist', 'notifications'], "note": "admin_script", "client_id": NetConfig.CLIENT_ID, "client_secret": NetConfig.CLIENT_SECRET, }; httpManager.clearAuthorization(); var res = await httpManager.netFetch( Address.getAuthorization(), json.encode(requestParams), null, Options(method: "post"), ); dynamic resultData; if (res != null && res.result) { await LocalStorage.save(Config.PW_KEY, password); var resultData = await getUserInfo(null); if (Config.DEBUG!) { printLog("user result ${resultData.result}"); printLog(resultData.data); printLog(res.data.toString()); } store.dispatch(UpdateUserAction(resultData.data)); } return DataResult(resultData, res!.result); } ///初始化用户信息 static initUserInfo(Store store, WidgetRef ref) async { var token = await LocalStorage.get(Config.TOKEN_KEY); var res = await getUserInfoLocal(); if (res != null && res.result && token != null) { store.dispatch(UpdateUserAction(res.data)); } ///读取主题 String? themeIndex = await LocalStorage.get(Config.THEME_COLOR); ref.read(appThemeStateProvider.notifier).pushTheme(themeIndex); ///切换语言 String? localeIndex = await LocalStorage.get(Config.LOCALE); ref.read(appLocalStateProvider.notifier).changeLocale(localeIndex); ///震动开关 String? vibrationEnable = await LocalStorage.get(Config.VIBRATION_ENABLE); bool enable = vibrationEnable != "false"; ref .read(appVibrationStateProvider.notifier) .changeVibration(enable, save: false); return DataResult(res.data, (res.result && (token != null))); } ///获取本地登录用户信息 static getUserInfoLocal() async { var userText = await LocalStorage.get(Config.USER_INFO); if (userText != null) { var userMap = json.decode(userText); User user = User.fromJson(userMap); return DataResult(user, true); } else { return DataResult(null, false); } } ///获取用户详细信息 static getUserInfo(String? userName, {needDb = false}) async { UserInfoDbProvider provider = UserInfoDbProvider(); next() async { dynamic res; if (userName == null) { res = await httpManager.netFetch( Address.getMyUserInfo(), null, null, null, ); } else { res = await httpManager.netFetch( Address.getUserInfo(userName), null, null, null, ); } if (res != null && res.result) { String? starred = "---"; if (res.data["type"] != "Organization") { var countRes = await getUserStaredCountNet(res.data["login"]); if (countRes.result) { starred = countRes.data; } } User user = User.fromJson(res.data); user.starred = starred; if (userName == null) { LocalStorage.save(Config.USER_INFO, json.encode(user.toJson())); } else { if (needDb) { provider.insert(userName, json.encode(user.toJson())); } } return DataResult(user, true); } else { return DataResult(res.data, false); } } if (needDb) { User? user = await provider.getUserInfo(userName); if (user == null) { return await next(); } DataResult dataResult = DataResult(user, true, next: next); return dataResult; } return await next(); } static clearAll(Store store) async { httpManager.clearAuthorization(); LocalStorage.remove(Config.USER_INFO); store.dispatch(UpdateUserAction(User.empty())); } /// 在header中提起stared count static getUserStaredCountNet(String userName) async { String url = Address.userStar(userName, null) + "&per_page=1"; var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result && res.headers != null) { try { StringList? link = res.headers['link']; if (link != null) { var [linkFirst] = link; int indexStart = linkFirst.lastIndexOf("page=") + 5; int indexEnd = linkFirst.lastIndexOf(">"); if (indexStart >= 0 && indexEnd >= 0) { String count = linkFirst.substring(indexStart, indexEnd); return DataResult(count, true); } } } catch (e) { printLog(e); } } return DataResult(null, false); } /// 获取用户粉丝列表 static getFollowerListRequest(String userName, page, {needDb = false}) async { UserFollowerDbProvider provider = UserFollowerDbProvider(); next() async { String url = Address.getUserFollower(userName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(User.fromJson(data[i])); } if (needDb) { provider.insert(userName, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(userName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 获取用户关注列表 static getFollowedListRequest(String userName, page, {needDb = false}) async { UserFollowedDbProvider provider = UserFollowedDbProvider(); next() async { String url = Address.getUserFollow(userName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(User.fromJson(data[i])); } if (needDb) { provider.insert(userName, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(userName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } /// 获取用户相关通知 static getNotifyRequest(bool all, bool participating, page) async { String tag = (!all && !participating) ? '?' : "&"; String url = Address.getNotifation(all, participating) + Address.getPageParams(tag, page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult([], true); } for (int i = 0; i < data.length; i++) { list.add(Model.Notification.fromJson(data[i])); } return DataResult(list, true); } else { return DataResult(null, false); } } /// 设置单个通知已读 static setNotificationAsReadRequest(id) async { String url = Address.setNotificationAsRead(id); var res = await httpManager.netFetch( url, null, null, Options(method: "PATCH"), noTip: true, ); return res; } /// 设置所有通知已读 static setAllNotificationAsReadRequest() async { String url = Address.setAllNotificationAsRead(); var res = await httpManager.netFetch( url, null, null, Options(method: "PUT"), ); return DataResult(res!.data, res.result); } /// 检查用户关注状态 static checkFollowRequest(String name) async { String url = Address.doFollow(name); var res = await httpManager.netFetch(url, null, null, null, noTip: true); return DataResult(res!.data, res.result); } /// 关注用户 static doFollowRequest(String name, bool followed) async { String url = Address.doFollow(name); var res = await httpManager.netFetch( url, null, null, Options(method: !followed ? "PUT" : "DELETE"), noTip: true, ); return DataResult(res!.data, res.result); } /// 组织成员 static getMemberRequest(String userName, page) async { String url = Address.getMember(userName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(User.fromJson(data[i])); } return DataResult(list, true); } else { return DataResult(null, false); } } /// 更新用户信息 static updateUserRequest(params, Store store) async { String url = Address.getMyUserInfo(); var res = await httpManager.netFetch( url, params, null, Options(method: "PATCH"), ); if (res != null && res.result) { var localResult = await getUserInfoLocal(); User newUser = User.fromJson(res.data); newUser.starred = localResult.data.starred; await LocalStorage.save(Config.USER_INFO, json.encode(newUser.toJson())); store.dispatch(UpdateUserAction(newUser)); return DataResult(newUser, true); } return DataResult(null, false); } /// 获取用户组织 static getUserOrgsRequest(String userName, page, {needDb = false}) async { UserOrgsDbProvider provider = UserOrgsDbProvider(); next() async { String url = Address.getUserOrgs(userName) + Address.getPageParams("?", page); var res = await httpManager.netFetch(url, null, null, null); if (res != null && res.result) { List list = []; var data = res.data; if (data == null || data.length == 0) { return DataResult(null, false); } for (int i = 0; i < data.length; i++) { list.add(UserOrg.fromJson(data[i])); } if (needDb) { provider.insert(userName, json.encode(data)); } return DataResult(list, true); } else { return DataResult(null, false); } } if (needDb) { List? list = await provider.geData(userName); if (list == null) { return await next(); } DataResult dataResult = DataResult(list, true, next: next); return dataResult; } return await next(); } static searchTrendUserRequest(String location, {String? cursor}) async { var result = await getTrendUser(location, cursor: cursor); if (result != null && result.data != null) { var endCursor = result.data!["search"]["pageInfo"]["endCursor"]; var dataList = result.data!["search"]["user"]; if (dataList == null || dataList.length == 0) { return DataResult(null, false); } List dataResult = []; dataList.forEach((item) { var userModel = SearchUserQL.fromMap(item["user"]); dataResult.add(userModel); }); return DataResult((dataResult, endCursor), true); } else { return DataResult(null, false); } } } ================================================ FILE: lib/common/router/anima_route.dart ================================================ import 'package:flutter/material.dart'; ///动画大小变化打开的路由 class SizeRoute extends PageRouteBuilder { final Widget? widget; SizeRoute({this.widget}) : super( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) => widget!, transitionsBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) { var begin = 0.0; var end = 1.0; var curve = Curves.ease; var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); return Align( child: SizeTransition( sizeFactor: animation.drive(tween), child: child, ), ); }, ); } class NoAnimationRoute extends PageRouteBuilder { final Widget? widget; NoAnimationRoute({this.widget}) : super( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) => widget!, transitionsBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) => SlideTransition( position: Tween( begin: const Offset(0.0, 0.0), end: const Offset(0.0, 0.0), ).animate(animation), child: child, ), ); } ================================================ FILE: lib/common/style/gsy_style.dart ================================================ import 'package:flutter/material.dart'; ///颜色 class GSYColors { static const int primaryIntValue = 0xFF24292E; static const MaterialColor primarySwatch = MaterialColor( primaryIntValue, { 50: Color(primaryIntValue), 100: Color(primaryIntValue), 200: Color(primaryIntValue), 300: Color(primaryIntValue), 400: Color(primaryIntValue), 500: Color(primaryIntValue), 600: Color(primaryIntValue), 700: Color(primaryIntValue), 800: Color(primaryIntValue), 900: Color(primaryIntValue), }, ); static const String primaryValueString = "#24292E"; static const String primaryLightValueString = "#42464b"; static const String primaryDarkValueString = "#121917"; static const String miWhiteString = "#ececec"; static const String actionBlueString = "#267aff"; static const String webDraculaBackgroundColorString = "#282a36"; static const Color primaryValue = Color(0xFF24292E); static const Color primaryLightValue = Color(0xFF42464b); static const Color primaryDarkValue = Color(0xFF121917); static const Color cardWhite = Color(0xFFFFFFFF); static const Color textWhite = Color(0xFFFFFFFF); static const Color miWhite = Color(0xffececec); static const Color white = Color(0xFFFFFFFF); static const Color actionBlue = Color(0xff267aff); static const Color subTextColor = Color(0xff959595); static const Color subLightTextColor = Color(0xffc4c4c4); static const Color mainBackgroundColor = miWhite; static const Color mainTextColor = primaryDarkValue; static const Color textColorWhite = white; } ///文本样式 class GSYConstant { static const String app_default_share_url = "https://github.com/CarGuo/gsy_github_app_flutter"; static const lagerTextSize = 30.0; static const bigTextSize = 23.0; static const normalTextSize = 18.0; static const middleTextWhiteSize = 16.0; static const smallTextSize = 14.0; static const minTextSize = 12.0; static const minText = TextStyle( color: GSYColors.subLightTextColor, fontSize: minTextSize, ); static const smallTextWhite = TextStyle( color: GSYColors.textColorWhite, fontSize: smallTextSize, ); static const smallText = TextStyle( color: GSYColors.mainTextColor, fontSize: smallTextSize, ); static const smallTextBold = TextStyle( color: GSYColors.mainTextColor, fontSize: smallTextSize, fontWeight: FontWeight.bold, ); static const smallSubLightText = TextStyle( color: GSYColors.subLightTextColor, fontSize: smallTextSize, ); static const smallActionLightText = TextStyle( color: GSYColors.actionBlue, fontSize: smallTextSize, ); static const smallMiLightText = TextStyle( color: GSYColors.miWhite, fontSize: smallTextSize, ); static const smallSubText = TextStyle( color: GSYColors.subTextColor, fontSize: smallTextSize, ); static const middleText = TextStyle( color: GSYColors.mainTextColor, fontSize: middleTextWhiteSize, ); static const middleTextWhite = TextStyle( color: GSYColors.textColorWhite, fontSize: middleTextWhiteSize, ); static const middleSubText = TextStyle( color: GSYColors.subTextColor, fontSize: middleTextWhiteSize, ); static const middleSubLightText = TextStyle( color: GSYColors.subLightTextColor, fontSize: middleTextWhiteSize, ); static const middleTextBold = TextStyle( color: GSYColors.mainTextColor, fontSize: middleTextWhiteSize, fontWeight: FontWeight.bold, ); static const middleTextWhiteBold = TextStyle( color: GSYColors.textColorWhite, fontSize: middleTextWhiteSize, fontWeight: FontWeight.bold, ); static const middleSubTextBold = TextStyle( color: GSYColors.subTextColor, fontSize: middleTextWhiteSize, fontWeight: FontWeight.bold, ); static const normalText = TextStyle( color: GSYColors.mainTextColor, fontSize: normalTextSize, ); static const normalTextBold = TextStyle( color: GSYColors.mainTextColor, fontSize: normalTextSize, fontWeight: FontWeight.bold, ); static const normalSubText = TextStyle( color: GSYColors.subTextColor, fontSize: normalTextSize, ); static const normalTextWhite = TextStyle( color: GSYColors.textColorWhite, fontSize: normalTextSize, ); static const normalTextMitWhiteBold = TextStyle( color: GSYColors.miWhite, fontSize: normalTextSize, fontWeight: FontWeight.bold, ); static const normalTextActionWhiteBold = TextStyle( color: GSYColors.actionBlue, fontSize: normalTextSize, fontWeight: FontWeight.bold, ); static const normalTextLight = TextStyle( color: GSYColors.primaryLightValue, fontSize: normalTextSize, ); static const largeText = TextStyle( color: GSYColors.mainTextColor, fontSize: bigTextSize, ); static const largeTextBold = TextStyle( color: GSYColors.mainTextColor, fontSize: bigTextSize, fontWeight: FontWeight.bold, ); static const largeTextWhite = TextStyle( color: GSYColors.textColorWhite, fontSize: bigTextSize, ); static const largeTextWhiteBold = TextStyle( color: GSYColors.textColorWhite, fontSize: bigTextSize, fontWeight: FontWeight.bold, ); static const largeLargeTextWhite = TextStyle( color: GSYColors.textColorWhite, fontSize: lagerTextSize, fontWeight: FontWeight.bold, ); static const largeLargeText = TextStyle( color: GSYColors.primaryValue, fontSize: lagerTextSize, fontWeight: FontWeight.bold, ); } class GSYICons { static const String FONT_FAMILY = 'wxcIconFont'; static const String DEFAULT_USER_ICON = 'static/images/logo.png'; static const String DEFAULT_IMAGE = 'static/images/default_img.png'; static const String DEFAULT_REMOTE_PIC = 'http://img.cdn.guoshuyu.cn/gsy_github_app_logo.png'; static const IconData HOME = IconData(0xe624, fontFamily: GSYICons.FONT_FAMILY); static const IconData MORE = IconData(0xe674, fontFamily: GSYICons.FONT_FAMILY); static const IconData SEARCH = IconData(0xe61c, fontFamily: GSYICons.FONT_FAMILY); static const IconData MAIN_DT = IconData(0xe684, fontFamily: GSYICons.FONT_FAMILY); static const IconData MAIN_QS = IconData(0xe818, fontFamily: GSYICons.FONT_FAMILY); static const IconData MAIN_MY = IconData(0xe6d0, fontFamily: GSYICons.FONT_FAMILY); static const IconData MAIN_SEARCH = IconData(0xe61c, fontFamily: GSYICons.FONT_FAMILY); static const IconData LOGIN_USER = IconData(0xe666, fontFamily: GSYICons.FONT_FAMILY); static const IconData LOGIN_PW = IconData(0xe60e, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_USER = IconData(0xe63e, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_STAR = IconData(0xe643, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_FORK = IconData(0xe67e, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_ISSUE = IconData(0xe661, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_STARED = IconData(0xe698, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_WATCH = IconData(0xe681, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_WATCHED = IconData(0xe629, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_DIR = Icons.folder; static const IconData REPOS_ITEM_FILE = IconData(0xea77, fontFamily: GSYICons.FONT_FAMILY); static const IconData REPOS_ITEM_NEXT = IconData(0xe610, fontFamily: GSYICons.FONT_FAMILY); static const IconData USER_ITEM_COMPANY = IconData(0xe63e, fontFamily: GSYICons.FONT_FAMILY); static const IconData USER_ITEM_LOCATION = IconData(0xe7e6, fontFamily: GSYICons.FONT_FAMILY); static const IconData USER_ITEM_LINK = IconData(0xe670, fontFamily: GSYICons.FONT_FAMILY); static const IconData USER_NOTIFY = IconData(0xe600, fontFamily: GSYICons.FONT_FAMILY); static const IconData ISSUE_ITEM_ISSUE = IconData(0xe661, fontFamily: GSYICons.FONT_FAMILY); static const IconData ISSUE_ITEM_COMMENT = IconData(0xe6ba, fontFamily: GSYICons.FONT_FAMILY); static const IconData ISSUE_ITEM_ADD = IconData(0xe662, fontFamily: GSYICons.FONT_FAMILY); static const IconData ISSUE_EDIT_H1 = Icons.filter_1; static const IconData ISSUE_EDIT_H2 = Icons.filter_2; static const IconData ISSUE_EDIT_H3 = Icons.filter_3; static const IconData ISSUE_EDIT_BOLD = Icons.format_bold; static const IconData ISSUE_EDIT_ITALIC = Icons.format_italic; static const IconData ISSUE_EDIT_QUOTE = Icons.format_quote; static const IconData ISSUE_EDIT_CODE = Icons.format_shapes; static const IconData ISSUE_EDIT_LINK = Icons.insert_link; static const IconData NOTIFY_ALL_READ = IconData(0xe62f, fontFamily: GSYICons.FONT_FAMILY); static const IconData PUSH_ITEM_EDIT = Icons.mode_edit; static const IconData PUSH_ITEM_ADD = Icons.add_box; static const IconData PUSH_ITEM_MIN = Icons.indeterminate_check_box; } ================================================ FILE: lib/common/toast.dart ================================================ import 'package:fluttertoast/fluttertoast.dart'; showToast(String message) { Fluttertoast.showToast( msg: message, gravity: ToastGravity.CENTER, toastLength: Toast.LENGTH_LONG); } ================================================ FILE: lib/common/utils/code_utils.dart ================================================ import 'dart:convert'; ///isolate 的 compute 需要静态方法 class CodeUtils { static List decodeListResult(String? data) { return json.decode(data!); } static Map decodeMapResult(String? data) { return json.decode(data!); } static String encodeToString(String data) { return json.encode(data); } } ================================================ FILE: lib/common/utils/common_utils.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/local/local_storage.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/net/address.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_flex_button.dart'; import 'package:gsy_github_app_flutter/page/issue/issue_edit_dIalog.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:url_launcher/url_launcher.dart'; /// 通用逻辑 /// Created by guoshuyu /// Date: 2018-07-16 typedef StringList = List; class CommonUtils { static const double MILLIS_LIMIT = 1000.0; static const double SECONDS_LIMIT = 60 * MILLIS_LIMIT; static const double MINUTES_LIMIT = 60 * SECONDS_LIMIT; static const double HOURS_LIMIT = 24 * MINUTES_LIMIT; static const double DAYS_LIMIT = 30 * HOURS_LIMIT; static Locale? curLocale; static String getDateStr(DateTime? date) { if (date == null || date.toString() == "") { return ""; } else if (date.toString().length < 10) { return date.toString(); } return date.toString().substring(0, 10); } static String getUserChartAddress(String userName) { return "${Address.graphicHost}${GSYColors.primaryValueString.replaceAll("#", "")}/$userName"; } ///日期格式转换 static String getNewsTimeStr(DateTime date) { int subTimes = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch; return switch (subTimes) { < MILLIS_LIMIT => (curLocale != null) ? (curLocale!.languageCode != "zh") ? "right now" : "刚刚" : "刚刚", < SECONDS_LIMIT => (subTimes / MILLIS_LIMIT).round().toString() + ((curLocale != null) ? (curLocale!.languageCode != "zh") ? " seconds ago" : " 秒前" : " 秒前"), < MINUTES_LIMIT => (subTimes / SECONDS_LIMIT).round().toString() + ((curLocale != null) ? (curLocale!.languageCode != "zh") ? " min ago" : " 分钟前" : " 分钟前"), < HOURS_LIMIT => (subTimes / MINUTES_LIMIT).round().toString() + ((curLocale != null) ? (curLocale!.languageCode != "zh") ? " hours ago" : " 小时前" : " 小时前"), < DAYS_LIMIT => (subTimes / HOURS_LIMIT).round().toString() + ((curLocale != null) ? (curLocale!.languageCode != "zh") ? " days ago" : " 天前" : " 天前"), _ => getDateStr(date) }; } static getLocalPath() async { Directory? appDir; if (Platform.isIOS) { appDir = await getApplicationDocumentsDirectory(); } else { appDir = await getExternalStorageDirectory(); } var status = await Permission.storage.status; if (status != PermissionStatus.granted) { Map statuses = await [ Permission.storage, ].request(); if (statuses[Permission.storage] != PermissionStatus.granted) { return null; } } String appDocPath = "${appDir!.path}/gsygithubappflutter"; Directory appPath = Directory(appDocPath); await appPath.create(recursive: true); return appPath; } static getApplicationDocumentsPath() async { Directory appDir; if (Platform.isIOS) { appDir = await getApplicationDocumentsDirectory(); } else { appDir = await getApplicationSupportDirectory(); } String appDocPath = "${appDir.path}/gsygithubappflutter"; Directory appPath = Directory(appDocPath); await appPath.create(recursive: true); return appPath.path; } static String? removeTextTag(String? description) { if (description != null) { String reg = ".+?"; RegExp tag = RegExp(reg); Iterable tags = tag.allMatches(description); for (Match m in tags) { String match = m .group(0)! .replaceAll(RegExp(""), "") .replaceAll(RegExp(""), ""); description = description!.replaceAll(RegExp(m.group(0)!), match); } } return description; } /*static saveImage(String url) async { Future _findPath(String imageUrl) async { final file = await Cache.DefaultCacheManager().getSingleFile(url); if (file == null) { return null; } Directory localPath = await CommonUtils.getLocalPath(); if (localPath == null) { return null; } final name = splitFileNameByPath(file.path); final result = await file.copy(localPath.path + name); return result.path; } return _findPath(url); }*/ static splitFileNameByPath(String path) { return path.substring(path.lastIndexOf("/")); } static getFullName(String? repository_url) { if (repository_url != null && repository_url.substring(repository_url.length - 1) == "/") { repository_url = repository_url.substring(0, repository_url.length - 1); } String fullName = ''; if (repository_url != null) { StringList splicurl = repository_url.split("/"); if (splicurl.length > 2) { fullName = "${splicurl[splicurl.length - 2]}/${splicurl[splicurl.length - 1]}"; } } return fullName; } static getThemeData(Color color) { return ThemeData( useMaterial3: false, ///用来适配 Theme.of(context).primaryColorLight 和 primaryColorDark 的颜色变化,不设置可能会是默认蓝色 primarySwatch: color as MaterialColor, /// Card 在 M3 下,会有 apply Overlay colorScheme: ColorScheme.fromSeed( seedColor: color, primary: color, brightness: Brightness.light, ///影响 card 的表色,因为 M3 下是 applySurfaceTint ,在 Material 里 surfaceTint: Colors.transparent, ), /// 受到 iconThemeData.isConcrete 的印象,需要全参数才不会进入 fallback iconTheme: const IconThemeData( size: 24.0, fill: 0.0, weight: 400.0, grade: 0.0, opticalSize: 48.0, color: Colors.white, opacity: 0.8, ), ///修改 FloatingActionButton的默认主题行为 floatingActionButtonTheme: FloatingActionButtonThemeData( foregroundColor: Colors.white, backgroundColor: color, shape: const CircleBorder()), appBarTheme: AppBarTheme( iconTheme: const IconThemeData( color: Colors.white, size: 24.0, ), backgroundColor: color, titleTextStyle: Typography.dense2021.titleLarge, systemOverlayStyle: SystemUiOverlayStyle.light, ), // 如果需要去除对应的水波纹效果 // splashFactory: NoSplash.splashFactory, // textButtonTheme: TextButtonThemeData( // style: ButtonStyle(splashFactory: NoSplash.splashFactory), // ), // elevatedButtonTheme: ElevatedButtonThemeData( // style: ButtonStyle(splashFactory: NoSplash.splashFactory), // ), ); } static showLanguageDialog(WidgetRef ref) { StringList list = [ ref.context.l10n.home_language_default, ref.context.l10n.home_language_zh, ref.context.l10n.home_language_en, ref.context.l10n.home_language_ko, ref.context.l10n.home_language_ja, ]; CommonUtils.showCommitOptionDialog(ref.context, list, (index) { ref.read(appLocalStateProvider.notifier).changeLocale(index.toString()); LocalStorage.save(Config.LOCALE, index.toString()); }, height: 300.0); } ///获取设备信息 static Future getDeviceInfo() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { return ""; } IosDeviceInfo iosInfo = await deviceInfo.iosInfo; return iosInfo.model; } static List getThemeListColor() { return [ GSYColors.primarySwatch, Colors.brown, Colors.blue, Colors.teal, Colors.amber, Colors.blueGrey, Colors.deepOrange, ]; } static const IMAGE_END = [".png", ".jpg", ".jpeg", ".gif", ".svg"]; static isImageEnd(path) { bool image = false; for (String item in IMAGE_END) { if (path.indexOf(item) + item.length == path.length) { image = true; } } return image; } static copy(String? data, BuildContext context) { if (data != null) { Clipboard.setData(ClipboardData(text: data)); showToast(context.l10n.option_share_copy_success); } } static gsyLaunchUrl(BuildContext context, String? url) { if (url == null && url!.isEmpty) return; Uri parseUrl = Uri.parse(url); bool isImage = isImageEnd(parseUrl.toString()); if (parseUrl.toString().endsWith("?raw=true")) { isImage = isImageEnd(parseUrl.toString().replaceAll("?raw=true", "")); } if (isImage) { NavigatorUtils.gotoPhotoViewPage(context, url); return; } if (parseUrl.host == "github.com" && parseUrl.path.isNotEmpty) { StringList pathnames = parseUrl.path.split("/"); switch (pathnames.length) { case == 2: //解析人 String userName = pathnames[1]; NavigatorUtils.goPerson(context, userName); break; case >= 3: //解析仓库 if (pathnames.length == 3) { var [_, userName, repoName] = pathnames; NavigatorUtils.goReposDetail(context, userName, repoName); } else { launchWebView(context, "", url); } break; } } else if (url.startsWith("http")) { launchWebView(context, "", url); } } static void launchWebView(BuildContext context, String? title, String url) { if (url.startsWith("http")) { NavigatorUtils.goGSYWebView(context, url, title); } else { NavigatorUtils.goGSYWebView( context, Uri.dataFromString(url, mimeType: 'text/html', encoding: Encoding.getByName("utf-8")) .toString(), title); } } static launchOutURL(String? url, BuildContext context) async { if (url != null && await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } else { showToast( // ignore: use_build_context_synchronously "${context.l10n.option_web_launcher_error}: ${url ?? ""}"); } } static Future showLoadingDialog(BuildContext context) { return NavigatorUtils.showGSYDialog( context: context, builder: (BuildContext context) { return Material( color: Colors.transparent, child: PopScope( canPop: false, child: Center( child: Container( width: 200.0, height: 200.0, padding: const EdgeInsets.all(4.0), decoration: const BoxDecoration( color: Colors.transparent, //用一个BoxDecoration装饰器提供背景图片 borderRadius: BorderRadius.all(Radius.circular(4.0)), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SpinKitCubeGrid(color: GSYColors.white), Container(height: 10.0), Text(context.l10n.loading_text, style: GSYConstant.normalTextWhite), ], ), ), ), )); }); } static Future showEditDialog( BuildContext context, String dialogTitle, ValueChanged? onTitleChanged, ValueChanged onContentChanged, VoidCallback onPressed, { TextEditingController? titleController, TextEditingController? valueController, bool needTitle = true, String? hintText, }) { return NavigatorUtils.showGSYDialog( context: context, builder: (BuildContext context) { return Center( child: IssueEditDialog( dialogTitle, onTitleChanged, onContentChanged, onPressed, titleController: titleController, valueController: valueController, needTitle: needTitle, hintText: hintText, ), ); }); } ///列表item dialog static Future showCommitOptionDialog( BuildContext context, List? commitMaps, ValueChanged onTap, { width = 250.0, height = 400.0, List? colorList, }) { return NavigatorUtils.showGSYDialog( context: context, builder: (BuildContext context) { return Center( child: Container( width: width, height: height, padding: const EdgeInsets.all(4.0), margin: const EdgeInsets.all(20.0), decoration: const BoxDecoration( color: GSYColors.white, //用一个BoxDecoration装饰器提供背景图片 borderRadius: BorderRadius.all(Radius.circular(4.0)), ), child: ListView.builder( itemCount: commitMaps?.length ?? 0, itemBuilder: (context, index) { return GSYFlexButton( maxLines: 1, mainAxisAlignment: MainAxisAlignment.start, fontSize: 14.0, color: colorList != null ? colorList[index] : Theme.of(context).primaryColor, text: commitMaps![index], textColor: GSYColors.white, onPress: () { Navigator.pop(context); onTap(index); }, ); }), ), ); }); } ///版本更新 static Future showUpdateDialog( BuildContext context, String contentMsg) { return NavigatorUtils.showGSYDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text(context.l10n.app_version_title), content: Text(contentMsg), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: Text(context.l10n.app_cancel)), TextButton( onPressed: () { launchUrl(Uri.parse(Address.updateUrl), mode: LaunchMode.externalApplication); Navigator.pop(context); }, child: Text(context.l10n.app_ok)), ], ); }); } } String getRawBaseUrl( {required String userName, required String repoName, required String branch}) { return "https://raw.githubusercontent.com/$userName/$repoName/$branch/"; } ================================================ FILE: lib/common/utils/event_utils.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/model/push_event_commit.dart'; import 'package:gsy_github_app_flutter/model/repo_commit.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; /// 事件逻辑 /// Created by guoshuyu /// Date: 2018-07-16 class EventUtils { static bool _isInvalidCompareBase(String? sha) { if (sha == null || sha.isEmpty) { return true; } return RegExp(r'^0+$').hasMatch(sha); } static String _shortSha(String? sha, [int length = 7]) { if (sha == null || sha.isEmpty) { return ""; } if (sha.length <= length) { return sha; } return sha.substring(0, length); } ///事件描述与动作 static ({String? actionStr, String? des}) getActionAndDes(Event event) { String? actionStr; String? des; switch (event.type) { case "CommitCommentEvent": actionStr = "Commit comment at ${event.repo!.name!}"; break; case "CreateEvent": if (event.payload!.refType == "repository") { actionStr = "Created repository ${event.repo!.name!}"; } else { actionStr = "Created ${event.payload!.refType!} ${event.payload!.ref!} at ${event.repo!.name!}"; } break; case "DeleteEvent": actionStr = "Delete ${event.payload!.refType!} ${event.payload!.ref!} at ${event.repo!.name!}"; break; case "ForkEvent": String oriRepo = event.repo!.name!; String newRepo = "${event.actor!.login!}/${event.repo!.name!}"; actionStr = "Forked $oriRepo to $newRepo"; break; case "GollumEvent": actionStr = "${event.actor!.login!} a wiki page "; break; case "InstallationEvent": actionStr = "${event.payload!.action!} an GitHub App "; break; case "InstallationRepositoriesEvent": actionStr = "${event.payload!.action!} repository from an installation "; break; case "IssueCommentEvent": actionStr = "${event.payload!.action!} comment on issue ${event.payload!.issue!.number} in ${event.repo!.name!}"; des = event.payload!.comment!.body; break; case "IssuesEvent": actionStr = "${event.payload!.action!} issue ${event.payload!.issue!.number} in ${event.repo!.name!}"; des = event.payload!.issue!.title; break; case "MarketplacePurchaseEvent": actionStr = "${event.payload!.action!} marketplace plan "; break; case "MemberEvent": actionStr = "${event.payload!.action!} member to ${event.repo!.name!}"; break; case "OrgBlockEvent": actionStr = "${event.payload!.action!} a user "; break; case "ProjectCardEvent": actionStr = "${event.payload!.action!} a project "; break; case "ProjectColumnEvent": actionStr = "${event.payload!.action!} a project "; break; case "ProjectEvent": actionStr = "${event.payload!.action!} a project "; break; case "PublicEvent": actionStr = "Made ${event.repo!.name!} public"; break; case "PullRequestEvent": actionStr = "${event.payload!.action!} pull request ${event.repo!.name!}"; break; case "PullRequestReviewEvent": actionStr = "${event.payload!.action!} pull request review at${event.repo!.name!}"; break; case "PullRequestReviewCommentEvent": actionStr = "${event.payload!.action!} pull request review comment at${event.repo!.name!}"; break; case "PushEvent": if (event.payload != null && event.payload?.ref != null) { String ref = event.payload!.ref!; ref = ref.substring(ref.lastIndexOf("/") + 1); actionStr = "Push to $ref at ${event.repo!.name!}"; String descSpan = ""; List commits = event.payload?.commits ?? []; int count = commits.length; int maxLines = 4; int max = count > maxLines ? maxLines - 1 : count; for (int i = 0; i < max; i++) { PushEventCommit commit = commits[i]; if (i != 0) { descSpan += ("\n"); } String sha = _shortSha(commit.sha); descSpan += sha; descSpan += " "; descSpan += (commit.message ?? "Commit"); } if (count > maxLines) { descSpan = "$descSpan\n..."; } if (descSpan.trim().isNotEmpty) { des = descSpan; } else if (event.payload?.description != null && event.payload!.description!.trim().isNotEmpty) { des = event.payload!.description; } else if (event.payload?.head != null && event.payload!.head!.trim().isNotEmpty) { String head = _shortSha(event.payload!.head); des = "head: $head"; } else { des = ""; } } else { actionStr = ""; } break; case "ReleaseEvent": actionStr = "${event.payload!.action!} release ${event.payload!.release!.tagName!} at ${event.repo!.name!}"; break; case "WatchEvent": actionStr = "${event.payload!.action!} ${event.repo!.name!}"; break; } return (actionStr: actionStr, des: des ?? ""); } ///跳转 static Future ActionUtils( BuildContext context, Event event, currentRepository, ) async { if (event.repo == null) { NavigatorUtils.goPerson(context, event.actor!.login); return; } var [owner, repositoryName] = event.repo!.name!.split("/"); String fullName = '$owner/$repositoryName'; switch (event.type) { case 'ForkEvent': String forkName = "${event.actor!.login!}/$repositoryName"; if (forkName.toLowerCase() == currentRepository.toLowerCase()) { return; } NavigatorUtils.goReposDetail( context, event.actor!.login!, repositoryName, ); break; case 'PushEvent': List commits = event.payload?.commits ?? []; String? beforeSha = event.payload?.before; String? headSha = event.payload?.head; List compareCommits = []; if (!_isInvalidCompareBase(beforeSha) && headSha != null && headSha.isNotEmpty && beforeSha != headSha) { CommonUtils.showLoadingDialog(context); try { var compareRes = await ReposRepository.getReposCompareRequest( owner, repositoryName, beforeSha!, headSha, ); if (compareRes != null && compareRes.result && compareRes.data != null) { compareCommits = compareRes.data.commits ?? []; } } finally { if (context.mounted) { Navigator.pop(context); } } } if (!context.mounted) { return; } if (compareCommits.length == 1 && compareCommits.first.sha != null) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, compareCommits.first.sha, true, ); return; } if (compareCommits.length > 1) { StringList list = []; for (int i = 0; i < compareCommits.length; i++) { RepoCommit commit = compareCommits[i]; String message = commit.commit?.message?.split('\n').first.trim() ?? "Commit"; if (message.isEmpty) { message = "Commit"; } list.add("$message ${_shortSha(commit.sha, 4)}"); } CommonUtils.showCommitOptionDialog(context, list, (index) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, compareCommits[index].sha, true, ); }); return; } if (commits.isEmpty) { if (headSha != null && headSha.isNotEmpty) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, headSha, true, ); return; } if (fullName.toLowerCase() == currentRepository.toLowerCase()) { return; } NavigatorUtils.goReposDetail(context, owner, repositoryName); } else if (commits.length == 1 && commits.first.sha != null) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, commits.first.sha, true, ); } else { List validCommits = commits .where((item) => item.sha != null && item.sha!.isNotEmpty) .toList(); if (validCommits.isEmpty) { if (headSha != null && headSha.isNotEmpty) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, headSha, true, ); } else { NavigatorUtils.goReposDetail(context, owner, repositoryName); } return; } if (validCommits.length == 1) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, validCommits.first.sha, true, ); return; } StringList list = []; for (int i = 0; i < validCommits.length; i++) { PushEventCommit commit = validCommits[i]; String shaShort = _shortSha(commit.sha, 4); String message = (commit.message == null || commit.message!.isEmpty) ? "Commit" : commit.message!; list.add("$message $shaShort"); } CommonUtils.showCommitOptionDialog(context, list, (index) { NavigatorUtils.goPushDetailPage( context, owner, repositoryName, validCommits[index].sha, true, ); }); } break; case 'ReleaseEvent': String url = event.payload!.release!.tarballUrl!; CommonUtils.launchWebView(context, repositoryName, url); break; case 'IssueCommentEvent': case 'IssuesEvent': NavigatorUtils.goIssueDetail( context, owner, repositoryName, event.payload!.issue!.number.toString(), needRightLocalIcon: true, ); break; default: if (fullName.toLowerCase() == currentRepository.toLowerCase()) { return; } NavigatorUtils.goReposDetail(context, owner, repositoryName); break; } } } ================================================ FILE: lib/common/utils/html_utils.dart ================================================ // ignore_for_file: unnecessary_string_escapes, prefer_adjacent_string_concatenation import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; /// Created by guoshuyu /// on 2018/7/27. class HtmlUtils { static generateCode2HTml(String? mdData, {String backgroundColor = GSYColors.miWhiteString, String lang = 'java', userBR = true}) { String currentData = (mdData != null && !mdData.contains("")) ? "\n
\n\n$mdData\n
\n\n" : "\n
\n${mdData!}
\n\n"; return generateHtml(currentData, backgroundColor: backgroundColor, userBR: userBR); } static generateHtml(String? mdData, {String backgroundColor = GSYColors.webDraculaBackgroundColorString, userBR = true}) { if (mdData == null) { return ""; } String mdDataCode = mdData; String regExCode = "<[\\s]*?code[^>]*?>[\\s\\S]*?<[\\s]*?\\/[\\s]*?code[\\s]*?>"; String regExPre = "<[\\s]*?pre[^>]*?>[\\s\\S]*?<[\\s]*?\\/[\\s]*?pre[\\s]*?>"; try { RegExp exp = RegExp(regExCode); Iterable tags = exp.allMatches(mdData); for (Match m in tags) { String match = m.group(0)!.replaceAll(RegExp("\n"), "\n\r
"); mdDataCode = mdDataCode.replaceAll(m.group(0)!, match); } } catch (e) { printLog(e); } try { RegExp exp = RegExp(regExPre); Iterable tags = exp.allMatches(mdDataCode); for (Match m in tags) { if (!m.group(0)!.contains("")) { String match = m.group(0)!.replaceAll(RegExp("\n"), "\n\r
"); mdDataCode = mdDataCode.replaceAll(m.group(0)!, match); } } } catch (e) { printLog(e); } try { RegExp exp = RegExp("
(([\\s\\S])*?)<\/pre>");
      Iterable tags = exp.allMatches(mdDataCode);
      for (Match m in tags) {
        if (!m.group(0)!.contains("")) {
          String match = m.group(0)!.replaceAll(RegExp("\n"), "\n\r
"); mdDataCode = mdDataCode.replaceAll(m.group(0)!, match); } } } catch (e) { printLog(e); } try { RegExp exp = RegExp("href=\"(.*?)\""); Iterable tags = exp.allMatches(mdDataCode); for (Match m in tags) { String capture = m.group(0)!; if (!capture.contains("http://") && !capture.contains("https://") && capture.indexOf("#") != 0) { mdDataCode = mdDataCode.replaceAll(m.group(0)!, "gsygithub://$capture"); } } } catch (e) { printLog(e); } return generateCodeHtml(mdDataCode, false, backgroundColor: backgroundColor, actionColor: GSYColors.actionBlueString, userBR: userBR); } /// style for mdHTml static generateCodeHtml(mdHTML, wrap, {backgroundColor = GSYColors.white, String actionColor = GSYColors.actionBlueString, userBR = true}) { // ignore: prefer_interpolation_to_compose_strings return "${"${"\n" + "\n" + "\n" + "\n" + "" + " " + "\n" + "" + "" + " " + "\n\n" + mdHTML}\n"; } static parseDiffSource(String? diffSource, bool wrap) { if (diffSource == null) { return ""; } List lines = diffSource.split("\n"); String source = ""; int addStartLine = -1; int removeStartLine = -1; int addLineNum = 0; int removeLineNum = 0; int normalLineNum = 0; for (int i = 0; i < lines.length; i++) { String line = lines[i]; String? lineNumberStr = ""; String classStr = ""; int curAddNumber = -1; int curRemoveNumber = -1; if (line.indexOf("+") == 0) { classStr = "class=\"hljs-addition\";"; curAddNumber = addStartLine + normalLineNum + addLineNum; addLineNum++; } else if (line.indexOf("-") == 0) { classStr = "class=\"hljs-deletion\";"; curRemoveNumber = removeStartLine + normalLineNum + removeLineNum; removeLineNum++; } else if (line.indexOf("@@") == 0) { classStr = "class=\"hljs-literal\";"; removeStartLine = getRemoveStartLine(line); addStartLine = getAddStartLine(line); addLineNum = 0; removeLineNum = 0; normalLineNum = 0; } else if (!(line.indexOf("\\") == 0)) { curAddNumber = addStartLine + normalLineNum + addLineNum; curRemoveNumber = removeStartLine + normalLineNum + removeLineNum; normalLineNum++; } lineNumberStr = getDiffLineNumber( curRemoveNumber == -1 ? "" : ("$curRemoveNumber"), curAddNumber == -1 ? "" : ("$curAddNumber")); source = "$source
${wrap ? "" : lineNumberStr! + getBlank(1)}$line
"; } return source; } static getRemoveStartLine(line) { try { return int.parse( line.substring(line.indexOf("-") + 1, line.indexOf(","))); } catch (e) { return 1; } } static getAddStartLine(line) { try { return int.parse(line.substring( line.indexOf("+") + 1, line.indexOf(",", line.indexOf("+")))); } catch (e) { return 1; } } static getDiffLineNumber(String removeNumber, String addNumber) { int minLength = 4; return getBlank(minLength - removeNumber.length) + removeNumber + getBlank(1) + getBlank(minLength - addNumber.length) + addNumber; } // ignore: avoid_types_as_parameter_names static getBlank(num) { String builder = ""; for (int i = 0; i < num; i++) { builder += " "; } return builder; } static resolveHtmlFile(var res, String defaultLang) { if (res != null && res.result) { String startTag = "class=\"instapaper_body "; int startLang = res.data.indexOf(startTag); int? endLang = res.data.indexOf("\" data-path=\""); String? lang; if (startLang >= 0 && endLang! >= 0) { String? tmpLang = res.data.substring(startLang + startTag.length, endLang); if (tmpLang != null) { lang = formName(tmpLang.toLowerCase()); } } lang ??= defaultLang; if ('markdown' == lang || 'md' == lang) { return generateHtml(res.data, backgroundColor: GSYColors.miWhiteString); } else { return generateCode2HTml(res.data, backgroundColor: GSYColors.webDraculaBackgroundColorString, lang: lang); } } else { return "

Not Support

"; } } static formName(name) { switch (name) { case 'sh': return 'shell'; case 'js': return 'javascript'; case 'kt': return 'kotlin'; case 'c': case 'cpp': return 'cpp'; case 'md': return 'markdown'; case 'html': return 'xml'; } return name; } } ================================================ FILE: lib/common/utils/navigator_utils.dart ================================================ import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/model/common_list_datatype.dart'; import 'package:gsy_github_app_flutter/page/code_detail_page_web.dart'; import 'package:gsy_github_app_flutter/page/common_list_page.dart'; import 'package:gsy_github_app_flutter/page/debug/debug_data_page.dart'; import 'package:gsy_github_app_flutter/page/gsy_webview.dart'; import 'package:gsy_github_app_flutter/page/home/home_page.dart'; import 'package:gsy_github_app_flutter/page/honor_list_page.dart'; import 'package:gsy_github_app_flutter/page/issue/issue_detail_page.dart'; import 'package:gsy_github_app_flutter/page/login/login_page.dart'; import 'package:gsy_github_app_flutter/page/login/login_webview.dart'; import 'package:gsy_github_app_flutter/page/notify/notify_page.dart'; import 'package:gsy_github_app_flutter/page/trend/trend_user_page.dart'; import 'package:gsy_github_app_flutter/page/user/person_page.dart'; import 'package:gsy_github_app_flutter/page/photoview_page.dart'; import 'package:gsy_github_app_flutter/page/push/push_detail_page.dart'; import 'package:gsy_github_app_flutter/page/release/release_page.dart'; import 'package:gsy_github_app_flutter/page/repos/repository_detail_page.dart'; import 'package:gsy_github_app_flutter/page/search/search_page.dart'; import 'package:gsy_github_app_flutter/page/user_profile_page.dart'; import 'package:gsy_github_app_flutter/widget/never_overscroll_indicator.dart'; /// 导航栏 /// Created by guoshuyu /// Date: 2018-07-16 class NavigatorUtils { ///替换 static pushReplacementNamed(BuildContext context, String routeName) { Navigator.pushReplacementNamed(context, routeName); // if (navigator == null) { // try { // navigator = Navigator.of(context); // } catch (e) { // error = true; // } // } // // if (replace) { // ///如果可以返回,清空开始,然后塞入 // if (!error && navigator.canPop()) { // navigator.pushAndRemoveUntil( // router, // ModalRoute.withName('/'), // ); // } else { // ///如果不可返回,直接替换当前 // navigator.pushReplacement(router); // } // } else { // navigator.push(router); // } } ///切换无参数页面 static pushNamed(BuildContext context, String routeName) { Navigator.pushNamed(context, routeName); } ///主页 static goHome(BuildContext context) { Navigator.pushReplacementNamed(context, HomePage.sName); } ///登录页 static goLogin(BuildContext context) { Navigator.pushReplacementNamed(context, LoginPage.sName); } ///图片预览 static gotoPhotoViewPage(BuildContext context, String? url) { Navigator.pushNamed(context, PhotoViewPage.sName, arguments: url); } ///个人中心 static goPerson(BuildContext context, String? userName) { NavigatorRouter(context, PersonPage(userName)); } ///请求数据调试页面 static goDebugDataPage(BuildContext context) { return NavigatorRouter(context, const DebugDataPage()); } ///仓库详情 static Future goReposDetail( BuildContext context, String? userName, String? reposName) { ///利用 SizeRoute 动画大小打开 return Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => RepositoryDetailPage(userName!, reposName!), transitionsBuilder: (context, animation, secondaryAnimation, child) { double begin = 0; double end = 1; var curve = Curves.ease; var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); return Align( child: SizeTransition( sizeFactor: animation.drive(tween), child: NeverOverScrollIndicator( needOverload: false, child: child, ), ), ); }, )); } ///荣耀列表 static Future goHonorListPage(BuildContext context, List? list) { return Navigator.push( context, PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => HonorListPage(list), transitionsBuilder: (context, animation, secondaryAnimation, child) { double begin = 0; double end = 1; var curve = Curves.ease; var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); return Align( child: SizeTransition( sizeFactor: animation.drive(tween), child: NeverOverScrollIndicator( needOverload: false, child: child, ), ), ); }, ), ); } ///仓库版本列表 static Future goReleasePage(BuildContext context, String? userName, String? reposName, String releaseUrl, String tagUrl) { return NavigatorRouter( context, ReleasePage( userName, reposName, releaseUrl, tagUrl, )); } ///issue详情 static Future goIssueDetail( BuildContext context, String? userName, String? reposName, String num, {bool needRightLocalIcon = false}) { return NavigatorRouter( context, IssueDetailPage( userName, reposName, num, needHomeIcon: needRightLocalIcon, )); } ///通用列表 static gotoCommonList(BuildContext context, String? title, String showType, CommonListDataType dataType, {String? userName, String? reposName}) { NavigatorRouter( context, CommonListPage( title, showType, dataType, userName: userName, reposName: reposName, )); } ///仓库详情通知 static Future goNotifyPage(BuildContext context) { return NavigatorRouter(context, const NotifyPage()); } ///用户趋势 static Future goTrendUserPage(BuildContext context) { return NavigatorRouter(context, const TrendUserPage()); } ///搜索 static Future goSearchPage(BuildContext context, Offset centerPosition) { return showGeneralDialog( context: context, pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { return Builder(builder: (BuildContext context) { return pageContainer(SearchPage(centerPosition), context); }); }, barrierDismissible: false, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, barrierColor: const Color(0x01000000), transitionDuration: const Duration(milliseconds: 150), transitionBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { return FadeTransition( opacity: CurvedAnimation( parent: animation, curve: Curves.easeOut, ), child: child, ); }, ); } ///提交详情 static Future goPushDetailPage(BuildContext context, String? userName, String? reposName, String? sha, bool needHomeIcon) { return NavigatorRouter( context, PushDetailPage( sha, userName, reposName, needHomeIcon: needHomeIcon, )); } ///全屏Web页面 static Future goGSYWebView(BuildContext context, String url, String? title) { return NavigatorRouter(context, GSYWebView(url, title)); } ///登陆Web页面 static Future goLoginWebView(BuildContext context, String url, String title) { return NavigatorRouter(context, LoginWebView(url, title)); } ///文件代码详情Web static gotoCodeDetailPageWeb(BuildContext context, {String? title, String? userName, String? reposName, String? path, String? data, String? branch, String? lang, String? htmlUrl}) { NavigatorRouter( context, CodeDetailPageWeb( title: title, userName: userName, reposName: reposName, path: path, data: data, lang: lang, branch: branch, htmlUrl: htmlUrl, )); } ///根据平台跳转文件代码详情Web static gotoCodeDetailPlatform(BuildContext context, {String? title, String? userName, String? reposName, String? path, String? data, String? branch, String? lang, String? htmlUrl}) { NavigatorUtils.gotoCodeDetailPageWeb( context, title: title, reposName: reposName, userName: userName, data: data, path: path, lang: lang, branch: branch, ); } ///用户配置 static gotoUserProfileInfo(BuildContext context) { NavigatorRouter(context, const UserProfileInfo()); } ///公共打开方式 static NavigatorRouter(BuildContext context, Widget widget) { return Navigator.push( context, CupertinoPageRoute( builder: (context) => pageContainer(widget, context))); } ///Page页面的容器,做一次通用自定义 static Widget pageContainer(widget, BuildContext context) { return MediaQuery( ///不受系统字体缩放影响 data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), child: NeverOverScrollIndicator( needOverload: false, child: widget, )); } ///弹出 dialog static Future showGSYDialog({ required BuildContext context, bool barrierDismissible = true, WidgetBuilder? builder, }) { return showDialog( context: context, barrierDismissible: barrierDismissible, builder: (context) { return MediaQuery( ///不受系统字体缩放影响 data: MediaQueryData.fromView( WidgetsBinding.instance.platformDispatcher.views.first) .copyWith(textScaler: TextScaler.noScaling), child: NeverOverScrollIndicator( needOverload: false, child: SafeArea(child: builder!(context)), )); }); } } ================================================ FILE: lib/db/provider/event/received_event_db_provider.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:sqflite/sqflite.dart'; /// 用户接受事件表 /// Created by guoshuyu /// Date: 2018-08-07 class ReceivedEventDbProvider extends BaseDbProvider { final String name = 'ReceivedEvent'; final String columnId = "_id"; final String columnData = "data"; int? id; String? data; ReceivedEventDbProvider(); Map toMap(String eventMapString) { Map map = {columnData: eventMapString}; if (id != null) { map[columnId] = id; } return map; } ReceivedEventDbProvider.fromMap(Map map) { id = map[columnId]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnData text not null) '''; } @override tableName() { return name; } ///插入到数据库 Future insert(String eventMapString) async { Database db = await getDataBase(); ///清空后再插入,因为只保存第一页面 db.execute("delete from $name"); return await db.insert(name, toMap(eventMapString)); } ///获取事件数据 Future>? getEvents() async { Database db = await getDataBase(); List maps = await db.query(name, columns: [columnId, columnData]); List list = []; if (maps.isNotEmpty) { ReceivedEventDbProvider provider = ReceivedEventDbProvider.fromMap(maps.first); ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data); list = await compute(decodeMapToObject, eventMap); } return list; } static List decodeMapToObject(List mapList) { List list = []; for (var item in mapList) { list.add(Event.fromJson(item)); } return list; } } ================================================ FILE: lib/db/provider/event/user_event_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:sqflite/sqflite.dart'; /// 用户动态表 /// Created by guoshuyu /// Date: 2018-08-07 class UserEventDbProvider extends BaseDbProvider { final String name = 'UserEvent'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserEventDbProvider(); Map toMap(String? userName, String eventMapString) { Map map = { columnUserName: userName, columnData: eventMapString }; if (id != null) { map[columnId] = id; } return map; } UserEventDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? userName) async { List maps = await db.query(name, columns: [columnId, columnData, columnUserName], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserEventDbProvider provider = UserEventDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? userName, String eventMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, eventMapString)); } ///获取事件数据 Future?> getEvents(userName) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); list = await compute(decodeMapToObject, eventMap); return list; } return null; } static List decodeMapToObject(List mapList) { List list = []; for (var item in mapList) { list.add(Event.fromJson(item)); } return list; } } ================================================ FILE: lib/db/provider/issue/issue_comment_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:sqflite/sqflite.dart'; /// issue评论表 /// Created by guoshuyu /// Date: 2018-08-07 class IssueCommentDbProvider extends BaseDbProvider { final String name = 'IssueComment'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnNumber = "number"; final String columnCommentId = "commentId"; final String columnData = "data"; int? id; String? fullName; String? commentId; String? number; String? data; IssueCommentDbProvider(); Map toMap(String? fullName, String number, String data) { Map map = { columnFullName: fullName, columnData: data, columnNumber: number }; if (id != null) { map[columnId] = id; } return map; } IssueCommentDbProvider.fromMap(Map map) { id = map[columnId]; number = map[columnNumber]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnNumber text not null, $columnCommentId text, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName, String number) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnNumber, columnData], where: "$columnFullName = ? and $columnNumber = ?", whereArgs: [fullName, number]); if (maps.isNotEmpty) { IssueCommentDbProvider provider = IssueCommentDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String number, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, number); if (provider != null) { await db.delete(name, where: "$columnFullName = ? and $columnNumber = ?", whereArgs: [fullName, number]); } return await db.insert(name, toMap(fullName, number, dataMapString)); } ///获取事件数据 Future?> getData(String? fullName, String number) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, number); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(Issue.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/issue/issue_detail_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/db/provider/repos/repository_detail_db_provider.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:sqflite/sqflite.dart'; /// issue详情表 /// Created by guoshuyu /// Date: 2018-08-07 class IssueDetailDbProvider extends BaseDbProvider { final String name = 'IssueDetail'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnNumber = "number"; final String columnData = "data"; int? id; String? fullName; String? number; String? data; IssueDetailDbProvider(); Map toMap(String? fullName, String number, String data) { Map map = { columnFullName: fullName, columnData: data, columnNumber: number }; if (id != null) { map[columnId] = id; } return map; } IssueDetailDbProvider.fromMap(Map map) { id = map[columnId]; number = map[columnNumber]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnNumber text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName, String number) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnNumber, columnData], where: "$columnFullName = ? and $columnNumber = ?", whereArgs: [fullName, number]); if (maps.isNotEmpty) { RepositoryDetailDbProvider provider = RepositoryDetailDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String number, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, number); if (provider != null) { await db.delete(name, where: "$columnFullName = ? and $columnNumber = ?", whereArgs: [fullName, number]); } return await db.insert(name, toMap(fullName, number, dataMapString)); } ///获取详情 Future getRepository(String? fullName, String number) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, number); if (provider != null) { ///使用 compute 的 Isolate 优化 json decode var mapData = await compute(CodeUtils.decodeMapResult, provider.data as String?); return Issue.fromJson(mapData); } return null; } } ================================================ FILE: lib/db/provider/repos/read_history_db_provider.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:sqflite/sqflite.dart'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; /// 本地已读历史表 /// Created by guoshuyu /// Date: 2018-08-07 class ReadHistoryDbProvider extends BaseDbProvider { final String name = 'ReadHistoryV2'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnReadDate = "readDate"; final String columnData = "data"; int? id; String? fullName; int? readDate; String? data; ReadHistoryDbProvider(); Map toMap(String? fullName, DateTime readDate, String data) { Map map = { columnFullName: fullName, columnReadDate: readDate.millisecondsSinceEpoch, columnData: data }; if (id != null) { map[columnId] = id; } return map; } ReadHistoryDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; readDate = map[columnReadDate]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnReadDate int not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, int page) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnReadDate, columnData], limit: Config.PAGE_SIZE, offset: (page - 1) * Config.PAGE_SIZE, orderBy: "$columnReadDate DESC"); if (maps.isNotEmpty) { return maps; } return null; } Future _getProviderInsert(Database db, String? fullName) async { List> maps = await db.query( name, columns: [columnId, columnFullName, columnReadDate, columnData], where: "$columnFullName = ?", whereArgs: [fullName], ); if (maps.isNotEmpty) { ReadHistoryDbProvider provider = ReadHistoryDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert( String? fullName, DateTime dateTime, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProviderInsert(db, fullName); if (provider != null) { await db .delete(name, where: "$columnFullName = ?", whereArgs: [fullName]); } return await db.insert(name, toMap(fullName, dateTime, dataMapString)); } ///获取事件数据 Future?> geData(int page) async { Database db = await getDataBase(); var provider = await _getProvider(db, page); if (provider != null) { List list = []; for (var providerMap in provider) { ReadHistoryDbProvider provider = ReadHistoryDbProvider.fromMap(providerMap); ///使用 compute 的 Isolate 优化 json decode var mapData = await compute(CodeUtils.decodeMapResult, provider.data); list.add(RepositoryQL.fromMap(mapData)); } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_branch_db_provider.dart ================================================ import 'package:gsy_github_app_flutter/db/sql_provider.dart'; /// 仓库分支表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryBranchDbProvider extends BaseDbProvider { final String name = 'RepositoryPulse'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; int? id; String? fullName; String? data; Map toMap() { Map map = {columnFullName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } RepositoryBranchDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() {} @override tableName() { return name; } } ================================================ FILE: lib/db/provider/repos/repository_commitInfo_detail_db_provider.dart ================================================ import 'package:gsy_github_app_flutter/db/sql_provider.dart'; /// 仓库提交信息详情表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryCommitInfoDetailDbProvider extends BaseDbProvider { final String name = 'RepositoryCommitInfoDetail'; int? id; String? fullName; String? data; String? sha; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnSha = "sha"; final String columnData = "data"; Map toMap() { Map map = { columnFullName: fullName, columnSha: sha, columnData: data }; if (id != null) { map[columnId] = id; } return map; } RepositoryCommitInfoDetailDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; sha = map[columnSha]; data = map[columnData]; } @override tableSqlString() {} @override tableName() { return name; } } ================================================ FILE: lib/db/provider/repos/repository_commits_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/repo_commit.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库提交信息表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryCommitsDbProvider extends BaseDbProvider { final String name = 'RepositoryCommits'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnBranch = "branch"; final String columnData = "data"; int? id; String? fullName; String? data; String? branch; RepositoryCommitsDbProvider(); Map toMap(String? fullName, String? branch, String data) { Map map = { columnFullName: fullName, columnBranch: branch, columnData: data }; if (id != null) { map[columnId] = id; } return map; } RepositoryCommitsDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; branch = map[columnBranch]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnBranch text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName, String? branch) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnBranch, columnData], where: "$columnFullName = ? and $columnBranch = ?", whereArgs: [fullName, branch]); if (maps.isNotEmpty) { RepositoryCommitsDbProvider provider = RepositoryCommitsDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String? branch, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, branch); if (provider != null) { await db.delete(name, where: "$columnFullName = ? and $columnBranch = ?", whereArgs: [fullName, branch]); } return await db.insert(name, toMap(fullName, branch, dataMapString)); } ///获取事件数据 Future?> getData(String? fullName, String? branch) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, branch); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(RepoCommit.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_detail_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库详情数据表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryDetailDbProvider extends BaseDbProvider { final String name = 'RepositoryDetail'; int? id; String? fullName; String? data; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; RepositoryDetailDbProvider(); Map toMap(String? fullName, String dataMapString) { Map map = { columnFullName: fullName, columnData: dataMapString }; if (id != null) { map[columnId] = id; } return map; } RepositoryDetailDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnData], where: "$columnFullName = ?", whereArgs: [fullName]); if (maps.isNotEmpty) { RepositoryDetailDbProvider provider = RepositoryDetailDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { await db .delete(name, where: "$columnFullName = ?", whereArgs: [fullName]); } return await db.insert(name, toMap(fullName, dataMapString)); } ///获取详情 Future getRepository(String? fullName) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { ///使用 compute 的 Isolate 优化 json decode var mapData = await compute(CodeUtils.decodeMapResult, provider.data as String?); return RepositoryQL.fromMap(mapData); } return null; } } ================================================ FILE: lib/db/provider/repos/repository_detail_readme_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库readme文件表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryDetailReadmeDbProvider extends BaseDbProvider { final String name = 'RepositoryDetailReadme'; int? id; String? fullName; String? data; String? branch; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnBranch = "branch"; final String columnData = "data"; RepositoryDetailReadmeDbProvider(); Map toMap( String? fullName, String? branch, String? dataMapString) { Map map = { columnFullName: fullName, columnBranch: branch, columnData: dataMapString }; if (id != null) { map[columnId] = id; } return map; } RepositoryDetailReadmeDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; branch = map[columnBranch]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnBranch text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName, String? branch) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnData], where: "$columnFullName = ? and $columnBranch = ?", whereArgs: [fullName, branch]); if (maps.isNotEmpty) { RepositoryDetailReadmeDbProvider provider = RepositoryDetailReadmeDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String? branch, String? dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, branch); if (provider != null) { await db.delete(name, where: "$columnFullName = ? and $columnBranch = ?", whereArgs: [fullName, branch]); } return await db.insert(name, toMap(fullName, branch, dataMapString)); } ///获取readme详情 Future getRepositoryReadme(String? fullName, String? branch) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, branch); if (provider != null) { return provider.data; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_event_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库活跃事件表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryEventDbProvider extends BaseDbProvider { final String name = 'RepositoryEvent'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; int? id; String? fullName; String? data; RepositoryEventDbProvider(); Map toMap(String? fullName, String data) { Map map = {columnFullName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } RepositoryEventDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnData], where: "$columnFullName = ?", whereArgs: [fullName]); if (maps.isNotEmpty) { RepositoryEventDbProvider provider = RepositoryEventDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { await db .delete(name, where: "$columnFullName = ?", whereArgs: [fullName]); } return await db.insert(name, toMap(fullName, dataMapString)); } ///获取事件数据 Future?> getEvents(String? fullName) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(Event.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_fork_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/repository.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库分支表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryForkDbProvider extends BaseDbProvider { final String name = 'RepositoryFork'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; int? id; String? fullName; String? data; RepositoryForkDbProvider(); Map toMap(String? fullName, String data) { Map map = {columnFullName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } RepositoryForkDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnData], where: "$columnFullName = ?", whereArgs: [fullName]); if (maps.isNotEmpty) { RepositoryForkDbProvider provider = RepositoryForkDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { await db .delete(name, where: "$columnFullName = ?", whereArgs: [fullName]); } return await db.insert(name, toMap(fullName, dataMapString)); } ///获取事件数据 Future?> geData(String? fullName) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(Repository.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_issue_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库issue表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryIssueDbProvider extends BaseDbProvider { final String name = 'RepositoryIssue'; int? id; String? fullName; String? data; String? state; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnState = "state"; final String columnData = "data"; RepositoryIssueDbProvider(); Map toMap(String? fullName, String state, String data) { Map map = { columnFullName: fullName, columnState: state, columnData: data }; if (id != null) { map[columnId] = id; } return map; } RepositoryIssueDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; state = map[columnState]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnState text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName, String state) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnState, columnData], where: "$columnFullName = ? and $columnState = ?", whereArgs: [fullName, state]); if (maps.isNotEmpty) { RepositoryIssueDbProvider provider = RepositoryIssueDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String state, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, state); if (provider != null) { await db.delete(name, where: "$columnFullName = ? and $columnState = ?", whereArgs: [fullName, state]); } return await db.insert(name, toMap(fullName, state, dataMapString)); } ///获取事件数据 Future?> getData(String? fullName, String branch) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName, branch); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(Issue.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_pulse_db_provider.dart ================================================ import 'package:gsy_github_app_flutter/db/sql_provider.dart'; /// 仓库pulse表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryPulseDbProvider extends BaseDbProvider { final String name = 'RepositoryPulse'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; int? id; String? fullName; String? data; Map toMap() { Map map = {columnFullName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } RepositoryPulseDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() {} @override tableName() { return name; } } ================================================ FILE: lib/db/provider/repos/repository_star_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库收藏用户表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryStarDbProvider extends BaseDbProvider { final String name = 'RepositoryStar'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; int? id; String? fullName; String? data; RepositoryStarDbProvider(); Map toMap(String? fullName, String data) { Map map = {columnFullName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } RepositoryStarDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnData], where: "$columnFullName = ?", whereArgs: [fullName]); if (maps.isNotEmpty) { RepositoryStarDbProvider provider = RepositoryStarDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { await db .delete(name, where: "$columnFullName = ?", whereArgs: [fullName]); } return await db.insert(name, toMap(fullName, dataMapString)); } ///获取事件数据 Future?> geData(String? fullName) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(User.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/repository_watcher_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /// 仓库订阅用户表 /// Created by guoshuyu /// Date: 2018-08-07 class RepositoryWatcherDbProvider extends BaseDbProvider { final String name = 'RepositoryWatcher'; final String columnId = "_id"; final String columnFullName = "fullName"; final String columnData = "data"; int? id; String? fullName; String? data; RepositoryWatcherDbProvider(); Map toMap(String? fullName, String data) { Map map = {columnFullName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } RepositoryWatcherDbProvider.fromMap(Map map) { id = map[columnId]; fullName = map[columnFullName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnFullName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? fullName) async { List> maps = await db.query(name, columns: [columnId, columnFullName, columnData], where: "$columnFullName = ?", whereArgs: [fullName]); if (maps.isNotEmpty) { RepositoryWatcherDbProvider provider = RepositoryWatcherDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? fullName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { await db .delete(name, where: "$columnFullName = ?", whereArgs: [fullName]); } return await db.insert(name, toMap(fullName, dataMapString)); } ///获取事件数据 Future?> geData(String? fullName) async { Database db = await getDataBase(); var provider = await _getProvider(db, fullName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(User.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/repos/trend_repository_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/trending_repo_model.dart'; import 'package:sqflite/sqflite.dart'; /// 趋势表 /// Created by guoshuyu /// Date: 2018-08-07 class TrendRepositoryDbProvider extends BaseDbProvider { final String name = 'TrendRepository'; int? id; String? fullName; String? data; String? since; String? languageType; final String columnId = "_id"; final String columnLanguageType = "languageType"; final String columnSince = "since"; final String columnData = "data"; TrendRepositoryDbProvider(); Map toMap( String language, String? since, String dataMapString) { Map map = { columnLanguageType: language, columnSince: since, columnData: dataMapString }; if (id != null) { map[columnId] = id; } return map; } TrendRepositoryDbProvider.fromMap(Map map) { id = map[columnId]; languageType = map[columnLanguageType]; since = map[columnSince]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnLanguageType text not null, $columnSince text not null, $columnData text not null) '''; } @override tableName() { return name; } ///插入到数据库 Future insert(String language, String? since, String dataMapString) async { Database db = await getDataBase(); ///清空后再插入,因为只保存第一页面 db.execute("delete from $name"); return await db.insert(name, toMap(language, since, dataMapString)); } ///获取事件数据 Future>? getData(String language, String? since) async { Database db = await getDataBase(); List maps = await db.query(name, columns: [columnId, columnLanguageType, columnSince, columnData], where: "$columnLanguageType = ? and $columnSince = ?", whereArgs: [language, since]); List list = []; if (maps.isNotEmpty) { TrendRepositoryDbProvider provider = TrendRepositoryDbProvider.fromMap(maps.first); ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(TrendingRepoModel.fromJson(item)); } } } return list; } } ================================================ FILE: lib/db/provider/user/org_member_db_provider.dart ================================================ import 'package:gsy_github_app_flutter/db/sql_provider.dart'; /// 用户关注表 /// /// Created by guoshuyu /// Date: 2018-08-07 class OrgMemberDbProvider extends BaseDbProvider { final String name = 'OrgMember'; final String columnId = "_id"; final String columnOrg = "org"; final String columnData = "data"; int? id; String? org; String? data; Map toMap() { Map map = {columnOrg: org, columnData: data}; if (id != null) { map[columnId] = id; } return map; } OrgMemberDbProvider.fromMap(Map map) { id = map[columnId]; org = map[columnOrg]; data = map[columnData]; } @override tableSqlString() {} @override tableName() { return name; } } ================================================ FILE: lib/db/provider/user/user_followed_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /// 用户关注表 /// Created by guoshuyu /// Date: 2018-08-07 class UserFollowedDbProvider extends BaseDbProvider { final String name = 'UserFollowed'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserFollowedDbProvider(); Map toMap(String? userName, String data) { Map map = {columnUserName: userName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } UserFollowedDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserFollowedDbProvider provider = UserFollowedDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? userName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, dataMapString)); } ///获取事件数据 Future?> geData(String? userName) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(User.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/user/user_follower_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /// 用户粉丝表 /// Created by guoshuyu /// Date: 2018-08-07 class UserFollowerDbProvider extends BaseDbProvider { final String name = 'UserFollower'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserFollowerDbProvider(); Map toMap(String? userName, String data) { Map map = {columnUserName: userName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } UserFollowerDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserFollowerDbProvider provider = UserFollowerDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? userName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, dataMapString)); } ///获取事件数据 Future?> geData(String? userName) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(User.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/user/user_orgs_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/user_org.dart'; import 'package:sqflite/sqflite.dart'; /// 用户组织表 /// Created by guoshuyu /// Date: 2018-08-07 class UserOrgsDbProvider extends BaseDbProvider { final String name = 'UserOrgs'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserOrgsDbProvider(); Map toMap(String? userName, String data) { Map map = {columnUserName: userName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } UserOrgsDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserOrgsDbProvider provider = UserOrgsDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? userName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, dataMapString)); } ///获取数据 Future?> geData(String? userName) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(UserOrg.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/user/user_repos_db_provider.dart ================================================ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:gsy_github_app_flutter/model/repository.dart'; import 'package:sqflite/sqflite.dart'; /// 用户仓库表 /// Created by guoshuyu /// Date: 2018-08-07 class UserReposDbProvider extends BaseDbProvider { final String name = 'UserRepos'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserReposDbProvider(); Map toMap(String? fullName, String data) { Map map = {columnUserName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } UserReposDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserReposDbProvider provider = UserReposDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? userName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, dataMapString)); } ///获取事件数据 Future?> geData(String? userName) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(Repository.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/user/user_stared_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/repository.dart'; import 'package:sqflite/sqflite.dart'; /// 用户收藏表 /// Created by guoshuyu /// Date: 2018-08-07 class UserStaredDbProvider extends BaseDbProvider { final String name = 'UserStared'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserStaredDbProvider(); Map toMap(String? fullName, String data) { Map map = {columnUserName: fullName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } UserStaredDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getProvider(Database db, String? userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserStaredDbProvider provider = UserStaredDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String? userName, String dataMapString) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, dataMapString)); } ///获取事件数据 Future?> geData(String? userName) async { Database db = await getDataBase(); var provider = await _getProvider(db, userName); if (provider != null) { List list = []; ///使用 compute 的 Isolate 优化 json decode List eventMap = await compute(CodeUtils.decodeListResult, provider.data as String?); if (eventMap.isNotEmpty) { for (var item in eventMap) { list.add(Repository.fromJson(item)); } } return list; } return null; } } ================================================ FILE: lib/db/provider/user/userinfo_db_provider.dart ================================================ import 'dart:async'; import 'package:gsy_github_app_flutter/common/utils/code_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/db/sql_provider.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /// 用户表 /// Created by guoshuyu /// Date: 2018-08-07 class UserInfoDbProvider extends BaseDbProvider { final String name = 'UserInfo'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; UserInfoDbProvider(); Map toMap(String userName, String data) { Map map = {columnUserName: userName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } UserInfoDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getUserProvider(Database db, String? userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { UserInfoDbProvider provider = UserInfoDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String userName, String eventMapString) async { Database db = await getDataBase(); var userProvider = await _getUserProvider(db, userName); if (userProvider != null) { await db .delete(name, where: "$columnUserName = ?", whereArgs: [userName]); } return await db.insert(name, toMap(userName, eventMapString)); } ///获取事件数据 Future getUserInfo(String? userName) async { Database db = await getDataBase(); var userProvider = await _getUserProvider(db, userName); if (userProvider != null) { ///使用 compute 的 Isolate 优化 json decode var mapData = await compute(CodeUtils.decodeMapResult, userProvider.data as String?); return User.fromJson(mapData); } return null; } } ================================================ FILE: lib/db/sql_manager.dart ================================================ import 'dart:async'; import 'dart:io'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /// 数据库管理 /// Created by guoshuyu /// Date: 2018-08-03 class SqlManager { static const _VERSION = 1; static const _NAME = "gsy_github_app_flutter.db"; static Database? _database; ///初始化 static init() async { // open the database var databasesPath = await getDatabasesPath(); var userRes = await UserRepository.getUserInfoLocal(); String dbName = _NAME; if (userRes != null && userRes.result) { User? user = userRes.data; if (user != null && user.login != null) { dbName = "${user.login!}_$_NAME"; } } String path = databasesPath + dbName; if (Platform.isIOS) { path = "$databasesPath/$dbName"; } _database = await openDatabase(path, version: _VERSION, onCreate: (Database db, int version) async { // When creating the db, create the table //await db.execute("CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)"); }); } /// 表是否存在 static isTableExits(String tableName) async { await getCurrentDatabase(); var res = await _database?.rawQuery( "select * from Sqlite_master where type = 'table' and name = '$tableName'"); return res != null && res.isNotEmpty; } ///获取当前数据库对象 static Future getCurrentDatabase() async { if (_database == null) { await init(); } return _database; } ///关闭 static close() { _database?.close(); _database = null; } } ================================================ FILE: lib/db/sql_provider.dart ================================================ import 'dart:async'; /** * 数据库表 * Created by guoshuyu * Date: 2018-08-03 */ import 'package:gsy_github_app_flutter/db/sql_manager.dart'; import 'package:meta/meta.dart'; import 'package:sqflite/sqflite.dart'; ///基类 abstract class BaseDbProvider { bool isTableExits = false; tableSqlString(); tableName(); tableBaseString(String name, String columnId) { return ''' create table $name ( $columnId integer primary key autoincrement, '''; } Future getDataBase() async { return await open(); } @mustCallSuper prepare(name, String? createSql) async { isTableExits = await SqlManager.isTableExits(name); if (!isTableExits) { Database? db = await SqlManager.getCurrentDatabase(); return await db?.execute(createSql!); } } @mustCallSuper open() async { if (!isTableExits) { await prepare(tableName(), tableSqlString()); } return await SqlManager.getCurrentDatabase(); } } ================================================ FILE: lib/env/AGENTS.md ================================================ # 环境配置协作说明 这个目录包含构建期环境配置和生成文件。 错误修改会直接影响启动、构建或运行环境切换。 ## 工作规则 - 优先改源配置,不要直接手改生成文件 - 修改 env 结构后,记得重新生成相关输出 - 不要把本地密钥写入仓库 - `ignoreConfig.dart` 不属于这里的通用 env 体系,它是本地或 CI 单独提供的 OAuth 配置 ## 修改后检查 - 应用能正常启动 - 相关环境值能被正确读取 - 若涉及生成文件,执行对应生成命令 ================================================ FILE: lib/env/config_wrapper.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/env/env_config.dart'; ///往下共享环境配置 class ConfigWrapper extends StatelessWidget { const ConfigWrapper({super.key, this.config, this.child}); @override Widget build(BuildContext context) { ///设置 Config.DEBUG 的静态变量 Config.DEBUG = config?.debug; if (kDebugMode) { printLog("ConfigWrapper build ${Config.DEBUG}"); } return _InheritedConfig(config: config, child: child!); } static EnvConfig? of(BuildContext context) { final _InheritedConfig inheritedConfig = context.dependOnInheritedWidgetOfExactType<_InheritedConfig>()!; return inheritedConfig.config; } final EnvConfig? config; final Widget? child; } class _InheritedConfig extends InheritedWidget { const _InheritedConfig( {required this.config, required super.child}); final EnvConfig? config; @override bool updateShouldNotify(_InheritedConfig oldWidget) => config != oldWidget.config; } ================================================ FILE: lib/env/dev.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'dev.g.dart'; @JsonLiteral('env_json_dev.json', asConst: true) Map get config => _$configJsonLiteral; ================================================ FILE: lib/env/dev.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'dev.dart'; // ************************************************************************** // JsonLiteralGenerator // ************************************************************************** const _$configJsonLiteral = {'env': 'dev', 'debug': true}; ================================================ FILE: lib/env/env_config.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'env_config.g.dart'; ///环境配置 @JsonSerializable(createToJson: false) class EnvConfig { final String? env; final bool? debug; EnvConfig({ this.env, this.debug, }); factory EnvConfig.fromJson(Map json) => _$EnvConfigFromJson(json); } ================================================ FILE: lib/env/env_config.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'env_config.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** EnvConfig _$EnvConfigFromJson(Map json) => EnvConfig(env: json['env'] as String?, debug: json['debug'] as bool?); ================================================ FILE: lib/env/env_json_dev.json ================================================ { "env": "dev", "debug": true } ================================================ FILE: lib/env/env_json_prod.json ================================================ { "env": "prod", "debug": false } ================================================ FILE: lib/env/prod.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'prod.g.dart'; @JsonLiteral('env_json_prod.json', asConst: true) Map get config => _$configJsonLiteral; ================================================ FILE: lib/env/prod.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'prod.dart'; // ************************************************************************** // JsonLiteralGenerator // ************************************************************************** const _$configJsonLiteral = {'env': 'prod', 'debug': false}; ================================================ FILE: lib/main.dart ================================================ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/app.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/env/config_wrapper.dart'; import 'package:gsy_github_app_flutter/env/env_config.dart'; import 'package:gsy_github_app_flutter/page/error_page.dart'; import 'env/dev.dart'; void main() { runZonedGuarded(() { ErrorWidget.builder = (FlutterErrorDetails details) { Zone.current.handleUncaughtError(details.exception, details.stack!); ///此处仅为展示,正规的实现方式参考 _defaultErrorWidgetBuilder 通过自定义 RenderErrorBox 实现 return ErrorPage( "${details.exception}\n ${details.stack}", details); }; runApp(ConfigWrapper( config: EnvConfig.fromJson(config), child: const FlutterReduxApp(), )); ///屏幕刷新率和显示率不一致时的优化,必须挪动到 runApp 之后 GestureBinding.instance.resamplingEnabled = true; }, (Object obj, StackTrace stack) { talker.error('Catch Dart error:', obj, stack); printLog(obj); printLog(stack); }); } ================================================ FILE: lib/main_prod.dart ================================================ import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/app.dart'; import 'package:gsy_github_app_flutter/env/config_wrapper.dart'; import 'package:gsy_github_app_flutter/env/env_config.dart'; import 'package:gsy_github_app_flutter/page/error_page.dart'; import 'env/prod.dart'; void main() { runZonedGuarded(() { ErrorWidget.builder = (FlutterErrorDetails details) { Zone.current.handleUncaughtError(details.exception, details.stack!); ///此处仅为展示,正规的实现方式参考 _defaultErrorWidgetBuilder 通过自定义 RenderErrorBox 实现 return ErrorPage( "${details.exception}\n ${details.stack}", details); }; runApp(ConfigWrapper( config: EnvConfig.fromJson(config), child: const FlutterReduxApp(), )); ///屏幕刷新率和显示率不一致时的优化 GestureBinding.instance.resamplingEnabled = true; }, (Object obj, StackTrace stack) { ///do not thing }); } ================================================ FILE: lib/model/branch.dart ================================================ import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; part 'branch.g.dart'; abstract class Branch implements Built { static Serializer get serializer => _$branchSerializer; String? get name; @BuiltValueField(wireName: 'tarball_url') String? get tarballUrl; @BuiltValueField(wireName: 'zipball_url') String? get zipballUrl; Branch._(); factory Branch([void Function(BranchBuilder)? updates]) = _$Branch; } ================================================ FILE: lib/model/branch.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'branch.dart'; // ************************************************************************** // BuiltValueGenerator // ************************************************************************** Serializer _$branchSerializer = _$BranchSerializer(); class _$BranchSerializer implements StructuredSerializer { @override final Iterable types = const [Branch, _$Branch]; @override final String wireName = 'Branch'; @override Iterable serialize( Serializers serializers, Branch object, { FullType specifiedType = FullType.unspecified, }) { final result = []; Object? value; value = object.name; if (value != null) { result ..add('name') ..add( serializers.serialize(value, specifiedType: const FullType(String)), ); } value = object.tarballUrl; if (value != null) { result ..add('tarball_url') ..add( serializers.serialize(value, specifiedType: const FullType(String)), ); } value = object.zipballUrl; if (value != null) { result ..add('zipball_url') ..add( serializers.serialize(value, specifiedType: const FullType(String)), ); } return result; } @override Branch deserialize( Serializers serializers, Iterable serialized, { FullType specifiedType = FullType.unspecified, }) { final result = BranchBuilder(); final iterator = serialized.iterator; while (iterator.moveNext()) { final key = iterator.current! as String; iterator.moveNext(); final Object? value = iterator.current; switch (key) { case 'name': result.name = serializers.deserialize( value, specifiedType: const FullType(String), ) as String?; break; case 'tarball_url': result.tarballUrl = serializers.deserialize( value, specifiedType: const FullType(String), ) as String?; break; case 'zipball_url': result.zipballUrl = serializers.deserialize( value, specifiedType: const FullType(String), ) as String?; break; } } return result.build(); } } class _$Branch extends Branch { @override final String? name; @override final String? tarballUrl; @override final String? zipballUrl; factory _$Branch([void Function(BranchBuilder)? updates]) => (BranchBuilder()..update(updates))._build(); _$Branch._({this.name, this.tarballUrl, this.zipballUrl}) : super._(); @override Branch rebuild(void Function(BranchBuilder) updates) => (toBuilder()..update(updates)).build(); @override BranchBuilder toBuilder() => BranchBuilder()..replace(this); @override bool operator ==(Object other) { if (identical(other, this)) return true; return other is Branch && name == other.name && tarballUrl == other.tarballUrl && zipballUrl == other.zipballUrl; } @override int get hashCode { var _$hash = 0; _$hash = $jc(_$hash, name.hashCode); _$hash = $jc(_$hash, tarballUrl.hashCode); _$hash = $jc(_$hash, zipballUrl.hashCode); _$hash = $jf(_$hash); return _$hash; } @override String toString() { return (newBuiltValueToStringHelper(r'Branch') ..add('name', name) ..add('tarballUrl', tarballUrl) ..add('zipballUrl', zipballUrl)) .toString(); } } class BranchBuilder implements Builder { _$Branch? _$v; String? _name; String? get name => _$this._name; set name(String? name) => _$this._name = name; String? _tarballUrl; String? get tarballUrl => _$this._tarballUrl; set tarballUrl(String? tarballUrl) => _$this._tarballUrl = tarballUrl; String? _zipballUrl; String? get zipballUrl => _$this._zipballUrl; set zipballUrl(String? zipballUrl) => _$this._zipballUrl = zipballUrl; BranchBuilder(); BranchBuilder get _$this { final $v = _$v; if ($v != null) { _name = $v.name; _tarballUrl = $v.tarballUrl; _zipballUrl = $v.zipballUrl; _$v = null; } return this; } @override void replace(Branch other) { _$v = other as _$Branch; } @override void update(void Function(BranchBuilder)? updates) { if (updates != null) updates(this); } @override Branch build() => _build(); _$Branch _build() { final _$result = _$v ?? _$Branch._(name: name, tarballUrl: tarballUrl, zipballUrl: zipballUrl); replace(_$result); return _$result; } } // ignore_for_file: deprecated_member_use_from_same_package,type=lint ================================================ FILE: lib/model/commitFile.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'commitFile.g.dart'; @JsonSerializable() class CommitFile { String? sha; @JsonKey(name: "filename") String? fileName; String? status; int? additions; int? deletions; int? changes; @JsonKey(name: "blob_url") String? blobUrl; @JsonKey(name: "raw_url") String? rawUrl; @JsonKey(name: "contents_url") String? contentsUrl; String? patch; CommitFile( this.sha, this.fileName, this.status, this.additions, this.deletions, this.changes, this.blobUrl, this.rawUrl, this.contentsUrl, this.patch, ); factory CommitFile.fromJson(Map json) => _$CommitFileFromJson(json); Map toJson() => _$CommitFileToJson(this); } ================================================ FILE: lib/model/commitFile.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'commitFile.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CommitFile _$CommitFileFromJson(Map json) => CommitFile( json['sha'] as String?, json['filename'] as String?, json['status'] as String?, (json['additions'] as num?)?.toInt(), (json['deletions'] as num?)?.toInt(), (json['changes'] as num?)?.toInt(), json['blob_url'] as String?, json['raw_url'] as String?, json['contents_url'] as String?, json['patch'] as String?, ); Map _$CommitFileToJson(CommitFile instance) => { 'sha': instance.sha, 'filename': instance.fileName, 'status': instance.status, 'additions': instance.additions, 'deletions': instance.deletions, 'changes': instance.changes, 'blob_url': instance.blobUrl, 'raw_url': instance.rawUrl, 'contents_url': instance.contentsUrl, 'patch': instance.patch, }; ================================================ FILE: lib/model/commit_comment.dart ================================================ import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'commit_comment.g.dart'; @JsonSerializable() class CommitComment{ int? id; String? body; String? path; int? position; int? line; @JsonKey(name: "commit_id") String? commitId; @JsonKey(name: "created_at") DateTime? createdAt; @JsonKey(name: "updated_at") DateTime? updatedAt; @JsonKey(name: "html_url") String? htmlUrl; String? url; User? user; CommitComment( this.id, this.body, this.path, this.position, this.line, this.commitId, this.createdAt, this.updatedAt, this.htmlUrl, this.url, this.user, ); factory CommitComment.fromJson(Map json) => _$CommitCommentFromJson(json); Map toJson() => _$CommitCommentToJson(this); } ================================================ FILE: lib/model/commit_comment.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'commit_comment.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CommitComment _$CommitCommentFromJson(Map json) => CommitComment( (json['id'] as num?)?.toInt(), json['body'] as String?, json['path'] as String?, (json['position'] as num?)?.toInt(), (json['line'] as num?)?.toInt(), json['commit_id'] as String?, json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), json['html_url'] as String?, json['url'] as String?, json['user'] == null ? null : User.fromJson(json['user'] as Map), ); Map _$CommitCommentToJson(CommitComment instance) => { 'id': instance.id, 'body': instance.body, 'path': instance.path, 'position': instance.position, 'line': instance.line, 'commit_id': instance.commitId, 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'html_url': instance.htmlUrl, 'url': instance.url, 'user': instance.user, }; ================================================ FILE: lib/model/commit_git_info.dart ================================================ import 'package:gsy_github_app_flutter/model/commit_git_user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'commit_git_info.g.dart'; @JsonSerializable() class CommitGitInfo { String? message; String? url; @JsonKey(name: "comment_count") int? commentCount; CommitGitUser? author; CommitGitUser? committer; CommitGitInfo( this.message, this.url, this.commentCount, this.author, this.committer, ); factory CommitGitInfo.fromJson(Map json) => _$CommitGitInfoFromJson(json); Map toJson() => _$CommitGitInfoToJson(this); } ================================================ FILE: lib/model/commit_git_info.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'commit_git_info.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CommitGitInfo _$CommitGitInfoFromJson(Map json) => CommitGitInfo( json['message'] as String?, json['url'] as String?, (json['comment_count'] as num?)?.toInt(), json['author'] == null ? null : CommitGitUser.fromJson(json['author'] as Map), json['committer'] == null ? null : CommitGitUser.fromJson(json['committer'] as Map), ); Map _$CommitGitInfoToJson(CommitGitInfo instance) => { 'message': instance.message, 'url': instance.url, 'comment_count': instance.commentCount, 'author': instance.author, 'committer': instance.committer, }; ================================================ FILE: lib/model/commit_git_user.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'commit_git_user.g.dart'; @JsonSerializable() class CommitGitUser{ String? name; String? email; DateTime? date; CommitGitUser(this.name, this.email, this.date); factory CommitGitUser.fromJson(Map json) => _$CommitGitUserFromJson(json); Map toJson() => _$CommitGitUserToJson(this); } ================================================ FILE: lib/model/commit_git_user.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'commit_git_user.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CommitGitUser _$CommitGitUserFromJson(Map json) => CommitGitUser( json['name'] as String?, json['email'] as String?, json['date'] == null ? null : DateTime.parse(json['date'] as String), ); Map _$CommitGitUserToJson(CommitGitUser instance) => { 'name': instance.name, 'email': instance.email, 'date': instance.date?.toIso8601String(), }; ================================================ FILE: lib/model/commit_stats.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'commit_stats.g.dart'; @JsonSerializable() class CommitStats { int? total; int? additions; int? deletions; CommitStats(this.total, this.additions, this.deletions); factory CommitStats.fromJson(Map json) => _$CommitStatsFromJson(json); Map toJson() => _$CommitStatsToJson(this); } ================================================ FILE: lib/model/commit_stats.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'commit_stats.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CommitStats _$CommitStatsFromJson(Map json) => CommitStats( (json['total'] as num?)?.toInt(), (json['additions'] as num?)?.toInt(), (json['deletions'] as num?)?.toInt(), ); Map _$CommitStatsToJson(CommitStats instance) => { 'total': instance.total, 'additions': instance.additions, 'deletions': instance.deletions, }; ================================================ FILE: lib/model/commits_comparison.dart ================================================ import 'package:gsy_github_app_flutter/model/commitFile.dart'; import 'package:json_annotation/json_annotation.dart'; import 'repo_commit.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'commits_comparison.g.dart'; @JsonSerializable() class CommitsComparison{ String? url; @JsonKey(name: "html_url") String? htmlUrl; @JsonKey(name: "base_commit") RepoCommit? baseCommit; @JsonKey(name: "merge_base_commit") RepoCommit? mergeBaseCommit; String? status; @JsonKey(name: "total_commits") int? totalCommits; List? commits; List? files; CommitsComparison( this.url, this.htmlUrl, this.baseCommit, this.mergeBaseCommit, this.status, this.totalCommits, this.commits, this.files, ); factory CommitsComparison.fromJson(Map json) => _$CommitsComparisonFromJson(json); Map toJson() => _$CommitsComparisonToJson(this); } ================================================ FILE: lib/model/commits_comparison.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'commits_comparison.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** CommitsComparison _$CommitsComparisonFromJson(Map json) => CommitsComparison( json['url'] as String?, json['html_url'] as String?, json['base_commit'] == null ? null : RepoCommit.fromJson(json['base_commit'] as Map), json['merge_base_commit'] == null ? null : RepoCommit.fromJson( json['merge_base_commit'] as Map, ), json['status'] as String?, (json['total_commits'] as num?)?.toInt(), (json['commits'] as List?) ?.map((e) => RepoCommit.fromJson(e as Map)) .toList(), (json['files'] as List?) ?.map((e) => CommitFile.fromJson(e as Map)) .toList(), ); Map _$CommitsComparisonToJson(CommitsComparison instance) => { 'url': instance.url, 'html_url': instance.htmlUrl, 'base_commit': instance.baseCommit, 'merge_base_commit': instance.mergeBaseCommit, 'status': instance.status, 'total_commits': instance.totalCommits, 'commits': instance.commits, 'files': instance.files, }; ================================================ FILE: lib/model/common_list_datatype.dart ================================================ enum CommonListDataType { follower("follower"), followed("followed"), userRepos('user_repos'), repoStar("repo_star"), userStar("user_star"), repoWatcher("repo_watcher"), repoFork("repo_fork"), repoRelease("repoRelease"), repoTag("repo_tag"), notify("notify"), history("history"), topics("topics"), userBeStared("user_be_stared"), userOrgs("user_orgs"); final String value; const CommonListDataType(this.value); } ================================================ FILE: lib/model/download_source.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'download_source.g.dart'; @JsonSerializable() class DownloadSource { String? url; bool? isSourceCode; String? name; int? size; DownloadSource( this.url, this.isSourceCode, this.name, this.size, ); factory DownloadSource.fromJson(Map json) => _$DownloadSourceFromJson(json); Map toJson() => _$DownloadSourceToJson(this); } ================================================ FILE: lib/model/download_source.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'download_source.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** DownloadSource _$DownloadSourceFromJson(Map json) => DownloadSource( json['url'] as String?, json['isSourceCode'] as bool?, json['name'] as String?, (json['size'] as num?)?.toInt(), ); Map _$DownloadSourceToJson(DownloadSource instance) => { 'url': instance.url, 'isSourceCode': instance.isSourceCode, 'name': instance.name, 'size': instance.size, }; ================================================ FILE: lib/model/event.dart ================================================ import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; import 'event_payload.dart'; import 'repository.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'event.g.dart'; @JsonSerializable() class Event { String? id; String? type; User? actor; Repository? repo; User? org; EventPayload? payload; @JsonKey(name: "public") bool? isPublic; @JsonKey(name: "created_at") DateTime? createdAt; Event( this.id, this.type, this.actor, this.repo, this.org, this.payload, this.isPublic, this.createdAt, ); factory Event.fromJson(Map json) => _$EventFromJson(json); Map toJson() => _$EventToJson(this); } ================================================ FILE: lib/model/event.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'event.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Event _$EventFromJson(Map json) => Event( json['id'] as String?, json['type'] as String?, json['actor'] == null ? null : User.fromJson(json['actor'] as Map), json['repo'] == null ? null : Repository.fromJson(json['repo'] as Map), json['org'] == null ? null : User.fromJson(json['org'] as Map), json['payload'] == null ? null : EventPayload.fromJson(json['payload'] as Map), json['public'] as bool?, json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), ); Map _$EventToJson(Event instance) => { 'id': instance.id, 'type': instance.type, 'actor': instance.actor, 'repo': instance.repo, 'org': instance.org, 'payload': instance.payload, 'public': instance.isPublic, 'created_at': instance.createdAt?.toIso8601String(), }; ================================================ FILE: lib/model/event_payload.dart ================================================ import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:gsy_github_app_flutter/model/issue_event.dart'; import 'package:gsy_github_app_flutter/model/push_event_commit.dart'; import 'package:gsy_github_app_flutter/model/release.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'event_payload.g.dart'; @JsonSerializable() class EventPayload { @JsonKey(name: "push_id") int? pushId; int? size; @JsonKey(name: "distinct_size") int? distinctSize; String? ref; String? head; String? before; List? commits; String? action; @JsonKey(name: "ref_type") String? refType; @JsonKey(name: "master_branch") String? masterBranch; String? description; @JsonKey(name: "pusher_type") String? pusherType; Release? release; Issue? issue; IssueEvent? comment; EventPayload(); factory EventPayload.fromJson(Map json) => _$EventPayloadFromJson(json); Map toJson() => _$EventPayloadToJson(this); } ================================================ FILE: lib/model/event_payload.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'event_payload.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** EventPayload _$EventPayloadFromJson(Map json) => EventPayload() ..pushId = (json['push_id'] as num?)?.toInt() ..size = (json['size'] as num?)?.toInt() ..distinctSize = (json['distinct_size'] as num?)?.toInt() ..ref = json['ref'] as String? ..head = json['head'] as String? ..before = json['before'] as String? ..commits = (json['commits'] as List?) ?.map((e) => PushEventCommit.fromJson(e as Map)) .toList() ..action = json['action'] as String? ..refType = json['ref_type'] as String? ..masterBranch = json['master_branch'] as String? ..description = json['description'] as String? ..pusherType = json['pusher_type'] as String? ..release = json['release'] == null ? null : Release.fromJson(json['release'] as Map) ..issue = json['issue'] == null ? null : Issue.fromJson(json['issue'] as Map) ..comment = json['comment'] == null ? null : IssueEvent.fromJson(json['comment'] as Map); Map _$EventPayloadToJson(EventPayload instance) => { 'push_id': instance.pushId, 'size': instance.size, 'distinct_size': instance.distinctSize, 'ref': instance.ref, 'head': instance.head, 'before': instance.before, 'commits': instance.commits, 'action': instance.action, 'ref_type': instance.refType, 'master_branch': instance.masterBranch, 'description': instance.description, 'pusher_type': instance.pusherType, 'release': instance.release, 'issue': instance.issue, 'comment': instance.comment, }; ================================================ FILE: lib/model/file_model.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'file_model.g.dart'; @JsonSerializable() class FileModel { String? name; String? path; String? sha; int? size; String? url; @JsonKey(name: "html_url") String? htmlUrl; @JsonKey(name: "git_url") String? gitUrl; @JsonKey(name: "download_url") String? downloadUrl; @JsonKey(name: "type") String? type; FileModel( this.name, this.path, this.sha, this.size, this.url, this.htmlUrl, this.gitUrl, this.downloadUrl, this.type, ); factory FileModel.fromJson(Map json) => _$FileModelFromJson(json); Map toJson() => _$FileModelToJson(this); } ================================================ FILE: lib/model/file_model.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'file_model.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** FileModel _$FileModelFromJson(Map json) => FileModel( json['name'] as String?, json['path'] as String?, json['sha'] as String?, (json['size'] as num?)?.toInt(), json['url'] as String?, json['html_url'] as String?, json['git_url'] as String?, json['download_url'] as String?, json['type'] as String?, ); Map _$FileModelToJson(FileModel instance) => { 'name': instance.name, 'path': instance.path, 'sha': instance.sha, 'size': instance.size, 'url': instance.url, 'html_url': instance.htmlUrl, 'git_url': instance.gitUrl, 'download_url': instance.downloadUrl, 'type': instance.type, }; ================================================ FILE: lib/model/issue.dart ================================================ import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'issue.g.dart'; @JsonSerializable() class Issue { int? id; int? number; String? title; String? state; bool? locked; @JsonKey(name: "comments") int? commentNum; @JsonKey(name: "created_at") DateTime? createdAt; @JsonKey(name: "updated_at") DateTime? updatedAt; @JsonKey(name: "closed_at") DateTime? closedAt; String? body; @JsonKey(name: "body_html") String? bodyHtml; User? user; @JsonKey(name: "repository_url") String? repoUrl; @JsonKey(name: "html_url") String? htmlUrl; @JsonKey(name: "closed_by") User? closeBy; Issue( this.id, this.number, this.title, this.state, this.locked, this.commentNum, this.createdAt, this.updatedAt, this.closedAt, this.body, this.bodyHtml, this.user, this.repoUrl, this.htmlUrl, this.closeBy, ); factory Issue.fromJson(Map json) => _$IssueFromJson(json); Map toJson() => _$IssueToJson(this); } ================================================ FILE: lib/model/issue.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'issue.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Issue _$IssueFromJson(Map json) => Issue( (json['id'] as num?)?.toInt(), (json['number'] as num?)?.toInt(), json['title'] as String?, json['state'] as String?, json['locked'] as bool?, (json['comments'] as num?)?.toInt(), json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), json['closed_at'] == null ? null : DateTime.parse(json['closed_at'] as String), json['body'] as String?, json['body_html'] as String?, json['user'] == null ? null : User.fromJson(json['user'] as Map), json['repository_url'] as String?, json['html_url'] as String?, json['closed_by'] == null ? null : User.fromJson(json['closed_by'] as Map), ); Map _$IssueToJson(Issue instance) => { 'id': instance.id, 'number': instance.number, 'title': instance.title, 'state': instance.state, 'locked': instance.locked, 'comments': instance.commentNum, 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'closed_at': instance.closedAt?.toIso8601String(), 'body': instance.body, 'body_html': instance.bodyHtml, 'user': instance.user, 'repository_url': instance.repoUrl, 'html_url': instance.htmlUrl, 'closed_by': instance.closeBy, }; ================================================ FILE: lib/model/issue_event.dart ================================================ import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'issue_event.g.dart'; @JsonSerializable() class IssueEvent{ int? id; User? user; @JsonKey(name: "created_at") DateTime? createdAt; @JsonKey(name: "updated_at") DateTime? updatedAt; @JsonKey(name: "author_association") String? authorAssociation; String? body; @JsonKey(name: "body_html") String? bodyHtml; @JsonKey(name: "event") String? type; @JsonKey(name: "html_url") String? htmlUrl; IssueEvent( this.id, this.user, this.createdAt, this.updatedAt, this.authorAssociation, this.body, this.bodyHtml, this.type, this.htmlUrl, ); factory IssueEvent.fromJson(Map json) => _$IssueEventFromJson(json); Map toJson() => _$IssueEventToJson(this); } ================================================ FILE: lib/model/issue_event.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'issue_event.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** IssueEvent _$IssueEventFromJson(Map json) => IssueEvent( (json['id'] as num?)?.toInt(), json['user'] == null ? null : User.fromJson(json['user'] as Map), json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), json['author_association'] as String?, json['body'] as String?, json['body_html'] as String?, json['event'] as String?, json['html_url'] as String?, ); Map _$IssueEventToJson(IssueEvent instance) => { 'id': instance.id, 'user': instance.user, 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'author_association': instance.authorAssociation, 'body': instance.body, 'body_html': instance.bodyHtml, 'event': instance.type, 'html_url': instance.htmlUrl, }; ================================================ FILE: lib/model/license.dart ================================================ /// Created by guoshuyu /// Date: 2018-07-31 library; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'license.g.dart'; @JsonSerializable() class License { String? name; License(this.name); factory License.fromJson(Map json) => _$LicenseFromJson(json); Map toJson() => _$LicenseToJson(this); } ================================================ FILE: lib/model/license.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'license.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** License _$LicenseFromJson(Map json) => License(json['name'] as String?); Map _$LicenseToJson(License instance) => { 'name': instance.name, }; ================================================ FILE: lib/model/notification.dart ================================================ import 'package:gsy_github_app_flutter/model/notification_subject.dart'; import 'package:json_annotation/json_annotation.dart'; import 'repository.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'notification.g.dart'; @JsonSerializable() class Notification { String? id; bool? unread; String? reason; @JsonKey(name: "updated_at") DateTime? updateAt; @JsonKey(name: "last_read_at") DateTime? lastReadAt; Repository? repository; NotificationSubject? subject; Notification(this.id, this.unread, this.reason, this.updateAt, this.lastReadAt, this.repository, this.subject); factory Notification.fromJson(Map json) => _$NotificationFromJson(json); Map toJson() => _$NotificationToJson(this); } ================================================ FILE: lib/model/notification.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'notification.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Notification _$NotificationFromJson(Map json) => Notification( json['id'] as String?, json['unread'] as bool?, json['reason'] as String?, json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), json['last_read_at'] == null ? null : DateTime.parse(json['last_read_at'] as String), json['repository'] == null ? null : Repository.fromJson(json['repository'] as Map), json['subject'] == null ? null : NotificationSubject.fromJson(json['subject'] as Map), ); Map _$NotificationToJson(Notification instance) => { 'id': instance.id, 'unread': instance.unread, 'reason': instance.reason, 'updated_at': instance.updateAt?.toIso8601String(), 'last_read_at': instance.lastReadAt?.toIso8601String(), 'repository': instance.repository, 'subject': instance.subject, }; ================================================ FILE: lib/model/notification_subject.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'notification_subject.g.dart'; @JsonSerializable() class NotificationSubject { String? title; String? url; String? type; NotificationSubject(this.title, this.url, this.type); factory NotificationSubject.fromJson(Map json) => _$NotificationSubjectFromJson(json); Map toJson() => _$NotificationSubjectToJson(this); } ================================================ FILE: lib/model/notification_subject.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'notification_subject.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** NotificationSubject _$NotificationSubjectFromJson(Map json) => NotificationSubject( json['title'] as String?, json['url'] as String?, json['type'] as String?, ); Map _$NotificationSubjectToJson( NotificationSubject instance, ) => { 'title': instance.title, 'url': instance.url, 'type': instance.type, }; ================================================ FILE: lib/model/push_commit.dart ================================================ import 'package:gsy_github_app_flutter/model/commitFile.dart'; import 'package:gsy_github_app_flutter/model/commit_git_info.dart'; import 'package:gsy_github_app_flutter/model/commit_stats.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; import 'repo_commit.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'push_commit.g.dart'; @JsonSerializable() class PushCommit { List? files; CommitStats? stats; String? sha; String? url; @JsonKey(name: "html_url") String? htmlUrl; @JsonKey(name: "comments_url") String? commentsUrl; CommitGitInfo? commit; User? author; User? committer; List? parents; PushCommit( this.files, this.stats, this.sha, this.url, this.htmlUrl, this.commentsUrl, this.commit, this.author, this.committer, this.parents, ); factory PushCommit.fromJson(Map json) => _$PushCommitFromJson(json); Map toJson() => _$PushCommitToJson(this); } ================================================ FILE: lib/model/push_commit.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'push_commit.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** PushCommit _$PushCommitFromJson(Map json) => PushCommit( (json['files'] as List?) ?.map((e) => CommitFile.fromJson(e as Map)) .toList(), json['stats'] == null ? null : CommitStats.fromJson(json['stats'] as Map), json['sha'] as String?, json['url'] as String?, json['html_url'] as String?, json['comments_url'] as String?, json['commit'] == null ? null : CommitGitInfo.fromJson(json['commit'] as Map), json['author'] == null ? null : User.fromJson(json['author'] as Map), json['committer'] == null ? null : User.fromJson(json['committer'] as Map), (json['parents'] as List?) ?.map((e) => RepoCommit.fromJson(e as Map)) .toList(), ); Map _$PushCommitToJson(PushCommit instance) => { 'files': instance.files, 'stats': instance.stats, 'sha': instance.sha, 'url': instance.url, 'html_url': instance.htmlUrl, 'comments_url': instance.commentsUrl, 'commit': instance.commit, 'author': instance.author, 'committer': instance.committer, 'parents': instance.parents, }; ================================================ FILE: lib/model/push_event_commit.dart ================================================ import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'push_event_commit.g.dart'; @JsonSerializable() class PushEventCommit { String? sha; User? author; String? message; bool? distinct; String? url; PushEventCommit( this.sha, this.author, this.message, this.distinct, this.url, ); factory PushEventCommit.fromJson(Map json) => _$PushEventCommitFromJson(json); Map toJson() => _$PushEventCommitToJson(this); } ================================================ FILE: lib/model/push_event_commit.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'push_event_commit.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** PushEventCommit _$PushEventCommitFromJson(Map json) => PushEventCommit( json['sha'] as String?, json['author'] == null ? null : User.fromJson(json['author'] as Map), json['message'] as String?, json['distinct'] as bool?, json['url'] as String?, ); Map _$PushEventCommitToJson(PushEventCommit instance) => { 'sha': instance.sha, 'author': instance.author, 'message': instance.message, 'distinct': instance.distinct, 'url': instance.url, }; ================================================ FILE: lib/model/release.dart ================================================ import 'package:gsy_github_app_flutter/model/release_asset.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'release.g.dart'; @JsonSerializable() class Release { int? id; @JsonKey(name: "tag_name") String? tagName; @JsonKey(name: "target_commitish") String? targetCommitish; String? name; String? body; @JsonKey(name: "body_html") String? bodyHtml; @JsonKey(name: "tarball_url") String? tarballUrl; @JsonKey(name: "zipball_url") String? zipballUrl; bool? draft; @JsonKey(name: "prerelease") bool? preRelease; @JsonKey(name: "created_at") DateTime? createdAt; @JsonKey(name: "published_at") DateTime? publishedAt; User? author; List? assets; Release( this.id, this.tagName, this.targetCommitish, this.name, this.body, this.bodyHtml, this.tarballUrl, this.zipballUrl, this.draft, this.preRelease, this.createdAt, this.publishedAt, this.author, this.assets, ); factory Release.fromJson(Map json) => _$ReleaseFromJson(json); Map toJson() => _$ReleaseToJson(this); } ================================================ FILE: lib/model/release.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'release.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Release _$ReleaseFromJson(Map json) => Release( (json['id'] as num?)?.toInt(), json['tag_name'] as String?, json['target_commitish'] as String?, json['name'] as String?, json['body'] as String?, json['body_html'] as String?, json['tarball_url'] as String?, json['zipball_url'] as String?, json['draft'] as bool?, json['prerelease'] as bool?, json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['published_at'] == null ? null : DateTime.parse(json['published_at'] as String), json['author'] == null ? null : User.fromJson(json['author'] as Map), (json['assets'] as List?) ?.map((e) => ReleaseAsset.fromJson(e as Map)) .toList(), ); Map _$ReleaseToJson(Release instance) => { 'id': instance.id, 'tag_name': instance.tagName, 'target_commitish': instance.targetCommitish, 'name': instance.name, 'body': instance.body, 'body_html': instance.bodyHtml, 'tarball_url': instance.tarballUrl, 'zipball_url': instance.zipballUrl, 'draft': instance.draft, 'prerelease': instance.preRelease, 'created_at': instance.createdAt?.toIso8601String(), 'published_at': instance.publishedAt?.toIso8601String(), 'author': instance.author, 'assets': instance.assets, }; ================================================ FILE: lib/model/release_asset.dart ================================================ import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'release_asset.g.dart'; @JsonSerializable() class ReleaseAsset { int? id; String? name; String? label; User? uploader; @JsonKey(name: "content_type") String? contentType; String? state; int? size; int? downloadCout; @JsonKey(name: "created_at") DateTime? createdAt; @JsonKey(name: "updated_at") DateTime? updatedAt; @JsonKey(name: "browser_download_url") String? downloadUrl; ReleaseAsset( this.id, this.name, this.label, this.uploader, this.contentType, this.state, this.size, this.downloadCout, this.createdAt, this.updatedAt, this.downloadUrl, ); factory ReleaseAsset.fromJson(Map json) => _$ReleaseAssetFromJson(json); Map toJson() => _$ReleaseAssetToJson(this); } ================================================ FILE: lib/model/release_asset.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'release_asset.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** ReleaseAsset _$ReleaseAssetFromJson(Map json) => ReleaseAsset( (json['id'] as num?)?.toInt(), json['name'] as String?, json['label'] as String?, json['uploader'] == null ? null : User.fromJson(json['uploader'] as Map), json['content_type'] as String?, json['state'] as String?, (json['size'] as num?)?.toInt(), (json['downloadCout'] as num?)?.toInt(), json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), json['browser_download_url'] as String?, ); Map _$ReleaseAssetToJson(ReleaseAsset instance) => { 'id': instance.id, 'name': instance.name, 'label': instance.label, 'uploader': instance.uploader, 'content_type': instance.contentType, 'state': instance.state, 'size': instance.size, 'downloadCout': instance.downloadCout, 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'browser_download_url': instance.downloadUrl, }; ================================================ FILE: lib/model/repo_commit.dart ================================================ import 'package:gsy_github_app_flutter/model/commit_git_info.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'repo_commit.g.dart'; @JsonSerializable() class RepoCommit { String? sha; String? url; @JsonKey(name: "html_url") String? htmlUrl; @JsonKey(name: "comments_url") String? commentsUrl; CommitGitInfo? commit; User? author; User? committer; List? parents; RepoCommit( this.sha, this.url, this.htmlUrl, this.commentsUrl, this.commit, this.author, this.committer, this.parents, ); factory RepoCommit.fromJson(Map json) => _$RepoCommitFromJson(json); Map toJson() => _$RepoCommitToJson(this); } ================================================ FILE: lib/model/repo_commit.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'repo_commit.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** RepoCommit _$RepoCommitFromJson(Map json) => RepoCommit( json['sha'] as String?, json['url'] as String?, json['html_url'] as String?, json['comments_url'] as String?, json['commit'] == null ? null : CommitGitInfo.fromJson(json['commit'] as Map), json['author'] == null ? null : User.fromJson(json['author'] as Map), json['committer'] == null ? null : User.fromJson(json['committer'] as Map), (json['parents'] as List?) ?.map((e) => RepoCommit.fromJson(e as Map)) .toList(), ); Map _$RepoCommitToJson(RepoCommit instance) => { 'sha': instance.sha, 'url': instance.url, 'html_url': instance.htmlUrl, 'comments_url': instance.commentsUrl, 'commit': instance.commit, 'author': instance.author, 'committer': instance.committer, 'parents': instance.parents, }; ================================================ FILE: lib/model/repository.dart ================================================ import 'package:gsy_github_app_flutter/model/license.dart'; import 'package:gsy_github_app_flutter/model/repository_permissions.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'repository.g.dart'; @JsonSerializable() class Repository { int? id; int? size; String? name; @JsonKey(name: "full_name") String? fullName; @JsonKey(name: "html_url") String? htmlUrl; String? description; String? language; @JsonKey(name: "default_branch") String? defaultBranch; @JsonKey(name: "created_at") DateTime? createdAt; @JsonKey(name: "updated_at") DateTime? updatedAt; @JsonKey(name: "pushed_at") DateTime? pushedAt; @JsonKey(name: "git_url") String? gitUrl; @JsonKey(name: "ssh_url") String? sshUrl; @JsonKey(name: "clone_url") String? cloneUrl; @JsonKey(name: "svn_url") String? svnUrl; @JsonKey(name: "stargazers_count") int? stargazersCount; @JsonKey(name: "watchers_count") int? watchersCount; @JsonKey(name: "forks_count") int? forksCount; @JsonKey(name: "open_issues_count") int? openIssuesCount; @JsonKey(name: "subscribers_count") int? subscribersCount; @JsonKey(name: "private") bool? private; bool? fork; @JsonKey(name: "has_issues") bool? hasIssues; @JsonKey(name: "has_projects") bool? hasProjects; @JsonKey(name: "has_downloads") bool? hasDownloads; @JsonKey(name: "has_wiki") bool? hasWiki; @JsonKey(name: "has_pages") bool? hasPages; User? owner; License? license; Repository? parent; RepositoryPermissions? permissions; List? topics; ///issue总数,不参加序列化 int? allIssueCount; Repository( this.id, this.size, this.name, this.fullName, this.htmlUrl, this.description, this.language, this.license, this.defaultBranch, this.createdAt, this.updatedAt, this.pushedAt, this.gitUrl, this.sshUrl, this.cloneUrl, this.svnUrl, this.stargazersCount, this.watchersCount, this.forksCount, this.openIssuesCount, this.subscribersCount, this.private, this.fork, this.hasIssues, this.hasProjects, this.hasDownloads, this.hasWiki, this.hasPages, this.owner, this.parent, this.permissions, this.topics, ); /// A necessary factory constructor for creating a new User instance /// from a map. We pass the map to the generated _$UserFromJson constructor. /// The constructor is named after the source class, in this case User. factory Repository.fromJson(Map json) => _$RepositoryFromJson(json); Map toJson() => _$RepositoryToJson(this); Repository.empty(); } ================================================ FILE: lib/model/repository.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'repository.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Repository _$RepositoryFromJson(Map json) => Repository( (json['id'] as num?)?.toInt(), (json['size'] as num?)?.toInt(), json['name'] as String?, json['full_name'] as String?, json['html_url'] as String?, json['description'] as String?, json['language'] as String?, json['license'] == null ? null : License.fromJson(json['license'] as Map), json['default_branch'] as String?, json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), json['pushed_at'] == null ? null : DateTime.parse(json['pushed_at'] as String), json['git_url'] as String?, json['ssh_url'] as String?, json['clone_url'] as String?, json['svn_url'] as String?, (json['stargazers_count'] as num?)?.toInt(), (json['watchers_count'] as num?)?.toInt(), (json['forks_count'] as num?)?.toInt(), (json['open_issues_count'] as num?)?.toInt(), (json['subscribers_count'] as num?)?.toInt(), json['private'] as bool?, json['fork'] as bool?, json['has_issues'] as bool?, json['has_projects'] as bool?, json['has_downloads'] as bool?, json['has_wiki'] as bool?, json['has_pages'] as bool?, json['owner'] == null ? null : User.fromJson(json['owner'] as Map), json['parent'] == null ? null : Repository.fromJson(json['parent'] as Map), json['permissions'] == null ? null : RepositoryPermissions.fromJson( json['permissions'] as Map, ), (json['topics'] as List?)?.map((e) => e as String).toList(), )..allIssueCount = (json['allIssueCount'] as num?)?.toInt(); Map _$RepositoryToJson(Repository instance) => { 'id': instance.id, 'size': instance.size, 'name': instance.name, 'full_name': instance.fullName, 'html_url': instance.htmlUrl, 'description': instance.description, 'language': instance.language, 'default_branch': instance.defaultBranch, 'created_at': instance.createdAt?.toIso8601String(), 'updated_at': instance.updatedAt?.toIso8601String(), 'pushed_at': instance.pushedAt?.toIso8601String(), 'git_url': instance.gitUrl, 'ssh_url': instance.sshUrl, 'clone_url': instance.cloneUrl, 'svn_url': instance.svnUrl, 'stargazers_count': instance.stargazersCount, 'watchers_count': instance.watchersCount, 'forks_count': instance.forksCount, 'open_issues_count': instance.openIssuesCount, 'subscribers_count': instance.subscribersCount, 'private': instance.private, 'fork': instance.fork, 'has_issues': instance.hasIssues, 'has_projects': instance.hasProjects, 'has_downloads': instance.hasDownloads, 'has_wiki': instance.hasWiki, 'has_pages': instance.hasPages, 'owner': instance.owner, 'license': instance.license, 'parent': instance.parent, 'permissions': instance.permissions, 'topics': instance.topics, 'allIssueCount': instance.allIssueCount, }; ================================================ FILE: lib/model/repository_permissions.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-07-31 */ part 'repository_permissions.g.dart'; @JsonSerializable() class RepositoryPermissions { bool? admin; bool? push; bool? pull; RepositoryPermissions( this.admin, this.push, this.pull, ); factory RepositoryPermissions.fromJson(Map json) => _$RepositoryPermissionsFromJson(json); Map toJson() => _$RepositoryPermissionsToJson(this); } ================================================ FILE: lib/model/repository_permissions.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'repository_permissions.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** RepositoryPermissions _$RepositoryPermissionsFromJson( Map json, ) => RepositoryPermissions( json['admin'] as bool?, json['push'] as bool?, json['pull'] as bool?, ); Map _$RepositoryPermissionsToJson( RepositoryPermissions instance, ) => { 'admin': instance.admin, 'push': instance.push, 'pull': instance.pull, }; ================================================ FILE: lib/model/repository_ql.dart ================================================ class RepositoryQL { final int? issuesClosed; final int? issuesOpen; final int? issuesTotal; final String? reposName; final String? reposFullName; final String? ownerName; final String? ownerAvatarUrl; final String? license; final int? forkCount; final int? starCount; final int? watcherCount; final bool? isFork; final bool? isStared; final bool? hasIssuesEnabled; final String? defaultBranch; final String? isSubscription; final String? language; final int? size; final String? createdAt; final String? pushAt; final String? sshUrl; final String? htmlUrl; final String? shortDescriptionHTML; final List? topics; final RepositoryQL? parent; RepositoryQL({ this.issuesClosed, this.issuesOpen, this.issuesTotal, this.reposName, this.reposFullName, this.ownerName, this.ownerAvatarUrl, this.license, this.forkCount, this.starCount, this.watcherCount, this.isFork, this.isStared, this.hasIssuesEnabled, this.defaultBranch, this.isSubscription, this.language, this.size, this.createdAt, this.pushAt, this.sshUrl, this.htmlUrl, this.shortDescriptionHTML, this.topics, this.parent, }); static fromMap(Map? map) { List topics = []; if (map == null) { return null; } Map? repositoryTopics = map["repositoryTopics"]; if (repositoryTopics != null) { List topicList = repositoryTopics["nodes"]; for (var item in topicList) { topics.add(item["topic"]["name"]); } } return RepositoryQL( issuesClosed: map["issuesClosed"]["totalCount"], issuesOpen: map["issuesOpen"]["totalCount"], issuesTotal: map["issues"]["totalCount"], defaultBranch: map["defaultBranchRef"] != null ? map["defaultBranchRef"]["name"] : null, reposName: map["name"], hasIssuesEnabled: map["hasIssuesEnabled"], reposFullName: map["nameWithOwner"], ownerName: map["owner"]["login"], ownerAvatarUrl: map["owner"]["avatarUrl"], license: map["licenseInfo"] != null ? map["licenseInfo"]["name"] : null, forkCount: map["forkCount"], watcherCount: map["watchers"]["totalCount"], isFork: map["isFork"], starCount: map["stargazers"]["totalCount"], isStared: map["viewerHasStarred"], isSubscription: map["viewerSubscription"], language: (map["languages"] != null && map["languages"]["nodes"] != null && map["languages"]["nodes"].length > 0) ? map["languages"]["nodes"][0]["name"] : null, size: map["languages"]["totalSize"], createdAt: map["createdAt"], pushAt: map["pushedAt"], sshUrl: map["sshUrl"], htmlUrl: map["url"], shortDescriptionHTML: map["shortDescriptionHTML"], topics: topics, parent: RepositoryQL.fromMap(map["parent"]), ); } static toMap(RepositoryQL? repositoryQL) { var topics = {}; if (repositoryQL == null) { return null; } if (repositoryQL.topics != null) { var list = []; for (var item in repositoryQL.topics!) { list.add({"topic": item}); } topics["nodes"] = list; } var map = { "issuesClosed": { "totalCount": repositoryQL.issuesClosed, }, "issuesOpen": { "totalCount": repositoryQL.issuesOpen, }, "issuesTotal": { "totalCount": repositoryQL.issuesTotal, }, "defaultBranchRef": { "name": repositoryQL.defaultBranch, }, "name": repositoryQL.reposName, "hasIssuesEnabled": repositoryQL.hasIssuesEnabled, "nameWithOwner": repositoryQL.reposFullName, "owner": { "login": repositoryQL.ownerName, "avatarUrl": repositoryQL.ownerAvatarUrl, }, "languages": { "nodes": [ { "name": repositoryQL.language, } ], "totalSize": repositoryQL.size, }, "licenseInfo": { "name": repositoryQL.license, }, "forkCount": repositoryQL.forkCount, "stargazers": {"totalSize": repositoryQL.starCount}, "watcherCount": { "watchers": repositoryQL.watcherCount, }, "isFork": repositoryQL.isFork, "viewerHasStarred": repositoryQL.isStared, "viewerSubscription": repositoryQL.isSubscription, "createdAt": repositoryQL.createdAt, "pushedAt": repositoryQL.pushAt, "sshUrl": repositoryQL.sshUrl, "url": repositoryQL.htmlUrl, "shortDescriptionHTML": repositoryQL.shortDescriptionHTML, "topic": topics, "parent": toMap(repositoryQL.parent), }; return map; } } ================================================ FILE: lib/model/search_user_ql.dart ================================================ class SearchUserQL { final int? followers; final String? name; final String? avatarUrl; final String? bio; final String? login; final String? lang; SearchUserQL({ this.followers, this.name, this.avatarUrl, this.bio, this.login, this.lang, }); static fromMap(Map map) { String? lang; if (map["lang"] != null && map["lang"]["nodes"] != null && map["lang"]["nodes"].length > 0 && map["lang"]["nodes"][0]["languages"] != null && map["lang"]["nodes"][0]["languages"]["nodes"] != null && map["lang"]["nodes"][0]["languages"]["nodes"].length > 0) { lang = map["lang"]["nodes"][0]["languages"]["nodes"][0]["name"]; } return SearchUserQL( followers: map["followers"]?["totalCount"], name: map["name"], avatarUrl: map["avatarUrl"], bio: map["bio"], login: map["login"], lang: lang, ); } } ================================================ FILE: lib/model/template.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'template.g.dart'; @JsonSerializable() class Template { String? name; int? id; @JsonKey(name: "push_id") int? pushId; Template(this.name, this.id, this.pushId); factory Template.fromJson(Map json) => _$TemplateFromJson(json); Map toJson() => _$TemplateToJson(this); } ================================================ FILE: lib/model/template.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'template.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** Template _$TemplateFromJson(Map json) => Template( json['name'] as String?, (json['id'] as num?)?.toInt(), (json['push_id'] as num?)?.toInt(), ); Map _$TemplateToJson(Template instance) => { 'name': instance.name, 'id': instance.id, 'push_id': instance.pushId, }; ================================================ FILE: lib/model/trending_repo_model.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-08-07 */ part 'trending_repo_model.g.dart'; @JsonSerializable() class TrendingRepoModel { String? fullName; String? url; String? description; String? language; String? meta; List? contributors; String? contributorsUrl; String? starCount; String? forkCount; String? name; String? reposName; TrendingRepoModel( this.fullName, this.url, this.description, this.language, this.meta, this.contributors, this.contributorsUrl, this.starCount, this.name, this.reposName, this.forkCount, ); TrendingRepoModel.empty(); factory TrendingRepoModel.fromJson(Map json) => _$TrendingRepoModelFromJson(json); Map toJson() => _$TrendingRepoModelToJson(this); } ================================================ FILE: lib/model/trending_repo_model.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'trending_repo_model.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** TrendingRepoModel _$TrendingRepoModelFromJson(Map json) => TrendingRepoModel( json['fullName'] as String?, json['url'] as String?, json['description'] as String?, json['language'] as String?, json['meta'] as String?, (json['contributors'] as List?) ?.map((e) => e as String) .toList(), json['contributorsUrl'] as String?, json['starCount'] as String?, json['name'] as String?, json['reposName'] as String?, json['forkCount'] as String?, ); Map _$TrendingRepoModelToJson(TrendingRepoModel instance) => { 'fullName': instance.fullName, 'url': instance.url, 'description': instance.description, 'language': instance.language, 'meta': instance.meta, 'contributors': instance.contributors, 'contributorsUrl': instance.contributorsUrl, 'starCount': instance.starCount, 'forkCount': instance.forkCount, 'name': instance.name, 'reposName': instance.reposName, }; ================================================ FILE: lib/model/user.dart ================================================ import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; @JsonSerializable() class User { User( this.login, this.id, this.node_id, this.avatar_url, this.gravatar_id, this.url, this.html_url, this.followers_url, this.following_url, this.gists_url, this.starred_url, this.subscriptions_url, this.organizations_url, this.repos_url, this.events_url, this.received_events_url, this.type, this.site_admin, this.name, this.company, this.blog, this.location, this.email, this.starred, this.bio, this.public_repos, this.public_gists, this.followers, this.following, this.created_at, this.updated_at, this.private_gists, this.total_private_repos, this.owned_private_repos, this.disk_usage, this.collaborators, this.two_factor_authentication); String? login; int? id; String? node_id; String? avatar_url; String? gravatar_id; String? url; String? html_url; String? followers_url; String? following_url; String? gists_url; String? starred_url; String? subscriptions_url; String? organizations_url; String? repos_url; String? events_url; String? received_events_url; String? type; bool? site_admin; String? name; String? company; String? blog; String? location; String? email; String? starred; String? bio; int? public_repos; int? public_gists; int? followers; int? following; DateTime? created_at; DateTime? updated_at; int? private_gists; int? total_private_repos; int? owned_private_repos; int? disk_usage; int? collaborators; bool? two_factor_authentication; factory User.fromJson(Map json) => _$UserFromJson(json); Map toJson() => _$UserToJson(this); // 命名构造函数 User.empty(); } ================================================ FILE: lib/model/user.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'user.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** User _$UserFromJson(Map json) => User( json['login'] as String?, (json['id'] as num?)?.toInt(), json['node_id'] as String?, json['avatar_url'] as String?, json['gravatar_id'] as String?, json['url'] as String?, json['html_url'] as String?, json['followers_url'] as String?, json['following_url'] as String?, json['gists_url'] as String?, json['starred_url'] as String?, json['subscriptions_url'] as String?, json['organizations_url'] as String?, json['repos_url'] as String?, json['events_url'] as String?, json['received_events_url'] as String?, json['type'] as String?, json['site_admin'] as bool?, json['name'] as String?, json['company'] as String?, json['blog'] as String?, json['location'] as String?, json['email'] as String?, json['starred'] as String?, json['bio'] as String?, (json['public_repos'] as num?)?.toInt(), (json['public_gists'] as num?)?.toInt(), (json['followers'] as num?)?.toInt(), (json['following'] as num?)?.toInt(), json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), (json['private_gists'] as num?)?.toInt(), (json['total_private_repos'] as num?)?.toInt(), (json['owned_private_repos'] as num?)?.toInt(), (json['disk_usage'] as num?)?.toInt(), (json['collaborators'] as num?)?.toInt(), json['two_factor_authentication'] as bool?, ); Map _$UserToJson(User instance) => { 'login': instance.login, 'id': instance.id, 'node_id': instance.node_id, 'avatar_url': instance.avatar_url, 'gravatar_id': instance.gravatar_id, 'url': instance.url, 'html_url': instance.html_url, 'followers_url': instance.followers_url, 'following_url': instance.following_url, 'gists_url': instance.gists_url, 'starred_url': instance.starred_url, 'subscriptions_url': instance.subscriptions_url, 'organizations_url': instance.organizations_url, 'repos_url': instance.repos_url, 'events_url': instance.events_url, 'received_events_url': instance.received_events_url, 'type': instance.type, 'site_admin': instance.site_admin, 'name': instance.name, 'company': instance.company, 'blog': instance.blog, 'location': instance.location, 'email': instance.email, 'starred': instance.starred, 'bio': instance.bio, 'public_repos': instance.public_repos, 'public_gists': instance.public_gists, 'followers': instance.followers, 'following': instance.following, 'created_at': instance.created_at?.toIso8601String(), 'updated_at': instance.updated_at?.toIso8601String(), 'private_gists': instance.private_gists, 'total_private_repos': instance.total_private_repos, 'owned_private_repos': instance.owned_private_repos, 'disk_usage': instance.disk_usage, 'collaborators': instance.collaborators, 'two_factor_authentication': instance.two_factor_authentication, }; ================================================ FILE: lib/model/user_org.dart ================================================ import 'package:json_annotation/json_annotation.dart'; /** * Created by guoshuyu * Date: 2018-08-10 */ part 'user_org.g.dart'; @JsonSerializable() class UserOrg { String? login; int? id; String? url; String? description; @JsonKey(name: "node_id") String? nodeId; @JsonKey(name: "repos_url") String? reposUrl; @JsonKey(name: "events_url") String? eventsUrl; @JsonKey(name: "hooks_url") String? hooksUrl; @JsonKey(name: "issues_url") String? issuesUrl; @JsonKey(name: "members_url") String? membersUrl; @JsonKey(name: "public_members_url") String? publicMembersUrl; @JsonKey(name: "avatar_url") String? avatarUrl; UserOrg( this.login, this.id, this.url, this.description, this.nodeId, this.reposUrl, this.eventsUrl, this.hooksUrl, this.issuesUrl, this.membersUrl, this.publicMembersUrl, this.avatarUrl, ); factory UserOrg.fromJson(Map json) => _$UserOrgFromJson(json); Map toJson() => _$UserOrgToJson(this); // 命名构造函数 UserOrg.empty(); } ================================================ FILE: lib/model/user_org.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'user_org.dart'; // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** UserOrg _$UserOrgFromJson(Map json) => UserOrg( json['login'] as String?, (json['id'] as num?)?.toInt(), json['url'] as String?, json['description'] as String?, json['node_id'] as String?, json['repos_url'] as String?, json['events_url'] as String?, json['hooks_url'] as String?, json['issues_url'] as String?, json['members_url'] as String?, json['public_members_url'] as String?, json['avatar_url'] as String?, ); Map _$UserOrgToJson(UserOrg instance) => { 'login': instance.login, 'id': instance.id, 'url': instance.url, 'description': instance.description, 'node_id': instance.nodeId, 'repos_url': instance.reposUrl, 'events_url': instance.eventsUrl, 'hooks_url': instance.hooksUrl, 'issues_url': instance.issuesUrl, 'members_url': instance.membersUrl, 'public_members_url': instance.publicMembersUrl, 'avatar_url': instance.avatarUrl, }; ================================================ FILE: lib/page/AGENTS.md ================================================ # 页面层协作说明 `lib/page/` 是功能页面主目录,但不同子目录使用的状态管理方式并不统一。 进入具体功能目录前,先看 `docs/02-features/` 对应文档。 ## 工作规则 - 页面问题优先局部修复,不要直接上升到 `lib/app.dart` - 先识别当前模块用的是 Redux、Provider、Riverpod 还是 Signals - 不要因为“看起来更现代”就替换当前模块状态方案 - 导航、路由、通用弹窗、共享组件优先复用现有工具类 ## 修改提示 - `login/`:登录结果走 Redux 链路 - `repos/`:跨 tab 状态共享,主要用 Provider - `trend/`:主要用 Riverpod,但夹杂局部状态 - `notify/`:主要用 Signals ## 最低验证 - 手工验证目标页面的进入、主交互、返回链路 - 如涉及列表页,验证刷新和分页 - 如涉及详情页,验证从入口页跳转到详情页再返回 ================================================ FILE: lib/page/code_detail_page_web.dart ================================================ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/html_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// 文件代码详情 /// Created by guoshuyu /// Date: 2018-07-24 class CodeDetailPageWeb extends StatefulWidget { final String? userName; final String? reposName; final String? path; final String? data; final String? title; final String? branch; final String? htmlUrl; final String? lang; const CodeDetailPageWeb( {super.key, this.title, this.userName, this.reposName, this.path, this.data, this.lang, this.branch, this.htmlUrl}); @override _CodeDetailPageState createState() => _CodeDetailPageState(); } class _CodeDetailPageState extends State { bool isLand = false; late final WebViewController controller; @override void initState() { controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted); super.initState(); } Future _getData() async { if (widget.data != null) { return Uri.dataFromString(widget.data!, mimeType: 'text/html', encoding: Encoding.getByName("utf-8")); } var res = await ReposRepository.getReposFileDirRequest( widget.userName!, widget.reposName!, path: widget.path, branch: widget.branch, text: true, isHtml: true); if (res != null && res.result) { String data2 = HtmlUtils.resolveHtmlFile(res, widget.lang ?? "java"); return Uri.dataFromString(data2, mimeType: 'text/html', encoding: Encoding.getByName("utf-8")); } return null; } @override void dispose() { SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: GSYTitleBar(widget.title), ), body: FutureBuilder( future: _getData(), builder: (context, result) { if (result.data == null) { return Center( child: Container( width: 200.0, height: 200.0, padding: const EdgeInsets.all(4.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SpinKitDoubleBounce(color: Theme.of(context).primaryColor), Container(width: 10.0), Text(context.l10n.loading_text, style: GSYConstant.middleText), ], ), ), ); } controller.loadRequest(result.data!); return WebViewWidget( controller: controller, ); }, ), floatingActionButton: FloatingActionButton( child: Icon( isLand ? Icons.screen_lock_landscape : Icons.screen_lock_portrait), onPressed: () { setState(() { if (isLand) { isLand = !isLand; SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, ]); } else { isLand = !isLand; SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, ]); } }); }, ), ); } } ================================================ FILE: lib/page/common_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/common_list_datatype.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/page/repos/widget/repos_item.dart'; import 'package:gsy_github_app_flutter/page/user/widget/user_item.dart'; /// 通用list /// Created by guoshuyu /// on 2018/7/22. class CommonListPage extends StatefulWidget { final String? userName; final String? reposName; final String showType; final CommonListDataType dataType; final String? title; const CommonListPage(this.title, this.showType, this.dataType, {super.key, this.userName, this.reposName}); @override _CommonListPageState createState() => _CommonListPageState(); } class _CommonListPageState extends State with AutomaticKeepAliveClientMixin, GSYListState { _CommonListPageState(); _renderItem(index) { if (pullLoadWidgetControl.dataList.isEmpty) { return null; } var data = pullLoadWidgetControl.dataList[index]; switch (widget.showType) { case 'repository': ReposViewModel reposViewModel = ReposViewModel.fromMap(data); return ReposItem(reposViewModel, onPressed: () { NavigatorUtils.goReposDetail( context, reposViewModel.ownerName, reposViewModel.repositoryName); }); case 'repositoryql': ReposViewModel reposViewModel = ReposViewModel.fromQL(data); return ReposItem(reposViewModel, onPressed: () { NavigatorUtils.goReposDetail( context, reposViewModel.ownerName, reposViewModel.repositoryName); }); case 'user': return UserItem(UserItemViewModel.fromMap(data), onPressed: () { NavigatorUtils.goPerson(context, data.login); }); case 'org': return UserItem(UserItemViewModel.fromOrgMap(data), onPressed: () { NavigatorUtils.goPerson(context, data.login); }); case 'issue': return null; case 'release': return null; case 'notify': return null; } } _getDataLogic() async { return switch (widget.dataType) { CommonListDataType.follower => await UserRepository.getFollowerListRequest( widget.userName!, page, needDb: page <= 1), CommonListDataType.followed => await UserRepository.getFollowedListRequest( widget.userName!, page, needDb: page <= 1), CommonListDataType.userRepos => await ReposRepository.getUserRepositoryRequest( widget.userName!, page, null, needDb: page <= 1), CommonListDataType.userStar => await ReposRepository.getStarRepositoryRequest( widget.userName!, page, null, needDb: page <= 1), CommonListDataType.repoStar => await ReposRepository.getRepositoryStarRequest( widget.userName!, widget.reposName!, page, needDb: page <= 1), CommonListDataType.repoWatcher => await ReposRepository.getRepositoryWatcherRequest( widget.userName!, widget.reposName!, page, needDb: page <= 1), CommonListDataType.repoFork => await ReposRepository.getRepositoryForksRequest( widget.userName!, widget.reposName!, page, needDb: page <= 1), CommonListDataType.history => await ReposRepository.getHistoryRequest(page), CommonListDataType.topics => await ReposRepository.searchTopicRepositoryRequest(widget.userName, page: page), CommonListDataType.userOrgs => await UserRepository.getUserOrgsRequest(widget.userName!, page, needDb: page <= 1), _ => null, }; } @override bool get wantKeepAlive => true; @override requestRefresh() async { return await _getDataLogic(); } @override requestLoadMore() async { return await _getDataLogic(); } @override bool get isRefreshFirst => true; @override bool get needHeader => false; @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. return Scaffold( appBar: AppBar( title: Text( widget.title ?? "", maxLines: 1, overflow: TextOverflow.ellipsis, )), body: GSYPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderItem(index), handleRefresh, onLoadMore, refreshKey: refreshIndicatorKey, ), ); } } ================================================ FILE: lib/page/debug/debug_data_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/log_interceptor.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/test/demo_tab_page.dart'; import 'package:gsy_github_app_flutter/widget/flutter_json_widget.dart'; import 'package:talker_flutter/talker_flutter.dart'; ///请求数据调 class DebugDataPage extends StatefulWidget { const DebugDataPage({super.key}); @override _DebugDataPageState createState() => _DebugDataPageState(); } class _DebugDataPageState extends State { int tabIndex = 0; /// tab _renderTab(String text, index) { return Tab( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [Text(text, style: const TextStyle(fontSize: 11))], ), ); } @override Widget build(BuildContext context) { return TabWidget( type: TabType.top, /// 返回数据和请求数据 tabItems: [ _renderTab("Responses", 0), _renderTab("Request", 1), _renderTab("HttpError", 2), _renderTab("ErrorLog", 3), ], title: const Text( "Debug", style: TextStyle(color: GSYColors.white), ), tabViews: [ DebugDataList(LogsInterceptors.sResponsesHttpUrl, LogsInterceptors.sHttpResponses), DebugDataList( LogsInterceptors.sRequestHttpUrl, LogsInterceptors.sHttpRequest), DebugDataList( LogsInterceptors.sHttpErrorUrl, LogsInterceptors.sHttpError), TalkerScreen( talker: talker, appBarLeading: const SizedBox(), appBarTitle: "", ) ], indicatorColor: GSYColors.primaryValue, onTap: (index) { setState(() { tabIndex = index; }); }); } } class DebugDataList extends StatefulWidget { final List dataList; final List titles; const DebugDataList(this.titles, this.dataList, {super.key}); @override _DebugDataListState createState() => _DebugDataListState(); } class _DebugDataListState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return Container( color: GSYColors.white, child: ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, i) { var index = widget.dataList.length - i - 1; return InkWell( child: Card( child: Row( children: [ Container( alignment: Alignment.center, margin: const EdgeInsets.only(right: 5), height: 24, width: 24, decoration: const BoxDecoration( color: GSYColors.primaryValue, borderRadius: BorderRadius.all( Radius.circular(12), ), ), child: Text( index.toString(), style: TextStyle( fontSize: 15, color: Colors.white.withAlpha(200), ), ), ), Expanded( child: Container( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 5), child: Text( widget.titles[index] ?? "", style: const TextStyle(fontSize: 15), ), )) ], ), ), onLongPress: () { try { Clipboard.setData( ClipboardData(text: "${widget.titles[index]}")); showToast("复制链接成功"); } catch (e) { printLog(e); } }, onDoubleTap: () { try { Clipboard.setData( ClipboardData(text: "${widget.dataList[index]}")); showToast("复制数据成功"); } catch (e) { printLog(e); } }, onTap: () { showBottomSheet( context: context, builder: (context) { return Material( color: Colors.transparent, child: Stack( children: [ Container( padding: const EdgeInsets.only(top: 30), color: Colors.white, child: SingleChildScrollView( child: JsonViewerWidget(widget.dataList[index] as Map)), ), Transform.translate( offset: const Offset(0, -10), child: Container( alignment: Alignment.topCenter, child: IconButton( icon: const Icon( Icons.arrow_drop_down, size: 30, color: Colors.black, ), onPressed: () { Navigator.of(context).pop(); }, ), ), ), ], ), ); }); }, ); }, itemCount: widget.titles.length, ), ); } } ================================================ FILE: lib/page/debug/debug_label.dart ================================================ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/env/config_wrapper.dart'; import 'package:package_info_plus/package_info_plus.dart'; class DebugLabel { static bool hadShow = false; static OverlayEntry? _overlayEntry; static showDebugLabel(BuildContext context) async { if (!ConfigWrapper.of(context)!.debug!) { return false; } if (hadShow) { return false; } hadShow = true; var gl = context.l10n; var overlayState = Overlay.of(context); var (version, platform) = await _getDeviceInfo(); PackageInfo packInfo = await PackageInfo.fromPlatform(); var language = gl.localeName; if (_overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; } _overlayEntry = OverlayEntry(builder: (context) { return GlobalLabel( version: packInfo.version, deviceInfo: version, platform: platform, language: language); }); overlayState.insert(_overlayEntry!); } static resetDebugLabel(BuildContext context) { hideDebugLabel(); showDebugLabel(context); } static hideDebugLabel() { hadShow = false; if (_overlayEntry != null) { _overlayEntry!.remove(); _overlayEntry = null; } } } Future<(String, String)> _getDeviceInfo() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid) { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; return (androidInfo.version.sdkInt.toString(), "Android"); } IosDeviceInfo iosInfo = await deviceInfo.iosInfo; String device = await CommonUtils.getDeviceInfo(); return (iosInfo.systemVersion , device); } class GlobalLabel extends StatefulWidget { final String? version; final String? deviceInfo; final String? platform; final String? language; const GlobalLabel({super.key, this.version, this.deviceInfo, this.platform, this.language}); @override _GlobalLabelState createState() => _GlobalLabelState(); } class _GlobalLabelState extends State { bool doubleClick = false; bool longClick = false; @override void dispose() { DebugLabel.hadShow = false; //_overlayEntry = null; super.dispose(); } @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { return Container( alignment: const Alignment(0.97, 0.8), child: Material( color: Colors.transparent, child: Container( decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.6), borderRadius: const BorderRadius.all( Radius.circular(5), ), ), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 3), child: InkWell( onLongPress: () { longClick = true; }, onDoubleTap: () { doubleClick = true; if (longClick && doubleClick) { NavigatorUtils.goDebugDataPage(context); } }, child: Text( "${widget.platform} ${widget.deviceInfo} ${widget.language} ${widget.version}", style: const TextStyle(color: Colors.white, fontSize: 10), ), ), ), ), ); }); } } ================================================ FILE: lib/page/dynamic/dynamic_bloc.dart ================================================ import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/repositories/event_repository.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_new_load_widget.dart'; /// Created by guoshuyu /// on 2019/3/23. class DynamicBloc { final GSYPullLoadWidgetControl pullLoadWidgetControl = GSYPullLoadWidgetControl(); int _page = 1; requestRefresh(String? userName, {doNextFlag = true}) async { pageReset(); var res = await EventRepository.getEventReceived(userName, page: _page, needDb: true); changeLoadMoreStatus(getLoadMoreStatus(res)); refreshData(res); if (doNextFlag) { await doNext(res); } return res; } requestLoadMore(String? userName) async { pageUp(); var res = await EventRepository.getEventReceived(userName, page: _page); changeLoadMoreStatus(getLoadMoreStatus(res)); loadMoreData(res); return res; } pageReset() { _page = 1; } pageUp() { _page++; } getLoadMoreStatus(res) { return (res != null && res.data != null && res.data.length == Config.PAGE_SIZE); } doNext(res) async { if (res?.next != null) { var resNext = await res.next(); if (resNext != null && resNext.result) { changeLoadMoreStatus(getLoadMoreStatus(resNext)); refreshData(resNext); } } } ///列表数据长度 int? getDataLength() { return pullLoadWidgetControl.dataList?.length; } ///修改加载更多 changeLoadMoreStatus(bool needLoadMore) { pullLoadWidgetControl.needLoadMore = needLoadMore; } ///是否需要头部 changeNeedHeaderStatus(bool needHeader) { pullLoadWidgetControl.needHeader = needHeader; } ///刷新列表数据 refreshData(res) { if (res != null) { pullLoadWidgetControl.dataList = res.data; } } ///加载更多数据 loadMoreData(res) { if (res != null) { pullLoadWidgetControl.addList(res.data); } } ///清理数据 clearData() { refreshData([]); } ///列表数据 get dataList => pullLoadWidgetControl.dataList; void dispose() { pullLoadWidgetControl.dispose(); } } ================================================ FILE: lib/page/dynamic/dynamic_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/page/dynamic/dynamic_bloc.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/common/utils/event_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_event_item.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_new_load_widget.dart'; import 'package:redux/redux.dart'; /// 主页动态tab页 /// Created by guoshuyu /// Date: 2018-07-16 class DynamicPage extends StatefulWidget { const DynamicPage({super.key}); @override DynamicPageState createState() => DynamicPageState(); } class DynamicPageState extends State with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { final DynamicBloc dynamicBloc = DynamicBloc(); ///控制列表滚动和监听 final ScrollController scrollController = ScrollController(); final GlobalKey refreshIndicatorKey = GlobalKey(); bool _ignoring = true; /// 模拟IOS下拉显示刷新 showRefreshLoading() { ///直接触发下拉 Future.delayed(const Duration(milliseconds: 500), () { scrollController .animateTo(-141, duration: const Duration(milliseconds: 600), curve: Curves.linear) .then((_) { /*setState(() { _ignoring = false; });*/ }); return true; }); } scrollToTop() { if (scrollController.offset <= 0) { scrollController .animateTo(0, duration: const Duration(milliseconds: 600), curve: Curves.linear) .then((_) { showRefreshLoading(); }); } else { scrollController.animateTo(0, duration: const Duration(milliseconds: 600), curve: Curves.linear); } } ///下拉刷新数据 Future requestRefresh() async { await dynamicBloc .requestRefresh(_getStore().state.userInfo?.login) .catchError((e) { printLog(e); }); setState(() { _ignoring = false; }); } ///上拉更多请求数据 Future requestLoadMore() async { return await dynamicBloc.requestLoadMore(_getStore().state.userInfo?.login); } _renderEventItem(Event e) { EventViewModel eventViewModel = EventViewModel.fromEventMap(e); return GSYEventItem( eventViewModel, onPressed: () { EventUtils.ActionUtils(context, e, ""); }, ); } Store _getStore() { return StoreProvider.of(context); } @override void initState() { super.initState(); ///监听生命周期,主要判断页面 resumed 的时候触发刷新 WidgetsBinding.instance.addObserver(this); ///获取网络端新版信息 ReposRepository.getNewsVersion(context, false); } @override void didChangeDependencies() { ///请求更新 if (dynamicBloc.getDataLength() == 0) { dynamicBloc.changeNeedHeaderStatus(false); ///先读数据库 dynamicBloc .requestRefresh(_getStore().state.userInfo?.login, doNextFlag: false) .then((_) { showRefreshLoading(); }); } super.didChangeDependencies(); } ///监听生命周期,主要判断页面 resumed 的时候触发刷新 @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { if (dynamicBloc.getDataLength() != 0) { showRefreshLoading(); } } } @override bool get wantKeepAlive => true; @override void dispose() { WidgetsBinding.instance.removeObserver(this); dynamicBloc.dispose(); super.dispose(); } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. var content = GSYPullLoadWidget( dynamicBloc.pullLoadWidgetControl, (BuildContext context, int index) => _renderEventItem(dynamicBloc.dataList[index]), requestRefresh, requestLoadMore, refreshKey: refreshIndicatorKey, scrollController: scrollController, ///使用ios模式的下拉刷新 userIos: true, ); return IgnorePointer( ignoring: _ignoring, child: content, ); } } ================================================ FILE: lib/page/error_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/repositories/issue_repository.dart'; import 'package:gsy_github_app_flutter/common/net/interceptors/log_interceptor.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; class ErrorPage extends StatefulWidget { final String errorMessage; final FlutterErrorDetails details; const ErrorPage(this.errorMessage, this.details, {super.key}); @override ErrorPageState createState() => ErrorPageState(); } class ErrorPageState extends State { static List?> sErrorStack = []; static List sErrorName = []; final TextEditingController textEditingController = TextEditingController(); addError(FlutterErrorDetails details) { try { var map = {}; map["error"] = details.toString(); LogsInterceptors.addLogic( sErrorName, details.exception.runtimeType.toString()); LogsInterceptors.addLogic(sErrorStack, map); } catch (e) { printLog(e); } } @override Widget build(BuildContext context) { double width = MediaQueryData.fromView(View.of(context)).size.width; return Container( color: GSYColors.primaryValue, child: Center( child: Container( alignment: Alignment.center, width: width, height: width, decoration: BoxDecoration( color: Colors.white.withAlpha(30), gradient: RadialGradient(tileMode: TileMode.mirror, radius: 0.1, colors: [ Colors.white.withAlpha(10), GSYColors.primaryValue.withAlpha(100), ]), borderRadius: BorderRadius.all(Radius.circular(width / 2)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 90.0, height: 90.0), const SizedBox( height: 11, ), const Material( color: GSYColors.primaryValue, child: Text( "Error Occur", style: TextStyle(fontSize: 24, color: Colors.white), ), ), const SizedBox( height: 40, ), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( style: TextButton.styleFrom( backgroundColor: GSYColors.white.withAlpha(100), ), onPressed: () { String content = widget.errorMessage; textEditingController.text = content; CommonUtils.showEditDialog( context, context.l10n.home_reply, (title) {}, (res) { content = res; }, () { if (content.isEmpty) { return; } CommonUtils.showLoadingDialog(context); IssueRepository.createIssueRequest( "CarGuo", "gsy_github_app_flutter", { "title": context.l10n.home_reply, "body": content }).then((result) { Navigator.pop(context); Navigator.pop(context); }); }, titleController: TextEditingController(), valueController: textEditingController, needTitle: false); }, child: const Text("Report"), ), const SizedBox( width: 40, ), TextButton( style: TextButton.styleFrom( backgroundColor: Colors.white.withAlpha(100)), onPressed: () { Navigator.of(context).pop(); }, child: const Text("Back")), ], ) ], ), ), ), ); } } ================================================ FILE: lib/page/gsy_webview.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:webview_flutter/webview_flutter.dart'; /// webview版本 /// Created by guoshuyu /// on 2018/7/27. class GSYWebView extends StatefulWidget { final String url; final String? title; const GSYWebView(this.url, this.title, {super.key}); @override _GSYWebViewState createState() => _GSYWebViewState(); } class _GSYWebViewState extends State { _renderTitle() { if (widget.url.isEmpty) { return Text(widget.title!); } return Row(children: [ Expanded( child: Text( widget.title!, maxLines: 1, overflow: TextOverflow.ellipsis, )), GSYCommonOptionWidget(url: widget.url), ]); } final FocusNode focusNode = FocusNode(); bool isLoading = true; late final WebViewController controller; @override void initState() { controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { // Update loading bar. }, onPageStarted: (String url) {}, onPageFinished: (String url) { setState(() { isLoading = false; }); }, onWebResourceError: (WebResourceError error) {}, ), ) ..addJavaScriptChannel("name", onMessageReceived: (message) { printLog(message.message); FocusScope.of(context).requestFocus(focusNode); }) ..loadRequest(Uri.parse(widget.url)); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, appBar: AppBar( title: _renderTitle(), ), body: Stack( children: [ TextField( focusNode: focusNode, ), WebViewWidget( controller: controller, ), if (isLoading) Center( child: Container( width: 200.0, height: 200.0, padding: const EdgeInsets.all(4.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SpinKitDoubleBounce(color: Theme.of(context).primaryColor), Container(width: 10.0), Text(context.l10n.loading_text, style: GSYConstant.middleText), ], ), ), ) ], ), ); } } ///测试 html 代码,不管 const testhtml = "" "" "" "" "" "Local Title" "" "" "" "" "" "" "" "" ""; ================================================ FILE: lib/page/home/home_page.dart ================================================ import 'dart:io'; import 'package:android_intent_plus/android_intent.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/dynamic/dynamic_page.dart'; import 'package:gsy_github_app_flutter/page/my_page.dart'; import 'package:gsy_github_app_flutter/page/trend/trend_page.dart'; import 'package:gsy_github_app_flutter/widget/gsy_tabbar_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:gsy_github_app_flutter/page/home/widget/home_drawer.dart'; import 'package:lottie/lottie.dart'; /// 主页 /// Created by guoshuyu /// Date: 2018-07-16 class HomePage extends StatefulWidget { static const String sName = "home"; const HomePage({super.key}); @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State { final GlobalKey dynamicKey = GlobalKey(); final GlobalKey trendKey = GlobalKey(); final GlobalKey myKey = GlobalKey(); final GlobalKey rightKey = GlobalKey(); /// 不退出 _dialogExitApp(BuildContext context) async { ///如果是 android 回到桌面 if (Platform.isAndroid) { AndroidIntent intent = const AndroidIntent( action: 'android.intent.action.MAIN', category: "android.intent.category.HOME", ); await intent.launch(); } } _renderTab(icon, text) { return Tab( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [Icon(icon, size: 16.0), Text(text)], ), ); } // This widget is the root of your application. @override Widget build(BuildContext context) { List tabs = [ _renderTab( GSYICons.MAIN_DT, context.l10n.home_dynamic), _renderTab(GSYICons.MAIN_QS, context.l10n.home_trend), _renderTab(GSYICons.MAIN_MY, context.l10n.home_my), ]; ///增加返回按键监听 return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { _dialogExitApp(context); }, child: GSYTabBarWidget( drawer: const HomeDrawer(), type: TabType.bottom, tabItems: tabs, tabViews: [ DynamicPage(key: dynamicKey), TrendPage(key: trendKey), MyPage(key: myKey), ], onDoublePress: (index) { switch (index) { case 0: dynamicKey.currentState?.scrollToTop(); break; case 1: trendKey.currentState?.scrollToTop(); break; case 2: myKey.currentState?.scrollToTop(); break; } }, backgroundColor: GSYColors.primarySwatch, indicatorColor: GSYColors.white, title: GSYTitleBar( context.l10n.app_name, rightWidget: InkWell( onTap: () { RenderBox renderBox2 = rightKey.currentContext?.findRenderObject() as RenderBox; var position = renderBox2.localToGlobal(Offset.zero); var size = renderBox2.size; var centerPosition = Offset( position.dx + size.width / 2, position.dy + size.height / 2, ); NavigatorUtils.goSearchPage(context, centerPosition); }, child: Container( key: rightKey, alignment: Alignment.centerRight, child: Lottie.asset('static/file/search.json', width: 70, height: 80, fit: BoxFit.cover, alignment: Alignment.centerRight), ), ), ), ), ); } } ================================================ FILE: lib/page/home/widget/home_drawer.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/repositories/issue_repository.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/common/local/local_storage.dart'; import 'package:gsy_github_app_flutter/model/common_list_datatype.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/redux/login_redux.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_flex_button.dart'; import 'package:package_info_plus/package_info_plus.dart'; /// 主页drawer /// Created by guoshuyu /// Date: 2018-07-18 class HomeDrawer extends StatelessWidget { const HomeDrawer({super.key}); showAboutDialog(BuildContext context, String? versionName) { versionName ??= "Null"; NavigatorUtils.showGSYDialog( context: context, builder: (BuildContext context) => AboutDialog( applicationName: context.l10n.app_name, applicationVersion: "${context.l10n.app_version}: ${versionName ?? ""}", applicationIcon: const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 50.0, height: 50.0, ), applicationLegalese: "http://github.com/CarGuo", ), ); } showThemeDialog(BuildContext context, WidgetRef ref) { StringList list = [ context.l10n.home_theme_default, context.l10n.home_theme_1, context.l10n.home_theme_2, context.l10n.home_theme_3, context.l10n.home_theme_4, context.l10n.home_theme_5, context.l10n.home_theme_6, ]; CommonUtils.showCommitOptionDialog(context, list, (index) { ref.read(appThemeStateProvider.notifier).pushTheme(index.toString()); LocalStorage.save(Config.THEME_COLOR, index.toString()); }, colorList: CommonUtils.getThemeListColor()); } @override Widget build(BuildContext context) { return Material( child: Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { var themeData = ref.watch(appThemeStateProvider); var vibrationEnable = ref.watch(appVibrationStateProvider); return StoreBuilder( builder: (context, store) { User user = store.state.userInfo!; return Drawer( ///侧边栏按钮Drawer child: Container( ///默认背景 color: themeData.primaryColor, child: SingleChildScrollView( ///item 背景 child: Container( constraints: BoxConstraints( minHeight: MediaQuery.sizeOf(context).height, ), child: Material( color: GSYColors.white, child: Column( children: [ UserAccountsDrawerHeader( //Material内置控件 accountName: Text( user.login ?? "---", style: GSYConstant.largeTextWhite, ), accountEmail: Text( user.email ?? user.name ?? "---", style: GSYConstant.normalTextLight, ), //用户名 //用户邮箱 currentAccountPicture: GestureDetector( //用户头像 onTap: () {}, child: CircleAvatar( //圆形图标控件 backgroundImage: NetworkImage( user.avatar_url ?? GSYICons.DEFAULT_REMOTE_PIC, ), ), ), decoration: BoxDecoration( //用一个BoxDecoration装饰器提供背景图片 color: themeData.primaryColor, ), ), ListTile( title: Text( context.l10n.home_reply, style: GSYConstant.normalText, ), onTap: () { String content = ""; CommonUtils.showEditDialog( context, context.l10n.home_reply, (title) {}, (res) { content = res; }, () { if (content.isEmpty) { return; } CommonUtils.showLoadingDialog(context); IssueRepository.createIssueRequest( "CarGuo", "gsy_github_app_flutter", { "title": context.l10n.home_reply, "body": content, }, ).then((result) { Navigator.pop(context); Navigator.pop(context); }); }, titleController: TextEditingController(), valueController: TextEditingController(), needTitle: false, hintText: context.l10n.feed_back_tip, ); }, ), ListTile( title: Text( context.l10n.home_history, style: GSYConstant.normalText, ), onTap: () { NavigatorUtils.gotoCommonList( context, context.l10n.home_history, "repositoryql", CommonListDataType.history, userName: "", reposName: "", ); }, ), ListTile( title: Hero( tag: "home_user_info", child: Material( color: Colors.transparent, child: Text( context.l10n.home_user_info, style: GSYConstant.normalTextBold, ), ), ), onTap: () { NavigatorUtils.gotoUserProfileInfo(context); }, ), ListTile( title: Text( context.l10n.home_change_theme, style: GSYConstant.normalText, ), onTap: () { showThemeDialog(context, ref); }, ), ListTile( title: Text( context.l10n.home_change_language, style: GSYConstant.normalText, ), onTap: () { CommonUtils.showLanguageDialog(ref); }, ), SwitchListTile( value: vibrationEnable, title: Text( context.l10n.home_vibration, style: GSYConstant.normalText, ), onChanged: (value) { ref .read(appVibrationStateProvider.notifier) .changeVibration(value); }, ), ListTile( title: Text( context.l10n.home_change_grey, style: GSYConstant.normalText, ), onTap: () { ref .read(appGrepStateProvider.notifier) .changeGrey(); }, ), ListTile( title: Text( context.l10n.home_check_update, style: GSYConstant.normalText, ), onTap: () { ReposRepository.getNewsVersion(context, true); }, ), ListTile( title: Text( context.l10n.home_about, style: GSYConstant.normalText, ), onLongPress: () { NavigatorUtils.goDebugDataPage(context); }, onTap: () { PackageInfo.fromPlatform().then((value) { printLog(value); if (!context.mounted) return; showAboutDialog(context, value.version); }); }, ), ListTile( title: GSYFlexButton( text: context.l10n.login_out, color: Colors.redAccent, textColor: GSYColors.textWhite, onPress: () { store.dispatch(LogoutAction(context)); }, ), onTap: () {}, ), ], ), ), ), ), ), ); }, ); }, ), ); } } ================================================ FILE: lib/page/honor_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/repos/widget/repos_item.dart'; /// 荣耀list /// Created by guoshuyu /// on 2018/7/22. class HonorListPage extends StatefulWidget { final List? list; const HonorListPage(this.list, {super.key}); @override _HonorListPageState createState() => _HonorListPageState(); } class _HonorListPageState extends State { _renderItem(item) { ReposViewModel reposViewModel = ReposViewModel.fromMap(item); return ReposItem(reposViewModel, onPressed: () { NavigatorUtils.goReposDetail( context, reposViewModel.ownerName, reposViewModel.repositoryName); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( context.l10n.user_tab_honor, maxLines: 1, overflow: TextOverflow.ellipsis, )), body: ListView.builder( itemBuilder: (context, index) { return _renderItem(widget.list![index]); }, itemCount: widget.list!.length, ), ); } } ================================================ FILE: lib/page/issue/issue_detail_page.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/repositories/issue_repository.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_flex_button.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:gsy_github_app_flutter/page/issue/widget/issue_header_item.dart'; import 'package:gsy_github_app_flutter/page/issue/widget/issue_item.dart'; /// Issue 详情页面 /// Created by guoshuyu /// on 2018/7/21. class IssueDetailPage extends StatefulWidget { final String? userName; final String? reposName; final String issueNum; final bool needHomeIcon; const IssueDetailPage(this.userName, this.reposName, this.issueNum, {super.key, this.needHomeIcon = false}); @override _IssueDetailPageState createState() => _IssueDetailPageState(); } class _IssueDetailPageState extends State with AutomaticKeepAliveClientMixin, GSYListState { int selectIndex = 0; ///头部信息数据是否加载成功,成功了就可以显示底部状态 bool headerStatus = false; String? htmlUrl; /// issue 的头部数据显示 IssueHeaderViewModel issueHeaderViewModel = IssueHeaderViewModel(); ///控制编辑时issue的title TextEditingController issueInfoTitleControl = TextEditingController(); ///控制编辑时issue的content TextEditingController issueInfoValueControl = TextEditingController(); ///绘制item _renderEventItem(index) { ///第一个绘制的是头部 if (index == 0) { return IssueHeaderItem(issueHeaderViewModel, onPressed: () {}); } Issue issue = pullLoadWidgetControl.dataList[index - 1]; return IssueItem( IssueItemViewModel.fromMap(issue, needTitle: false), hideBottom: true, limitComment: false, onPressed: () { NavigatorUtils.showGSYDialog( context: context, builder: (BuildContext context) { return Center( child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), color: GSYColors.white, border: Border.all( color: GSYColors.subTextColor, width: 0.3)), margin: const EdgeInsets.all(10.0), child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ GSYFlexButton( color: GSYColors.white, textColor: GSYColors.primaryDarkValue, text: context.l10n.issue_edit_issue_edit_commit, onPress: () { _editCommit(issue.id.toString(), issue.body); }, ), GSYFlexButton( color: GSYColors.white, text: context.l10n.issue_edit_issue_delete_commit, onPress: () { _deleteCommit(issue.id.toString()); }, ), GSYFlexButton( color: GSYColors.white, text: context.l10n.issue_edit_issue_copy_commit, onPress: () { CommonUtils.copy(issue.body, context); }, ), ], ), ), ); }); }, ); } ///获取页面数据 _getDataLogic() async { ///刷新时同时更新头部信息 if (page <= 1) { _getHeaderInfo(); } return await IssueRepository.getIssueCommentRequest( widget.userName, widget.reposName, widget.issueNum, page: page, needDb: page <= 1); } ///获取头部数据 _getHeaderInfo() { IssueRepository.getIssueInfoRequest( widget.userName, widget.reposName, widget.issueNum) .then((res) { if (res != null && res.result) { _resolveHeaderInfo(res); return res.next?.call(); } return Future.value(null); }).then((res) { if (res != null && res.result) { _resolveHeaderInfo(res); } }); } ///数据转化显示 _resolveHeaderInfo(res) { Issue? issue = res.data; setState(() { issueHeaderViewModel = IssueHeaderViewModel.fromMap(issue!); htmlUrl = issue.htmlUrl; headerStatus = true; }); } ///编辑回复 _editCommit(id, content) { Navigator.pop(context); String? contentData = content; issueInfoValueControl = TextEditingController(text: contentData); //编译Issue Info CommonUtils.showEditDialog( context, context.l10n.issue_edit_issue, null, (contentValue) { contentData = contentValue; }, () { if (contentData == null || contentData!.trim().isEmpty) { showToast(context.l10n.issue_edit_issue_content_not_be_null); return; } CommonUtils.showLoadingDialog(context); //提交修改 IssueRepository.editCommentRequest(widget.userName, widget.reposName, widget.issueNum, id, {"body": contentData}).then((result) { showRefreshLoading(); Navigator.pop(context); Navigator.pop(context); }); }, valueController: issueInfoValueControl, needTitle: false, ); } ///删除回复 _deleteCommit(id) { Navigator.pop(context); CommonUtils.showLoadingDialog(context); //提交修改 IssueRepository.deleteCommentRequest( widget.userName, widget.reposName, widget.issueNum, id) .then((result) { Navigator.pop(context); showRefreshLoading(); }); } ///编译 issue _editIssue() { String? title = issueHeaderViewModel.issueComment; String? content = issueHeaderViewModel.issueDesHtml; issueInfoTitleControl = TextEditingController(text: title); issueInfoValueControl = TextEditingController(text: content); //编译Issue Info CommonUtils.showEditDialog( context, context.l10n.issue_edit_issue, (titleValue) { title = titleValue; }, (contentValue) { content = contentValue; }, () { if (title == null || title!.trim().isEmpty) { showToast(context.l10n.issue_edit_issue_title_not_be_null); return; } if (content == null || content!.trim().isEmpty) { showToast(context.l10n.issue_edit_issue_content_not_be_null); return; } CommonUtils.showLoadingDialog(context); //提交修改 IssueRepository.editIssueRequest(widget.userName, widget.reposName, widget.issueNum, {"title": title, "body": content}).then((result) { _getHeaderInfo(); Navigator.pop(context); Navigator.pop(context); }); }, titleController: issueInfoTitleControl, valueController: issueInfoValueControl, needTitle: true, ); } ///回复 issue _replyIssue() { //回复 Info issueInfoTitleControl = TextEditingController(text: ""); issueInfoValueControl = TextEditingController(text: ""); String? content = ""; CommonUtils.showEditDialog( context, context.l10n.issue_reply_issue, null, (replyContent) { content = replyContent; }, () { if (content == null || content?.trim().isEmpty == true) { showToast(context.l10n.issue_edit_issue_content_not_be_null); return; } CommonUtils.showLoadingDialog(context); //提交评论 IssueRepository.addIssueCommentRequest( widget.userName, widget.reposName, widget.issueNum, content) .then((result) { showRefreshLoading(); Navigator.pop(context); Navigator.pop(context); }); }, needTitle: false, titleController: issueInfoTitleControl, valueController: issueInfoValueControl, ); } ///获取底部状态控件显示 _getBottomWidget() { List bottomWidget = (!headerStatus) ? [] : [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { _replyIssue(); }, child: Text(context.l10n.issue_reply, style: GSYConstant.smallText), ), Container( width: 0.3, height: 30.0, color: GSYColors.subLightTextColor), TextButton( onPressed: () { _editIssue(); }, child: Text(context.l10n.issue_edit, style: GSYConstant.smallText), ), Container( width: 0.3, height: 30.0, color: GSYColors.subLightTextColor), TextButton( onPressed: () { CommonUtils.showLoadingDialog(context); IssueRepository.editIssueRequest( widget.userName, widget.reposName, widget.issueNum, { "state": (issueHeaderViewModel.state == "closed") ? 'open' : 'closed' }).then((result) { _getHeaderInfo(); Navigator.pop(context); }); }, child: Text( (issueHeaderViewModel.state == 'closed') ? context.l10n.issue_open : context.l10n.issue_close, style: GSYConstant.smallText)), Container( width: 0.3, height: 30.0, color: GSYColors.subLightTextColor), TextButton( onPressed: () { CommonUtils.showLoadingDialog(context); IssueRepository.lockIssueRequest( widget.userName, widget.reposName, widget.issueNum, issueHeaderViewModel.locked) .then((result) { _getHeaderInfo(); Navigator.pop(context); }); }, child: Text( issueHeaderViewModel.locked! ? context.l10n.issue_unlock : context.l10n.issue_lock, style: GSYConstant.smallText)), ], ) ]; return bottomWidget; } @override bool get wantKeepAlive => true; @override requestRefresh() async { return await _getDataLogic(); } @override requestLoadMore() async { return await _getDataLogic(); } @override bool get isRefreshFirst => true; @override bool get needHeader => true; @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. Widget? widgetContent = (widget.needHomeIcon) ? null : GSYCommonOptionWidget(url: htmlUrl); return Scaffold( persistentFooterButtons: _getBottomWidget(), appBar: AppBar( title: GSYTitleBar( widget.reposName, rightWidget: widgetContent, needRightLocalIcon: widget.needHomeIcon, iconData: GSYICons.HOME, onRightIconPressed: (_) { NavigatorUtils.goReposDetail( context, widget.userName, widget.reposName); }, ), ), body: GSYPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderEventItem(index), handleRefresh, onLoadMore, refreshKey: refreshIndicatorKey, ), ); } } ================================================ FILE: lib/page/issue/issue_edit_dIalog.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_input_widget.dart'; /// issue 编辑输入框 /// Created by guoshuyu /// on 2018/7/21. class IssueEditDialog extends StatefulWidget { final String dialogTitle; final ValueChanged? onTitleChanged; final ValueChanged onContentChanged; final VoidCallback onPressed; final TextEditingController? titleController; final TextEditingController? valueController; final bool needTitle; final String? hintText; const IssueEditDialog( this.dialogTitle, this.onTitleChanged, this.onContentChanged, this.onPressed, { super.key, this.titleController, this.valueController, this.needTitle = true, this.hintText, }); @override _IssueEditDialogState createState() => _IssueEditDialogState(); } class _IssueEditDialogState extends State { _IssueEditDialogState(); ///标题输入框 renderTitleInput() { return (widget.needTitle) ? Padding( padding: const EdgeInsets.all(5.0), child: GSYInputWidget( onChanged: widget.onTitleChanged, controller: widget.titleController, hintText: context.l10n.issue_edit_issue_title_tip, obscureText: false, )) : Container(); } ///快速输入框 _renderFastInputContainer() { ///因为是Column下包含了ListView,所以需要设置高度 return SizedBox( height: 30.0, child: ListView.builder( scrollDirection: Axis.horizontal, itemBuilder: (context, index) { return RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.only( left: 8.0, right: 8.0, top: 5.0, bottom: 5.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: Icon(FAST_INPUT_LIST[index].iconData, size: 16.0), onPressed: () { String text = FAST_INPUT_LIST[index].content; String newText = ""; if (widget.valueController?.value != null) { newText = widget.valueController!.value.text; } newText = newText + text; setState(() { widget.valueController!.value = TextEditingValue(text: newText); }); widget.onContentChanged.call(newText); }); }, itemCount: FAST_INPUT_LIST.length, ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.transparent, body: SafeArea( child: SingleChildScrollView( child: Container( height: MediaQuery.sizeOf(context).height, width: MediaQuery.sizeOf(context).width, color: Colors.black12, ///触摸收起键盘 child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { FocusScope.of(context).requestFocus(FocusNode()); }, child: Center( child: GSYCardItem( margin: const EdgeInsets.only(left: 50.0, right: 50.0), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0))), child: Padding( padding: const EdgeInsets.all(12.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ ///dialog标题 Padding( padding: const EdgeInsets.only(top: 5.0, bottom: 15.0), child: Center( child: Text(widget.dialogTitle, style: GSYConstant.normalTextBold), )), ///标题输入框 renderTitleInput(), ///内容输入框 Container( height: MediaQuery.sizeOf(context).width * 3 / 4, decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), color: GSYColors.white, border: Border.all( color: GSYColors.subTextColor, width: .3), ), padding: const EdgeInsets.only( left: 20.0, top: 12.0, right: 20.0, bottom: 12.0), child: Column( children: [ Expanded( child: TextField( autofocus: false, maxLines: 999, onChanged: widget.onContentChanged, controller: widget.valueController, decoration: InputDecoration( hintText: widget.hintText ?? context .l10n.issue_edit_issue_title_tip, hintStyle: GSYConstant.middleSubText, isDense: true, border: InputBorder.none, ), style: GSYConstant.middleText, ), ), ///快速输入框 _renderFastInputContainer(), ], ), ), Container(height: 10.0), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ ///取消 Expanded( child: RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(4.0), constraints: const BoxConstraints( minWidth: 0.0, minHeight: 0.0), child: Text(context.l10n.app_cancel, style: GSYConstant.normalSubText), onPressed: () { Navigator.pop(context); })), Container( width: 0.3, height: 25.0, color: GSYColors.subTextColor), ///确定 Expanded( child: RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(4.0), constraints: const BoxConstraints( minWidth: 0.0, minHeight: 0.0), onPressed: widget.onPressed, child: Text(context.l10n.app_ok, style: GSYConstant.normalTextBold))), ], ) ], ), ), ), ), ), ), ), )); } } var FAST_INPUT_LIST = [ FastInputIconModel(GSYICons.ISSUE_EDIT_H1, "\n# "), FastInputIconModel(GSYICons.ISSUE_EDIT_H2, "\n## "), FastInputIconModel(GSYICons.ISSUE_EDIT_H3, "\n### "), FastInputIconModel(GSYICons.ISSUE_EDIT_BOLD, "****"), FastInputIconModel(GSYICons.ISSUE_EDIT_ITALIC, "__"), FastInputIconModel(GSYICons.ISSUE_EDIT_QUOTE, "` `"), FastInputIconModel(GSYICons.ISSUE_EDIT_CODE, " \n``` \n\n``` \n"), FastInputIconModel(GSYICons.ISSUE_EDIT_LINK, "[](url)"), ]; class FastInputIconModel { final IconData iconData; final String content; FastInputIconModel(this.iconData, this.content); } ================================================ FILE: lib/page/issue/widget/issue_header_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; import 'package:gsy_github_app_flutter/widget/markdown/gsy_markdown_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_user_icon_widget.dart'; /// Issue 详情头 /// Created by guoshuyu /// on 2018/7/21. class IssueHeaderItem extends StatelessWidget { final IssueHeaderViewModel issueHeaderViewModel; final VoidCallback? onPressed; const IssueHeaderItem(this.issueHeaderViewModel, {super.key, this.onPressed}); _renderBottomContainer() { Color issueStateColor = issueHeaderViewModel.state == "open" ? Colors.green : Colors.red; ///底部Issue状态 Widget bottomContainer = Row( children: [ ///issue 关闭打开状态 GSYIConText( GSYICons.ISSUE_ITEM_ISSUE, issueHeaderViewModel.state, TextStyle( color: issueStateColor, fontSize: GSYConstant.smallTextSize, ), issueStateColor, 15.0, padding: 2.0, ), const Padding(padding: EdgeInsets.all(2.0)), ///issue issue编码 Text(issueHeaderViewModel.issueTag, style: GSYConstant.smallTextWhite), const Padding(padding: EdgeInsets.all(2.0)), ///issue 评论数 GSYIConText( GSYICons.ISSUE_ITEM_COMMENT, issueHeaderViewModel.commentCount, GSYConstant.smallTextWhite, GSYColors.white, 15.0, padding: 2.0, ), ], ); return bottomContainer; } ///关闭操作人 _renderCloseByText() { return (issueHeaderViewModel.closedBy == null || issueHeaderViewModel.closedBy!.trim().isEmpty) ? Container() : Container( margin: const EdgeInsets.only(right: 5.0, top: 10.0, bottom: 10.0), alignment: Alignment.topRight, child: Text( "Close By ${issueHeaderViewModel.closedBy!}", style: GSYConstant.smallSubLightText, )); } @override Widget build(BuildContext context) { return GSYCardItem( color: Theme.of(context).primaryColor, child: TextButton( style: TextButton.styleFrom(padding: const EdgeInsets.all(0.0)), onPressed: onPressed, child: Padding( padding: const EdgeInsets.all(10.0), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///头像 GSYUserIconWidget( padding: const EdgeInsets.only( top: 0.0, right: 10.0, left: 0.0), width: 50.0, height: 50.0, image: issueHeaderViewModel.actionUserPic ?? GSYICons.DEFAULT_REMOTE_PIC, onPressed: () { NavigatorUtils.goPerson( context, issueHeaderViewModel.actionUser); }), Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ ///名称 Expanded( child: Text(issueHeaderViewModel.actionUser!, style: GSYConstant.normalTextWhite)), ///时间 Text( issueHeaderViewModel.actionTime, style: GSYConstant.smallSubLightText, overflow: TextOverflow.ellipsis, ), ], ), const Padding(padding: EdgeInsets.all(2.0)), ///底部Item _renderBottomContainer(), Container( ///评论标题 margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, ///评论标题 child: Text( issueHeaderViewModel.issueComment!, style: GSYConstant.smallTextWhite, )), const Padding( padding: EdgeInsets.only( left: 0.0, top: 2.0, right: 0.0, bottom: 0.0), ), ], ), ), ], ), ///评论内容 GSYMarkdownWidget( markdownData: issueHeaderViewModel.issueDesHtml, style: GSYMarkdownWidget.DARK_THEME, baseUrl: "", shrinkWrap: true, scroll: false, ), ///close 用户 _renderCloseByText() ], ), ), ), ); } } class IssueHeaderViewModel { String actionTime = "---"; String? actionUser = "---"; String? actionUserPic; String? closedBy = ""; bool? locked = false; String? issueComment = "---"; String? issueDesHtml = "---"; String commentCount = "---"; String? state = "---"; String issueDes = "---"; String issueTag = "---"; IssueHeaderViewModel(); IssueHeaderViewModel.fromMap(Issue issueMap) { actionTime = CommonUtils.getNewsTimeStr(issueMap.createdAt!); actionUser = issueMap.user!.login; actionUserPic = issueMap.user!.avatar_url; closedBy = issueMap.closeBy != null ? issueMap.closeBy!.login : ""; locked = issueMap.locked; issueComment = issueMap.title; issueDesHtml = issueMap.bodyHtml ?? ((issueMap.body != null) ? issueMap.body : ""); commentCount = "${issueMap.commentNum}"; state = issueMap.state; issueDes = issueMap.body != null ? ": \n${issueMap.body!}" : ''; issueTag = "#${issueMap.number}"; } } ================================================ FILE: lib/page/issue/widget/issue_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/model/issue.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; import 'package:gsy_github_app_flutter/widget/markdown/gsy_markdown_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_user_icon_widget.dart'; /// Issue相关item /// Created by guoshuyu /// Date: 2018-07-19 class IssueItem extends StatelessWidget { final IssueItemViewModel issueItemViewModel; ///点击 final GestureTapCallback? onPressed; ///长按 final GestureTapCallback? onLongPress; ///是否需要底部状态 final bool hideBottom; ///是否需要限制内容行数 final bool limitComment; const IssueItem(this.issueItemViewModel, {super.key, this.onPressed, this.onLongPress, this.hideBottom = false, this.limitComment = true}); ///issue 底部状态 _renderBottomContainer() { Color issueStateColor = issueItemViewModel.state == "open" ? Colors.green : Colors.red; return (hideBottom) ? Container() : Row( children: [ ///issue 关闭打开状态 GSYIConText( GSYICons.ISSUE_ITEM_ISSUE, issueItemViewModel.state, TextStyle( color: issueStateColor, fontSize: GSYConstant.smallTextSize, ), issueStateColor, 15.0, padding: 2.0, ), const Padding(padding: EdgeInsets.all(2.0)), ///issue标号 Expanded( child: Text(issueItemViewModel.issueTag, style: GSYConstant.smallSubText), ), ///评论数 GSYIConText( GSYICons.ISSUE_ITEM_COMMENT, issueItemViewModel.commentCount, GSYConstant.smallSubText, GSYColors.subTextColor, 15.0, padding: 2.0, ), ], ); } ///评论内容 _renderCommentText() { return (limitComment) ? Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text( issueItemViewModel.issueComment, style: GSYConstant.smallSubText, maxLines: limitComment ? 2 : 1000, ), ) : GSYMarkdownWidget( markdownData: issueItemViewModel.issueComment, baseUrl: "", shrinkWrap: true, scroll: false, ); } @override Widget build(BuildContext context) { return GSYCardItem( child: InkWell( onTap: onPressed, onLongPress: onLongPress, child: Padding( padding: const EdgeInsets.only( left: 5.0, top: 5.0, right: 10.0, bottom: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///头像 GSYUserIconWidget( width: 30.0, height: 30.0, image: issueItemViewModel.actionUserPic, onPressed: () { NavigatorUtils.goPerson( context, issueItemViewModel.actionUser); }), Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ ///用户名 Expanded( child: Text(issueItemViewModel.actionUser!, style: GSYConstant.smallTextBold)), Text( issueItemViewModel.actionTime, style: GSYConstant.smallSubText, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ///评论内容 _renderCommentText(), const Padding( padding: EdgeInsets.only( left: 0.0, top: 2.0, right: 0.0, bottom: 0.0), ), _renderBottomContainer(), ], ), ), ]), ), ), ); } } class IssueItemViewModel { String actionTime = "---"; String? actionUser = "---"; String? actionUserPic; String issueComment = "---"; String commentCount = "---"; String? state = "---"; String issueTag = "---"; String number = "---"; String id = ""; IssueItemViewModel(); IssueItemViewModel.fromMap(Issue issueMap, {needTitle = true}) { String fullName = CommonUtils.getFullName(issueMap.repoUrl); actionTime = CommonUtils.getNewsTimeStr(issueMap.createdAt!); actionUser = issueMap.user!.login; actionUserPic = issueMap.user!.avatar_url; if (needTitle) { issueComment = "$fullName- ${issueMap.title!}"; commentCount = issueMap.commentNum.toString(); state = issueMap.state; issueTag = "#${issueMap.number}"; number = issueMap.number.toString(); } else { issueComment = issueMap.body ?? ""; id = issueMap.id.toString(); } } } ================================================ FILE: lib/page/login/login_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/local/local_storage.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/net/address.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/redux/login_redux.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/widget/animated_background.dart'; import 'package:gsy_github_app_flutter/widget/gsy_flex_button.dart'; import 'package:gsy_github_app_flutter/widget/gsy_input_widget.dart'; import 'package:gsy_github_app_flutter/widget/particle/particle_widget.dart'; /// 登录页 /// Created by guoshuyu /// Date: 2018-07-16 class LoginPage extends StatefulWidget { static const String sName = "login"; const LoginPage({super.key}); @override State createState() { return _LoginPageState(); } } class _LoginPageState extends State with LoginBLoC { @override Widget build(BuildContext context) { /// 触摸收起键盘 return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { FocusScope.of(context).requestFocus(FocusNode()); }, child: Scaffold( body: Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { return Container( color: Theme.of(context).primaryColor, child: Stack(children: [ const Positioned.fill(child: AnimatedBackground()), const Positioned.fill(child: ParticlesWidget(30)), Center( ///防止overFlow的现象 child: SafeArea( ///同时弹出键盘不遮挡 child: SingleChildScrollView( child: Card( elevation: 5.0, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0))), color: GSYColors.cardWhite, margin: const EdgeInsets.only(left: 30.0, right: 30.0), child: Padding( padding: const EdgeInsets.only( left: 30.0, top: 40.0, right: 30.0, bottom: 0.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 90.0, height: 90.0), const Padding(padding: EdgeInsets.all(10.0)), GSYInputWidget( hintText: context.l10n.login_username_hint_text, iconData: GSYICons.LOGIN_USER, onChanged: (String value) { _userName = value; }, controller: userController, ), const Padding(padding: EdgeInsets.all(10.0)), GSYInputWidget( hintText: context.l10n.login_password_hint_text, iconData: GSYICons.LOGIN_PW, obscureText: true, onChanged: (String value) { _password = value; }, controller: pwController, ), const Padding(padding: EdgeInsets.all(10.0)), SizedBox( height: 50, child: Row( children: [ Expanded( child: GSYFlexButton( text: context.l10n.login_text, color: Theme.of(context).primaryColor, textColor: GSYColors.textWhite, fontSize: 16, onPress: loginIn, ), ), const SizedBox( width: 10, ), Expanded( child: GSYFlexButton( text: context.l10n.oauth_text, color: Theme.of(context).primaryColor, textColor: GSYColors.textWhite, fontSize: 16, onPress: oauthLogin, ), ), ], ), ), const Padding(padding: EdgeInsets.all(15.0)), InkWell( onTap: () { CommonUtils.showLanguageDialog(ref); }, child: Text( context.l10n.switch_language, style: const TextStyle( color: GSYColors.subTextColor), ), ), const Padding(padding: EdgeInsets.all(15.0)), ], ), ), ), ), ), ) ]), ); }), ), ); } } mixin LoginBLoC on State { final TextEditingController userController = TextEditingController(); final TextEditingController pwController = TextEditingController(); String? _userName = ""; String? _password = ""; @override void initState() { super.initState(); initParams(); } @override void dispose() { super.dispose(); userController.removeListener(_usernameChange); pwController.removeListener(_passwordChange); } _usernameChange() { _userName = userController.text; } _passwordChange() { _password = pwController.text; } initParams() async { _userName = await LocalStorage.get(Config.USER_NAME_KEY); _password = await LocalStorage.get(Config.PW_KEY); userController.value = TextEditingValue(text: _userName ?? ""); pwController.value = TextEditingValue(text: _password ?? ""); } loginIn() async { showToast(context.l10n.login_deprecated); return; // if (_userName == null || _userName.isEmpty) { // return; // } // if (_password == null || _password.isEmpty) { // return; // } // // ///通过 redux 去执行登陆流程 // StoreProvider.of(context) // .dispatch(LoginAction(context, _userName, _password)); } oauthLogin() async { var st = StoreProvider.of(context); String? code = await NavigatorUtils.goLoginWebView( context, Address.getOAuthUrl(), context.l10n.oauth_text); if (code != null && code.isNotEmpty) { ///通过 redux 去执行登陆流程 // ignore: use_build_context_synchronously st.dispatch(OAuthAction(context, code)); } } } ================================================ FILE: lib/page/login/login_webview.dart ================================================ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:webview_flutter/webview_flutter.dart'; class LoginWebView extends StatefulWidget { final String url; final String title; const LoginWebView(this.url, this.title, {super.key}); @override _LoginWebViewState createState() => _LoginWebViewState(); } class _LoginWebViewState extends State { late final WebViewController controller; final GlobalKey webViewKey = GlobalKey(); InAppWebViewController? webViewController; late final PlatformWebViewControllerCreationParams params; @override void initState() { super.initState(); } _renderTitle() { if (widget.url.isEmpty) { return Text(widget.title); } return Row(children: [ Expanded( child: Text( widget.title, maxLines: 1, overflow: TextOverflow.ellipsis, )), GSYCommonOptionWidget(url: widget.url), ]); } final FocusNode focusNode = FocusNode(); bool isLoading = true; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: _renderTitle(), ), body: Stack( children: [ TextField( focusNode: focusNode, ), InAppWebView( key: webViewKey, initialUrlRequest: URLRequest(url: WebUri(widget.url)), onWebViewCreated: (controller) { webViewController = controller; webViewController?.loadUrl( urlRequest: URLRequest(url: WebUri(widget.url))); }, onLoadStart: (controller, url) { setState(() { isLoading = true; }); }, initialSettings: InAppWebViewSettings( useHybridComposition: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserGesture: true, useShouldOverrideUrlLoading: Platform.isIOS ? true : false, ), shouldOverrideUrlLoading: (controller, navigationAction) async { var url = navigationAction.request.url!.toString(); if (url.startsWith("gsygithubapp://authed")) { var code = Uri.parse(url).queryParameters["code"]; printLog("code $code"); Navigator.of(context).pop(code); return NavigationActionPolicy.CANCEL; } return NavigationActionPolicy.ALLOW; }, onLoadStop: (controller, url) async { setState(() { isLoading = false; }); if (url.toString().startsWith("gsygithubapp://authed")) { var code = Uri.parse(url.toString()).queryParameters["code"]; printLog("code $code"); Navigator.of(context).pop(code); } }, onProgressChanged: (controller, progress) { if (progress == 100) { setState(() { isLoading = false; }); } }, ), if (isLoading) Center( child: Container( width: 200.0, height: 200.0, padding: const EdgeInsets.all(4.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SpinKitDoubleBounce(color: Theme.of(context).primaryColor), Container(width: 10.0), Text(context.l10n.loading_text, style: GSYConstant.middleText), ], ), ), ) ], ), ); } } ================================================ FILE: lib/page/my_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/common/repositories/event_repository.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/page/user/base_person_provider.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/redux/user_redux.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_nested_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/page/user/base_person_state.dart'; import 'package:redux/redux.dart'; /// 主页我的tab页 /// Created by guoshuyu /// Date: 2018-07-16 class MyPage extends StatefulWidget { const MyPage({super.key}); @override MyPageState createState() => MyPageState(); } class MyPageState extends BasePersonState { final ScrollController scrollController = ScrollController(); String beStaredCount = '---'; Color notifyColor = GSYColors.subTextColor; Store? _getStore() { return StoreProvider.of(context); } ///从全局状态中获取我的用户名 _getUserName() { if (_getStore()?.state.userInfo == null) { return null; } return _getStore()?.state.userInfo?.login; } ///从全局状态中获取我的用户类型 getUserType() { if (_getStore()?.state.userInfo == null) { return null; } return _getStore()?.state.userInfo?.type; } ///更新通知图标颜色 _refreshNotify() { UserRepository.getNotifyRequest(false, false, 0).then((res) { Color newColor; if (res != null && res.result && res.data.length > 0) { newColor = GSYColors.actionBlue; } else { newColor = GSYColors.subLightTextColor; } if (isShow) { setState(() { notifyColor = newColor; }); } }); } scrollToTop() { if (scrollController.offset <= 0) { scrollController .animateTo(0, duration: const Duration(milliseconds: 600), curve: Curves.linear) .then((_) { showRefreshLoading(); }); } else { scrollController.animateTo(0, duration: const Duration(milliseconds: 600), curve: Curves.linear); } } @override bool get wantKeepAlive => true; @override void initState() { pullLoadWidgetControl.needHeader = true; super.initState(); } _getDataLogic() async { if (_getUserName() == null) { return []; } if (getUserType() == "Organization") { return await UserRepository.getMemberRequest(_getUserName(), page); } return await EventRepository.getEventRequest(_getUserName(), page: page, needDb: page <= 1); } @override requestRefresh() async { if (_getUserName() != null) { /*User.getUserInfo(null).then((res) { if (res != null && res.result) { _getStore()?.dispatch(UpdateUserAction(res.data)); //todo getUserOrg(_getUserName()); } });*/ ///通过 redux 提交更新用户数据行为 ///触发网络请求更新 _getStore()?.dispatch(FetchUserAction()); ///获取用户组织信息 getUserOrg(_getUserName()); ///获取用户仓库前100个star统计数据 getHonor(); _refreshNotify(); } return await _getDataLogic(); } @override requestLoadMore() async { return await _getDataLogic(); } @override bool get isRefreshFirst => false; @override bool get needHeader => false; @override FetchHonorDataProvider get headerProvider { return fetchHonorDataProvider(_getUserName()); } @override void didChangeDependencies() { if (pullLoadWidgetControl.dataList.isEmpty) { showRefreshLoading(); } super.didChangeDependencies(); } @override Widget buildContainer(BuildContext context) { return StoreBuilder( builder: (context, store) { return GSYNestedPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => renderItem( index, store.state.userInfo!, beStaredCount, notifyColor, () { _refreshNotify(); }, orgList), handleRefresh, onLoadMore, scrollController: scrollController, refreshKey: refreshIKey, headerSliverBuilder: (context, innerBoxIsScrolled) { return sliverBuilder(context, innerBoxIsScrolled, store.state.userInfo!, notifyColor, beStaredCount, () { _refreshNotify(); }); }, ); }, ); } } ================================================ FILE: lib/page/notify/notify_page.dart ================================================ import 'dart:async'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_event_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_select_item_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:gsy_github_app_flutter/model/notification.dart' as Model; import 'package:signals/signals_flutter.dart'; /// 通知消息 /// Created by guoshuyu /// Date: 2018-07-24 class NotifyPage extends StatefulWidget { const NotifyPage({super.key}); @override _NotifyPageState createState() => _NotifyPageState(); } class _NotifyPageState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, SignalsMixin { final EasyRefreshController controller = EasyRefreshController(controlFinishLoad: true); late Completer isLoading; bool _hasMore = true; late var notifySignal = createListSignal([]); late var notifyIndexSignal = createSignal(0); late var signalPage = createSignal(-1); @override void initState() { super.initState(); createEffect(() async { notifyIndexSignal.value; signalPage.value; loadData(); }); } loadData() async { if (signalPage.value == -1) { return; } DataResult res = await _getDataLogic(signalPage.value); if (res.result && res.data is List) { var data = res.data as List; _hasMore = data.length >= Config.PAGE_SIZE; if (_hasMore) { controller.finishLoad(IndicatorResult.success); } else { controller.finishLoad(IndicatorResult.noMore); } if (signalPage.value == 1) { notifySignal.value = data; } else { notifySignal.addAll(data); } } if (!isLoading.isCompleted) { isLoading.complete(true); } } ///绘制 Item _renderItem(index) { Model.Notification notification = notifySignal[index]; if (notifyIndexSignal.value != 0) { return _renderEventItem(notification); } ///只有未读消息支持 Slidable 滑动效果 return Slidable( key: ValueKey("${index}_${notifyIndexSignal.value}"), endActionPane: ActionPane( dragDismissible: false, motion: const ScrollMotion(), dismissible: DismissiblePane(onDismissed: () { UserRepository.setNotificationAsReadRequest( notification.id.toString()) .then((res) { notifySignal.remove(notification); }); }), children: [ SlidableAction( label: context.l10n.notify_readed, backgroundColor: Colors.redAccent, icon: Icons.delete, onPressed: (c) { UserRepository.setNotificationAsReadRequest( notification.id.toString()) .then((res) { notifySignal.remove(notification); }); }, ), ], ), child: _renderEventItem(notification), ); } ///绘制实际的内容数据item _renderEventItem(Model.Notification notification) { EventViewModel eventViewModel = EventViewModel.fromNotify(context, notification); return GSYEventItem(eventViewModel, onPressed: () { if (notification.unread!) { UserRepository.setNotificationAsReadRequest(notification.id.toString()); } if (notification.subject!.type == 'Issue') { String url = notification.subject!.url!; StringList tmp = url.split("/"); String number = tmp[tmp.length - 1]; String? userName = notification.repository!.owner!.login; String? reposName = notification.repository!.name; NavigatorUtils.goIssueDetail(context, userName, reposName, number, needRightLocalIcon: true) .then((res) { _forceRefresh(); }); } }, needImage: false); } _getDataLogic(int page) async { return await UserRepository.getNotifyRequest( notifyIndexSignal.value == 2, notifyIndexSignal.value == 1, page); } requestLoadMore() async { if (!_hasMore) { controller.finishLoad(IndicatorResult.noMore); return; } isLoading = Completer(); signalPage.value++; await isLoading.future; } requestRefresh() async { isLoading = Completer(); _hasMore = true; controller.finishLoad(IndicatorResult.none); signalPage.value = 1; await isLoading.future; } _forceRefresh() async { signalPage.value = -1; controller.callRefresh(); } @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, appBar: AppBar( title: GSYTitleBar( context.l10n.notify_title, iconData: GSYICons.NOTIFY_ALL_READ, needRightLocalIcon: true, onRightIconPressed: (_) { CommonUtils.showLoadingDialog(context); UserRepository.setAllNotificationAsReadRequest().then((res) { Navigator.pop(context); _forceRefresh(); }); }, ), bottom: GSYSelectItemWidget( [ context.l10n.notify_tab_unread, context.l10n.notify_tab_part, context.l10n.notify_tab_all, ], (selectIndex) { notifyIndexSignal.value = selectIndex; }, height: 30.0, margin: const EdgeInsets.all(0.0), elevation: 0.0, ), elevation: 4.0, ), body: EasyRefresh( controller: controller, header: const MaterialHeader(), footer: const BezierFooter(), refreshOnStart: true, onRefresh: requestRefresh, onLoad: requestLoadMore, child: ListView.builder( itemBuilder: (_, int index) => _renderItem(index), itemCount: notifySignal.length, ), ), ); } } ================================================ FILE: lib/page/photoview_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:photo_view/photo_view.dart'; /// 图片预览 /// Created by guoshuyu /// Date: 2018-08-09 class PhotoViewPage extends StatelessWidget { static const String sName = "PhotoViewPage"; const PhotoViewPage({super.key}); @override Widget build(BuildContext context) { final String? url = ModalRoute.of(context)!.settings.arguments as String?; return Scaffold( floatingActionButton: FloatingActionButton( child: const Icon(Icons.file_download), onPressed: () { /* CommonUtils.saveImage(url).then((res) { if (res != null) { Fluttertoast.showToast(msg: res); if (Platform.isAndroid) { const updateAlbum = const MethodChannel( 'com.shuyu.gsygithub.gsygithubflutter/UpdateAlbumPlugin'); updateAlbum.invokeMethod('updateAlbum', { 'path': res, 'name': CommonUtils.splitFileNameByPath(res) }); } } });*/ }, ), appBar: AppBar( title: GSYTitleBar("", rightWidget: GSYCommonOptionWidget(url: url)), ), body: Container( color: Colors.black, child: PhotoView( imageProvider: NetworkImage(url ?? GSYICons.DEFAULT_REMOTE_PIC), loadingBuilder: ( BuildContext context, ImageChunkEvent? event, ) { return Stack( children: [ Center( child: Image.asset(GSYICons.DEFAULT_IMAGE, height: 180.0, width: 180.0)), const Center( child: SpinKitFoldingCube( color: Colors.white30, size: 60.0)), ], ); }), )); } } ================================================ FILE: lib/page/push/push_detail_page.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/model/push_commit.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:gsy_github_app_flutter/page/push/widget/push_coed_item.dart'; import 'package:gsy_github_app_flutter/page/push/widget/push_header.dart'; import 'package:gsy_github_app_flutter/common/utils/html_utils.dart'; /// 提交信息详情页 /// Created by guoshuyu /// Date: 2018-07-27 class PushDetailPage extends StatefulWidget { final String? userName; final String? reposName; final String? sha; final bool needHomeIcon; const PushDetailPage(this.sha, this.userName, this.reposName, {super.key, this.needHomeIcon = false}); @override _PushDetailPageState createState() => _PushDetailPageState(); } class _PushDetailPageState extends State with AutomaticKeepAliveClientMixin, GSYListState { ///提价信息页面的头部数据实体 PushHeaderViewModel pushHeaderViewModel = PushHeaderViewModel(); String? htmlUrl; @override Future handleRefresh() async { if (isLoading) { return; } isLoading = true; page = 1; ///获取提交信息 var res = await _getDataLogic(); if (res != null && res.result) { PushCommit? pushCommit = res.data; pullLoadWidgetControl.dataList.clear(); if (isShow) { setState(() { pushHeaderViewModel = PushHeaderViewModel.forMap(pushCommit!); pullLoadWidgetControl.dataList.addAll(pushCommit.files!); pullLoadWidgetControl.needLoadMore.value = false; htmlUrl = pushCommit.htmlUrl; }); } } isLoading = false; return; } ///绘制头部和提交item _renderEventItem(index) { if (index == 0) { return PushHeader(pushHeaderViewModel); } PushCodeItemViewModel itemViewModel = PushCodeItemViewModel.fromMap( pullLoadWidgetControl.dataList[index - 1]); return PushCodeItem(itemViewModel, () { String html = HtmlUtils.generateCode2HTml( HtmlUtils.parseDiffSource(itemViewModel.patch, false), backgroundColor: GSYColors.webDraculaBackgroundColorString, lang: '', userBR: false); NavigatorUtils.gotoCodeDetailPlatform( context, title: itemViewModel.name, reposName: widget.reposName, userName: widget.userName, path: itemViewModel.patch, data: html, branch: "", ); }); } _getDataLogic() async { return await ReposRepository.getReposCommitsInfoRequest( widget.userName!, widget.reposName!, widget.sha); } @override bool get wantKeepAlive => true; @override requestRefresh() async {} @override requestLoadMore() async { return null; } @override bool get isRefreshFirst => true; @override bool get needHeader => true; @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. Widget? widgetContent = (widget.needHomeIcon) ? null : GSYCommonOptionWidget(url: htmlUrl); return Scaffold( appBar: AppBar( title: GSYTitleBar( widget.reposName, rightWidget: widgetContent, needRightLocalIcon: widget.needHomeIcon, iconData: GSYICons.HOME, onRightIconPressed: (_) { NavigatorUtils.goReposDetail( context, widget.userName, widget.reposName); }, ), ), body: GSYPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderEventItem(index), handleRefresh, onLoadMore, refreshKey: refreshIndicatorKey, ), ); } } ================================================ FILE: lib/page/push/widget/push_coed_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/model/commitFile.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; /// 推送修改代码Item /// Created by guoshuyu /// Date: 2018-07-27 class PushCodeItem extends StatelessWidget { final PushCodeItemViewModel pushCodeItemViewModel; final VoidCallback onPressed; const PushCodeItem(this.pushCodeItemViewModel, this.onPressed, {super.key}); @override Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( ///修改文件路径 margin: const EdgeInsets.only(left: 10.0, top: 5.0, right: 10.0, bottom: 0.0), child: Text( pushCodeItemViewModel.path, style: GSYConstant.smallSubLightText, ), ), GSYCardItem( ///修改文件名 margin: const EdgeInsets.only(left: 10.0, top: 5.0, right: 10.0, bottom: 5.0), child: ListTile( title: Text(pushCodeItemViewModel.name!, style: GSYConstant.smallSubText), leading: const Icon( GSYICons.REPOS_ITEM_FILE, size: 15.0, ), onTap: () { onPressed(); }, ), ), ]); } } class PushCodeItemViewModel { late String path; String? name; String? patch; String? blob_url; PushCodeItemViewModel(); PushCodeItemViewModel.fromMap(CommitFile map) { String filename = map.fileName!; List nameSplit = filename.split("/"); name = nameSplit[nameSplit.length - 1]; path = filename; patch = map.patch; blob_url = map.blobUrl; } } ================================================ FILE: lib/page/push/widget/push_header.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/model/push_commit.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; import 'package:gsy_github_app_flutter/widget/gsy_user_icon_widget.dart'; /// 提交详情的头 /// Created by guoshuyu /// Date: 2018-07-27 class PushHeader extends StatelessWidget { final PushHeaderViewModel pushHeaderViewModel; const PushHeader(this.pushHeaderViewModel, {super.key}); /// 头部变化数量图标 _getIconItem(IconData icon, String text) { return GSYIConText( icon, text, GSYConstant.smallSubLightText, GSYColors.subLightTextColor, 15.0, padding: 0.0, ); } @override Widget build(BuildContext context) { return GSYCardItem( color: Theme.of(context).primaryColor, child: TextButton( style: TextButton.styleFrom(padding: const EdgeInsets.all(0.0)), onPressed: () {}, child: Padding( padding: const EdgeInsets.all(10.0), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///用户头像 GSYUserIconWidget( padding: const EdgeInsets.only( top: 0.0, right: 5.0, left: 0.0), width: 40.0, height: 40.0, image: pushHeaderViewModel.actionUserPic, onPressed: () { NavigatorUtils.goPerson( context, pushHeaderViewModel.actionUser); }), Expanded( child: Column( mainAxisSize: MainAxisSize.min, children: [ ///变化状态 Row( children: [ _getIconItem(GSYICons.PUSH_ITEM_EDIT, pushHeaderViewModel.editCount), Container(width: 8.0), _getIconItem(GSYICons.PUSH_ITEM_ADD, pushHeaderViewModel.addCount), Container(width: 8.0), _getIconItem(GSYICons.PUSH_ITEM_MIN, pushHeaderViewModel.deleteCount), Container(width: 8.0), ], ), const Padding(padding: EdgeInsets.all(2.0)), ///修改时间 Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text( pushHeaderViewModel.pushTime, style: GSYConstant.smallTextWhite, maxLines: 2, )), ///修改的commit内容 Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text( pushHeaderViewModel.pushDes, style: GSYConstant.smallTextWhite, maxLines: 2, )), const Padding( padding: EdgeInsets.only( left: 0.0, top: 2.0, right: 0.0, bottom: 0.0), ), ], ), ), ], ), ], ), ), ), ); } } class PushHeaderViewModel { String? actionUser = "---"; String? actionUserPic; String pushDes = "---"; String pushTime = "---"; String editCount = "---"; String addCount = "---"; String deleteCount = "---"; String? htmlUrl = GSYConstant.app_default_share_url; PushHeaderViewModel(); PushHeaderViewModel.forMap(PushCommit pushMap) { String? name = "---"; String? pic; if (pushMap.committer != null) { name = pushMap.committer!.login; } else if (pushMap.commit != null && pushMap.commit!.author != null) { name = pushMap.commit!.author!.name; } if (pushMap.committer != null && pushMap.committer!.avatar_url != null) { pic = pushMap.committer!.avatar_url; } actionUser = name; actionUserPic = pic; pushDes = "Push at ${pushMap.commit!.message!}"; pushTime = CommonUtils.getNewsTimeStr(pushMap.commit!.committer!.date!); editCount = "${pushMap.files!.length}"; addCount = "${pushMap.stats!.additions}"; deleteCount = "${pushMap.stats!.deletions}"; htmlUrl = pushMap.htmlUrl; } } ================================================ FILE: lib/page/release/release_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/html_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_select_item_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:gsy_github_app_flutter/page/release/widget/release_item.dart'; import 'package:url_launcher/url_launcher.dart'; /// 版本页 /// Created by guoshuyu /// Date: 2018-07-30 class ReleasePage extends StatefulWidget { final String? userName; final String? reposName; final String releaseUrl; final String tagUrl; const ReleasePage(this.userName, this.reposName, this.releaseUrl, this.tagUrl, {super.key}); @override _ReleasePageState createState() => _ReleasePageState(); } class _ReleasePageState extends State with AutomaticKeepAliveClientMixin, GSYListState { ///显示tag还是relase int selectIndex = 0; ///绘制item _renderEventItem(index) { ReleaseItemViewModel releaseItemViewModel = ReleaseItemViewModel.fromMap(pullLoadWidgetControl.dataList[index]); return ReleaseItem( releaseItemViewModel, onPressed: () { ///没有 release 提示就不要了 if (selectIndex == 0 && releaseItemViewModel.actionTargetHtml != null && releaseItemViewModel.actionTargetHtml!.isNotEmpty) { String html = HtmlUtils.generateHtml( releaseItemViewModel.actionTargetHtml, backgroundColor: GSYColors.miWhiteString, userBR: false); CommonUtils.launchWebView( context, releaseItemViewModel.actionTitle, html); } }, onLongPress: () { _launchURL(); }, ); } ///打开外部url _launchURL() async { String url = _getUrl(); var gl = context.l10n; if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } else { showToast("${gl.option_web_launcher_error}: $url"); } } String _getUrl() { return selectIndex == 0 ? widget.releaseUrl : widget.tagUrl; } _resolveSelectIndex() { clearData(); showRefreshLoading(); } _getDataLogic() async { return await ReposRepository.getRepositoryReleaseRequest( widget.userName!, widget.reposName!, page, needHtml: true, release: selectIndex == 0); } @override bool get wantKeepAlive => false; @override bool get needHeader => false; @override bool get isRefreshFirst => true; @override requestLoadMore() async { return await _getDataLogic(); } @override requestRefresh() async { return await _getDataLogic(); } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, appBar: AppBar( title: GSYTitleBar( widget.reposName, rightWidget: GSYCommonOptionWidget( url: _getUrl(), ), ), bottom: GSYSelectItemWidget( [ context.l10n.release_tab_release, context.l10n.release_tab_tag, ], (selectIndex) { this.selectIndex = selectIndex; _resolveSelectIndex(); }, height: 30.0, margin: const EdgeInsets.all(0.0), elevation: 0.0, ), elevation: 4.0, ), body: GSYPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderEventItem(index), handleRefresh, onLoadMore, refreshKey: refreshIndicatorKey, ), ); } } ================================================ FILE: lib/page/release/widget/release_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/model/release.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; /// 版本TagItem /// Created by guoshuyu /// Date: 2018-07-30 class ReleaseItem extends StatelessWidget { final ReleaseItemViewModel releaseItemViewModel; final GestureTapCallback? onPressed; final GestureLongPressCallback? onLongPress; const ReleaseItem(this.releaseItemViewModel, {super.key, this.onPressed, this.onLongPress}); @override Widget build(BuildContext context) { return GSYCardItem( child: InkWell( onTap: onPressed, onLongPress: onLongPress, child: Padding( padding: const EdgeInsets.only(left: 10.0, top: 15.0, right: 10.0, bottom: 15.0), child: Row( children: [ Expanded(child: Text(releaseItemViewModel.actionTitle!, style: GSYConstant.smallTextBold)), Text(releaseItemViewModel.actionTime ?? "", style: GSYConstant.smallSubText), ], ), ), ), ); } } class ReleaseItemViewModel { String? actionTime; String? actionTitle; String? actionMode; String? actionTarget; String? actionTargetHtml; String? body; ReleaseItemViewModel(); ReleaseItemViewModel.fromMap(Release map) { if (map.publishedAt != null) { actionTime = CommonUtils.getNewsTimeStr(map.publishedAt!); } actionTitle = map.name ?? map.tagName; actionTarget = map.targetCommitish; actionTargetHtml = map.bodyHtml; body = map.body ?? ""; } } ================================================ FILE: lib/page/repos/provider/repos_detail_provider.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:gsy_github_app_flutter/page/repos/repository_detail_page.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_network_provider.dart'; ///仓库详情数据实体,包含有当前index,仓库数据,分支等等 class ReposDetailProvider with ChangeNotifier { late ReposNetWorkProvider network; final String userName; final String reposName; ReposDetailProvider({required this.userName, required this.reposName}); int _currentIndex = 0; int get currentIndex => _currentIndex; set currentIndex(int data) { _currentIndex = data; notifyListeners(); } String _currentBranch = ""; String get currentBranch => _currentBranch; set currentBranch(String data) { _currentBranch = data; notifyListeners(); } BottomStatusModel? _bottomModel; BottomStatusModel? get bottomModel => _bottomModel; set bottomModel(BottomStatusModel? data) { _bottomModel = data; notifyListeners(); } List? _footerButtons; List? get footerButtons => _footerButtons; set footerButtons(List? data) { _footerButtons = data; notifyListeners(); } List? _branchList; List? get branchList => _branchList; set branchList(List? data) { _branchList = data; notifyListeners(); } RepositoryQL? _repository; RepositoryQL? get repository => _repository; set repository(RepositoryQL? data) { _repository = data; notifyListeners(); } String? _markdownData; String? get markdownData => _markdownData; set markdownData(String? data) { _markdownData = data; notifyListeners(); } ///#################################################/// ///获取网络端仓库的star等状态 getReposStatus( List Function(ReposDetailProvider p) getBottomWidget) async { String watchText = repository!.isSubscription == "SUBSCRIBED" ? "UnWatch" : "Watch"; String starText = repository!.isStared! ? "UnStar" : "Star"; IconData watchIcon = repository!.isSubscription == "SUBSCRIBED" ? GSYICons.REPOS_ITEM_WATCHED : GSYICons.REPOS_ITEM_WATCH; IconData starIcon = repository!.isStared! ? GSYICons.REPOS_ITEM_STARED : GSYICons.REPOS_ITEM_STAR; BottomStatusModel model = BottomStatusModel(watchText, starText, watchIcon, starIcon); bottomModel = model; footerButtons = getBottomWidget(this); } ///获取分支数据 getBranchList() async { var result = await ReposRepository.getBranchesRequest(userName, reposName); if (result != null && result.result) { var res = result.data as List; branchList = res.where((item) => item != null).cast().toList(); } } ///####################### 单纯为了展示 provider 里使用 provider ##########################/// Future refreshReadme() async { var res = await network.refreshReadme(userName, reposName, currentBranch); if (res != null && res.result) { markdownData = res.data; } if (res.next != null) { res = await res.next?.call(); markdownData = res.data; } } Future getRepositoryDetailRequest( List Function(ReposDetailProvider p) getBottomWidget) async { var result = await network.getRepositoryDetailRequest( userName, reposName, currentBranch); if (result.data.defaultBranch != null && result.data.defaultBranch.length > 0) { currentBranch = result.data.defaultBranch; } repository = result.data; getReposStatus(getBottomWidget); if (result.next != null) { result = await result.next?.call(); repository = result.data; getReposStatus(getBottomWidget); } } getReposCommitsRequest({page = 0, needDb = false}) async { return network.getReposCommitsRequest( userName, reposName, page: page, branch: currentBranch, needDb: page <= 1, ); } getRepositoryEventRequest({page = 0, needDb = false}) async { return network.getRepositoryEventRequest( userName, reposName, page: page, branch: currentBranch, needDb: page <= 1, ); } createForkRequest() async { return network.createForkRequest(userName, reposName); } doRepositoryWatchRequest() async { return network.doRepositoryWatchRequest( userName, reposName, repository!.isSubscription == "SUBSCRIBED"); } doRepositoryStarRequest() async { return network.doRepositoryStarRequest( userName, reposName, repository!.isStared); } getReposFileDirRequest({path = '', text = false, isHtml = false}) async { return network.getReposFileDirRequest(userName, reposName, path: path, branch: currentBranch, text: text, isHtml: isHtml); } getRepositoryIssueRequest(state, {sort, direction, page = 0, needDb = false}) async { return network.getRepositoryIssueRequest(userName, reposName, state, sort: sort, direction: direction, page: page, needDb: needDb); } searchRepositoryRequest(q, state, {page = 1}) async { return network.searchRepositoryRequest(q, userName, reposName, state, page: page); } createIssueRequest(issue) async { return network.createIssueRequest(userName, reposName, issue); } } ================================================ FILE: lib/page/repos/provider/repos_network_provider.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/repositories/issue_repository.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; ///#######################这部分没什么意思#########################/// ///######## 就是单纯展示 Provider 依赖 Provider ###################/// class ReposNetWorkProvider with ChangeNotifier { refreshReadme(String userName, String reposName, String currentBranch) { return ReposRepository.getRepositoryDetailReadmeRequest( userName, reposName, currentBranch); } getRepositoryDetailRequest(String userName, String reposName, String branch, {needDb = true}) { return ReposRepository.getRepositoryDetailRequest( userName, reposName, branch); } getReposCommitsRequest(String userName, String reposName, {page = 0, branch = "master", needDb = false}) async { return ReposRepository.getReposCommitsRequest( userName, reposName, page: page, branch: branch, needDb: page <= 1, ); } getRepositoryEventRequest(String userName, String reposName, {page = 0, branch = "master", needDb = false}) async { return ReposRepository.getRepositoryEventRequest( userName, reposName, page: page, branch: branch, needDb: page <= 1, ); } createForkRequest(String userName, String reposName) async { return ReposRepository.createForkRequest(userName, reposName); } doRepositoryWatchRequest(String userName, String reposName, watch) async { return ReposRepository.doRepositoryWatchRequest(userName, reposName, watch); } doRepositoryStarRequest(String userName, String reposName, star) async { return ReposRepository.doRepositoryStarRequest(userName, reposName, star); } getReposFileDirRequest(String userName, String reposName, {path = '', branch, text = false, isHtml = false}) async { return ReposRepository.getReposFileDirRequest(userName, reposName, path: path, branch: branch, text: text, isHtml: isHtml); } getRepositoryIssueRequest(String userName, String repository, state, {sort, direction, page = 0, needDb = false}) async { return IssueRepository.getRepositoryIssueRequest( userName, repository, state, sort: sort, direction: direction, page: page, needDb: needDb); } searchRepositoryRequest(String q, String name, String reposName, state, {page = 1}) async { return IssueRepository.searchRepositoryRequest(q, name, reposName, state, page: page); } createIssueRequest(String userName, String repository, issue) async { return IssueRepository.createIssueRequest(userName, repository, issue); } } ================================================ FILE: lib/page/repos/repository_detail_issue_list_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_detail_provider.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_nested_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_sliver_header_delegate.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/nested_refresh.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/page/search/widget/gsy_search_input_widget.dart'; import 'package:gsy_github_app_flutter/page/issue/widget/issue_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_select_item_widget.dart'; import 'package:provider/provider.dart'; /// 仓库详情issue列表 /// Created by guoshuyu /// Date: 2018-07-19 class RepositoryDetailIssuePage extends StatefulWidget { const RepositoryDetailIssuePage({super.key}); @override RepositoryDetailIssuePageState createState() => RepositoryDetailIssuePageState(); } ///页面 KeepAlive ,同时支持 动画Ticker class RepositoryDetailIssuePageState extends State with AutomaticKeepAliveClientMixin, GSYListState, SingleTickerProviderStateMixin { /// NestedScrollView 的刷新状态 GlobalKey ,方便主动刷新使用 final GlobalKey refreshIKey = GlobalKey(); ///搜索 issue 文本 String? searchText; ///过滤 issue 状态 String? issueState; ///显示 issue 状态 tag index int? selectIndex; ///滑动控制器 final ScrollController scrollController = ScrollController(); @override showRefreshLoading() { Future.delayed(const Duration(seconds: 0), () { refreshIKey.currentState?.show().then((e) {}); return true; }); } ///绘制issue item _renderIssueItem(index, ReposDetailProvider provider) { IssueItemViewModel issueItemViewModel = IssueItemViewModel.fromMap(pullLoadWidgetControl.dataList[index]); return IssueItem( issueItemViewModel, onPressed: () { NavigatorUtils.goIssueDetail(context, provider.userName, provider.reposName, issueItemViewModel.number); }, ); } ///切换显示状态 _resolveSelectIndex() { clearData(); switch (selectIndex) { case 0: issueState = null; break; case 1: issueState = 'open'; break; case 2: issueState = "closed"; break; } ///回滚到最初位置 scrollController .animateTo(0, duration: const Duration(milliseconds: 100), curve: Curves.bounceIn) .then((_) { showRefreshLoading(); }); } ///获取数据 _getDataLogic(String? searchString) async { if (searchString == null || searchString.trim().isEmpty) { return await context .read() .getRepositoryIssueRequest(issueState, page: page, needDb: page <= 1); } return await context .read() .searchRepositoryRequest(searchString, issueState, page: page); } @override bool get wantKeepAlive => true; @override bool get needHeader => false; @override bool get isRefreshFirst => true; @override requestLoadMore() async { return await _getDataLogic(searchText); } @override requestRefresh() async { return await _getDataLogic(searchText); } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. var provider = context.watch(); return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, appBar: AppBar( leading: Container(), flexibleSpace: (provider.repository?.hasIssuesEnabled == false) ? Container() : GSYSearchInputWidget(onSubmitted: (value) { searchText = value; _resolveSelectIndex(); }, onSubmitPressed: () { _resolveSelectIndex(); }), elevation: 0.0, backgroundColor: GSYColors.mainBackgroundColor, ), body: (provider.repository?.hasIssuesEnabled == false) ? Container( alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () {}, child: const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 70.0, height: 70.0), ), Text(context.l10n.repos_no_support_issue, style: GSYConstant.normalText), ], ), ) ///支持嵌套滚动 : GSYNestedPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderIssueItem(index, provider), handleRefresh, onLoadMore, refreshKey: refreshIKey, scrollController: scrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return _sliverBuilder(context, innerBoxIsScrolled); }, ), ); } ///绘制内置Header,支持部分停靠支持 List _sliverBuilder(BuildContext context, bool innerBoxIsScrolled) { double height = 60; return [ ///头部信息显示 SliverPersistentHeader( pinned: true, /// SliverPersistentHeaderDelegate 的实现 delegate: GSYSliverHeaderDelegate( maxHeight: height, minHeight: height, changeSize: true, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), builder: (BuildContext context, double shrinkOffset, bool overlapsContent) { ///根据数值计算偏差 var lr = 10 - shrinkOffset / height * 10; var radius = Radius.circular(4 - shrinkOffset / height * 4); return SizedBox.expand( child: Padding( padding: EdgeInsets.only(top: lr, bottom: 10, left: lr, right: lr), child: GSYSelectItemWidget( [ context.l10n.repos_tab_issue_all, context.l10n.repos_tab_issue_open, context.l10n.repos_tab_issue_closed, ], (selectIndex) { this.selectIndex = selectIndex; _resolveSelectIndex(); }, margin: const EdgeInsets.all(0.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(radius), ), ), ), ); }), ), ]; } } ================================================ FILE: lib/page/repos/repository_detail_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:gsy_github_app_flutter/page/repos/repository_detail_issue_list_page.dart'; import 'package:gsy_github_app_flutter/page/repos/repository_detail_readme_page.dart'; import 'package:gsy_github_app_flutter/page/repos/repository_file_list_page.dart'; import 'package:gsy_github_app_flutter/page/repos/repostory_detail_info_page.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_detail_provider.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_network_provider.dart'; import 'package:gsy_github_app_flutter/widget/gsy_bottom_action_bar.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_tabbar_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; import 'package:provider/provider.dart'; /// 仓库详情 /// Created by guoshuyu /// Date: 2018-07-18 class RepositoryDetailPage extends StatefulWidget { ///用户名 final String userName; ///仓库名 final String reposName; const RepositoryDetailPage(this.userName, this.reposName, {super.key}); @override _RepositoryDetailPageState createState() => _RepositoryDetailPageState(); } class _RepositoryDetailPageState extends State with SingleTickerProviderStateMixin { /// 文件列表页的 GlobalKey ,可用于当前控件控制文件也行为 GlobalKey fileListKey = GlobalKey(); /// 详情信息页的 GlobalKey ,可用于当前控件控制文件也行为 GlobalKey infoListKey = GlobalKey(); /// readme 页面的 GlobalKey ,可用于当前控件控制文件也行为 GlobalKey readmeKey = GlobalKey(); /// issue 列表页的 GlobalKey ,可用于当前控件控制文件也行为 GlobalKey issueListKey = GlobalKey(); ///动画控制器,用于底部发布 issue 按键动画 late AnimationController animationController; ///仓库的详情数据实体 late ReposDetailProvider reposDetailProvider; ///渲染 Tab 的 Item _renderTabItem() { var itemList = [ context.l10n.repos_tab_info, context.l10n.repos_tab_readme, context.l10n.repos_tab_issue, context.l10n.repos_tab_file, ]; renderItem(String item, int i) { return Container( padding: const EdgeInsets.symmetric(vertical: 10), child: Text( item, style: GSYConstant.smallTextWhite, maxLines: 1, )); } List list = []; for (int i = 0; i < itemList.length; i++) { list.add(renderItem(itemList[i], i)); } return list; } ///title 显示更多弹出item _getMoreOtherItem(RepositoryQL? repository) { return [ ///Release Page GSYOptionModel( context.l10n.repos_option_release, context.l10n.repos_option_release, (model) { String releaseUrl = ""; String tagUrl = ""; if (infoListKey.currentState == null) { releaseUrl = GSYConstant.app_default_share_url; tagUrl = GSYConstant.app_default_share_url; } else { releaseUrl = repository == null ? GSYConstant.app_default_share_url : "${repository.htmlUrl!}/releases"; tagUrl = repository == null ? GSYConstant.app_default_share_url : "${repository.htmlUrl!}/tags"; } NavigatorUtils.goReleasePage( context, widget.userName, widget.reposName, releaseUrl, tagUrl); }), ///Branch Page GSYOptionModel( context.l10n.repos_option_branch, context.l10n.repos_option_branch, (model) { if (reposDetailProvider.branchList!.isEmpty) { return; } CommonUtils.showCommitOptionDialog( context, reposDetailProvider.branchList, (value) { reposDetailProvider.currentBranch = reposDetailProvider.branchList![value]; if (infoListKey.currentState != null && infoListKey.currentState!.mounted) { infoListKey.currentState!.showRefreshLoading(); } if (fileListKey.currentState != null && fileListKey.currentState!.mounted) { fileListKey.currentState!.showRefreshLoading(); } if (readmeKey.currentState != null && readmeKey.currentState!.mounted) { readmeKey.currentState!.refreshReadme(); } }); }), ]; } ///创建 issue _createIssue(ReposDetailProvider provider) { String title = ""; String content = ""; CommonUtils.showEditDialog(context, context.l10n.issue_edit_issue, (titleValue) { title = titleValue; }, (contentValue) { content = contentValue; }, () { if (title.trim().isEmpty) { showToast(context.l10n.issue_edit_issue_title_not_be_null); return; } if (content.trim().isEmpty) { showToast(context.l10n.issue_edit_issue_content_not_be_null); return; } CommonUtils.showLoadingDialog(context); //提交修改 provider .createIssueRequest({"title": title, "body": content}).then((result) { if (issueListKey.currentState != null && issueListKey.currentState!.mounted) { issueListKey.currentState!.showRefreshLoading(); } Navigator.pop(context); Navigator.pop(context); }); }, needTitle: true, titleController: TextEditingController(), valueController: TextEditingController()); } @override void initState() { super.initState(); ///仓库的详情数据实体 reposDetailProvider = ReposDetailProvider( userName: widget.userName, reposName: widget.reposName); reposDetailProvider.getBranchList(); ///悬浮按键动画控制器 animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 800)); animationController.forward(); } @override Widget build(BuildContext context) { ///跨 tab 共享状态 ///这是只是为了展示 Provider 状态管理的 Demo,所以在应用里使用了多种不同的状态管理框架 return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => ReposNetWorkProvider()), ChangeNotifierProxyProvider( update: (context, network, previousMessages) => previousMessages!..network = network, create: (BuildContext context) => reposDetailProvider, ), ], child: Consumer( builder: (BuildContext context, provider, Widget? child) { Widget widgetContent = (provider.repository != null && provider.repository!.htmlUrl != null) ? GSYCommonOptionWidget( url: provider.repository?.htmlUrl, otherList: _getMoreOtherItem(provider.repository)) : Container(); ///绘制顶部 tab 控件 return GSYTabBarWidget( type: TabType.top, tabItems: _renderTabItem(), resizeToAvoidBottomPadding: false, //footerButtons: model.footerButtons, tabViews: [ ReposDetailInfoPage(key: infoListKey), RepositoryDetailReadmePage(key: readmeKey), RepositoryDetailIssuePage( key: issueListKey, ), RepositoryDetailFileListPage(key: fileListKey), ], backgroundColor: GSYColors.primarySwatch, indicatorColor: GSYColors.white, title: GSYTitleBar( widget.reposName, rightWidget: widgetContent, ), onPageChanged: (index) { reposDetailProvider.currentIndex = index; }, ///悬浮按键,增加出现动画 floatingActionButton: ScaleTransition( //scale: CurvedAnimation(parent: animationController, curve: Curves.bounceInOut), scale: CurvedAnimation( parent: animationController, curve: Curves.decelerate), child: FloatingActionButton( onPressed: () { if (provider.repository?.hasIssuesEnabled == false) { showToast(context.l10n.repos_no_support_issue); return; } _createIssue(provider); }, backgroundColor: Theme.of(context).primaryColor, child: const Icon(Icons.add), ), ), ///悬浮按键位置 floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, ///底部bar,增加对悬浮按键的缺省容器处理 bottomBar: GSYBottomAppBar( color: GSYColors.white, fabLocation: FloatingActionButtonLocation.endDocked, shape: const CircularNotchedRectangle(), rowContents: (provider.footerButtons == null) ? [ SizedBox.fromSize( size: const Size(0, 0), ) ] : provider.footerButtons!.isEmpty == true ? [ SizedBox.fromSize( size: const Size(0, 0), ) ] : provider.footerButtons), ); }), ); } } ///底部状态实体 class BottomStatusModel { final String watchText; final String starText; final IconData watchIcon; final IconData starIcon; BottomStatusModel( this.watchText, this.starText, this.watchIcon, this.starIcon); } ================================================ FILE: lib/page/repos/repository_detail_readme_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_detail_provider.dart'; import 'package:gsy_github_app_flutter/widget/markdown/gsy_markdown_widget.dart'; import 'package:provider/provider.dart'; /// Readme /// Created by guoshuyu /// Date: 2018-07-18 class RepositoryDetailReadmePage extends StatefulWidget { const RepositoryDetailReadmePage({super.key}); @override RepositoryDetailReadmePageState createState() => RepositoryDetailReadmePageState(); } class RepositoryDetailReadmePageState extends State with AutomaticKeepAliveClientMixin { RepositoryDetailReadmePageState(); Future? request; refreshReadme() { context.read().refreshReadme(); } @override bool get wantKeepAlive => true; @override void initState() { super.initState(); refreshReadme(); } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { super.build(context); ///展示 select var markdownData = context.select((p) => p.markdownData); var rp = context.read(); var widget = (markdownData == null) ? Center( child: Container( width: 200.0, height: 200.0, padding: const EdgeInsets.all(4.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SpinKitDoubleBounce(color: Theme.of(context).primaryColor), Container(width: 10.0), Text(context.l10n.loading_text, style: GSYConstant.middleText), ], ), ), ) : GSYMarkdownWidget( markdownData: markdownData, baseUrl: getRawBaseUrl( repoName: rp.reposName, userName: rp.userName, branch: rp.currentBranch)); return widget; } } ================================================ FILE: lib/page/repos/repository_file_list_page.dart ================================================ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/model/file_model.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_detail_provider.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:provider/provider.dart'; import 'package:signals/signals_flutter.dart'; /// 仓库文件列表 /// Created by guoshuyu /// on 2018/7/20. class RepositoryDetailFileListPage extends StatefulWidget { const RepositoryDetailFileListPage({super.key}); @override RepositoryDetailFileListPageState createState() => RepositoryDetailFileListPageState(); } class RepositoryDetailFileListPageState extends State with AutomaticKeepAliveClientMixin, SignalsMixin { ///使用 signal 例子,整个页面没用 setState late var dataList = createListSignal([]); late var headerList = createListSignal(["."]); final EasyRefreshController controller = EasyRefreshController(); bool _isLoading = false; String path = ''; String? searchText; String? issueState; ///渲染文件item _renderEventItem(index) { var item = dataList.value[index]; FileItemViewModel fileItemViewModel = FileItemViewModel.fromMap(item); IconData iconData = (fileItemViewModel.type == "file") ? GSYICons.REPOS_ITEM_FILE : GSYICons.REPOS_ITEM_DIR; Widget? trailing = (fileItemViewModel.type == "file") ? null : const Icon(GSYICons.REPOS_ITEM_NEXT, size: 12.0); return GSYCardItem( margin: const EdgeInsets.only(left: 10.0, top: 5.0, right: 10.0, bottom: 5.0), child: ListTile( title: Text(fileItemViewModel.name!, style: GSYConstant.smallSubText), leading: Icon( iconData, size: 16.0, ), onTap: () { _resolveItemClick(fileItemViewModel); }, trailing: trailing, ), ); } ///渲染头部列表 _renderHeader() { return Container( margin: const EdgeInsets.only(left: 3.0, right: 3.0), child: ListView.builder( scrollDirection: Axis.horizontal, itemBuilder: (context, index) { return RawMaterialButton( constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), padding: const EdgeInsets.all(4.0), onPressed: () { _resolveHeaderClick(index); }, child: Text("${headerList[index]} > ", style: GSYConstant.smallText), ); }, itemCount: headerList.length, ), ); } ///头部列表点击 _resolveHeaderClick(index) { if (_isLoading) { showToast(context.l10n.loading_text); return; } if (headerList.isNotEmpty && index != -1 && headerList[index] != ".") { List newHeaderList = headerList.sublist(0, index + 1); String path = newHeaderList.sublist(1, newHeaderList.length).join("/"); this.path = path; headerList.value = newHeaderList; showRefreshLoading(); } else { path = ""; headerList.value = ["."]; showRefreshLoading(); } } ///item文件列表点击 _resolveItemClick(FileItemViewModel fileItemViewModel) { var provider = context.read(); if (fileItemViewModel.type == "dir") { if (_isLoading) { showToast(context.l10n.loading_text); return; } headerList.add(fileItemViewModel.name!); String path = headerList.sublist(1, headerList.length).join("/"); this.path = path; showRefreshLoading(); } else { String path = "${headerList.sublist(1, headerList.length).join("/")}/${fileItemViewModel.name!}"; if (CommonUtils.isImageEnd(fileItemViewModel.name)) { NavigatorUtils.gotoPhotoViewPage( context, "${fileItemViewModel.htmlUrl!}?raw=true"); } else { String? lang; var typeIndex = fileItemViewModel.name!.lastIndexOf("."); if (typeIndex != -1) { lang = fileItemViewModel.name!.substring(typeIndex + 1); } NavigatorUtils.gotoCodeDetailPlatform( context, title: fileItemViewModel.name, reposName: provider.reposName, userName: provider.userName, path: path, lang: lang, branch: context.read().currentBranch, ); } } } Future _getDataLogic(String? searchString) async { return await context .read() .getReposFileDirRequest(path: path); } /// 模拟IOS下拉显示刷新 showRefreshLoading() { ///直接触发下拉 Future.delayed(const Duration(seconds: 0), () { controller.callRefresh(); return true; }); } @override bool get wantKeepAlive => true; requestRefresh() async { _isLoading = true; var res = await _getDataLogic(searchText); try { if (res.result) { var data = res.data; if (data != null) { dataList.value = data as List; } } if (res.next != null) { var data = await res.next?.call(); if (data != null) { dataList.value = data as List; } } } catch (e) { printLog(e); } _isLoading = false; } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. var provider = context.watch(); return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, appBar: AppBar( flexibleSpace: _renderHeader(), backgroundColor: GSYColors.mainBackgroundColor, leading: Container(), elevation: 0.0, ), body: PopScope( canPop: provider.currentIndex != 3 || headerList.length == 1, onPopInvokedWithResult: (didPop, _) { if (didPop == false) { _resolveHeaderClick(headerList.length - 2); } }, child: EasyRefresh( controller: controller, header: const MaterialHeader(), refreshOnStart: true, onRefresh: requestRefresh, child: ListView.builder( itemBuilder: (_, int index) => _renderEventItem(index), itemCount: dataList.length, ), ), ), ); } } class FileItemViewModel { String? type; String? name; String? htmlUrl; FileItemViewModel(); FileItemViewModel.fromMap(FileModel map) { name = map.name; type = map.type; htmlUrl = map.htmlUrl; } } ================================================ FILE: lib/page/repos/repostory_detail_info_page.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/event_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/model/repo_commit.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:gsy_github_app_flutter/page/repos/provider/repos_detail_provider.dart'; import 'package:gsy_github_app_flutter/widget/gsy_event_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_nested_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_sliver_header_delegate.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/nested_refresh.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/widget/gsy_select_item_widget.dart'; import 'package:gsy_github_app_flutter/page/repos/widget/repos_header_item.dart'; import 'package:provider/provider.dart'; /// 仓库详情动态信息页面 /// Created by guoshuyu /// Date: 2018-07-18 class ReposDetailInfoPage extends StatefulWidget { const ReposDetailInfoPage({super.key}); @override ReposDetailInfoPageState createState() => ReposDetailInfoPageState(); } ///页面 KeepAlive ,同时支持 动画Ticker class ReposDetailInfoPageState extends State with AutomaticKeepAliveClientMixin, GSYListState, TickerProviderStateMixin { ///滑动监听 final ScrollController scrollController = ScrollController(); ///当前显示tab int selectIndex = 0; ///初始化 header 默认大小,后面动态调整 double headerSize = 270; /// NestedScrollView 的刷新状态 GlobalKey ,方便主动刷新使用 final GlobalKey refreshIKey = GlobalKey(); ///动画控制器 AnimationController? animationController; @override showRefreshLoading() { Future.delayed(const Duration(seconds: 0), () { refreshIKey.currentState!.show().then((e) {}); return true; }); } ///渲染时间Item或者提交Item _renderEventItem(index) { var provider = context.read(); var item = pullLoadWidgetControl.dataList[index]; if (selectIndex == 1 && item is RepoCommit) { ///提交 return GSYEventItem( EventViewModel.fromCommitMap(item), onPressed: () { RepoCommit model = pullLoadWidgetControl.dataList[index]; NavigatorUtils.goPushDetailPage( context, provider.userName, provider.reposName, model.sha, false, ); }, needImage: false, ); } else if (selectIndex == 0 && item is Event) { return GSYEventItem( EventViewModel.fromEventMap(pullLoadWidgetControl.dataList[index]), onPressed: () { EventUtils.ActionUtils( context, pullLoadWidgetControl.dataList[index], "${provider.userName}/${provider.reposName}", ); }, ); } else { return const SizedBox(); } } ///获取列表 _getDataLogic() async { var provider = context.read(); if (selectIndex == 1) { return await provider.getReposCommitsRequest( page: page, needDb: page <= 1, ); } return await provider.getRepositoryEventRequest( page: page, needDb: page <= 1, ); } ///获取详情 _getReposDetail() { context.read().getRepositoryDetailRequest( _getBottomWidget, ); } ///绘制底部状态 List _getBottomWidget(ReposDetailProvider provider) { ///根据网络返回数据,返回底部状态数据 List bottomWidget = (provider.bottomModel == null) ? [] : [ /// star _renderBottomItem( provider.bottomModel!.starText, provider.bottomModel!.starIcon, () { CommonUtils.showLoadingDialog(context); return provider.doRepositoryStarRequest().then((result) { showRefreshLoading(); var context = this.context; if (!context.mounted) return; Navigator.pop(context); }); }, ), /// watch _renderBottomItem( provider.bottomModel!.watchText, provider.bottomModel!.watchIcon, () { CommonUtils.showLoadingDialog(context); return provider.doRepositoryWatchRequest().then((result) { showRefreshLoading(); Navigator.pop(context); }); }, ), ///fork _renderBottomItem("fork", GSYICons.REPOS_ITEM_FORK, () { CommonUtils.showLoadingDialog(context); return provider.createForkRequest().then((result) { showRefreshLoading(); Navigator.pop(context); }); }), ]; return bottomWidget; } ///绘制底部状态 item _renderBottomItem(var text, var icon, var onPressed) { return TextButton( onPressed: onPressed, child: GSYIConText( icon, text, GSYConstant.smallText, GSYColors.primaryValue, 15.0, padding: 5.0, mainAxisAlignment: MainAxisAlignment.center, ), ); } @override bool get wantKeepAlive => true; @override requestRefresh() async { _getReposDetail(); return await _getDataLogic(); } @override requestLoadMore() async { return await _getDataLogic(); } @override bool get isRefreshFirst => true; @override bool get needHeader => false; @override void initState() { super.initState(); animationController = AnimationController(vsync: this); } @override Widget build(BuildContext context) { super.build(context); ///展示 select context.select((p) => p.repository); return GSYNestedPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderEventItem(index), handleRefresh, onLoadMore, refreshKey: refreshIKey, scrollController: scrollController, headerSliverBuilder: (context, innerBoxIsScrolled) { return _sliverBuilder( context, innerBoxIsScrolled, context.read(), ); }, ); } ///绘制内置Header,支持部分停靠支持 List _sliverBuilder( BuildContext context, bool innerBoxIsScrolled, ReposDetailProvider provider, ) { return [ ///头部信息 SliverPersistentHeader( delegate: GSYSliverHeaderDelegate( maxHeight: headerSize, minHeight: headerSize, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), child: OverflowBox( maxHeight: 1000, child: ReposHeaderItem( ReposHeaderViewModel.fromHttpMap( provider.userName, provider.reposName, provider.repository, ), layoutListener: (size) { setState(() { headerSize = size.height; }); }, ), ), ), ), ///动态放大缩小的tab控件 SliverPersistentHeader( pinned: true, /// SliverPersistentHeaderDelegate 的实现 delegate: GSYSliverHeaderDelegate( maxHeight: 60, minHeight: 60, changeSize: true, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), builder: ( BuildContext context, double shrinkOffset, bool overlapsContent, ) { ///根据数值计算偏差 var lr = 10 - shrinkOffset / 60 * 10; var radius = Radius.circular(4 - shrinkOffset / 60 * 4); return SizedBox.expand( child: Padding( padding: EdgeInsets.only(bottom: 10, left: lr, right: lr), child: GSYSelectItemWidget( [ context.l10n.repos_tab_activity, context.l10n.repos_tab_commits, ], (index) { ///切换时先滑动 scrollController .animateTo( 0, duration: const Duration(milliseconds: 200), curve: Curves.bounceInOut, ) .then((_) { selectIndex = index; clearData(); showRefreshLoading(); }); }, margin: EdgeInsets.zero, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(radius), ), ), ), ); }, ), ), ]; } } ================================================ FILE: lib/page/repos/widget/repos_header_item.dart ================================================ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/common_list_datatype.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; /// 仓库详情信息头控件 /// Created by guoshuyu /// Date: 2018-07-18 class ReposHeaderItem extends StatefulWidget { final ReposHeaderViewModel reposHeaderViewModel; final ValueChanged? layoutListener; const ReposHeaderItem(this.reposHeaderViewModel, {super.key, this.layoutListener}); @override _ReposHeaderItemState createState() => _ReposHeaderItemState(); } class _ReposHeaderItemState extends State { final GlobalKey layoutKey = GlobalKey(); final GlobalKey layoutTopicContainerKey = GlobalKey(); final GlobalKey layoutLastTopicKey = GlobalKey(); double widgetHeight = 0; ///底部仓库状态信息,比如star数量等 _getBottomItem(IconData icon, String text, onPressed) { return Expanded( child: Center( child: RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), onPressed: onPressed, child: GSYIConText( icon, text, GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]), GSYColors.subLightTextColor, 15.0, padding: 3.0, mainAxisAlignment: MainAxisAlignment.center, ))), ); } _renderTopicItem(BuildContext context, String item, index) { return RawMaterialButton( key: index == widget.reposHeaderViewModel.topics!.length - 1 ? layoutLastTopicKey : null, onPressed: () { NavigatorUtils.gotoCommonList( context, item, "repository", CommonListDataType.topics, userName: item, reposName: ""); }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(0.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: Container( padding: const EdgeInsets.only( left: 5.0, right: 5.0, top: 2.5, bottom: 2.5), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), color: Colors.white30, border: Border.all(color: Colors.white30, width: 0.0), ), child: Text( item, style: GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]), ), )); } ///话题组控件 _renderTopicGroup(BuildContext context) { if (widget.reposHeaderViewModel.topics == null || widget.reposHeaderViewModel.topics!.isEmpty) { return Container( key: layoutTopicContainerKey, ); } List list = []; for (int i = 0; i < widget.reposHeaderViewModel.topics!.length; i++) { var item = widget.reposHeaderViewModel.topics![i]!; list.add(_renderTopicItem(context, item, i)); } return Container( key: layoutTopicContainerKey, alignment: Alignment.topLeft, //margin: EdgeInsets.only(top: 5.0), child: Wrap( spacing: 10.0, runSpacing: 5.0, children: list, ), ); } ///仓库创建和提交状态信息 _getInfoText(BuildContext context) { String createStr = widget.reposHeaderViewModel.repositoryIsFork! ? '${context.l10n.repos_fork_at}${widget.reposHeaderViewModel.repositoryParentName!}\n' : "${context.l10n.repos_create_at}${widget.reposHeaderViewModel.created_at}\n"; String updateStr = context.l10n.repos_last_commit + widget.reposHeaderViewModel.push_at; return createStr + updateStr; } ///顶部信息 _renderTopNameInfo() { return Row( children: [ ///用户名 RawMaterialButton( constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), padding: const EdgeInsets.all(0.0), onPressed: () { NavigatorUtils.goPerson( context, widget.reposHeaderViewModel.ownerName); }, child: Text(widget.reposHeaderViewModel.ownerName!, style: GSYConstant.normalTextActionWhiteBold.copyWith(shadows: [ const BoxShadow(color: Colors.black, offset: Offset(0.5, 0.5)) ])), ), Text(" / ", style: GSYConstant.normalTextMitWhiteBold.copyWith(shadows: [ const BoxShadow(color: Colors.black, offset: Offset(0.5, 0.5)) ])), ///仓库名, Expanded( child: Text(widget.reposHeaderViewModel.repositoryName!, maxLines: 1, overflow: TextOverflow.ellipsis, style: GSYConstant.normalTextMitWhiteBold.copyWith(shadows: [ const BoxShadow(color: Colors.black, offset: Offset(0.5, 0.5)) ]))) ], ); } ///次要信息 _renderSubInfo() { return Row( children: [ ///仓库语言 Text(widget.reposHeaderViewModel.repositoryType ?? "--", style: GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ])), const SizedBox(width: 5.3, height: 1.0), ///仓库大小 Text(widget.reposHeaderViewModel.repositorySize, style: GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ])), const SizedBox(width: 5.3, height: 1.0), ///仓库协议 Expanded( child: Text(widget.reposHeaderViewModel.license ?? "--", maxLines: 1, overflow: TextOverflow.ellipsis, style: GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]))), ], ); } ///仓库描述 renderDes() { return Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text( CommonUtils.removeTextTag( widget.reposHeaderViewModel.repositoryDes) ?? "---", style: GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]), maxLines: 3, overflow: TextOverflow.ellipsis, )); } /// 右下角的信息 renderRepoStatus() { return Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0, right: 5.0), alignment: Alignment.topRight, child: RawMaterialButton( onPressed: () { if (widget.reposHeaderViewModel.repositoryIsFork!) { NavigatorUtils.goReposDetail( context, widget.reposHeaderViewModel.repositoryParentUser, widget.reposHeaderViewModel.repositoryName); } }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(0.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: Text(_getInfoText(context), style: widget.reposHeaderViewModel.repositoryIsFork! ? GSYConstant.smallActionLightText.copyWith(shadows: [ const BoxShadow( color: Colors.grey, offset: Offset(0.5, 0.5)) ]) : GSYConstant.smallSubLightText.copyWith(shadows: [ const BoxShadow( color: Colors.grey, offset: Offset(0.5, 0.5)) ])), ), ); } ///底部仓库状态信息 renderBottomInfo() { return Padding( padding: const EdgeInsets.all(0.0), ///创建数值状态 child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ ///star状态 _getBottomItem( GSYICons.REPOS_ITEM_STAR, widget.reposHeaderViewModel.repositoryStar, () { NavigatorUtils.gotoCommonList( context, widget.reposHeaderViewModel.repositoryName, "user", CommonListDataType.repoStar, userName: widget.reposHeaderViewModel.ownerName, reposName: widget.reposHeaderViewModel.repositoryName); }, ), Container( width: 0.3, height: 25.0, decoration: const BoxDecoration( color: GSYColors.subLightTextColor, boxShadow: [ BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]), ), ///fork状态 _getBottomItem( GSYICons.REPOS_ITEM_FORK, widget.reposHeaderViewModel.repositoryFork, () { NavigatorUtils.gotoCommonList( context, widget.reposHeaderViewModel.repositoryName, "repository", CommonListDataType.repoFork, userName: widget.reposHeaderViewModel.ownerName, reposName: widget.reposHeaderViewModel.repositoryName); }, ), Container( width: 0.3, height: 25.0, decoration: const BoxDecoration( color: GSYColors.subLightTextColor, boxShadow: [ BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]), ), ///订阅状态 _getBottomItem( GSYICons.REPOS_ITEM_WATCH, widget.reposHeaderViewModel.repositoryWatch, () { NavigatorUtils.gotoCommonList( context, widget.reposHeaderViewModel.repositoryName, "user", CommonListDataType.repoWatcher, userName: widget.reposHeaderViewModel.ownerName, reposName: widget.reposHeaderViewModel.repositoryName); }, ), Container( width: 0.3, height: 25.0, decoration: const BoxDecoration( color: GSYColors.subLightTextColor, boxShadow: [ BoxShadow(color: Colors.grey, offset: Offset(0.5, 0.5)) ]), ), ///issue状态 _getBottomItem( GSYICons.REPOS_ITEM_ISSUE, widget.reposHeaderViewModel.repositoryIssue, () { if (widget.reposHeaderViewModel.allIssueCount == null || widget.reposHeaderViewModel.allIssueCount! <= 0) { return; } StringList list = [ context.l10n.repos_all_issue_count + widget.reposHeaderViewModel.allIssueCount.toString(), context.l10n.repos_open_issue_count + widget.reposHeaderViewModel.openIssuesCount.toString(), context.l10n.repos_close_issue_count + (widget.reposHeaderViewModel.allIssueCount! - widget.reposHeaderViewModel.openIssuesCount!) .toString(), ]; CommonUtils.showCommitOptionDialog(context, list, (index) {}, height: 150.0); }, ), ], )); } @override void didUpdateWidget(ReposHeaderItem oldWidget) { super.didUpdateWidget(oldWidget); ///如果存在tag,根据tag去判断,修复溢出 Future.delayed(const Duration(seconds: 0), () { /// tag 所在 container RenderBox? renderBox2 = layoutTopicContainerKey.currentContext ?.findRenderObject() as RenderBox?; var dy = renderBox2 ?.localToGlobal(Offset.zero, ancestor: layoutKey.currentContext!.findRenderObject()) .dy ?? 0; var sizeTagContainer = layoutTopicContainerKey.currentContext?.size; var headerSize = layoutKey.currentContext?.size; if (dy > 0 && headerSize != null && sizeTagContainer != null) { /// 20是 card 的上下 padding var newSize = dy + sizeTagContainer.height + 20; if (widgetHeight != newSize && newSize > 0) { printLog("widget?.layoutListener?.call"); widgetHeight = newSize; widget.layoutListener ?.call(Size(layoutKey.currentContext!.size!.width, widgetHeight)); } } }); } @override Widget build(BuildContext context) { return Container( key: layoutKey, child: GSYCardItem( color: Theme.of(context).primaryColorDark, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(4.0)), child: Container( ///背景头像 decoration: BoxDecoration( image: DecorationImage( fit: BoxFit.cover, image: NetworkImage(widget.reposHeaderViewModel.ownerPic ?? GSYICons.DEFAULT_REMOTE_PIC), ), ), child: BackdropFilter( ///高斯模糊 filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), child: Padding( padding: const EdgeInsets.only( left: 10.0, top: 0.0, right: 10.0, bottom: 10.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ _renderTopNameInfo(), _renderSubInfo(), ///仓库描述 renderDes(), ///创建状态 renderRepoStatus(), const Divider( color: GSYColors.subTextColor, ), ///底部信息 renderBottomInfo(), ///底部tag _renderTopicGroup(context), ], ), ), ), ), ), ), ); } } class ReposHeaderViewModel { String? ownerName = '---'; String? ownerPic; String? repositoryName = "---"; String repositorySize = "---"; String repositoryStar = "---"; String repositoryFork = "---"; String repositoryWatch = "---"; String repositoryIssue = "---"; String repositoryIssueClose = ""; String repositoryIssueAll = ""; String? repositoryType = "---"; String? repositoryDes = "---"; String repositoryLastActivity = ""; String? repositoryParentName = ""; String? repositoryParentUser = ""; String created_at = ""; String push_at = ""; String? license = ""; List? topics; int? allIssueCount = 0; int? openIssuesCount = 0; bool repositoryStared = false; bool repositoryForked = false; bool repositoryWatched = false; bool? repositoryIsFork = false; ReposHeaderViewModel(); ReposHeaderViewModel.fromHttpMap( this.ownerName, reposName, RepositoryQL? map) { if (map == null || map.ownerName == null) { return; } ownerPic = map.ownerAvatarUrl; repositoryName = reposName; allIssueCount = map.issuesTotal; topics = map.topics; openIssuesCount = map.issuesOpen; repositoryStar = map.starCount != null ? map.starCount.toString() : ""; repositoryFork = map.forkCount != null ? map.forkCount.toString() : ""; repositoryWatch = map.watcherCount != null ? map.watcherCount.toString() : ""; repositoryIssue = map.issuesOpen != null ? map.issuesOpen.toString() : ""; //this.repositoryIssueClose = map.closedIssuesCount != null ? map.closed_issues_count.toString() : ""; //this.repositoryIssueAll = map.all_issues_count != null ? map.all_issues_count.toString() : ""; repositorySize = "${(map.size! / 1024.0).toString().substring(0, 3)}M"; repositoryType = map.language; repositoryDes = map.shortDescriptionHTML; repositoryIsFork = map.isFork; license = map.license ?? ""; repositoryParentName = map.parent?.reposName; repositoryParentUser = map.parent?.ownerName; created_at = CommonUtils.getNewsTimeStr(DateTime.parse(map.createdAt!)); push_at = CommonUtils.getNewsTimeStr((DateTime.parse(map.pushAt!))); } } ================================================ FILE: lib/page/repos/widget/repos_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/repository.dart'; import 'package:gsy_github_app_flutter/model/repository_ql.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; import 'package:gsy_github_app_flutter/widget/gsy_user_icon_widget.dart'; /// 仓库Item /// Created by guoshuyu /// Date: 2018-07-16 class ReposItem extends StatelessWidget { final ReposViewModel reposViewModel; final VoidCallback? onPressed; const ReposItem(this.reposViewModel, {super.key, this.onPressed}); ///仓库item的底部状态,比如star数量等 _getBottomItem(BuildContext context, IconData icon, String? text, {int flex = 3}) { return Expanded( flex: flex, child: Center( child: GSYIConText( icon, text, GSYConstant.smallSubText, GSYColors.subTextColor, 15.0, padding: 5.0, textWidth: flex == 4 ? (MediaQuery.sizeOf(context).width - 100) / 3 : (MediaQuery.sizeOf(context).width - 100) / 5, ), ), ); } @override Widget build(BuildContext context) { return GSYCardItem( child: TextButton( onPressed: onPressed, child: Padding( padding: const EdgeInsets.only( left: 0.0, top: 10.0, right: 10.0, bottom: 10.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///头像 GSYUserIconWidget( padding: const EdgeInsets.only( top: 0.0, right: 5.0, left: 0.0), width: 40.0, height: 40.0, image: reposViewModel.ownerPic, onPressed: () { NavigatorUtils.goPerson( context, reposViewModel.ownerName); }), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///仓库名 Text(reposViewModel.repositoryName ?? "", style: GSYConstant.normalTextBold), ///用户名 GSYIConText( GSYICons.REPOS_ITEM_USER, reposViewModel.ownerName, GSYConstant.smallSubLightText, GSYColors.subLightTextColor, 10.0, padding: 3.0, ), ], ), ), ///仓库语言 Text(reposViewModel.repositoryType!, style: GSYConstant.smallSubText), ], ), Container( ///仓库描述 margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, ///仓库描述 child: Text( reposViewModel.repositoryDes!, style: GSYConstant.smallSubText, maxLines: 3, overflow: TextOverflow.ellipsis, )), const Padding(padding: EdgeInsets.all(10.0)), ///仓库状态数值 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _getBottomItem(context, GSYICons.REPOS_ITEM_STAR, reposViewModel.repositoryStar), const SizedBox( width: 5, ), _getBottomItem(context, GSYICons.REPOS_ITEM_FORK, reposViewModel.repositoryFork), const SizedBox( width: 5, ), _getBottomItem(context, GSYICons.REPOS_ITEM_ISSUE, reposViewModel.repositoryWatch, flex: 4), ], ), ], ), ))); } } class ReposViewModel { String? ownerName; String? ownerPic; String? repositoryName; String? repositoryStar; String? repositoryFork; String? repositoryWatch; String? hideWatchIcon; String? repositoryType = ""; String? repositoryDes; ReposViewModel(); ReposViewModel.fromMap(Repository data) { ownerName = data.owner!.login; ownerPic = data.owner!.avatar_url; repositoryName = data.name; repositoryStar = data.watchersCount.toString(); repositoryFork = data.forksCount.toString(); repositoryWatch = data.openIssuesCount.toString(); repositoryType = data.language ?? '---'; repositoryDes = data.description ?? '---'; } ReposViewModel.fromQL(RepositoryQL data) { ownerName = data.ownerName; ownerPic = data.ownerAvatarUrl; repositoryName = data.reposName; repositoryStar = data.starCount.toString(); repositoryFork = data.forkCount.toString(); repositoryWatch = data.watcherCount.toString(); repositoryType = data.language ?? '---'; repositoryDes = CommonUtils.removeTextTag(data.shortDescriptionHTML) ?? '---'; } ReposViewModel.fromTrendMap(model) { ownerName = model.name; if (model.contributors.length > 0) { ownerPic = model.contributors[0]; } else { ownerPic = ""; } repositoryName = model.reposName; repositoryStar = model.starCount; repositoryFork = model.forkCount; repositoryWatch = model.meta; repositoryType = model.language; repositoryDes = CommonUtils.removeTextTag(model.description); } } ================================================ FILE: lib/page/search/search_bloc.dart ================================================ import 'package:flutter/cupertino.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:gsy_github_app_flutter/page/search/widget/gsy_search_drawer.dart'; class SearchBLoC { ///搜索仓库还是人 int selectIndex = 0; ///搜索文件 String? get searchText { return textEditingController.text; } ///排序类型 String? type = searchFilterType[0].value; ///排序 String? sort = sortType[0].value; ///过滤语言 String? language = searchLanguageType[0].value; final TextEditingController textEditingController = TextEditingController(); ///获取搜索数据 getDataLogic(int page) async { return await ReposRepository.searchRepositoryRequest(searchText, language, type, sort, selectIndex == 0 ? null : 'user', page, Config.PAGE_SIZE); } void resetFilters() { resetSearchDrawerFilters(); type = searchFilterType[0].value; sort = sortType[0].value; language = searchLanguageType[0].value; selectIndex = 0; } void dispose() { resetFilters(); textEditingController.dispose(); } } ================================================ FILE: lib/page/search/search_page.dart ================================================ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/search/search_bloc.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/page/search/widget/gsy_search_drawer.dart'; import 'package:gsy_github_app_flutter/page/search/widget/gsy_search_input_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_select_item_widget.dart'; import 'package:gsy_github_app_flutter/page/repos/widget/repos_item.dart'; import 'package:gsy_github_app_flutter/page/user/widget/user_item.dart'; /// 搜索页面 /// Created by guoshuyu /// on 2018/7/24. class SearchPage extends StatefulWidget { final Offset centerPosition; const SearchPage(this.centerPosition, {super.key}); @override _SearchPageState createState() => _SearchPageState(); } class _SearchPageState extends State with AutomaticKeepAliveClientMixin, GSYListState, SingleTickerProviderStateMixin { final SearchBLoC searchBLoC = SearchBLoC(); late AnimationController controller; Animation? animation; bool endAnima = false; ///绘制item _renderItem(index) { var data = pullLoadWidgetControl.dataList[index]; if (searchBLoC.selectIndex == 0) { ReposViewModel reposViewModel = ReposViewModel.fromMap(data); return ReposItem(reposViewModel, onPressed: () { NavigatorUtils.goReposDetail( context, reposViewModel.ownerName, reposViewModel.repositoryName); }); } else if (searchBLoC.selectIndex == 1) { return UserItem(UserItemViewModel.fromMap(data), onPressed: () { NavigatorUtils.goPerson( context, UserItemViewModel.fromMap(data).userName); }); } } ///切换tab _resolveSelectIndex() { clearData(); showRefreshLoading(); } ///获取搜索数据 _getDataLogic() async { return await searchBLoC.getDataLogic(page); } @override bool get wantKeepAlive => true; @override bool get needHeader => false; @override bool get isRefreshFirst => false; @override requestLoadMore() async { return await _getDataLogic(); } @override requestRefresh() async { return await _getDataLogic(); } _search() { if (searchBLoC.searchText == null || searchBLoC.searchText?.trim().isEmpty == true) { return; } if (isLoading) { return; } _resolveSelectIndex(); } @override void initState() { super.initState(); controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); animation = CurvedAnimation( parent: controller, curve: Curves.easeInCubic, )..addListener(() { setState(() {}); }); Future.delayed(const Duration(seconds: 0), () { controller.forward().then((_) { setState(() { endAnima = true; }); }); }); } @override void dispose() { searchBLoC.dispose(); controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. return Container( ///填充剩下半圆颜色 color: endAnima ? Theme.of(context).primaryColor : Colors.transparent, child: CRAnimation( minR: MediaQuery.sizeOf(context).height - 8, maxR: 0, offset: widget.centerPosition, animation: animation as Animation?, child: Scaffold( resizeToAvoidBottomInset: false, ///右侧 Drawer endDrawer: GSYSearchDrawer( (String? type) { ///排序类型 searchBLoC.type = type; Navigator.pop(context); _resolveSelectIndex(); }, (String? sort) { ///排序状态 searchBLoC.sort = sort; Navigator.pop(context); _resolveSelectIndex(); }, (String? language) { ///过滤语言 searchBLoC.language = language; Navigator.pop(context); _resolveSelectIndex(); }, ), appBar: AppBar( leading: IconButton( highlightColor: Colors.transparent, icon: const BackButtonIcon(), onPressed: () { setState(() { endAnima = false; }); controller.reverse().then((_) { Navigator.maybePop(context); }); }, ), title: Text(context.l10n.search_title), bottom: SearchBottom( textEditingController: searchBLoC.textEditingController, onSubmitted: (_) { _search(); }, onSubmitPressed: () { _search(); }, selectItemChanged: (selectIndex) { if (searchBLoC.searchText == null || searchBLoC.searchText?.trim().isEmpty == true) { return; } if (isLoading) { return; } searchBLoC.selectIndex = selectIndex; _resolveSelectIndex(); })), body: GSYPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => _renderItem(index), handleRefresh, onLoadMore, refreshKey: refreshIndicatorKey, ), ), ), ); } } ///实现 PreferredSizeWidget 实现自定义 appbar bottom 控件 class SearchBottom extends StatelessWidget implements PreferredSizeWidget { final SelectItemChanged? onSubmitted; final SelectItemChanged? selectItemChanged; final VoidCallback? onSubmitPressed; final TextEditingController? textEditingController; const SearchBottom( {super.key, this.onSubmitted, this.onSubmitPressed, this.selectItemChanged, this.textEditingController}); @override Widget build(BuildContext context) { return Column( children: [ GSYSearchInputWidget( controller: textEditingController, onSubmitted: onSubmitted, onSubmitPressed: onSubmitPressed), GSYSelectItemWidget( [ context.l10n.search_tab_repos, context.l10n.search_tab_user, ], selectItemChanged, elevation: 0.0, margin: const EdgeInsets.all(5.0), ) ], ); } @override Size get preferredSize { return const Size.fromHeight(100.0); } } class CRAnimation extends StatelessWidget { final Offset? offset; final double? minR; final double? maxR; final Widget child; final Animation? animation; const CRAnimation({super.key, required this.child, required this.animation, this.offset, this.minR, this.maxR, }); @override Widget build(BuildContext context) { return AnimatedBuilder( animation: animation!, builder: (_, __) { return ClipPath( clipper: AnimationClipper( value: animation!.value, minR: minR, maxR: maxR, offset: offset, ), child: child, ); }, ); } } class AnimationClipper extends CustomClipper { final double? value; final double? minR; final double? maxR; final Offset? offset; AnimationClipper({ this.value, this.offset, this.minR, this.maxR, }); @override bool shouldReclip(oldClipper) => true; @override Path getClip(Size size) { var path = Path(); var offset = this.offset ?? Offset(size.width / 2, size.height / 2); var maxRadius = minR ?? radiusSize(size, offset); var minRadius = maxR ?? 0; var radius = lerpDouble(minRadius, maxRadius, value!)!; var rect = Rect.fromCircle( radius: radius, center: offset, ); path.addOval(rect); return path; } double radiusSize(Size size, Offset offset) { final height = max(offset.dy, size.height - offset.dy); final width = max(offset.dx, size.width - offset.dx); return sqrt(width * width + height * height); } } ================================================ FILE: lib/page/search/widget/gsy_search_drawer.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; /// 搜索drawer /// Created by guoshuyu /// Date: 2018-07-18 typedef SearchSelectItemChanged = void Function(String value); class GSYSearchDrawer extends StatefulWidget { final SearchSelectItemChanged typeCallback; final SearchSelectItemChanged sortCallback; final SearchSelectItemChanged languageCallback; const GSYSearchDrawer(this.typeCallback, this.sortCallback, this.languageCallback, {super.key}); @override _GSYSearchDrawerState createState() => _GSYSearchDrawerState(); } class _GSYSearchDrawerState extends State { final double itemWidth = 200.0; @override Widget build(BuildContext context) { return SafeArea( child: Container( color: Theme.of(context).primaryColor, child: Container( color: GSYColors.white, child: SingleChildScrollView( child: Column( children: _renderList(), ), ), ), ), ); } _renderList() { List list = []; list.add(Container( width: itemWidth, )); list.add(_renderTitle(context.l10n.search_type)); for (int i = 0; i < searchFilterType.length; i++) { FilterModel model = searchFilterType[i]; list.add(_renderItem(model, searchFilterType, i, widget.typeCallback)); list.add(_renderDivider()); } list.add(_renderTitle(context.l10n.search_sort)); for (int i = 0; i < sortType.length; i++) { FilterModel model = sortType[i]; list.add(_renderItem(model, sortType, i, widget.sortCallback)); list.add(_renderDivider()); } list.add(_renderTitle(context.l10n.search_language)); for (int i = 0; i < searchLanguageType.length; i++) { FilterModel model = searchLanguageType[i]; list.add( _renderItem(model, searchLanguageType, i, widget.languageCallback)); list.add(_renderDivider()); } return list; } _renderTitle(String title) { return Container( color: Theme.of(context).primaryColor, width: itemWidth + 50, height: 50.0, child: Center( child: Text( title, style: GSYConstant.middleTextWhite, textAlign: TextAlign.start, ), ), ); } _renderDivider() { return Container( color: GSYColors.subTextColor, width: itemWidth, height: 0.3, ); } _renderItem(FilterModel model, List list, int index, SearchSelectItemChanged? select) { return Stack( children: [ SizedBox( height: 50.0, child: SizedBox( width: itemWidth, child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Checkbox( value: model.select, onChanged: (value) {})), Center(child: Text(model.name!)), ], ), ), ), TextButton( onPressed: () { setState(() { for (FilterModel model in list) { model.select = false; } list[index].select = true; }); select?.call(model.value); }, child: Container( width: itemWidth, ), ) ], ); } } class FilterModel { String? name; String? value; bool? select; FilterModel({this.name, this.value, this.select}); } var sortType = [ FilterModel(name: 'desc', value: 'desc', select: true), FilterModel(name: 'asc', value: 'asc', select: false), ]; var searchFilterType = [ FilterModel(name: "best_match", value: 'best%20match', select: true), FilterModel(name: "stars", value: 'stars', select: false), FilterModel(name: "forks", value: 'forks', select: false), FilterModel(name: "updated", value: 'updated', select: false), ]; var searchLanguageType = [ FilterModel(name: "trendAll", value: null, select: true), FilterModel(name: "Java", value: 'Java', select: false), FilterModel(name: "Dart", value: 'Dart', select: false), FilterModel(name: "Objective_C", value: 'Objective-C', select: false), FilterModel(name: "Swift", value: 'Swift', select: false), FilterModel(name: "JavaScript", value: 'JavaScript', select: false), FilterModel(name: "PHP", value: 'PHP', select: false), FilterModel(name: "C__", value: 'C++', select: false), FilterModel(name: "C", value: 'C', select: false), FilterModel(name: "HTML", value: 'HTML', select: false), FilterModel(name: "CSS", value: 'CSS', select: false), ]; void resetSearchDrawerFilters() { _resetFilterList(sortType); _resetFilterList(searchFilterType); _resetFilterList(searchLanguageType); } void _resetFilterList(List list) { for (int i = 0; i < list.length; i++) { list[i].select = i == 0; } } ================================================ FILE: lib/page/search/widget/gsy_search_input_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; /// 搜索输入框 /// Created by guoshuyu /// Date: 2018-07-20 class GSYSearchInputWidget extends StatelessWidget { final TextEditingController? controller; final ValueChanged? onSubmitted; final VoidCallback? onSubmitPressed; const GSYSearchInputWidget( {super.key, this.controller, this.onSubmitted, this.onSubmitPressed}); @override Widget build(BuildContext context) { return Container( height: kToolbarHeight, decoration: BoxDecoration( borderRadius: const BorderRadius.only( bottomRight: Radius.circular(0.0), bottomLeft: Radius.circular(0.0)), color: GSYColors.white, border: Border.all(color: Theme.of(context).primaryColor, width: 0.3), boxShadow: [ BoxShadow( color: Theme.of(context).primaryColorDark, blurRadius: 4.0) ]), padding: const EdgeInsets.only(left: 20.0, top: 12.0, right: 20.0, bottom: 12.0), child: Row( children: [ Expanded( child: TextField( autofocus: false, controller: controller, decoration: InputDecoration( hintText: context.l10n.repos_issue_search, hintStyle: GSYConstant.middleSubText, border: InputBorder.none, isDense: true, ), ///关闭 3.7 的放大镜 magnifierConfiguration: TextMagnifierConfiguration(magnifierBuilder: ( BuildContext context, MagnifierController controller, ValueNotifier magnifierInfo, ) { return null; }), style: GSYConstant.middleText .copyWith(textBaseline: TextBaseline.alphabetic), onSubmitted: onSubmitted), ), RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.only(right: 5.0, left: 10.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), onPressed: onSubmitPressed, child: Icon( GSYICons.SEARCH, size: 15.0, color: Theme.of(context).primaryColorDark, )) ], ), ); } } ================================================ FILE: lib/page/trend/trend_page.dart ================================================ import 'dart:async'; import 'package:animations/animations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/page/repos/repository_detail_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:gsy_github_app_flutter/model/trending_repo_model.dart'; import 'package:gsy_github_app_flutter/page/trend/trend_provider.dart'; import 'package:gsy_github_app_flutter/page/trend/trend_user_page.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_sliver_header_delegate.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/nested_refresh.dart'; import 'package:gsy_github_app_flutter/page/repos/widget/repos_item.dart'; import 'package:lottie/lottie.dart'; /// 主页趋势tab页 /// 目前采用纯 bloc 的 rxdart(stream) + streamBuilder /// Created by guoshuyu /// Date: 2018-07-16 class TrendPage extends ConsumerStatefulWidget { const TrendPage({super.key}); @override TrendPageState createState() => TrendPageState(); } class TrendPageState extends ConsumerState with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { ///显示数据时间 TrendTypeModel? selectTime; int selectTimeIndex = 0; ///显示过滤语言 TrendTypeModel? selectType; int selectTypeIndex = 0; /// NestedScrollView 的刷新状态 GlobalKey ,方便主动刷新使用 final GlobalKey refreshIndicatorKey = GlobalKey(); ///滚动控制与监听 final ScrollController scrollController = ScrollController(); ///显示刷新 _showRefreshLoading() { Future.delayed(const Duration(seconds: 0), () { refreshIndicatorKey.currentState!.show().then((e) {}); return true; }); } scrollToTop() { if (scrollController.offset <= 0) { scrollController .animateTo(0, duration: const Duration(milliseconds: 600), curve: Curves.linear) .then((_) { _showRefreshLoading(); }); } else { scrollController.animateTo(0, duration: const Duration(milliseconds: 600), curve: Curves.linear); } } ///绘制tiem _renderItem(e) { ReposViewModel reposViewModel = ReposViewModel.fromTrendMap(e); return OpenContainer( closedColor: Colors.transparent, closedElevation: 0, transitionType: ContainerTransitionType.fade, openBuilder: (BuildContext context, VoidCallback _) { return NavigatorUtils.pageContainer( RepositoryDetailPage( reposViewModel.ownerName!, reposViewModel.repositoryName!), context); }, tappable: true, closedBuilder: (BuildContext _, VoidCallback openContainer) { return ReposItem(reposViewModel, onPressed: null); }, ); } ///绘制头部可选item _renderHeader(Radius radius) { if (selectTime == null && selectType == null) { return Container(); } var trendTimeList = trendTime(context); var trendTypeList = trendType(context); return GSYCardItem( color: ref.watch(appThemeStateProvider).primaryColor, margin: const EdgeInsets.all(0.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(radius), ), child: Padding( padding: const EdgeInsets.only(left: 0.0, top: 5.0, right: 0.0, bottom: 5.0), child: Row( children: [ _renderHeaderPopItem(selectTime!.name, trendTimeList, (TrendTypeModel result) { if (trendLoadingState) { showToast(context.l10n.loading_text); return; } scrollController .animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.bounceInOut) .then((_) { setState(() { selectTime = result; selectTimeIndex = trendTimeList.indexOf(result); }); _showRefreshLoading(); }); }), Container(height: 10.0, width: 0.5, color: GSYColors.white), _renderHeaderPopItem(selectType!.name, trendTypeList, (TrendTypeModel result) { if (trendLoadingState) { showToast(context.l10n.loading_text); return; } scrollController .animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.bounceInOut) .then((_) { setState(() { selectType = result; selectTypeIndex = trendTypeList.indexOf(result); }); _showRefreshLoading(); }); }), ], ), ), ); } ///或者头部可选弹出item容器 _renderHeaderPopItem(String data, List list, PopupMenuItemSelected onSelected) { return Expanded( child: PopupMenuButton( onSelected: onSelected, itemBuilder: (BuildContext context) { return _renderHeaderPopItemChild(list); }, child: Center(child: Text(data, style: GSYConstant.middleTextWhite)), ), ); } ///或者头部可选弹出item _renderHeaderPopItemChild(List data) { List> list = []; for (TrendTypeModel item in data) { list.add(PopupMenuItem( value: item, child: Text(item.name), )); } return list; } Future requestRefresh() async { var _ = ref.refresh(trendFirstProvider(selectTime?.value, selectType?.value)); await ref.read(trendFirstProvider(selectTime?.value, selectType?.value).future); } @override bool get wantKeepAlive => true; @override void didChangeDependencies() { if (!trendRequestedState) { setState(() { selectTime = trendTime(context)[0]; selectType = trendType(context)[0]; }); _showRefreshLoading(); } else { if (selectTimeIndex >= 0) { selectTime = trendTime(context)[selectTimeIndex]; } if (selectTypeIndex >= 0) { selectType = trendType(context)[selectTypeIndex]; } setState(() {}); } super.didChangeDependencies(); } ///空页面 Widget _buildEmpty() { var mediaQueryData = MediaQueryData.fromView(View.of(context)); var statusBar = mediaQueryData.padding.top; var bottomArea = mediaQueryData.padding.bottom; var height = MediaQuery.sizeOf(context).height - statusBar - bottomArea - kBottomNavigationBarHeight - kToolbarHeight; return SingleChildScrollView( child: SizedBox( height: height, width: MediaQuery.sizeOf(context).width, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () {}, child: const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 70.0, height: 70.0), ), Text(context.l10n.app_empty, style: GSYConstant.normalText), ], ), ), ); } @override Widget build(BuildContext context) { super.build(context); // See AutomaticKeepAliveClientMixin. ///展示非注解的 riverpod 并且配置先后顺序 return Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { final firstAsync = ref.watch(trendFirstProvider(selectTime?.value, selectType?.value)); final secondAsync = ref .watch(trendSecondProvider(selectTime?.value, selectType?.value)); var result = secondAsync.value?.data as List? ?? firstAsync.value?.data as List?; return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, body: NestedScrollViewRefreshIndicator( key: refreshIndicatorKey, onRefresh: requestRefresh, ///嵌套滚动 child: NestedScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), headerSliverBuilder: (context, innerBoxIsScrolled) { return _sliverBuilder(context, innerBoxIsScrolled); }, body: (result == null || result.isEmpty) ? _buildEmpty() : ListView.builder( physics: const AlwaysScrollableScrollPhysics(), itemBuilder: (context, index) { return _renderItem(result[index]); }, itemCount: result.length, ), ), ), floatingActionButton: trendUserButton(), ); }); } trendUserButton() { const double size = 56.0; return OpenContainer( transitionType: ContainerTransitionType.fade, openBuilder: (BuildContext context, VoidCallback _) { return NavigatorUtils.pageContainer(const TrendUserPage(), context); }, closedElevation: 6.0, closedShape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(size / 2), ), ), closedColor: Theme.of(context).primaryColor, closedBuilder: (BuildContext context, VoidCallback openContainer) { return SizedBox( width: size, height: size, child: Lottie.asset("static/file/user.json", fit: BoxFit.cover), ); }, ); } ///嵌套可滚动头部 List _sliverBuilder(BuildContext context, bool innerBoxIsScrolled) { return [ ///动态头部 SliverPersistentHeader( pinned: true, ///SliverPersistentHeaderDelegate 实现 delegate: GSYSliverHeaderDelegate( maxHeight: 65, minHeight: 65, changeSize: true, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), builder: (BuildContext context, double shrinkOffset, bool overlapsContent) { ///根据数值计算偏差 var lr = 10 - shrinkOffset / 65 * 10; var radius = Radius.circular(4 - shrinkOffset / 65 * 4); return SizedBox.expand( child: Padding( padding: EdgeInsets.only(top: lr, bottom: 15, left: lr, right: lr), child: _renderHeader(radius), ), ); }), ), ]; } } ///趋势数据过滤显示item class TrendTypeModel { final String name; final String? value; TrendTypeModel(this.name, this.value); } ///趋势数据时间过滤 List trendTime(BuildContext context) { return [ TrendTypeModel(context.l10n.trend_day, "daily"), TrendTypeModel(context.l10n.trend_week, "weekly"), TrendTypeModel(context.l10n.trend_month, "monthly"), ]; } ///趋势数据语言过滤 List trendType(BuildContext context) { return [ TrendTypeModel(context.l10n.trend_all, null), TrendTypeModel("Java", "Java"), TrendTypeModel("Kotlin", "Kotlin"), TrendTypeModel("Dart", "Dart"), TrendTypeModel("Objective-C", "Objective-C"), TrendTypeModel("Swift", "Swift"), TrendTypeModel("JavaScript", "JavaScript"), TrendTypeModel("PHP", "PHP"), TrendTypeModel("Go", "Go"), TrendTypeModel("C++", "C++"), TrendTypeModel("C", "C"), TrendTypeModel("HTML", "HTML"), TrendTypeModel("CSS", "CSS"), TrendTypeModel("Python", "Python"), TrendTypeModel("C#", "c%23"), ]; } ================================================ FILE: lib/page/trend/trend_provider.dart ================================================ import 'package:gsy_github_app_flutter/common/repositories/data_result.dart'; import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'trend_provider.g.dart'; ///展示非注解 riverpod ,并且针对处理数据库 & 服务器数据请求,两个 provider 先后顺序 bool trendLoadingState = false; bool trendRequestedState = false; @riverpod Future trendFirst(Ref ref, String? since, String? selectType) async { trendLoadingState = true; var res = await ReposRepository.getTrendRequest( since: since, languageType: selectType); trendLoadingState = false; trendRequestedState = true; if (res != null && res.result) { return res; } return null; } @riverpod Future trendSecond(Ref ref, String? since, String? selectType) async { trendLoadingState = true; final res = await ref.watch(trendFirstProvider(since, selectType).future); trendLoadingState = false; trendRequestedState = true; if (res != null && res.next != null) { var resNext = await res.next?.call(); if (resNext != null && resNext.result) { return res; } } return null; } ================================================ FILE: lib/page/trend/trend_provider.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'trend_provider.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning @ProviderFor(trendFirst) const trendFirstProvider = TrendFirstFamily._(); final class TrendFirstProvider extends $FunctionalProvider< AsyncValue, DataResult?, FutureOr > with $FutureModifier, $FutureProvider { const TrendFirstProvider._({ required TrendFirstFamily super.from, required (String?, String?) super.argument, }) : super( retry: null, name: r'trendFirstProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$trendFirstHash(); @override String toString() { return r'trendFirstProvider' '' '$argument'; } @$internal @override $FutureProviderElement $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override FutureOr create(Ref ref) { final argument = this.argument as (String?, String?); return trendFirst(ref, argument.$1, argument.$2); } @override bool operator ==(Object other) { return other is TrendFirstProvider && other.argument == argument; } @override int get hashCode { return argument.hashCode; } } String _$trendFirstHash() => r'3e6901281c2b4209f6a6af2bd8d224bed08ec011'; final class TrendFirstFamily extends $Family with $FunctionalFamilyOverride, (String?, String?)> { const TrendFirstFamily._() : super( retry: null, name: r'trendFirstProvider', dependencies: null, $allTransitiveDependencies: null, isAutoDispose: true, ); TrendFirstProvider call(String? since, String? selectType) => TrendFirstProvider._(argument: (since, selectType), from: this); @override String toString() => r'trendFirstProvider'; } @ProviderFor(trendSecond) const trendSecondProvider = TrendSecondFamily._(); final class TrendSecondProvider extends $FunctionalProvider< AsyncValue, DataResult?, FutureOr > with $FutureModifier, $FutureProvider { const TrendSecondProvider._({ required TrendSecondFamily super.from, required (String?, String?) super.argument, }) : super( retry: null, name: r'trendSecondProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$trendSecondHash(); @override String toString() { return r'trendSecondProvider' '' '$argument'; } @$internal @override $FutureProviderElement $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override FutureOr create(Ref ref) { final argument = this.argument as (String?, String?); return trendSecond(ref, argument.$1, argument.$2); } @override bool operator ==(Object other) { return other is TrendSecondProvider && other.argument == argument; } @override int get hashCode { return argument.hashCode; } } String _$trendSecondHash() => r'99470661772032da2b765be93e555eb936aa5383'; final class TrendSecondFamily extends $Family with $FunctionalFamilyOverride, (String?, String?)> { const TrendSecondFamily._() : super( retry: null, name: r'trendSecondProvider', dependencies: null, $allTransitiveDependencies: null, isAutoDispose: true, ); TrendSecondProvider call(String? since, String? selectType) => TrendSecondProvider._(argument: (since, selectType), from: this); @override String toString() => r'trendSecondProvider'; } ================================================ FILE: lib/page/trend/trend_user_page.dart ================================================ import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/search_user_ql.dart'; import 'package:gsy_github_app_flutter/page/trend/trend_user_provider.dart'; import 'package:gsy_github_app_flutter/page/user/widget/user_item.dart'; class TrendUserPage extends ConsumerStatefulWidget { const TrendUserPage({super.key}); @override _TrendUserPageState createState() => _TrendUserPageState(); } class _TrendUserPageState extends ConsumerState { String? endCursor; _renderItem(SearchUserQL data, int index) { return UserItem(UserItemViewModel.fromQL(data, index + 1), onPressed: () { NavigatorUtils.goPerson(context, data.login); }); } Future loadData(WidgetRef ref, {bool isRefresh = false}) async { /// getTrendUserProvider 这里只有一处地方使用,所以不需要做局部单实例共享 final result = await ref.read( searchTrendUserRequestProvider("China", isRefresh, cursor: endCursor) .future); if (result != null) { var (dataList, cursor) = result; endCursor = cursor; } } requestRefresh() async { endCursor = null; await loadData(ref, isRefresh: true); } requestLoadMore() async { await loadData(ref); } @override Widget build(BuildContext context) { var dataList = ref.watch(trendCNUserListProvider); return Scaffold( appBar: AppBar( title: Text( context.l10n.trend_user_title, maxLines: 1, overflow: TextOverflow.ellipsis, )), body: EasyRefresh( header: const MaterialHeader(), footer: const BezierFooter(), refreshOnStart: true, onRefresh: requestRefresh, onLoad: requestLoadMore, child: ListView.builder( itemBuilder: (_, int index) => _renderItem(dataList[index], index), itemCount: dataList.length, ), )); } } ================================================ FILE: lib/page/trend/trend_user_provider.dart ================================================ import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/model/search_user_ql.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; ///展示 riverpod ,并且支持翻页场景 part 'trend_user_provider.g.dart'; ///无需释放,这样内存里就会保存着列表,下次进来不会空数据 @Riverpod(keepAlive: true) class TrendCNUserList extends _$TrendCNUserList { ///如果调用 ref.refresh ,数据会被重置 @override List build() { return []; } void setList(List list) { state = list; } void addList(List list) { ///需要为新的列表,不然不会触发更新 state = [...state, ...list]; } void clear() { state = []; } } @riverpod Future<(List, String)?> searchTrendUserRequest( Ref ref, String location, bool isRefresh, {String? cursor}) async { var trendRef = ref.read(trendCNUserListProvider.notifier); var result = await UserRepository.searchTrendUserRequest("China", cursor: cursor); if (result.data != null) { var value = result.data; if (isRefresh) { trendRef.setList(value.$1); } else { trendRef.addList(value.$1); } // 这里 refresh 会导致数据在更新后又被清空 //var _ = ref.refresh(trendCNUserListProvider.notifier); //如果是 ref.invalidate ,则是标记过时,下次读取时触发,延迟更新 return result.value; } return null; } ================================================ FILE: lib/page/trend/trend_user_provider.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'trend_user_provider.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning ///无需释放,这样内存里就会保存着列表,��次进来不会空数据 @ProviderFor(TrendCNUserList) const trendCNUserListProvider = TrendCNUserListProvider._(); ///无需释放,这样内存里就会保存着列表,��次进来不会空数据 final class TrendCNUserListProvider extends $NotifierProvider> { ///无需释放,这样内存里就会保存着列表,��次进来不会空数据 const TrendCNUserListProvider._() : super( from: null, argument: null, retry: null, name: r'trendCNUserListProvider', isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$trendCNUserListHash(); @$internal @override TrendCNUserList create() => TrendCNUserList(); /// {@macro riverpod.override_with_value} Override overrideWithValue(List value) { return $ProviderOverride( origin: this, providerOverride: $SyncValueProvider>(value), ); } } String _$trendCNUserListHash() => r'aea06336b92d3cc570f969d63fda016132cdf064'; ///无需释放,这样内存里就会保存着列表,��次进来不会空数据 abstract class _$TrendCNUserList extends $Notifier> { List build(); @$mustCallSuper @override void runBuild() { final created = build(); final ref = this.ref as $Ref, List>; final element = ref.element as $ClassProviderElement< AnyNotifier, List>, List, Object?, Object? >; element.handleValue(ref, created); } } @ProviderFor(searchTrendUserRequest) const searchTrendUserRequestProvider = SearchTrendUserRequestFamily._(); final class SearchTrendUserRequestProvider extends $FunctionalProvider< AsyncValue<(List, String)?>, (List, String)?, FutureOr<(List, String)?> > with $FutureModifier<(List, String)?>, $FutureProvider<(List, String)?> { const SearchTrendUserRequestProvider._({ required SearchTrendUserRequestFamily super.from, required (String, bool, {String? cursor}) super.argument, }) : super( retry: null, name: r'searchTrendUserRequestProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$searchTrendUserRequestHash(); @override String toString() { return r'searchTrendUserRequestProvider' '' '$argument'; } @$internal @override $FutureProviderElement<(List, String)?> $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override FutureOr<(List, String)?> create(Ref ref) { final argument = this.argument as (String, bool, {String? cursor}); return searchTrendUserRequest( ref, argument.$1, argument.$2, cursor: argument.cursor, ); } @override bool operator ==(Object other) { return other is SearchTrendUserRequestProvider && other.argument == argument; } @override int get hashCode { return argument.hashCode; } } String _$searchTrendUserRequestHash() => r'ac778f5a6b971be10cac54dba88243725747e346'; final class SearchTrendUserRequestFamily extends $Family with $FunctionalFamilyOverride< FutureOr<(List, String)?>, (String, bool, {String? cursor}) > { const SearchTrendUserRequestFamily._() : super( retry: null, name: r'searchTrendUserRequestProvider', dependencies: null, $allTransitiveDependencies: null, isAutoDispose: true, ); SearchTrendUserRequestProvider call( String location, bool isRefresh, { String? cursor, }) => SearchTrendUserRequestProvider._( argument: (location, isRefresh, cursor: cursor), from: this, ); @override String toString() => r'searchTrendUserRequestProvider'; } ================================================ FILE: lib/page/user/base_person_provider.dart ================================================ import 'package:gsy_github_app_flutter/common/repositories/repos_repository.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'base_person_provider.g.dart'; ///指定作用域,让该 provider as scoped ,[]表示不依赖其他,让其每次使用都在上下文独立 @Riverpod(dependencies: []) Future fetchHonorData(Ref ref, String userName) async { var res = await ReposRepository.getUserRepository100StatusRequest(userName); if (res != null && res.result) { return HonorModel.fromJson(res.data); } return null; } class HonorModel { int? beStaredCount; List? honorList; HonorModel.fromJson(Map map) { beStaredCount = map["stared"]; honorList = map["list"]; } } ================================================ FILE: lib/page/user/base_person_provider.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'base_person_provider.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning ///指定作用域,让该 provider as scoped ,[]表示不依赖其他,让其每次使用都在上下文独立 @ProviderFor(fetchHonorData) const fetchHonorDataProvider = FetchHonorDataFamily._(); ///指定作用域,让该 provider as scoped ,[]表示不依赖其他,让其每次使用都在上下文独立 final class FetchHonorDataProvider extends $FunctionalProvider< AsyncValue, HonorModel?, FutureOr > with $FutureModifier, $FutureProvider { ///指定作用域,让该 provider as scoped ,[]表示不依赖其他,让其每次使用都在上下文独立 const FetchHonorDataProvider._({ required FetchHonorDataFamily super.from, required String super.argument, }) : super( retry: null, name: r'fetchHonorDataProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$fetchHonorDataHash(); @override String toString() { return r'fetchHonorDataProvider' '' '($argument)'; } @$internal @override $FutureProviderElement $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override FutureOr create(Ref ref) { final argument = this.argument as String; return fetchHonorData(ref, argument); } @override bool operator ==(Object other) { return other is FetchHonorDataProvider && other.argument == argument; } @override int get hashCode { return argument.hashCode; } } String _$fetchHonorDataHash() => r'7f84f6895bdda593859d93add87021d7c91dce4d'; ///指定作用域,让该 provider as scoped ,[]表示不依赖其他,让其每次使用都在上下文独立 final class FetchHonorDataFamily extends $Family with $FunctionalFamilyOverride, String> { const FetchHonorDataFamily._() : super( retry: null, name: r'fetchHonorDataProvider', dependencies: const [], $allTransitiveDependencies: const [], isAutoDispose: true, ); ///指定作用域,让该 provider as scoped ,[]表示不依赖其他,让其每次使用都在上下文独立 FetchHonorDataProvider call(String userName) => FetchHonorDataProvider._(argument: userName, from: this); @override String toString() => r'fetchHonorDataProvider'; } ================================================ FILE: lib/page/user/base_person_state.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/model/user_org.dart'; import 'package:gsy_github_app_flutter/common/utils/event_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/user/base_person_provider.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; import 'package:gsy_github_app_flutter/widget/gsy_event_item.dart'; import 'package:gsy_github_app_flutter/widget/only_share_widget.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_sliver_header_delegate.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/nested_refresh.dart'; import 'package:gsy_github_app_flutter/widget/state/gsy_list_state.dart'; import 'package:gsy_github_app_flutter/page/user/widget/user_header.dart'; import 'package:gsy_github_app_flutter/page/user/widget/user_item.dart'; /// Created by guoshuyu /// Date: 2018-08-30 abstract class BasePersonState extends State with AutomaticKeepAliveClientMixin, GSYListState, SingleTickerProviderStateMixin { final GlobalKey refreshIKey = GlobalKey(); final List orgList = []; @override showRefreshLoading() { Future.delayed(const Duration(seconds: 0), () { refreshIKey.currentState!.show().then((e) {}); return true; }); } @protected renderItem(index, User userInfo, String beStaredCount, Color? notifyColor, VoidCallback? refreshCallBack, List orgList) { if (userInfo.type == "Organization") { return UserItem( UserItemViewModel.fromMap(pullLoadWidgetControl.dataList[index]), onPressed: () { NavigatorUtils.goPerson( context, UserItemViewModel.fromMap(pullLoadWidgetControl.dataList[index]) .userName); }); } else { Event event = pullLoadWidgetControl.dataList[index]; return GSYEventItem(EventViewModel.fromEventMap(event), onPressed: () { EventUtils.ActionUtils(context, event, ""); }); } } @override bool get wantKeepAlive => true; @override bool get isRefreshFirst => true; @override bool get needHeader => true; @protected FetchHonorDataProvider get headerProvider; @protected Widget buildContainer(BuildContext context); @protected getUserOrg(String? userName) { if (page <= 1 && userName != null) { UserRepository.getUserOrgsRequest(userName, page, needDb: true) .then((res) { if (res != null && res.result) { setState(() { orgList.clear(); orgList.addAll(res.data); }); return res.next?.call(); } return Future.value(null); }).then((res) { if (res != null && res.result) { setState(() { orgList.clear(); orgList.addAll(res.data); }); } }); } } @protected List sliverBuilder( BuildContext context, bool innerBoxIsScrolled, User userInfo, Color? notifyColor, String beStaredCount, refreshCallBack) { double headerSize = 210; double bottomSize = 70; double chartSize = (userInfo.login != null && userInfo.type == "Organization") ? 70 : 215; return [ ///头部信息 SliverPersistentHeader( pinned: true, delegate: GSYSliverHeaderDelegate( maxHeight: headerSize, minHeight: headerSize, changeSize: true, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), builder: (BuildContext context, double shrinkOffset, bool overlapsContent) { return Transform.translate( offset: Offset(0, -shrinkOffset), child: SizedBox.expand( child: UserHeaderItem( userInfo, beStaredCount, Theme.of(context).primaryColor, notifyColor: notifyColor, refreshCallBack: refreshCallBack, orgList: orgList), ), ); }), ), ///悬停的item SliverPersistentHeader( pinned: true, floating: true, delegate: GSYSliverHeaderDelegate( maxHeight: bottomSize, minHeight: bottomSize, changeSize: true, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), builder: (BuildContext context, double shrinkOffset, bool overlapsContent) { var radius = Radius.circular(10 - shrinkOffset / bottomSize * 10); return SizedBox.expand( child: Padding( padding: const EdgeInsets.only(bottom: 10, left: 0, right: 0), child: OnlyShareInstanceWidget( value: headerProvider, child: UserHeaderBottom(userInfo, radius), ), ), ); }), ), ///提交图表 SliverPersistentHeader( delegate: GSYSliverHeaderDelegate( maxHeight: chartSize, minHeight: chartSize, changeSize: true, vSyncs: this, snapConfig: FloatingHeaderSnapConfiguration( curve: Curves.bounceInOut, duration: const Duration(milliseconds: 10), ), builder: (BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand( child: SizedBox( height: chartSize, child: UserHeaderChart(userInfo), ), ); }), ), ]; } @override Widget build(BuildContext context) { super.build(context);// See AutomaticKeepAliveClientMixin. ///局部 scoped 的 riverpod provider 方案 ///配合 @Riverpod(dependencies: []) return ProviderScope( /// 必要时还可以覆盖 //overrides: [], child: buildContainer(context), ); } ///获取用户仓库前100个star统计数据 getHonor() async { var _ = globalContainer.refresh(headerProvider); } } ================================================ FILE: lib/page/user/person_page.dart ================================================ // ignore_for_file: annotate_overrides import 'dart:async'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/repositories/event_repository.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/common/toast.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/model/user_org.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/page/user/base_person_provider.dart'; import 'package:gsy_github_app_flutter/widget/pull/nested/gsy_nested_pull_load_widget.dart'; import 'package:gsy_github_app_flutter/page/user/base_person_state.dart'; import 'package:gsy_github_app_flutter/widget/gsy_common_option_widget.dart'; import 'package:gsy_github_app_flutter/widget/gsy_title_bar.dart'; /// 个人详情 /// Created by guoshuyu /// Date: 2018-07-18 class PersonPage extends StatefulWidget { static const String sName = "person"; final String? userName; const PersonPage(this.userName, {super.key}); @override PersonState createState() => PersonState(); } class PersonState extends BasePersonState { String beStaredCount = "---"; bool focusStatus = false; String focus = ""; User? userInfo = User.empty(); // ignore: overridden_fields final List orgList = []; PersonState(); ///处理用户信息显示 _resolveUserInfo(res) { if (isShow) { setState(() { userInfo = res.data; }); } } @override Future handleRefresh() async { if (isLoading) { return; } isLoading = true; page = 1; ///获取网络用户数据 var userResult = await UserRepository.getUserInfo(widget.userName, needDb: true); if (userResult != null && userResult.result) { _resolveUserInfo(userResult); if (userResult.next != null) { userResult.next().then((resNext) { _resolveUserInfo(resNext); }); } } else { return; } ///获取用户动态或者组织成员 var res = await _getDataLogic(); resolveRefreshResult(res); resolveDataResult(res); if (res.next != null) { var resNext = await res.next(); resolveRefreshResult(resNext); resolveDataResult(resNext); } isLoading = false; ///获取当前用户的关注状态 _getFocusStatus(); ///获取用户仓库前100个star统计数据 getHonor(); return; } ///获取当前用户的关注状态 _getFocusStatus() async { var focusRes = await UserRepository.checkFollowRequest(widget.userName!); if (isShow) { setState(() { focus = (focusRes != null && focusRes.result) ? context.l10n.user_focus : context.l10n.user_un_focus; focusStatus = (focusRes != null && focusRes.result); }); } } ///获取用户信息里的用户名 _getUserName() { if (userInfo == null) { return User.empty(); } return userInfo!.login; } ///获取用户动态或者组织成员 _getDataLogic() async { if (userInfo!.type == "Organization") { return await UserRepository.getMemberRequest(_getUserName(), page); } getUserOrg(_getUserName()); return await EventRepository.getEventRequest(_getUserName(), page: page, needDb: page <= 1); } @override bool get wantKeepAlive => true; @override requestRefresh() async {} @override requestLoadMore() async { return await _getDataLogic(); } @override bool get isRefreshFirst => true; @override bool get needHeader => false; @override Widget buildContainer(BuildContext context) { return Scaffold( appBar: AppBar( title: GSYTitleBar( (userInfo != null && userInfo!.login != null) ? userInfo!.login : "", rightWidget: GSYCommonOptionWidget( url: userInfo?.html_url, ), )), floatingActionButton: FloatingActionButton( child: AutoSizeText( focus, minFontSize: 8, maxLines: 1, ), onPressed: () { ///非组织成员可以关注 if (focus == '') { return; } if (userInfo!.type == "Organization") { showToast(context.l10n.user_focus_no_support); return; } CommonUtils.showLoadingDialog(context); UserRepository.doFollowRequest(widget.userName!, focusStatus) .then((res) { Navigator.pop(context); _getFocusStatus(); }); }), body: GSYNestedPullLoadWidget( pullLoadWidgetControl, (BuildContext context, int index) => renderItem(index, userInfo!, beStaredCount, null, null, orgList), handleRefresh, onLoadMore, refreshKey: refreshIKey, headerSliverBuilder: (context, innerBoxIsScrolled) { return sliverBuilder(context, innerBoxIsScrolled, userInfo!, null, beStaredCount, null); }, )); } @override FetchHonorDataProvider get headerProvider { return fetchHonorDataProvider(_getUserName()); } } ================================================ FILE: lib/page/user/widget/user_header.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/model/common_list_datatype.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/model/user_org.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/page/user/base_person_provider.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_icon_text.dart'; import 'package:gsy_github_app_flutter/widget/gsy_user_icon_widget.dart'; import 'package:gsy_github_app_flutter/widget/only_share_widget.dart'; /// 用户详情头部 /// Created by guoshuyu /// Date: 2018-07-17 /// /// class UserHeaderItem extends StatelessWidget { final User userInfo; final String beStaredCount; final Color? notifyColor; final Color themeColor; final VoidCallback? refreshCallBack; final List? orgList; const UserHeaderItem(this.userInfo, this.beStaredCount, this.themeColor, {super.key, this.notifyColor, this.refreshCallBack, this.orgList}); ///通知Icon _getNotifyIcon(BuildContext context, Color? color) { if (notifyColor == null) { return Container(); } return RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.only(top: 0.0, right: 5.0, left: 5.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: ClipOval( child: Icon( GSYICons.USER_NOTIFY, color: color, size: 18.0, ), ), onPressed: () { NavigatorUtils.goNotifyPage(context).then((res) { refreshCallBack?.call(); }); }); } ///用户组织 _renderOrgs(BuildContext context, List? orgList) { if (orgList == null || orgList.isEmpty) { return Container(); } List list = []; renderOrgsItem(UserOrg orgs) { return GSYUserIconWidget( padding: const EdgeInsets.only(right: 5.0, left: 5.0), width: 30.0, height: 30.0, image: orgs.avatarUrl ?? GSYICons.DEFAULT_REMOTE_PIC, onPressed: () { NavigatorUtils.goPerson(context, orgs.login); }); } int length = orgList.length > 3 ? 3 : orgList.length; list.add(Text("${context.l10n.user_orgs_title}:", style: GSYConstant.smallSubLightText)); for (int i = 0; i < length; i++) { list.add(renderOrgsItem(orgList[i])); } if (orgList.length > 3) { list.add(RawMaterialButton( onPressed: () { NavigatorUtils.gotoCommonList( context, "${userInfo.login!} ${context.l10n.user_orgs_title}", "org", CommonListDataType.userOrgs, userName: userInfo.login); }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.only(right: 5.0, left: 5.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: const Icon( Icons.more_horiz, color: GSYColors.white, size: 18.0, ))); } return Row(children: list); } _renderImg(BuildContext context) { return RawMaterialButton( onPressed: () { if (userInfo.avatar_url != null) { NavigatorUtils.gotoPhotoViewPage(context, userInfo.avatar_url); } }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(0.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: ClipOval( child: FadeInImage.assetNetwork( placeholder: GSYICons.DEFAULT_USER_ICON, key: (userInfo.avatar_url != null && userInfo.avatar_url!.isNotEmpty) ? ValueKey(userInfo.avatar_url) : null, //预览图 fit: BoxFit.fitWidth, image: (userInfo.avatar_url != null && userInfo.avatar_url!.isNotEmpty) ? userInfo.avatar_url! : "https://github.com/CarGuo/gsy_github_app_flutter/blob/master/logo.png?raw=true", width: 80.0, height: 80.0, ))); } _renderUserInfo(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ ///用户名 Text(userInfo.login ?? "", style: GSYConstant.largeTextWhiteBold), _getNotifyIcon(context, notifyColor), ], ), Text(userInfo.name == null ? "" : userInfo.name!, style: GSYConstant.smallSubLightText), ///用户组织 GSYIConText( GSYICons.USER_ITEM_COMPANY, userInfo.company ?? context.l10n.nothing_now, GSYConstant.smallSubLightText, GSYColors.subLightTextColor, 10.0, padding: 3.0, ), ///用户位置 GSYIConText( GSYICons.USER_ITEM_LOCATION, userInfo.location ?? context.l10n.nothing_now, GSYConstant.smallSubLightText, GSYColors.subLightTextColor, 10.0, padding: 3.0, ), ], ); } _renderBlog(BuildContext context) { return Container( ///用户博客 margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, ///用户博客 child: RawMaterialButton( onPressed: () { if (userInfo.blog != null) { CommonUtils.launchOutURL(userInfo.blog!, context); } }, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(0.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: GSYIConText( GSYICons.USER_ITEM_LINK, userInfo.blog ?? context.l10n.nothing_now, (userInfo.blog == null) ? GSYConstant.smallSubLightText : GSYConstant.smallActionLightText, GSYColors.subLightTextColor, 10.0, padding: 3.0, textWidth: MediaQuery.sizeOf(context).width - 50, ), )); } @override Widget build(BuildContext context) { return GSYCardItem( color: themeColor, elevation: 0, margin: const EdgeInsets.all(0.0), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(0.0), bottomRight: Radius.circular(0.0))), child: Padding( padding: const EdgeInsets.only( left: 10.0, top: 10.0, right: 10.0, bottom: 0.0), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ///用户头像 _renderImg(context), const Padding(padding: EdgeInsets.all(10.0)), Expanded( child: _renderUserInfo(context), ), ], ), _renderBlog(context), ///组织 _renderOrgs(context, orgList), ///用户描述 Container( alignment: Alignment.topLeft, child: Text( userInfo.bio == null ? "" : userInfo.bio!, style: GSYConstant.smallSubLightText, maxLines: 3, overflow: TextOverflow.ellipsis, )), ///用户创建时长 Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text( context.l10n.user_create_at + CommonUtils.getDateStr(userInfo.created_at), style: GSYConstant.smallSubLightText, overflow: TextOverflow.ellipsis, )), const Padding(padding: EdgeInsets.only(bottom: 5.0)), ], ), )); } } class UserHeaderBottom extends StatelessWidget { final User userInfo; final Radius radius; const UserHeaderBottom(this.userInfo, this.radius, {super.key}); ///底部状态栏 _getBottomItem(String? title, var value, onPressed) { String data = value == null ? "" : value.toString(); TextStyle valueStyle = (value != null && value.toString().length > 6) ? GSYConstant.minText : GSYConstant.smallSubLightText; TextStyle titleStyle = (title != null && title.toString().length > 6) ? GSYConstant.minText : GSYConstant.smallSubLightText; return Expanded( child: Center( child: RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.only(top: 5.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), onPressed: onPressed, child: RichText( textAlign: TextAlign.center, text: TextSpan( children: [ TextSpan(text: title, style: titleStyle), TextSpan(text: "\n", style: valueStyle), TextSpan(text: data, style: valueStyle) ], ), ))), ); } @override Widget build(BuildContext context) { ///用户底部状态 return GSYCardItem( color: Theme.of(context).primaryColor, margin: const EdgeInsets.all(0.0), shape: RoundedRectangleBorder( borderRadius: BorderRadius.only(bottomLeft: radius, bottomRight: radius)), child: Container( alignment: Alignment.center, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ _getBottomItem( context.l10n.user_tab_repos, userInfo.public_repos, () { NavigatorUtils.gotoCommonList(context, userInfo.login, "repository", CommonListDataType.userRepos, userName: userInfo.login); }, ), Container( width: 0.3, height: 40.0, alignment: Alignment.center, color: GSYColors.subLightTextColor), _getBottomItem( context.l10n.user_tab_fans, userInfo.followers, () { NavigatorUtils.gotoCommonList(context, userInfo.login, "user", CommonListDataType.follower, userName: userInfo.login); }, ), Container( width: 0.3, height: 40.0, alignment: Alignment.center, color: GSYColors.subLightTextColor), _getBottomItem( context.l10n.user_tab_focus, userInfo.following, () { NavigatorUtils.gotoCommonList(context, userInfo.login, "user", CommonListDataType.followed, userName: userInfo.login); }, ), Container( width: 0.3, height: 40.0, alignment: Alignment.center, color: GSYColors.subLightTextColor), _getBottomItem( context.l10n.user_tab_star, userInfo.starred, () { NavigatorUtils.gotoCommonList(context, userInfo.login, "repository", CommonListDataType.userStar, userName: userInfo.login); }, ), Container( width: 0.3, height: 40.0, alignment: Alignment.center, color: GSYColors.subLightTextColor), Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { var data = ref.watch( OnlyShareInstanceWidget.of(context)!); return _getBottomItem( context.l10n.user_tab_honor, switch (data) { AsyncData(:final value) => value?.beStaredCount.toString() ?? "---", AsyncError() => "----", _ => "---", }, () { var list = data.when( data: (result) { return result?.honorList; }, error: (_, __) => null, loading: () => null); if (list != null && list.isNotEmpty) { NavigatorUtils.goHonorListPage(context, list); } }, ); }) // _getBottomItem(context.l10n.user_tab_honor, // honorModel?.beStaredCount, () { // if (honorModel?.honorList != null) { // NavigatorUtils.goHonorListPage(context, honorModel?.honorList); // } // }), ], ), ), ); } } class UserHeaderChart extends StatelessWidget { final User userInfo; const UserHeaderChart(this.userInfo, {super.key}); _renderChart(BuildContext context) { double height = 140.0; double width = 3 * MediaQuery.sizeOf(context).width / 2; if (userInfo.login != null && userInfo.type == "Organization") { return Container(); } return (userInfo.login != null) ? Card( margin: const EdgeInsets.only( top: 0.0, left: 10.0, right: 10.0, bottom: 0.0), color: GSYColors.white, child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Container( padding: const EdgeInsets.only(left: 10.0, right: 10.0), width: width, height: height, ///svg chart child: SvgPicture.network( CommonUtils.getUserChartAddress(userInfo.login!), width: width, height: height - 10, allowDrawingOutsideViewBox: true, placeholderBuilder: (BuildContext context) => SizedBox( height: height, width: width, child: Center( child: SpinKitRipple(color: Theme.of(context).primaryColor), ), ), ), ), ), ) : SizedBox( height: height, child: Center( child: SpinKitRipple(color: Theme.of(context).primaryColor), ), ); } @override Widget build(BuildContext context) { return Column( children: [ Container( margin: const EdgeInsets.only(top: 15.0, bottom: 15.0, left: 12.0), alignment: Alignment.topLeft, child: Text( (userInfo.type == "Organization") ? context.l10n.user_dynamic_group : context.l10n.user_dynamic_title, style: GSYConstant.normalTextBold, overflow: TextOverflow.ellipsis, )), _renderChart(context), ], ); } } ================================================ FILE: lib/page/user/widget/user_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/model/search_user_ql.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/model/user_org.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; /// 用户item /// Created by guoshuyu /// Date: 2018-07-23 class UserItem extends StatelessWidget { final UserItemViewModel userItemViewModel; final VoidCallback? onPressed; final bool needImage; const UserItem(this.userItemViewModel, {super.key, this.onPressed, this.needImage = true}); @override Widget build(BuildContext context) { var me = StoreProvider.of(context).state.userInfo!; Widget userImage = IconButton( padding: const EdgeInsets.only( top: 0.0, left: 0.0, bottom: 0.0, right: 10.0), icon: ClipOval( child: FadeInImage.assetNetwork( placeholder: GSYICons.DEFAULT_USER_ICON, key: (userItemViewModel.userPic != null && userItemViewModel.userPic!.isNotEmpty) ? ValueKey(userItemViewModel.userPic) : null, //预览图 fit: BoxFit.fitWidth, image: (userItemViewModel.userPic != null && userItemViewModel.userPic!.isNotEmpty) ? userItemViewModel.userPic! : "https://github.com/CarGuo/gsy_github_app_flutter/blob/master/logo.png?raw=true", width: 40.0, height: 40.0, ), ), onPressed: null); return GSYCardItem( color: me.login == userItemViewModel.login ? Colors.amber : (userItemViewModel.login == "CarGuo") ? Colors.pink : Colors.white, child: TextButton( onPressed: onPressed, child: Padding( padding: const EdgeInsets.only( left: 0.0, top: 5.0, right: 0.0, bottom: 10.0), child: Row( children: [ if (userItemViewModel.index != null) Padding( padding: const EdgeInsets.only(right: 10), child: Text(userItemViewModel.index!, style: GSYConstant.middleSubTextBold), ), userImage, Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, children: [ Row( children: [ Text(userItemViewModel.userName ?? "null", style: GSYConstant.smallTextBold), if (userItemViewModel.followers != null) Expanded( child: Align( alignment: Alignment.centerRight, child: Text( "followers: ${userItemViewModel.followers}", style: GSYConstant.smallSubText), ), ), ], ), if (userItemViewModel.bio != null && userItemViewModel.bio!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 5), child: Text(userItemViewModel.bio!, maxLines: 2, overflow: TextOverflow.ellipsis, style: GSYConstant.smallText), ), if (userItemViewModel.lang != null) Padding( padding: const EdgeInsets.only(top: 5, right: 10), child: Text(userItemViewModel.lang!, maxLines: 2, overflow: TextOverflow.ellipsis, style: GSYConstant.smallSubText), ), ], ), ), ], ), ), ), ); } } class UserItemViewModel { String? userPic; String? userName; String? bio; int? followers; String? login; String? lang; String? index; UserItemViewModel.fromMap(User user) { userName = user.login; userPic = user.avatar_url; followers = user.followers; } UserItemViewModel.fromQL(SearchUserQL user, int? index) { userName = user.name; userPic = user.avatarUrl; followers = user.followers; bio = user.bio; login = user.login; lang = user.lang; this.index = index.toString(); } UserItemViewModel.fromOrgMap(UserOrg org) { userName = org.login; userPic = org.avatarUrl; } } ================================================ FILE: lib/page/user_profile_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:redux/redux.dart'; const String user_profile_name = "名字"; const String user_profile_email = "邮箱"; const String user_profile_link = "链接"; const String user_profile_org = "公司"; const String user_profile_location = "位置"; const String user_profile_info = "简介"; /// 用户信息中心 /// Created by guoshuyu /// Date: 2018-08-08 class UserProfileInfo extends StatefulWidget { const UserProfileInfo({super.key}); @override _UserProfileState createState() => _UserProfileState(); } class _UserProfileState extends State { _renderItem( IconData leftIcon, String title, String value, VoidCallback onPressed) { return GSYCardItem( child: RawMaterialButton( onPressed: onPressed, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.all(15.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), child: Row( children: [ Icon(leftIcon), Container( width: 10.0, ), Text(title, style: GSYConstant.normalSubText), Container( width: 10.0, ), Expanded(child: Text(value, style: GSYConstant.normalText)), Container( width: 10.0, ), const Icon(GSYICons.REPOS_ITEM_NEXT, size: 12.0), ], ), ), ); } _showEditDialog(String title, String? value, String key, Store store) { String content = value ?? ""; CommonUtils.showEditDialog(context, title, (title) {}, (res) { content = res; }, () { if (content.isEmpty) { return; } CommonUtils.showLoadingDialog(context); UserRepository.updateUserRequest({key: content}, store).then((res) { Navigator.of(context).pop(); if (res != null && res.result) { Navigator.of(context).pop(); } }); }, titleController: TextEditingController(), valueController: TextEditingController(text: value), needTitle: false); } List _renderList(User userInfo, Store store) { return [ _renderItem( Icons.info, context.l10n.user_profile_name, userInfo.name ?? "---", () { _showEditDialog( context.l10n.user_profile_name, userInfo.name, "name", store); }), _renderItem( Icons.email, context.l10n.user_profile_email, userInfo.email ?? "---", () { _showEditDialog( context.l10n.user_profile_email, userInfo.email, "email", store); }), _renderItem( Icons.link, context.l10n.user_profile_link, userInfo.blog ?? "---", () { _showEditDialog( context.l10n.user_profile_link, userInfo.blog, "blog", store); }), _renderItem( Icons.group, context.l10n.user_profile_org, userInfo.company ?? "---", () { _showEditDialog( context.l10n.user_profile_org, userInfo.company, "company", store); }), _renderItem(Icons.location_on, context.l10n.user_profile_location, userInfo.location ?? "---", () { _showEditDialog(context.l10n.user_profile_location, userInfo.location, "location", store); }), _renderItem( Icons.message, context.l10n.user_profile_info, userInfo.bio ?? "---", () { _showEditDialog( context.l10n.user_profile_info, userInfo.bio, "bio", store); }), ]; } @override Widget build(BuildContext context) { return StoreBuilder(builder: (context, store) { return Scaffold( appBar: AppBar( title: Hero( tag: "home_user_info", child: Material( color: Colors.transparent, child: Text( context.l10n.home_user_info, style: GSYConstant.normalTextWhite, )))), body: Container( color: GSYColors.white, child: SingleChildScrollView( child: Column( children: _renderList(store.state.userInfo!, store), ), ), ), ); }); } } ================================================ FILE: lib/page/welcome_page.dart ================================================ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart' show ConsumerState, ConsumerStatefulWidget; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/widget/diff_scale_text.dart'; import 'package:gsy_github_app_flutter/widget/mole_widget.dart'; import 'package:redux/redux.dart'; import 'package:rive/rive.dart' as rive; /// 欢迎页 /// Created by guoshuyu /// Date: 2018-07-16 class WelcomePage extends ConsumerStatefulWidget { static const String sName = "/"; const WelcomePage({super.key}); @override _WelcomePageState createState() => _WelcomePageState(); } class _WelcomePageState extends ConsumerState { bool hadInit = false; String text = ""; double fontSize = 76; @override void didChangeDependencies() { super.didChangeDependencies(); if (hadInit) { return; } hadInit = true; ///防止多次进入 Store store = StoreProvider.of(context); Future.delayed(const Duration(milliseconds: 500), () { setState(() { text = "Welcome"; fontSize = 60; }); }); Future.delayed(const Duration(seconds: 1, milliseconds: 500), () { setState(() { text = "GSYGithubApp"; fontSize = 60; }); }); Future.delayed(const Duration(seconds: 3, milliseconds: 500), () { UserRepository.initUserInfo(store, ref).then((res) { if (res != null && res.result) { NavigatorUtils.goHome(context); } else { NavigatorUtils.goLogin(context); } return true; }); }); } @override Widget build(BuildContext context) { return StoreBuilder( builder: (context, store) { double size = 200; return Material( child: Container( color: GSYColors.white, child: Stack( children: [ const Center( child: Image(image: AssetImage('static/images/welcome.png')), ), Align( alignment: const Alignment(0.0, 0.3), child: DiffScaleText( text: text, textStyle: const TextStyle(fontFamily: 'Akronim').copyWith( color: GSYColors.primaryDarkValue, fontSize: fontSize, ), ), ), const Align( alignment: Alignment(0.0, 0.8), child: Mole(), ), Align( alignment: const Alignment(0.0, .9), child: SizedBox( width: size, height: size, child: rive.RiveAnimation.asset( 'static/file/launch.riv', animations: const ["lookUp"], onInit: (arb) { var controller = rive.StateMachineController.fromArtboard( arb, "birb"); var smi = controller?.findInput("dance"); arb.addController(controller!); smi?.value == true; }, ), ), ) ], ), ), ); }, ); } } ================================================ FILE: lib/provider/app_state_provider.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/local/local_storage.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_state_provider.g.dart'; final globalContainer = ProviderContainer(); final appStateProvider = Provider<(bool, Locale, ThemeData)>((ref) { final var1 = ref.watch(appGrepStateProvider); final var2 = ref.watch(appLocalStateProvider); final var3 = ref.watch(appThemeStateProvider); return (var1, var2, var3); }); @riverpod class AppVibrationState extends _$AppVibrationState { @override bool build() { ref.keepAlive(); return true; } void changeVibration(bool enable, {bool save = true}) { state = enable; if (save) { LocalStorage.save(Config.VIBRATION_ENABLE, enable.toString()); } } } ColorFilter greyscale = const ColorFilter.matrix([ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]); /// 控制 App 灰度效果 @riverpod class AppGrepState extends _$AppGrepState { @override bool build() => false; void changeGrey() { state = !state; } } /// 控制 App 语言 @riverpod class AppLocalState extends _$AppLocalState { @override Locale build() { return WidgetsBinding.instance.platformDispatcher.locale; } _getLocale(int? index) { switch (index) { case 1: return const Locale('zh', 'CH'); case 2: return const Locale('en', 'US'); case 3: return const Locale('ko', 'KR'); case 4: return const Locale('ja', 'JP'); default: return WidgetsBinding.instance.platformDispatcher.locale; } } void changeLocale(String? index) { int? localeIndex = (index != null && index.isNotEmpty) ? int.parse(index) : null; state = _getLocale(localeIndex); } } /// 控制 App 主题 @riverpod class AppThemeState extends _$AppThemeState { @override ThemeData build() { return CommonUtils.getThemeData(GSYColors.primarySwatch); } void pushTheme(String? index) { int? localeIndex = (index != null && index.isNotEmpty) ? int.parse(index) : null; if (localeIndex != null) { List colors = CommonUtils.getThemeListColor(); state = _getThemeData(colors[localeIndex]); } } ThemeData _getThemeData(Color color) { return ThemeData( useMaterial3: false, ///用来适配 Theme.of(context).primaryColorLight 和 primaryColorDark 的颜色变化,不设置可能会是默认蓝色 primarySwatch: color as MaterialColor, /// Card 在 M3 下,会有 apply Overlay colorScheme: ColorScheme.fromSeed( seedColor: color, primary: color, brightness: Brightness.light, ///影响 card 的表色,因为 M3 下是 applySurfaceTint ,在 Material 里 surfaceTint: Colors.transparent, ), /// 受到 iconThemeData.isConcrete 的印象,需要全参数才不会进入 fallback iconTheme: const IconThemeData( size: 24.0, fill: 0.0, weight: 400.0, grade: 0.0, opticalSize: 48.0, color: Colors.white, opacity: 0.8, ), ///修改 FloatingActionButton的默认主题行为 floatingActionButtonTheme: FloatingActionButtonThemeData( foregroundColor: Colors.white, backgroundColor: color, shape: const CircleBorder(), ), appBarTheme: AppBarTheme( iconTheme: const IconThemeData(color: Colors.white, size: 24.0), backgroundColor: color, titleTextStyle: Typography.dense2021.titleLarge, systemOverlayStyle: SystemUiOverlayStyle.light, ), // 如果需要去除对应的水波纹效果 // splashFactory: NoSplash.splashFactory, // textButtonTheme: TextButtonThemeData( // style: ButtonStyle(splashFactory: NoSplash.splashFactory), // ), // elevatedButtonTheme: ElevatedButtonThemeData( // style: ButtonStyle(splashFactory: NoSplash.splashFactory), // ), ); } } ================================================ FILE: lib/provider/app_state_provider.g.dart ================================================ // GENERATED CODE - DO NOT MODIFY BY HAND part of 'app_state_provider.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning @ProviderFor(AppVibrationState) const appVibrationStateProvider = AppVibrationStateProvider._(); final class AppVibrationStateProvider extends $NotifierProvider { const AppVibrationStateProvider._() : super( from: null, argument: null, retry: null, name: r'appVibrationStateProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$appVibrationStateHash(); @$internal @override AppVibrationState create() => AppVibrationState(); /// {@macro riverpod.override_with_value} Override overrideWithValue(bool value) { return $ProviderOverride( origin: this, providerOverride: $SyncValueProvider(value), ); } } String _$appVibrationStateHash() => r'85e7e422a4e3b34dfe1e67f7aa562cf40340fa69'; abstract class _$AppVibrationState extends $Notifier { bool build(); @$mustCallSuper @override void runBuild() { final created = build(); final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement< AnyNotifier, bool, Object?, Object? >; element.handleValue(ref, created); } } /// 控制 App 灰度效果 @ProviderFor(AppGrepState) const appGrepStateProvider = AppGrepStateProvider._(); /// 控制 App 灰度效果 final class AppGrepStateProvider extends $NotifierProvider { /// 控制 App 灰度效果 const AppGrepStateProvider._() : super( from: null, argument: null, retry: null, name: r'appGrepStateProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$appGrepStateHash(); @$internal @override AppGrepState create() => AppGrepState(); /// {@macro riverpod.override_with_value} Override overrideWithValue(bool value) { return $ProviderOverride( origin: this, providerOverride: $SyncValueProvider(value), ); } } String _$appGrepStateHash() => r'2d597c1ee2158b81668c77d0c4c4773dae175e41'; /// 控制 App 灰度效果 abstract class _$AppGrepState extends $Notifier { bool build(); @$mustCallSuper @override void runBuild() { final created = build(); final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement< AnyNotifier, bool, Object?, Object? >; element.handleValue(ref, created); } } /// 控制 App 语言 @ProviderFor(AppLocalState) const appLocalStateProvider = AppLocalStateProvider._(); /// 控制 App 语言 final class AppLocalStateProvider extends $NotifierProvider { /// 控制 App 语言 const AppLocalStateProvider._() : super( from: null, argument: null, retry: null, name: r'appLocalStateProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$appLocalStateHash(); @$internal @override AppLocalState create() => AppLocalState(); /// {@macro riverpod.override_with_value} Override overrideWithValue(Locale value) { return $ProviderOverride( origin: this, providerOverride: $SyncValueProvider(value), ); } } String _$appLocalStateHash() => r'094022d96deb55273c2bc53466ad2bf5ee8bdce0'; /// 控制 App 语言 abstract class _$AppLocalState extends $Notifier { Locale build(); @$mustCallSuper @override void runBuild() { final created = build(); final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement< AnyNotifier, Locale, Object?, Object? >; element.handleValue(ref, created); } } /// 控制 App 主题 @ProviderFor(AppThemeState) const appThemeStateProvider = AppThemeStateProvider._(); /// 控制 App 主题 final class AppThemeStateProvider extends $NotifierProvider { /// 控制 App 主题 const AppThemeStateProvider._() : super( from: null, argument: null, retry: null, name: r'appThemeStateProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override String debugGetCreateSourceHash() => _$appThemeStateHash(); @$internal @override AppThemeState create() => AppThemeState(); /// {@macro riverpod.override_with_value} Override overrideWithValue(ThemeData value) { return $ProviderOverride( origin: this, providerOverride: $SyncValueProvider(value), ); } } String _$appThemeStateHash() => r'a02ca99fb2b47827f007b77c8d1d371cb171b17e'; /// 控制 App 主题 abstract class _$AppThemeState extends $Notifier { ThemeData build(); @$mustCallSuper @override void runBuild() { final created = build(); final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement< AnyNotifier, ThemeData, Object?, Object? >; element.handleValue(ref, created); } } ================================================ FILE: lib/redux/gsy_state.dart ================================================ // ignore_for_file: implicit_call_tearoffs import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/redux/login_redux.dart'; import 'package:gsy_github_app_flutter/redux/user_redux.dart'; import 'package:redux/redux.dart'; import 'middleware/epic_middleware.dart'; /** * Redux全局State * Created by guoshuyu * Date: 2018-07-16 */ ///全局Redux store 的对象,保存State数据 class GSYState { ///用户信息 User? userInfo; ///是否登录 bool? login; ///构造方法 GSYState( {this.userInfo, this.login,}); } ///创建 Reducer ///源码中 Reducer 是一个方法 typedef State Reducer(State state, dynamic action); ///我们自定义了 appReducer 用于创建 store GSYState appReducer(GSYState state, action) { return GSYState( ///通过 UserReducer 将 GSYState 内的 userInfo 和 action 关联在一起 userInfo: UserReducer(state.userInfo, action), login: LoginReducer(state.login, action), ); } final List> middleware = [ EpicMiddleware(loginEpic), EpicMiddleware(userInfoEpic), EpicMiddleware(oauthEpic), UserInfoMiddleware(), LoginMiddleware(), ]; ================================================ FILE: lib/redux/login_redux.dart ================================================ // ignore_for_file: implicit_call_tearoffs, use_build_context_synchronously import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/db/sql_manager.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:redux/redux.dart'; import 'package:rxdart/rxdart.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'middleware/epic_store.dart'; /// 登录相关Redux /// Created by guoshuyu /// Date: 2018-07-16 /// redux 的 combineReducers, 通过 TypedReducer 将 LoginSuccessAction、 LogoutAction 与 reducers 关联起来 final LoginReducer = combineReducers([ TypedReducer(_loginResult) , TypedReducer(_logoutResult), ]); /// 如果有 LoginSuccessAction 发起一个请求时 /// 就会调用到 _loginResult /// _loginResult 这里接返回结果的同时进行跳转 bool? _loginResult(bool? result, LoginSuccessAction action) { if (action.success == true) { NavigatorUtils.goHome(action.context); } return action.success; } bool? _logoutResult(bool? result, LogoutAction action) { return true; } ///定一个 LoginSuccessAction ,用于发起 登陆成功后 的改变 ///类名随你喜欢定义,只要通过上面TypedReducer 绑定就好 class LoginSuccessAction { final BuildContext context; final bool success; LoginSuccessAction(this.context, this.success); } class LogoutAction { final BuildContext context; LogoutAction(this.context); } class LoginAction { final BuildContext context; final String? username; final String? password; LoginAction(this.context, this.username, this.password); } class OAuthAction { final BuildContext context; final String code; OAuthAction(this.context, this.code); } ///中间过程处理 class LoginMiddleware implements MiddlewareClass { @override void call(Store store, dynamic action, NextDispatcher next) { if (action is LogoutAction) { UserRepository.clearAll(store); WebViewCookieManager().clearCookies(); SqlManager.close(); NavigatorUtils.goLogin(action.context); } // Make sure to forward actions to the next middleware in the chain! next(action); } } ///中间过程处理 Stream loginEpic(Stream actions, EpicStore store) { Stream loginIn( LoginAction action, EpicStore store) async* { CommonUtils.showLoadingDialog(action.context); var nv = Navigator.of(action.context); var res = await UserRepository.login( action.username!.trim(), action.password!.trim(), store); nv.pop(action); yield LoginSuccessAction(action.context, (res != null && res.result)); } return actions .whereType() .switchMap((action) => loginIn(action, store)); } ///中间过程处理 Stream oauthEpic(Stream actions, EpicStore store) { Stream loginIn( OAuthAction action, EpicStore store) async* { CommonUtils.showLoadingDialog(action.context); var res = await UserRepository.oauth(action.code, store); Navigator.pop(action.context); yield LoginSuccessAction(action.context, (res != null && res.result)); } return actions .whereType() .switchMap((action) => loginIn(action, store)); } ================================================ FILE: lib/redux/middleware/combine_epics.dart ================================================ import 'dart:async'; import 'package:rxdart/streams.dart'; import 'epic.dart'; import 'epic_store.dart'; /// Combines a list of [Epic]s into one. /// /// Rather than having one massive [Epic] that handles every possible type of /// action, it's best to break [Epic]s down into smaller, more manageable and /// testable units. This way we could have a `searchEpic`, a `chatEpic`, /// and an `updateProfileEpic`, for example. /// /// However, the [EpicMiddleware] accepts only one [Epic]. So what are we to do? /// Fear not: redux_epics includes class for combining [Epic]s together! /// /// Example: /// /// final epic = combineEpics([ /// searchEpic, /// chatEpic, /// updateProfileEpic, /// ]); Epic combineEpics(List> epics) { return (Stream actions, EpicStore store) { return MergeStream( epics.map((epic) => epic(actions, store)).toList()); }; } ================================================ FILE: lib/redux/middleware/epic.dart ================================================ import 'dart:async'; import 'epic_store.dart'; /// A function that transforms one stream of actions into another /// stream of actions. /// /// Actions in, actions out. /// /// The best part: Epics are based on Dart Streams. This makes routine tasks /// easy, and complex tasks such as asynchronous error handling, cancellation, /// and debouncing a breeze. Once you're inside your Epic, use any stream /// patterns you desire as long as anything output from the final, returned /// stream, is an action. The actions you emit will be immediately dispatched /// through the rest of the middleware chain. /// /// Epics run alongside the normal Redux dispatch channel, meaning you cannot /// accidentally "swallow" an incoming action. Actions always run through the /// rest of your middleware chain to your reducers before your Epics even /// receive the next action. /// /// Note: Since the Actions you emit from your Epics are dispatched to your /// store, writing an Epic that simply returns the original actions Stream will /// result in an infinite loop. Do not do this! /// /// ## Example /// /// Let's say your app has a search box. When a user submits a search term, /// you dispatch a `PerformSearchAction` which contains the term. In order to /// actually listen for the `PerformSearchAction` and make a network request /// for the results, we can create an Epic! /// /// In this instance, our Epic will need to filter all incoming actions it /// receives to only the `Action` it is interested in: the `PerformSearchAction`. /// Then, we need to make a network request using the provided search term. /// Finally, we need to transform those results into an action that contains /// the search results. If an error has occurred, we'll want to return an /// error action so our app can respond accordingly. /// /// ### Code /// /// Stream exampleEpic( /// Stream actions, /// EpicStore store, /// ) { /// return actions /// .where((action) => action is PerformSearchAction) /// .asyncMap((action) => /// // Pseudo api that returns a Future of SearchResults /// api.search((action as PerformSearch).searchTerm) /// .then((results) => new SearchResultsAction(results)) /// .catchError((error) => new SearchErrorAction(error))); /// } typedef Epic = Stream Function( Stream actions, EpicStore store, ); /// A class that acts as an [Epic], transforming one stream of actions into /// another stream of actions. Generally, [Epic] functions are simpler, but /// you may have advanced use cases that require a type-safe class. /// /// ### Example /// /// class ExampleEpic extends EpicClass { /// @override /// Stream call(Stream actions, EpicStore store) { /// return actions /// .where((action) => action is PerformSearchAction) /// .asyncMap((action) => /// // Pseudo api that returns a Future of SearchResults /// api.search((action as PerformSearch).searchTerm) /// .then((results) => new SearchResultsAction(results)) /// .catchError((error) => new SearchErrorAction(error))); /// } /// } abstract class EpicClass { Stream call( Stream actions, EpicStore store, ); } /// An wrapper that allows you to create Epics which handle actions of a /// specific type, rather than all actions. /// /// ### Example /// /// Stream searchEpic( /// // Note: this epic only works with PerformSearchActions /// Stream actions, /// EpicStore store, /// ) { /// return actions /// .asyncMap((action) => /// api.search(action.searchTerm) /// .then((results) => new SearchResultsAction(results)) /// .catchError((error) => new SearchErrorAction(error))); /// } /// /// final epic = new TypedEpic(typedSearchEpic); /// /// ### Combining Typed Epics /// /// final epic = combineEpics([ /// new TypedEpic(searchEpic), /// new TypedEpic(profileEpic), /// new TypedEpic(chatEpic), /// ]); class TypedEpic extends EpicClass { final Stream Function( Stream actions, EpicStore store, ) epic; TypedEpic(this.epic); @override Stream call(Stream actions, EpicStore store) { return epic( actions.transform(StreamTransformer.fromHandlers( handleData: (dynamic action, EventSink sink) { if (action is Action) { sink.add(action); } }, )), store, ); } } ================================================ FILE: lib/redux/middleware/epic_middleware.dart ================================================ import 'dart:async'; import 'package:redux/redux.dart'; import 'package:rxdart/transformers.dart'; import 'epic.dart'; import 'epic_store.dart'; /// A [Redux](https://pub.dartlang.org/packages/redux) middleware that passes /// a stream of dispatched actions to the given [Epic]. /// /// It is recommended that you put your `EpicMiddleware` first when constructing /// the list of middleware for your store so any actions dispatched from /// your [Epic] will be intercepted by the remaining Middleware. /// /// Example: /// /// var epicMiddleware = new EpicMiddleware(new ExampleEpic()); /// var store = new Store, Action>(reducer, /// initialState: [], middleware: [epicMiddleware]); class EpicMiddleware extends MiddlewareClass { final StreamController _actions = StreamController.broadcast(); final StreamController> _epics = StreamController.broadcast(sync: true); final bool supportAsyncGenerators; Epic _epic; bool _isSubscribed = false; EpicMiddleware(Epic epic, {this.supportAsyncGenerators = true}) : _epic = epic; @override void call(Store store, dynamic action, NextDispatcher next) { if (!_isSubscribed) { _epics.stream .switchMap((epic) => epic(_actions.stream, EpicStore(store))) .listen(store.dispatch); _epics.add(_epic); _isSubscribed = true; } next(action); if (supportAsyncGenerators) { // Future.delayed is an ugly hack to support async* functions. // // See: https://github.com/dart-lang/sdk/issues/33818 Future.delayed(Duration.zero, () { _actions.add(action); }); } else { _actions.add(action); } } /// Gets or replaces the epic currently used by the middleware. /// /// Replacing epics is considered an advanced API. You might need this if your /// app grows large and want to instantiate Epics on the fly, rather than /// as a whole up front. Epic get epic => _epic; set epic(Epic newEpic) { _epic = newEpic; _epics.add(newEpic); } } ================================================ FILE: lib/redux/middleware/epic_store.dart ================================================ import 'dart:async'; import 'package:redux/redux.dart'; /// A stripped-down Redux [Store]. Removes unsupported [Store] methods. /// /// Due to the way streams are implemented with Dart, it's impossible to /// perform `store.dispatch` from within an [Epic] or observe the store directly. class EpicStore { final Store _store; EpicStore(this._store); /// Returns the current state of the redux store State get state => _store.state; Stream get onChange => _store.onChange; /// through to the reducer. dynamic dispatch(dynamic action) { return _store.dispatch(action); } } ================================================ FILE: lib/redux/user_redux.dart ================================================ // ignore_for_file: implicit_call_tearoffs import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/repositories/user_repository.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:redux/redux.dart'; import 'package:rxdart/rxdart.dart'; import 'middleware/epic_store.dart'; /** * 用户相关Redux * Created by guoshuyu * Date: 2018-07-16 */ /// redux 的 combineReducers, 通过 TypedReducer 将 UpdateUserAction 与 reducers 关联起来 final UserReducer = combineReducers([ TypedReducer(_updateLoaded), ]); /// 如果有 UpdateUserAction 发起一个请求时 /// 就会调用到 _updateLoaded /// _updateLoaded 这里接受一个新的userInfo,并返回 User? _updateLoaded(User? user, action) { user = action.userInfo; return user; } ///定一个 UpdateUserAction ,用于发起 userInfo 的的改变 ///类名随你喜欢定义,只要通过上面TypedReducer绑定就好 class UpdateUserAction { final User? userInfo; UpdateUserAction(this.userInfo); } class FetchUserAction {} class UserInfoMiddleware implements MiddlewareClass { @override void call(Store store, dynamic action, NextDispatcher next) { if (action is UpdateUserAction) { printLog("*********** UserInfoMiddleware *********** "); } // Make sure to forward actions to the next middleware in the chain! next(action); } } Stream userInfoEpic( Stream actions, EpicStore store) { // Use the async* function to make easier Stream loadUserInfo() async* { printLog("*********** userInfoEpic _loadUserInfo ***********"); var res = await UserRepository.getUserInfo(null); yield UpdateUserAction(res.data); } return actions // to UpdateUserAction actions .whereType() // Don't start until the 10ms .debounce(((_) => TimerStream(true, const Duration(milliseconds: 10)))) .switchMap((action) => loadUserInfo()); } ================================================ FILE: lib/test/demo_app.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/test/demo_page.dart'; class DemoApp extends StatelessWidget { const DemoApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(home: DemoPage()); } } ================================================ FILE: lib/test/demo_appbar.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class ImageAppbar extends StatelessWidget implements PreferredSizeWidget { final Widget? leading; final bool needLeading; final double leadingWidth; final List? actions; final IconData backBtnIconData; final VoidCallback? backPress; final Color iconColor; final Color textColor; final Widget? title; const ImageAppbar( {super.key, this.leading, this.actions, this.needLeading = true, this.title, this.backBtnIconData = Icons.arrow_back_ios, this.backPress, this.leadingWidth = 56.0, this.iconColor = Colors.white, this.textColor = Colors.white}); @override Widget build(BuildContext context) { var leading = this.leading; leading ??= needLeading ? IconButton( highlightColor: Colors.transparent, icon: Icon( backBtnIconData, color: iconColor, ), color: iconColor, onPressed: () { if (backPress != null) { backPress!(); return; } Navigator.maybePop(context); }, ) : Container(); leading = ConstrainedBox( constraints: BoxConstraints.tightFor(width: leadingWidth), child: leading, ); TextStyle? centerStyle = Theme.of(context).textTheme.titleLarge ?? Theme.of(context).appBarTheme.titleTextStyle ?? Theme.of(context).primaryTextTheme.titleLarge; Widget? title = this.title; if (title != null) { title = DefaultTextStyle( style: centerStyle!, softWrap: false, overflow: TextOverflow.ellipsis, child: title, ); } var content = Stack( children: [ Container( decoration: const BoxDecoration( image: DecorationImage( image: AssetImage("static/images/logo.png"), fit: BoxFit.cover), ), ), SafeArea( child: Container( alignment: Alignment.centerLeft, child: leading, ), ), SafeArea( child: Row( children: [ Expanded( child: Center( child: title ?? Container(), )), ], ), ), SafeArea( child: Row( children: [ Expanded(child: Container()), Row( children: actions ?? [], ) ], ), ) ], ); return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: content, ); } @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } ================================================ FILE: lib/test/demo_bloc_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; import 'package:gsy_github_app_flutter/redux/login_redux.dart'; import 'package:shared_preferences/shared_preferences.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State with LoginBLoC { @override Widget build(BuildContext context) { ///共享 store return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { /// 触摸收起键盘 FocusScope.of(context).requestFocus(FocusNode()); }, child: Scaffold( ///使用主题颜色做背景 body: Container( color: Theme.of(context).primaryColor, child: Center( ///同时弹出键盘不遮挡 child: SingleChildScrollView( ///显示卡片 child: Card( elevation: 5.0, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0))), color: Colors.white, margin: const EdgeInsets.only(left: 30.0, right: 30.0), child: Padding( padding: const EdgeInsets.only( left: 30.0, top: 40.0, right: 30.0, bottom: 0.0), ///内容 child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ const Padding(padding: EdgeInsets.all(10.0)), ///用户名输入框 TextField( controller: userController, decoration: const InputDecoration( hintText: "请输入用户名", icon: Icon(Icons.person), ), ), const Padding(padding: EdgeInsets.all(30.0)), ///密码输入框 TextField( controller: pwController, obscureText: true, decoration: const InputDecoration( hintText: "请输入密码", icon: Icon(Icons.person)), ), const Padding(padding: EdgeInsets.all(15.0)), ///登陆按键 LayoutBuilder( builder: (context, constraints) { return SizedBox( height: 40, width: constraints.maxWidth, child: TextButton( style: TextButton.styleFrom( textStyle: const TextStyle( color: Colors.white, ), backgroundColor: Theme.of(context).primaryColor), onPressed: (){ loginIn(context); }, child: const Text("登陆", style: TextStyle(fontSize: 14), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis)), ); }, ), const Padding(padding: EdgeInsets.all(15.0)), ], ), ), ), ), ), ), ), ); } } mixin LoginBLoC on State { final TextEditingController userController = TextEditingController(); final TextEditingController pwController = TextEditingController(); String? _userName = ""; String? _password = ""; @override void initState() { super.initState(); _initState(); userController.addListener(_usernameChange); pwController.addListener(_passwordChange); } @override void dispose() { super.dispose(); userController.removeListener(_usernameChange); pwController.removeListener(_passwordChange); } _usernameChange() { _userName = userController.text; } _passwordChange() { _password = pwController.text; } _initState() async { SharedPreferences prefs = await SharedPreferences.getInstance(); _userName = await (prefs.get("username") as Future); _password = await (prefs.get("password") as Future); userController.value = TextEditingValue(text: _userName ?? ""); pwController.value = TextEditingValue(text: _password ?? ""); } loginIn(BuildContext ctx) async { if (_userName == null || _userName!.isEmpty) { return; } if (_password == null || _password!.isEmpty) { return; } SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString("username", _userName!); await prefs.setString("password", _password!); if(ctx.mounted) { ///通过 redux 去执行登陆流程 StoreProvider.of(ctx) .dispatch(LoginAction(ctx, _userName, _password)); } } } ================================================ FILE: lib/test/demo_db.dart ================================================ import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:sqflite/sqflite.dart'; /** * Created by guoshuyu * Date: 2018-08-06 */ ///数据库管理 class DemoSqlManager { static const _VERSION = 1; static const _NAME = "demo_github_app_flutter.db"; static Database? _database; ///初始化 static init() async { // open the database var databasesPath = await getDatabasesPath(); String path = databasesPath + _NAME; _database = await openDatabase(path, version: _VERSION, onCreate: (Database db, int version) async { // When creating the db, create the table //await db.execute("CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)"); }); } /// 表是否存在 static isTableExits(String tableName) async { await getCurrentDatabase(); var res = await _database?.rawQuery("select * from Sqlite_master where type = 'table' and name = '$tableName'"); return res != null && res.isNotEmpty; } ///获取当前数据库对象 static Future getCurrentDatabase() async { if (_database == null) { await init(); } return _database; } ///关闭 static close() { if (_database != null) { _database!.close(); _database = null; } } } ///数据库数据提供的抽象基类 abstract class DemoBaseDbProvider { bool isTableExits = false; tableSqlString(); tableName(); tableBaseString(String name, String columnId) { return ''' create table $name ( $columnId integer primary key autoincrement, '''; } Future getDataBase() async { return await open(); } @mustCallSuper prepare(name, String? createSql) async { isTableExits = await DemoSqlManager.isTableExits(name); if (!isTableExits) { Database?db = await DemoSqlManager.getCurrentDatabase(); return await db?.execute(createSql!); } } @mustCallSuper open() async { if (!isTableExits) { await prepare(tableName(), tableSqlString()); } return await DemoSqlManager.getCurrentDatabase(); } } /// 用户表 class DemoUserInfoDbProvider extends DemoBaseDbProvider { final String name = 'UserInfo'; final String columnId = "_id"; final String columnUserName = "userName"; final String columnData = "data"; int? id; String? userName; String? data; DemoUserInfoDbProvider(); Map toMap(String userName, String data) { Map map = {columnUserName: userName, columnData: data}; if (id != null) { map[columnId] = id; } return map; } DemoUserInfoDbProvider.fromMap(Map map) { id = map[columnId]; userName = map[columnUserName]; data = map[columnData]; } @override tableSqlString() { return tableBaseString(name, columnId) + ''' $columnUserName text not null, $columnData text not null) '''; } @override tableName() { return name; } Future _getUserProvider(Database db, String userName) async { List> maps = await db.query(name, columns: [columnId, columnUserName, columnData], where: "$columnUserName = ?", whereArgs: [userName]); if (maps.isNotEmpty) { DemoUserInfoDbProvider provider = DemoUserInfoDbProvider.fromMap(maps.first); return provider; } return null; } ///插入到数据库 Future insert(String userName, String eventMapString) async { Database db = await getDataBase(); var userProvider = await _getUserProvider(db, userName); if (userProvider != null) { var result = await db.delete(name, where: "$columnUserName = ?", whereArgs: [userName]); if (kDebugMode) { print(result); } } return await db.insert(name, toMap(userName, eventMapString)); } ///获取事件数据 Future getUserInfo(String userName) async { Database db = await getDataBase(); var userProvider = await _getUserProvider(db, userName); if (userProvider != null) { return User.fromJson(json.decode(userProvider.data)); } return null; } } ================================================ FILE: lib/test/demo_item.dart ================================================ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class DemoItem extends StatelessWidget { const DemoItem({super.key}); ///返回一个居中带图标和文本的Item _getBottomItem(IconData icon, String text) { ///充满 Row 横向的布局 return Expanded( flex: 1, ///居中显示 child: Center( ///横向布局 child: Row( ///主轴居中,即是横向居中 mainAxisAlignment: MainAxisAlignment.start, ///大小按照最大充满 mainAxisSize: MainAxisSize.max, ///竖向也居中 crossAxisAlignment: CrossAxisAlignment.center, children: [ ///一个星星图标 Icon( icon, size: 16.0, color: Colors.grey, ), ///间隔 const Padding(padding: EdgeInsets.all(5.0)), SizedBox( width: 60, child: ///显示数量文本 Text( text, style: const TextStyle(color: Colors.grey, fontSize: 14.0), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ], ), ), ); } @override Widget build(BuildContext context) { return Card( ///增加点击效果 child: TextButton( onPressed: () { if (kDebugMode) { print("点击了哦"); } }, child: Padding( padding: const EdgeInsets.only( left: 0.0, top: 10.0, right: 10.0, bottom: 10.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ ///文本描述 Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: const Text( "这是一点描述", style: TextStyle( color: Colors.blueGrey, fontSize: 14.0, ), ///最长三行,超过 ... 显示 maxLines: 3, overflow: TextOverflow.ellipsis, )), const Padding(padding: EdgeInsets.all(10.0)), ///三个平均分配的横向图标文字 Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _getBottomItem(Icons.star, "10000000000000000000000000000000000000000000000000"), _getBottomItem(Icons.link, "1000"), _getBottomItem(Icons.subject, "1000"), ], ), ], ), ))); } } ================================================ FILE: lib/test/demo_mixins.dart ================================================ /// Created by guoshuyu /// Date: 2018-10-12 // ignore_for_file: avoid_print, annotate_overrides library; import 'package:flutter/foundation.dart'; abstract class Base { a() { if (kDebugMode) { print("base a()"); } } b() { print("base b()"); } c() { print("base c()"); } } mixin A on Base { a() { print("A.a()"); //super.a(); } b() { if (kDebugMode) { print("A.b()"); } super.b(); } } mixin A2 on Base { a() { print("A2.a()"); super.a(); } } class B extends Base { a() { print("B.a()"); super.a(); } b() { if (kDebugMode) { print("B.b()"); } super.b(); } c() { if (kDebugMode) { print("B.c()"); } super.c(); } } class G extends B with A, A2 { } testMixins() { G t = G(); t.a(); t.b(); t.c(); } ///I/flutter (13627): A2.a() ///I/flutter (13627): A.a() ///I/flutter (13627): B.a() ///I/flutter (13627): base a() ///I/flutter (13627): A.b() ///I/flutter (13627): B.b() ///I/flutter (13627): base b() ///I/flutter (13627): B.c() ///I/flutter (13627): base c() ================================================ FILE: lib/test/demo_page.dart ================================================ import 'package:flutter/material.dart'; class DemoPage extends StatefulWidget { const DemoPage({super.key}); @override _DemoPageState createState() => _DemoPageState(); } class _DemoPageState extends State { @override Widget build(BuildContext context) { ///一个页面的开始 ///如果是新页面,会自带返回按键 return Scaffold( ///背景样式 backgroundColor: Colors.blue, ///标题栏,当然不仅仅是标题栏 appBar: AppBar( ///这个title是一个Widget title: const Text("Title"), ), ///正式的页面开始 ///一个ListView,20个Item body: ListView.builder( itemBuilder: (context, index) { return Card( ///设置阴影的深度 elevation: 5.0, ///增加圆角 shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0))), color: Colors.white, margin: const EdgeInsets.only(left: 30.0, right: 30.0, top: 30), child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 10), height: 80, child: Text("显示文本 $index"), ), ); }, itemCount: 20, ), floatingActionButton: Builder(builder: (builderContext) { ///利用 builder 的 builderContext ///才能获取到 Scaffold.of(builderContext) 的 ScaffoldState return FloatingActionButton( onPressed: () { ScaffoldMessenger.of(builderContext) .showSnackBar(const SnackBar(content: Text("SnackBar"))); }, ); }), ); } } ================================================ FILE: lib/test/demo_tab_page.dart ================================================ import 'package:flutter/material.dart'; class DemoTabPage extends StatefulWidget { const DemoTabPage({super.key}); @override _DemoTabPageState createState() => _DemoTabPageState(); } class _DemoTabPageState extends State { @override Widget build(BuildContext context) { return TabWidget( type: TabType.bottom, title: const Text("标题"), tabItems: getTab(), tabViews: getPages(), ); } getTab() { return [ const Tab( text: "tab1", icon: Icon(Icons.access_alarm), ), const Tab( text: "tab2", icon: Icon(Icons.android), ), const Tab( text: "tab3", icon: Icon(Icons.ac_unit), ), ]; } getPages() { return [ Container( color: Colors.blue, child: const KeepAliveList(), ), Container( color: Colors.red, child: const KeepAliveList(), ), Container( color: Colors.amber, child: const KeepAliveList(), ), ]; } } enum TabType { bottom, top } class TabWidget extends StatefulWidget { final TabType type; final List? tabItems; final List? tabViews; final Color indicatorColor; final Widget? title; final PageController? pageController; final ValueChanged? onPageChanged; final ValueChanged? onTap; final int initTabIndex; const TabWidget({ super.key, this.type = TabType.top, this.tabItems, this.tabViews, this.indicatorColor = Colors.green, this.title, this.initTabIndex = 0, this.pageController, this.onPageChanged, this.onTap, }); @override _GSYTabBarState createState() => _GSYTabBarState(); } class _GSYTabBarState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); _tabController = TabController( vsync: this, length: widget.tabViews!.length, initialIndex: widget.initTabIndex); _pageController = widget.pageController; _pageController ??= PageController(); } TabController? _tabController; PageController? _pageController; @override void dispose() { _tabController!.dispose(); super.dispose(); } ///返回底部类型的 Tab Widget _getBottomNavByType() { /// 为了主题风格,包一层 Material 实现风格套用 return Material( /// 底部导航栏主题颜色 color: Colors.black, child: Container( decoration: BoxDecoration( ///设置一个底部分割线 border: Border( top: Divider.createBorderSide(context, color: Colors.grey), ), ), ///使用 SafeArea 判断底部安全区域 child: SafeArea( ///使用 Theme 嵌套去除 splashColor 的点击颜色 child: Theme( data: Theme.of(context).copyWith(splashColor: Colors.transparent), child: TabBar( controller: _tabController, labelPadding: EdgeInsets.zero, tabs: widget.tabItems!, /// tab标签的下划线颜色 indicatorColor: widget.indicatorColor, onTap: (index) { ///点击时 500 毫秒的滑动 _pageController!.animateToPage(index, curve: Curves.linear, duration: const Duration(milliseconds: 200)); if (widget.onTap != null) { widget.onTap!(index); } }, )), ), ), ); } @override Widget build(BuildContext context) { if (widget.type == TabType.top) { ///顶部 tab bar return Scaffold( appBar: AppBar( title: widget.title, bottom: TabBar( controller: _tabController, tabs: widget.tabItems!, indicatorColor: widget.indicatorColor), ), body: TabBarView( controller: _tabController, children: widget.tabViews!, ), ); } ///底部tab bar return Scaffold( appBar: AppBar( title: widget.title, ), body: PageView( controller: _pageController, children: widget.tabViews!, onPageChanged: (index) { if (!_tabController!.indexIsChanging) { _tabController!.animateTo(index, curve: Curves.linear, duration: const Duration(milliseconds: 20)); } widget.onPageChanged?.call(index); }, ), bottomNavigationBar: _getBottomNavByType()); } } class KeepAliveList extends StatefulWidget { const KeepAliveList({super.key}); @override _KeepAliveListState createState() => _KeepAliveListState(); } class _KeepAliveListState extends State with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return ListView.builder( itemBuilder: (context, index) { return Card( ///设置阴影的深度 elevation: 5.0, ///增加圆角 shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10.0))), color: Colors.white, margin: const EdgeInsets.only(left: 30.0, right: 30.0, top: 30), child: Container( alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 10), height: 80, child: Text("显示文本 $index"), ), ); }, itemCount: 20, ); } } ================================================ FILE: lib/test/demo_text_field_page.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class DemoTextFieldPage extends StatefulWidget { const DemoTextFieldPage({super.key}); @override _DemoTextFieldPageState createState() => _DemoTextFieldPageState(); } class _DemoTextFieldPageState extends State { final TextEditingController controller = TextEditingController(); final FocusNode focusNode = FocusNode(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("DemoTextFieldPage"), ), body: Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 10), child: TextField( controller: controller, obscureText: true, keyboardType: TextInputType.number, focusNode: focusNode, decoration: const InputDecoration( hintText: "请输入密码", icon: Icon(Icons.keyboard), prefix: Icon(Icons.person), suffix: Icon(Icons.remove_red_eye), labelText: "labelText", helperText: "helperText", counterText: "counterText", enabledBorder: OutlineInputBorder( /*边角*/ borderRadius: BorderRadius.all( Radius.circular(10), //边角为30 ), borderSide: BorderSide( color: Colors.blue, //边线颜色为黄色 width: 2, //边线宽度为2 ), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.black, width: 5, )), ), inputFormatters: [ FilteringTextInputFormatter.digitsOnly ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { controller.text = ""; FocusScope.of(context).requestFocus(FocusNode()); }, ), ); } } ================================================ FILE: lib/test/demo_user_store.dart ================================================ /// Created by guoshuyu /// Date: 2018-08-06 library; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:gsy_github_app_flutter/model/user.dart'; import 'package:gsy_github_app_flutter/redux/gsy_state.dart'; class DemoUseStorePage extends StatelessWidget { const DemoUseStorePage({super.key}); @override Widget build(BuildContext context) { ///通过 StoreConnector 关联 GSYState 中的 User return StoreConnector( ///通过 converter 将 GSYState 中的 userInfo返回 converter: (store) => store.state.userInfo, ///在 userInfo 中返回实际渲染的控件 builder: (context, userInfo) { return Text( userInfo!.name!, style: Theme.of(context).textTheme.headlineMedium, ); }, ); } } ================================================ FILE: lib/test/demo_widget.dart ================================================ // ignore_for_file: no_logic_in_create_state import 'dart:async'; import 'package:flutter/material.dart'; class DEMOWidget extends StatelessWidget { final String? text; const DEMOWidget(this.text, {super.key}); @override Widget build(BuildContext context) { //这里返回你需要的控件 //这里末尾有没有的逗号,对于格式化代码而已是不一样的。 return Container( color: Colors.white, child: Text(text ?? "这就是无状态DMEO"), ); } } class DemoStateWidget extends StatefulWidget { final String text; const DemoStateWidget(this.text, {super.key}); @override _DemoStateWidgetState createState() => _DemoStateWidgetState(text); } class _DemoStateWidgetState extends State with AutomaticKeepAliveClientMixin { String? text; _DemoStateWidgetState(this.text); @override void initState() { ///初始化,这个函数在生命周期中只调用一次 super.initState(); } @override void dispose() { ///销毁 super.dispose(); } @override void didChangeDependencies() { ///在initState之后调 Called when a dependency of this [State] object changes. super.didChangeDependencies(); Future.delayed(const Duration(seconds: 1), () { setState(() { text = "这就变了数值"; }); return true; }); } @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return Text(text ?? "这就是有状态DMEO"); } } ================================================ FILE: lib/widget/anima/curves_bezier.dart ================================================ //import 'package:flutter/material.dart'; //import 'package:vector_math/vector_math.dart'; /*class CurveBezier extends Curve { final quadraticCurve = new QuadraticBezier( [new Vector2(-4.0, 1.0), new Vector2(-2.0, -1.0), new Vector2(1.0, 1.0)]); @override double transformInternal(double t) { return quadraticCurve.pointAt(t).s; } }*/ ================================================ FILE: lib/widget/animated_background.dart ================================================ import 'package:flutter/material.dart'; import 'package:simple_animations/simple_animations.dart'; import 'package:supercharged/supercharged.dart'; enum _ColorTween { color1, color2 } class AnimatedBackground extends StatelessWidget { const AnimatedBackground({super.key}); @override Widget build(BuildContext context) { final tween = MovieTween() ..tween(_ColorTween.color1, ColorTween(begin: const Color(0xffD38312), end: Colors.lightBlue.shade900), duration: 3.seconds, curve: Curves.easeIn) ..tween( _ColorTween.color2, ColorTween(begin: const Color(0xffA83279), end: Colors.blue.shade600), duration: 3.seconds, ); return MirrorAnimationBuilder( tween: tween, duration: tween.duration, builder: (context, value, child) { return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ value.get(_ColorTween.color1), value.get(_ColorTween.color2) ])), ); }, ); } } ================================================ FILE: lib/widget/diff_scale_text.dart ================================================ import 'dart:math' as Math; import 'package:flutter/material.dart'; class DiffScaleText extends StatefulWidget { final String? text; final TextStyle? textStyle; const DiffScaleText({super.key, required this.text, this.textStyle}) : assert(text != null); @override _DiffScaleTextState createState() => _DiffScaleTextState(); } class _DiffScaleTextState extends State with TickerProviderStateMixin { late AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 400)); _animationController.addStatusListener((status) {}); } @override void didUpdateWidget(DiffScaleText oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.text != widget.text) { if (!_animationController.isAnimating) { _animationController.value = 0; _animationController.forward(); } } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { TextStyle? textStyle = widget.textStyle ?? const TextStyle( fontSize: 20, color: Colors.white, ); return AnimatedBuilder( animation: _animationController, builder: (BuildContext context, Widget? child) { return RepaintBoundary( child: CustomPaint( foregroundPainter: _DiffText( text: widget.text ?? "", textStyle: textStyle, progress: _animationController.value), child: Text(widget.text ?? "", style: textStyle.merge(const TextStyle(color: Color(0x00000000))), maxLines: 1, textDirection: TextDirection.ltr), )); }, ); } } class _DiffText extends CustomPainter { final String? text; final TextStyle? textStyle; final double progress; String? _oldText; List<_TextLayoutInfo> _textLayoutInfo = []; List<_TextLayoutInfo> _oldTextLayoutInfo = []; _DiffText({required this.text, required this.textStyle, this.progress = 1}) : assert(text != null), assert(textStyle != null); @override void paint(Canvas canvas, Size size) { double percent = Math.max(0, Math.min(1, progress)); if (_textLayoutInfo.isEmpty) { calculateLayoutInfo(text ?? "", _textLayoutInfo); } canvas.save(); if (_oldTextLayoutInfo.isNotEmpty) { for (_TextLayoutInfo oldTextLayoutInfo in _oldTextLayoutInfo) { if (oldTextLayoutInfo.needMove) { double p = percent * 2; p = p > 1 ? 1 : p; drawText( canvas, oldTextLayoutInfo.text, 1, 1, Offset( oldTextLayoutInfo.offsetX! - (oldTextLayoutInfo.offsetX! - oldTextLayoutInfo.toX!) * p, oldTextLayoutInfo.offsetY), oldTextLayoutInfo); } else { drawText( canvas, oldTextLayoutInfo.text, 1 - percent, percent, Offset(oldTextLayoutInfo.offsetX!, oldTextLayoutInfo.offsetY), oldTextLayoutInfo); } } } else { //no oldText percent = 1; } for (_TextLayoutInfo textLayoutInfo in _textLayoutInfo) { if (!textLayoutInfo.needMove) { drawText( canvas, textLayoutInfo.text, percent, percent, Offset(textLayoutInfo.offsetX!, textLayoutInfo.offsetY), textLayoutInfo); } } canvas.restore(); } void drawText(Canvas canvas, String? text, double textScaleFactor, double alphaFactor, Offset offset, _TextLayoutInfo textLayoutInfo) { var textPaint = Paint(); if (alphaFactor == 1) { textPaint.color = textStyle!.color!; } else { textPaint.color = textStyle!.color! .withAlpha((textStyle!.color!.a * alphaFactor).floor()); } var textPainter = TextPainter( text: TextSpan( text: text, style: textStyle?.merge(TextStyle( color: null, foreground: textPaint, textBaseline: TextBaseline.ideographic))), textDirection: TextDirection.ltr); textPainter.textAlign = TextAlign.center; textPainter.textScaler = TextScaler.linear(textScaleFactor); textPainter.textDirection = TextDirection.ltr; textPainter.layout(); textPainter.paint( canvas, Offset(offset.dx, offset.dy + (textLayoutInfo.height - textPainter.height) / 2)); } @override bool shouldRepaint(CustomPainter oldDelegate) { if (oldDelegate is _DiffText) { String oldFrameText = oldDelegate.text ?? ""; if (oldFrameText == text) { _oldText = oldDelegate._oldText; _oldTextLayoutInfo = oldDelegate._oldTextLayoutInfo; _textLayoutInfo = oldDelegate._textLayoutInfo; if (progress == oldDelegate.progress) { return false; } } else { _oldText = oldDelegate.text; calculateLayoutInfo(text ?? "", _textLayoutInfo); calculateLayoutInfo(_oldText!, _oldTextLayoutInfo); calculateMove(); } } return true; } void calculateLayoutInfo(String text, List<_TextLayoutInfo> list) { list.clear(); TextPainter textPainter = TextPainter( text: TextSpan(text: text, style: textStyle), textDirection: TextDirection.ltr, maxLines: 1); textPainter.layout(); for (int i = 0; i < text.length; i++) { var forCaret = textPainter.getOffsetForCaret(TextPosition(offset: i), Rect.zero); var offsetX = forCaret.dx; if (i > 0 && offsetX == 0) { break; } var textLayoutInfo = _TextLayoutInfo(); textLayoutInfo.text = text.substring(i, i + 1); textLayoutInfo.offsetX = offsetX; textLayoutInfo.offsetY = forCaret.dy; textLayoutInfo.width = 0; textLayoutInfo.height = textPainter.height; textLayoutInfo.baseline = textPainter.computeDistanceToActualBaseline(TextBaseline.ideographic); list.add(textLayoutInfo); } } void calculateMove() { if (_oldTextLayoutInfo.isEmpty) { return; } if (_textLayoutInfo.isEmpty) { return; } for (_TextLayoutInfo oldText in _oldTextLayoutInfo) { for (_TextLayoutInfo text in _textLayoutInfo) { if (!text.needMove && !oldText.needMove && text.text == oldText.text) { text.fromX = oldText.offsetX; oldText.toX = text.offsetX; text.needMove = true; oldText.needMove = true; } } } } } class _TextLayoutInfo { String? text; double? offsetX; late double offsetY; double? baseline; double? width; late double height; double? fromX = 0; double? toX = 0; bool needMove = false; } ================================================ FILE: lib/widget/flutter_json_widget.dart ================================================ // ignore_for_file: unnecessary_string_escapes import 'package:flutter/material.dart'; class JsonViewerWidget extends StatefulWidget { final Map jsonObj; final bool notRoot; const JsonViewerWidget(this.jsonObj, {super.key, this.notRoot = false}); @override JsonViewerWidgetState createState() => JsonViewerWidgetState(); } class JsonViewerWidgetState extends State { Map openFlag = {}; @override Widget build(BuildContext context) { if (widget.notRoot) { return Container( padding: const EdgeInsets.only(left: 14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: _getList())); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: _getList()); } _getList() { List list = []; for (MapEntry entry in widget.jsonObj.entries) { bool ex = isExtensible(entry.value); bool ink = isInkWell(entry.value); list.add(Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ex ? ((openFlag[entry.key] ?? false) ? Icon(Icons.arrow_drop_down, size: 14, color: Colors.grey[700]) : Icon(Icons.arrow_right, size: 14, color: Colors.grey[700])) : const Icon( Icons.arrow_right, color: Color.fromARGB(0, 0, 0, 0), size: 14, ), (ex && ink) ? InkWell( child: Text(entry.key, style: TextStyle(color: Colors.purple[900])), onTap: () { setState(() { openFlag[entry.key] = !(openFlag[entry.key] ?? false); }); }) : Text(entry.key, style: TextStyle( color: entry.value == null ? Colors.grey : Colors.purple[900])), const Text( ':', style: TextStyle(color: Colors.grey), ), const SizedBox(width: 3), getValueWidget(entry) ], )); list.add(const SizedBox(height: 4)); if (openFlag[entry.key] ?? false) { list.add(getContentWidget(entry.value)); } } return list; } static getContentWidget(dynamic content) { if (content is List) { return JsonArrayViewerWidget(content, notRoot: true); } else { return JsonViewerWidget(content, notRoot: true); } } static isInkWell(dynamic content) { if (content == null) { return false; } else if (content is int) { return false; } else if (content is String) { return false; } else if (content is bool) { return false; } else if (content is double) { return false; } else if (content is List) { if (content.isEmpty) { return false; } else { return true; } } return true; } getValueWidget(MapEntry entry) { if (entry.value == null) { return const Expanded( child: Text( 'undefined', style: TextStyle(color: Colors.grey), )); } else if (entry.value is int) { return Expanded( child: Text( entry.value.toString(), style: const TextStyle(color: Colors.teal), )); } else if (entry.value is String) { return Expanded( child: Text( // ignore: prefer_interpolation_to_compose_strings '${'\"' + entry.value}\"', style: const TextStyle(color: Colors.redAccent), )); } else if (entry.value is bool) { return Expanded( child: Text( entry.value.toString(), style: const TextStyle(color: Colors.purple), )); } else if (entry.value is double) { return Expanded( child: Text( entry.value.toString(), style: const TextStyle(color: Colors.teal), )); } else if (entry.value is List) { if (entry.value.isEmpty) { return const Text( 'Array[0]', style: TextStyle(color: Colors.grey), ); } else { return InkWell( child: Text( 'Array<${getTypeName(entry.value[0])}>[${entry.value.length}]', style: const TextStyle(color: Colors.grey), ), onTap: () { setState(() { openFlag[entry.key] = !(openFlag[entry.key] ?? false); }); }); } } return InkWell( child: const Text( 'Object', style: TextStyle(color: Colors.grey), ), onTap: () { setState(() { openFlag[entry.key] = !(openFlag[entry.key] ?? false); }); }); } static isExtensible(dynamic content) { if (content == null) { return false; } else if (content is int) { return false; } else if (content is String) { return false; } else if (content is bool) { return false; } else if (content is double) { return false; } return true; } static getTypeName(dynamic content) { if (content is int) { return 'int'; } else if (content is String) { return 'String'; } else if (content is bool) { return 'bool'; } else if (content is double) { return 'double'; } else if (content is List) { return 'List'; } return 'Object'; } } class JsonArrayViewerWidget extends StatefulWidget { final List jsonArray; final bool? notRoot; const JsonArrayViewerWidget(this.jsonArray, {super.key, this.notRoot = false}); @override _JsonArrayViewerWidgetState createState() => _JsonArrayViewerWidgetState(); } class _JsonArrayViewerWidgetState extends State { List? openFlag; @override Widget build(BuildContext context) { if (widget.notRoot ?? false) { return Container( padding: const EdgeInsets.only(left: 14.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: _getList())); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: _getList()); } @override void initState() { super.initState(); openFlag = List.filled(widget.jsonArray.length, false); } _getList() { List list = []; int i = 0; for (dynamic content in widget.jsonArray) { bool ex = JsonViewerWidgetState.isExtensible(content); bool ink = JsonViewerWidgetState.isInkWell(content); list.add(Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ex ? ((openFlag?[i] ?? false) ? Icon(Icons.arrow_drop_down, size: 14, color: Colors.grey[700]) : Icon(Icons.arrow_right, size: 14, color: Colors.grey[700])) : const Icon( Icons.arrow_right, color: Color.fromARGB(0, 0, 0, 0), size: 14, ), (ex && ink) ? getInkWell(i) : Text('[$i]', style: TextStyle( color: content == null ? Colors.grey : Colors.purple[900])), const Text( ':', style: TextStyle(color: Colors.grey), ), const SizedBox(width: 3), getValueWidget(content, i) ], )); list.add(const SizedBox(height: 4)); if (openFlag?[i] ?? false) { list.add(JsonViewerWidgetState.getContentWidget(content)); } i++; } return list; } getInkWell(int index) { return InkWell( child: Text('[$index]', style: TextStyle(color: Colors.purple[900])), onTap: () { setState(() { openFlag?[index] = !(openFlag?[index] ?? false); }); }); } getValueWidget(dynamic content, int index) { if (content == null) { return const Expanded( child: Text( 'undefined', style: TextStyle(color: Colors.grey), )); } else if (content is int) { return Expanded( child: Text( content.toString(), style: const TextStyle(color: Colors.teal), )); } else if (content is String) { return Expanded( child: Text( '\"$content\"', style: const TextStyle(color: Colors.redAccent), )); } else if (content is bool) { return Expanded( child: Text( content.toString(), style: const TextStyle(color: Colors.purple), )); } else if (content is double) { return Expanded( child: Text( content.toString(), style: const TextStyle(color: Colors.teal), )); } else if (content is List) { if (content.isEmpty) { return const Text( 'Array[0]', style: TextStyle(color: Colors.grey), ); } else { return InkWell( child: Text( 'Array<${JsonViewerWidgetState.getTypeName(content)}>[${content.length}]', style: const TextStyle(color: Colors.grey), ), onTap: () { setState(() { openFlag?[index] = !(openFlag?[index] ?? false); }); }); } } return InkWell( child: const Text( 'Object', style: TextStyle(color: Colors.grey), ), onTap: () { setState(() { openFlag?[index] = !(openFlag?[index] ?? false); }); }); } } ================================================ FILE: lib/widget/gsy_bottom_action_bar.dart ================================================ import 'package:flutter/material.dart'; class GSYBottomAppBar extends StatelessWidget { const GSYBottomAppBar({super.key, this.color, this.fabLocation, this.shape, this.rowContents, }); final Color? color; final FloatingActionButtonLocation? fabLocation; final NotchedShape? shape; final List? rowContents; static final List kCenterLocations = [ FloatingActionButtonLocation.centerDocked, FloatingActionButtonLocation.centerFloat, ]; @override Widget build(BuildContext context) { return BottomAppBar( color: color, shape: shape, child: Row(children: rowContents!), ); } } ================================================ FILE: lib/widget/gsy_card_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; /// Card Widget /// Created by guoshuyu /// Date: 2018-07-16 class GSYCardItem extends StatelessWidget { final Widget child; final EdgeInsets? margin; final Color? color; final RoundedRectangleBorder? shape; final double elevation; const GSYCardItem( {super.key, required this.child, this.margin, this.color, this.shape, this.elevation = 5.0}); @override Widget build(BuildContext context) { EdgeInsets? margin = this.margin; RoundedRectangleBorder? shape = this.shape; Color? color = this.color; margin ??= const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0, bottom: 10.0); shape ??= const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4.0))); color ??= GSYColors.cardWhite; return Card( elevation: elevation, shape: shape, color: color, margin: margin, child: child); } } ================================================ FILE: lib/widget/gsy_common_option_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:share_plus/share_plus.dart'; /// Created by guoshuyu /// Date: 2018-07-26 class GSYCommonOptionWidget extends StatelessWidget { final List? otherList; final String? url; const GSYCommonOptionWidget({super.key, this.otherList, String? url}) : url = (url == null) ? GSYConstant.app_default_share_url : url; _renderHeaderPopItem(List list) { return PopupMenuButton( child: const Icon(GSYICons.MORE), onSelected: (model) { model.selected(model); }, itemBuilder: (BuildContext context) { return _renderHeaderPopItemChild(list); }, ); } _renderHeaderPopItemChild(List data) { List> list = []; for (GSYOptionModel item in data) { list.add(PopupMenuItem( value: item, child: Text(item.name), )); } return list; } @override Widget build(BuildContext context) { List constList = [ GSYOptionModel(context.l10n.option_web, context.l10n.option_web, (model) { CommonUtils.launchOutURL(url, context); }), GSYOptionModel(context.l10n.option_copy, context.l10n.option_copy, (model) { CommonUtils.copy(url ?? "", context); }), GSYOptionModel(context.l10n.option_share, context.l10n.option_share, (model) { SharePlus.instance.share(ShareParams(text: context.l10n.option_share_title + (url ?? ""))); }), ]; var list = [...constList, ...?otherList]; return _renderHeaderPopItem(list); } } class GSYOptionModel { final String name; final String value; final PopupMenuItemSelected selected; GSYOptionModel(this.name, this.value, this.selected); } ================================================ FILE: lib/widget/gsy_event_item.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/model/event.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/event_utils.dart'; import 'package:gsy_github_app_flutter/common/utils/navigator_utils.dart'; import 'package:gsy_github_app_flutter/model/repo_commit.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; import 'package:gsy_github_app_flutter/widget/gsy_user_icon_widget.dart'; import 'package:gsy_github_app_flutter/model/notification.dart' as Model; /// 事件Item /// Created by guoshuyu /// Date: 2018-07-16 class GSYEventItem extends StatelessWidget { final EventViewModel eventViewModel; final VoidCallback? onPressed; final bool needImage; const GSYEventItem(this.eventViewModel, {super.key, this.onPressed, this.needImage = true}); @override Widget build(BuildContext context) { Widget des = (eventViewModel.actionDes == null || eventViewModel.actionDes!.isEmpty) ? Container() : Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text( eventViewModel.actionDes!, style: GSYConstant.smallSubText, maxLines: 3, )); Widget userImage = (needImage) ? GSYUserIconWidget( padding: const EdgeInsets.only(top: 0.0, right: 5.0, left: 0.0), width: 30.0, height: 30.0, image: eventViewModel.actionUserPic, onPressed: () { NavigatorUtils.goPerson(context, eventViewModel.actionUser); }) : Container(); return GSYCardItem( child: TextButton( onPressed: onPressed, child: Padding( padding: const EdgeInsets.only( left: 0.0, top: 10.0, right: 0.0, bottom: 10.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ userImage, Expanded( child: Text(eventViewModel.actionUser!, style: GSYConstant.smallTextBold)), Text(eventViewModel.actionTime, style: GSYConstant.smallSubText), ], ), Container( margin: const EdgeInsets.only(top: 6.0, bottom: 2.0), alignment: Alignment.topLeft, child: Text(eventViewModel.actionTarget ?? "", style: GSYConstant.smallTextBold)), des, ], ), ))); } } class EventViewModel { String? actionUser; String? actionUserPic; String? actionDes; late String actionTime; String? actionTarget; EventViewModel.fromEventMap(Event event) { actionTime = CommonUtils.getNewsTimeStr(event.createdAt!); actionUser = event.actor!.login; actionUserPic = event.actor!.avatar_url; var as = EventUtils.getActionAndDes(event); actionDes = as.des; actionTarget = as.actionStr; } EventViewModel.fromCommitMap(RepoCommit eventMap) { actionTime = CommonUtils.getNewsTimeStr(eventMap.commit!.committer!.date!); actionUser = eventMap.commit!.committer!.name; actionDes = "sha:${eventMap.sha!}"; actionTarget = eventMap.commit!.message; } EventViewModel.fromNotify(BuildContext context, Model.Notification eventMap) { actionTime = CommonUtils.getNewsTimeStr(eventMap.updateAt!); actionUser = eventMap.repository!.fullName; String? type = eventMap.subject!.type; String status = eventMap.unread! ? context.l10n.notify_unread : context.l10n.notify_readed; actionDes = "${eventMap.reason!}${context.l10n.notify_type}:$type,${context.l10n.notify_status}:$status"; actionTarget = eventMap.subject!.title; } } ================================================ FILE: lib/widget/gsy_flex_button.dart ================================================ import 'package:flutter/material.dart'; /// 充满的button /// Created by guoshuyu /// Date: 2018-07-16 class GSYFlexButton extends StatelessWidget { final String? text; final Color? color; final Color textColor; final VoidCallback? onPress; final double fontSize; final int maxLines; final MainAxisAlignment mainAxisAlignment; const GSYFlexButton( {super.key, this.text, this.color, this.textColor = Colors.black, this.onPress, this.fontSize = 20.0, this.mainAxisAlignment = MainAxisAlignment.center, this.maxLines = 1}); @override Widget build(BuildContext context) { return ElevatedButton( style: TextButton.styleFrom( backgroundColor: color, padding: const EdgeInsets.only( left: 20.0, top: 10.0, right: 20.0, bottom: 10.0)), child: Flex( mainAxisAlignment: mainAxisAlignment, direction: Axis.horizontal, children: [ Expanded( child: Text(text!, style: TextStyle( color: textColor, fontSize: fontSize, height: 1), textAlign: TextAlign.center, maxLines: maxLines, overflow: TextOverflow.ellipsis), ) ], ), onPressed: () { onPress?.call(); }); } } ================================================ FILE: lib/widget/gsy_icon_text.dart ================================================ import 'package:flutter/material.dart'; /// 带图标Icon的文本,可调节 /// Created by guoshuyu /// Date: 2018-07-16 class GSYIConText extends StatelessWidget { final String? iconText; final IconData iconData; final TextStyle textStyle; final Color iconColor; final double padding; final double iconSize; final VoidCallback? onPressed; final MainAxisAlignment mainAxisAlignment; final MainAxisSize mainAxisSize; final double textWidth; const GSYIConText( this.iconData, this.iconText, this.textStyle, this.iconColor, this.iconSize, {super.key, this.padding = 0.0, this.onPressed, this.mainAxisAlignment = MainAxisAlignment.start, this.mainAxisSize = MainAxisSize.max, this.textWidth = -1, }); @override Widget build(BuildContext context) { Widget showText = (textWidth == -1) ? Text( iconText ?? "", style: textStyle .merge(const TextStyle(textBaseline: TextBaseline.alphabetic)), overflow: TextOverflow.ellipsis, maxLines: 1, ) : SizedBox( width: textWidth, child: ///显示数量文本 Text( iconText!, style: textStyle .merge(const TextStyle(textBaseline: TextBaseline.alphabetic)), overflow: TextOverflow.ellipsis, maxLines: 1, )); return Row( mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( iconData, size: iconSize, color: iconColor, ), Padding(padding: EdgeInsets.all(padding)), showText ], ); } } ================================================ FILE: lib/widget/gsy_input_widget.dart ================================================ import 'package:flutter/material.dart'; /// 带图标的输入框 class GSYInputWidget extends StatefulWidget { final bool obscureText; final String? hintText; final IconData? iconData; final ValueChanged? onChanged; final TextStyle? textStyle; final TextEditingController? controller; const GSYInputWidget( {super.key, this.hintText, this.iconData, this.onChanged, this.textStyle, this.controller, this.obscureText = false}); @override _GSYInputWidgetState createState() => _GSYInputWidgetState(); } /// State for [GSYInputWidget] widgets. class _GSYInputWidgetState extends State { @override Widget build(BuildContext context) { return TextField( controller: widget.controller, onChanged: widget.onChanged, obscureText: widget.obscureText, decoration: InputDecoration( hintText: widget.hintText, icon: widget.iconData == null ? null : Icon(widget.iconData), ), magnifierConfiguration: TextMagnifierConfiguration(magnifierBuilder: ( BuildContext context, MagnifierController controller, ValueNotifier magnifierInfo, ) { return null; })); } } ================================================ FILE: lib/widget/gsy_select_item_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_card_item.dart'; /// 详情issue列表头部,PreferredSizeWidget /// Created by guoshuyu /// Date: 2018-07-19 typedef SelectItemChanged = void Function(int value); class GSYSelectItemWidget extends StatefulWidget implements PreferredSizeWidget { final List itemNames; final SelectItemChanged? selectItemChanged; final RoundedRectangleBorder? shape; final double elevation; final double height; final EdgeInsets margin; const GSYSelectItemWidget( this.itemNames, this.selectItemChanged, {super.key, this.elevation = 5.0, this.height = 70.0, this.shape, this.margin = const EdgeInsets.all(10.0), }); @override _GSYSelectItemWidgetState createState() => _GSYSelectItemWidgetState(); @override Size get preferredSize { return Size.fromHeight(height); } } class _GSYSelectItemWidgetState extends State { int selectIndex = 0; int preSelIndex = 0; List keys = [false,false]; _GSYSelectItemWidgetState(); @override void initState() { super.initState(); keys = widget.itemNames.map((e) => false).toList(); } _renderItem(String name, int index) { var style = index == selectIndex ? GSYConstant.middleTextWhite : GSYConstant.middleSubLightText; if(preSelIndex!=index && index == selectIndex){ //说明此项是变项,key值取反 keys[index] = !keys[index]; } return Expanded( child: AnimatedSwitcher( transitionBuilder: (child, anim) { return ScaleTransition(scale: anim, child: child); }, duration: const Duration(milliseconds: 300), child: RawMaterialButton( key: ValueKey(keys[index]), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), padding: const EdgeInsets.all(10.0), child: Text( name, style: style, textAlign: TextAlign.center, ), onPressed: () { if (selectIndex != index) { widget.selectItemChanged?.call(index); } setState(() { preSelIndex = selectIndex; selectIndex = index; }); }), ), ); } _renderList() { List list = []; for (int i = 0; i < widget.itemNames.length; i++) { if (i == widget.itemNames.length - 1) { list.add(_renderItem(widget.itemNames[i], i)); } else { list.add(_renderItem(widget.itemNames[i], i)); list.add(Container( width: 1.0, height: 25.0, color: GSYColors.subLightTextColor)); } } return list; } @override Widget build(BuildContext context) { return GSYCardItem( elevation: widget.elevation, margin: widget.margin, color: Theme.of(context).primaryColor, shape: widget.shape ?? const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4.0)), ), child: Row( children: _renderList(), )); } } ================================================ FILE: lib/widget/gsy_tabbar_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/gsy_tabs.dart' as GSYTab; ///支持顶部和顶部的TabBar控件 ///配合AutomaticKeepAliveClientMixin可以keep住 class GSYTabBarWidget extends StatefulWidget { final TabType type; final bool resizeToAvoidBottomPadding; final List? tabItems; final List? tabViews; final Color? backgroundColor; final Color? indicatorColor; final Widget? title; final Widget? drawer; final Widget? floatingActionButton; final FloatingActionButtonLocation? floatingActionButtonLocation; final Widget? bottomBar; final List? footerButtons; final ValueChanged? onPageChanged; final ValueChanged? onDoublePress; final ValueChanged? onSinglePress; const GSYTabBarWidget({ super.key, this.type = TabType.top, this.tabItems, this.tabViews, this.backgroundColor, this.indicatorColor, this.title, this.drawer, this.bottomBar, this.onDoublePress, this.onSinglePress, this.floatingActionButtonLocation, this.floatingActionButton, this.resizeToAvoidBottomPadding = true, this.footerButtons, this.onPageChanged, }); @override _GSYTabBarState createState() => _GSYTabBarState(); } class _GSYTabBarState extends State with SingleTickerProviderStateMixin { final PageController _pageController = PageController(); TabController? _tabController; int _index = 0; @override void initState() { super.initState(); _tabController = TabController(vsync: this, length: widget.tabItems!.length); } ///整个页面dispose时,记得把控制器也dispose掉,释放内存 @override void dispose() { _tabController!.dispose(); super.dispose(); } _navigationPageChanged(index) { if (_index == index) { return; } _index = index; _tabController!.animateTo(index); widget.onPageChanged?.call(index); } _navigationTapClick(index) { if (_index == index) { return; } _index = index; widget.onPageChanged?.call(index); ///不想要动画 _pageController.jumpTo(MediaQuery.sizeOf(context).width * index); widget.onSinglePress?.call(index); } _navigationDoubleTapClick(index) { _navigationTapClick(index); widget.onDoublePress?.call(index); } @override Widget build(BuildContext context) { if (widget.type == TabType.top) { ///顶部tab bar return Scaffold( backgroundColor: GSYColors.mainBackgroundColor, resizeToAvoidBottomInset: widget.resizeToAvoidBottomPadding, floatingActionButton: SafeArea(child: widget.floatingActionButton ?? Container()), floatingActionButtonLocation: widget.floatingActionButtonLocation, persistentFooterButtons: widget.footerButtons, appBar: AppBar( backgroundColor: Theme.of(context).primaryColor, title: widget.title, bottom: TabBar( controller: _tabController, tabs: widget.tabItems!, indicatorColor: widget.indicatorColor, onTap: _navigationTapClick), ), body: PageView( controller: _pageController, onPageChanged: _navigationPageChanged, children: widget.tabViews!, ), bottomNavigationBar: widget.bottomBar, ); } ///底部tab bar return Scaffold( drawer: widget.drawer, appBar: AppBar( backgroundColor: Theme.of(context).primaryColor, title: widget.title, ), body: PageView( controller: _pageController, onPageChanged: _navigationPageChanged, children: widget.tabViews!, ), bottomNavigationBar: Material( //为了适配主题风格,包一层Material实现风格套用 color: Theme.of(context).primaryColor, //底部导航栏主题颜色 child: SafeArea( child: GSYTab.TabBar( //TabBar导航标签,底部导航放到Scaffold的bottomNavigationBar中 controller: _tabController, //配置控制器 tabs: widget.tabItems!, indicatorColor: widget.indicatorColor, onDoubleTap: _navigationDoubleTapClick, onTap: _navigationTapClick, //tab标签的下划线颜色 ), ), )); } } enum TabType { top, bottom } ================================================ FILE: lib/widget/gsy_tabs.dart ================================================ import 'dart:async'; import 'dart:ui' show lerpDouble; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; const double _kTabHeight = 46.0; const double _kTextAndIconTabHeight = 72.0; /// Defines how the bounds of the selected tab indicator are computed. /// /// See also: /// /// * [TabBar], which displays a row of tabs. /// * [TabBarView], which displays a widget for the currently selected tab. /// * [TabBar.indicator], which defines the appearance of the selected tab /// indicator relative to the tab's bounds. enum TabBarIndicatorSize { /// The tab indicator's bounds are as wide as the space occupied by the tab /// in the tab bar: from the right edge of the previous tab to the left edge /// of the next tab. tab, /// The tab's bounds are only as wide as the (centered) tab widget itself. /// /// This value is used to align the tab's label, typically a [Tab] /// widget's text or icon, with the selected tab indicator. label, } /// A material design [TabBar] tab. /// /// If both [icon] and [text] are provided, the text is displayed below /// the icon. /// /// See also: /// /// * [TabBar], which displays a row of tabs. /// * [TabBarView], which displays a widget for the currently selected tab. /// * [TabController], which coordinates tab selection between a [TabBar] and a [TabBarView]. /// * class Tab extends StatelessWidget { /// Creates a material design [TabBar] tab. /// /// At least one of [text], [icon], and [child] must be non-null. The [text] /// and [child] arguments must not be used at the same time. const Tab({ super.key, this.text, this.icon, this.child, }) : assert(text != null || child != null || icon != null), assert(!(text != null && null != child)); /// The text to display as the tab's label. /// /// Must not be used in combination with [child]. final String? text; /// The widget to be used as the tab's label. /// /// Usually a [Text] widget, possibly wrapped in a [Semantics] widget. /// /// Must not be used in combination with [text]. final Widget? child; /// An icon to display as the tab's label. final Widget? icon; Widget _buildLabelText() { return child ?? Text(text!, softWrap: false, overflow: TextOverflow.fade); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); double height; Widget? label; if (icon == null) { height = _kTabHeight; label = _buildLabelText(); } else if (text == null && child == null) { height = _kTabHeight; label = icon; } else { height = _kTextAndIconTabHeight; label = Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( margin: const EdgeInsets.only(bottom: 10.0), child: icon, ), _buildLabelText(), ], ); } return SizedBox( height: height, child: Center( widthFactor: 1.0, child: label, ), ); } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(StringProperty('text', text, defaultValue: null)); properties .add(DiagnosticsProperty('icon', icon, defaultValue: null)); } } class _TabStyle extends AnimatedWidget { const _TabStyle({ required Animation animation, this.selected, this.labelColor, this.unselectedLabelColor, this.labelStyle, this.unselectedLabelStyle, required this.child, }) : super(listenable: animation); final TextStyle? labelStyle; final TextStyle? unselectedLabelStyle; final bool? selected; final Color? labelColor; final Color? unselectedLabelColor; final Widget? child; @override Widget build(BuildContext context) { final ThemeData themeData = Theme.of(context); final TabBarThemeData tabBarTheme = TabBarTheme.of(context); final Animation animation = listenable as Animation; // To enable TextStyle.lerp(style1, style2, value), both styles must have // the same value of inherit. Force that to be inherit=true here. final TextStyle defaultStyle = (labelStyle ?? tabBarTheme.labelStyle ?? themeData.primaryTextTheme.bodyLarge)! .copyWith(inherit: true); final TextStyle defaultUnselectedStyle = (unselectedLabelStyle ?? tabBarTheme.unselectedLabelStyle ?? labelStyle ?? themeData.primaryTextTheme.bodyLarge)! .copyWith(inherit: true); final TextStyle textStyle = selected! ? TextStyle.lerp(defaultStyle, defaultUnselectedStyle, animation.value)! : TextStyle.lerp( defaultUnselectedStyle, defaultStyle, animation.value)!; final Color? selectedColor = labelColor ?? tabBarTheme.labelColor ?? themeData.primaryTextTheme.bodyLarge!.color; final Color unselectedColor = unselectedLabelColor ?? tabBarTheme.unselectedLabelColor ?? selectedColor!.withAlpha(0xB2); // 70% alpha final Color? color = selected! ? Color.lerp(selectedColor, unselectedColor, animation.value) : Color.lerp(unselectedColor, selectedColor, animation.value); return DefaultTextStyle( style: textStyle.copyWith(color: color), child: IconTheme.merge( data: IconThemeData( size: 24.0, color: color, ), child: child!, ), ); } } typedef _LayoutCallback = void Function( List xOffsets, TextDirection? textDirection, double width); class _TabLabelBarRenderer extends RenderFlex { _TabLabelBarRenderer({ required super.direction, required super.mainAxisSize, required super.mainAxisAlignment, required super.crossAxisAlignment, required TextDirection super.textDirection, required super.verticalDirection, required this.onPerformLayout, }) : assert(onPerformLayout != null); _LayoutCallback? onPerformLayout; @override void performLayout() { super.performLayout(); // xOffsets will contain childCount+1 values, giving the offsets of the // leading edge of the first tab as the first value, of the leading edge of // the each subsequent tab as each subsequent value, and of the trailing // edge of the last tab as the last value. RenderBox? child = firstChild; final List xOffsets = []; while (child != null) { final FlexParentData childParentData = child.parentData as FlexParentData; xOffsets.add(childParentData.offset.dx); assert(child.parentData == childParentData); child = childParentData.nextSibling; } assert(textDirection != null); switch (textDirection) { case null: case TextDirection.rtl: xOffsets.insert(0, size.width); break; case TextDirection.ltr: xOffsets.add(size.width); break; } onPerformLayout!(xOffsets, textDirection, size.width); } } // This class and its renderer class only exist to report the widths of the tabs // upon layout. The tab widths are only used at paint time (see _IndicatorPainter) // or in response to input. class _TabLabelBar extends Flex { const _TabLabelBar({ super.children, this.onPerformLayout, }) : super( direction: Axis.horizontal, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, verticalDirection: VerticalDirection.down, ); final _LayoutCallback? onPerformLayout; @override RenderFlex createRenderObject(BuildContext context) { return _TabLabelBarRenderer( direction: direction, mainAxisAlignment: mainAxisAlignment, mainAxisSize: mainAxisSize, crossAxisAlignment: crossAxisAlignment, textDirection: getEffectiveTextDirection(context)!, verticalDirection: verticalDirection, onPerformLayout: onPerformLayout!, ); } @override void updateRenderObject( BuildContext context, _TabLabelBarRenderer renderObject) { super.updateRenderObject(context, renderObject); renderObject.onPerformLayout = onPerformLayout; } } double _indexChangeProgress(TabController controller) { final double controllerValue = controller.animation!.value; final double previousIndex = controller.previousIndex.toDouble(); final double currentIndex = controller.index.toDouble(); // The controller's offset is changing because the user is dragging the // TabBarView's PageView to the left or right. if (!controller.indexIsChanging) { return (currentIndex - controllerValue).abs().clamp(0.0, 1.0); } // The TabController animation's value is changing from previousIndex to currentIndex. return (controllerValue - currentIndex).abs() / (currentIndex - previousIndex).abs(); } class _IndicatorPainter extends CustomPainter { _IndicatorPainter({ required this.controller, required this.indicator, required this.indicatorSize, required this.tabKeys, _IndicatorPainter? old, }) : super(repaint: controller.animation) { if (old != null) { saveTabOffsets(old._currentTabOffsets, old._currentTextDirection); } } final TabController controller; final Decoration indicator; final TabBarIndicatorSize? indicatorSize; final List? tabKeys; List? _currentTabOffsets; TextDirection? _currentTextDirection; Rect? _currentRect; BoxPainter? _painter; bool _needsPaint = false; void markNeedsPaint() { _needsPaint = true; } void dispose() { _painter?.dispose(); } void saveTabOffsets(List? tabOffsets, TextDirection? textDirection) { _currentTabOffsets = tabOffsets; _currentTextDirection = textDirection; } // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab. int get maxTabIndex => _currentTabOffsets!.length - 2; double centerOf(int tabIndex) { assert(_currentTabOffsets != null); assert(_currentTabOffsets!.isNotEmpty); assert(tabIndex >= 0); assert(tabIndex <= maxTabIndex); return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) / 2.0; } Rect indicatorRect(Size tabBarSize, int tabIndex) { assert(_currentTabOffsets != null); assert(_currentTextDirection != null); assert(_currentTabOffsets!.isNotEmpty); assert(tabIndex >= 0); assert(tabIndex <= maxTabIndex); double? tabLeft; double? tabRight; switch (_currentTextDirection) { case null: case TextDirection.rtl: tabLeft = _currentTabOffsets![tabIndex + 1]; tabRight = _currentTabOffsets![tabIndex]; break; case TextDirection.ltr: tabLeft = _currentTabOffsets![tabIndex]; tabRight = _currentTabOffsets![tabIndex + 1]; break; } if (indicatorSize == TabBarIndicatorSize.label) { final double tabWidth = tabKeys![tabIndex].currentContext!.size!.width; final double delta = ((tabRight - tabLeft) - tabWidth) / 2.0; tabLeft += delta; tabRight -= delta; } return Rect.fromLTWH(tabLeft, 0.0, tabRight - tabLeft, tabBarSize.height); } @override void paint(Canvas canvas, Size size) { _needsPaint = false; _painter ??= indicator.createBoxPainter(markNeedsPaint); if (controller.indexIsChanging) { // The user tapped on a tab, the tab controller's animation is running. final Rect targetRect = indicatorRect(size, controller.index); _currentRect = Rect.lerp(targetRect, _currentRect ?? targetRect, _indexChangeProgress(controller)); } else { // The user is dragging the TabBarView's PageView left or right. final int currentIndex = controller.index; final Rect? previous = currentIndex > 0 ? indicatorRect(size, currentIndex - 1) : null; final Rect middle = indicatorRect(size, currentIndex); final Rect? next = currentIndex < maxTabIndex ? indicatorRect(size, currentIndex + 1) : null; final double index = controller.index.toDouble(); final double value = controller.animation!.value; if (value == index - 1.0) { _currentRect = previous ?? middle; } else if (value == index + 1.0) { _currentRect = next ?? middle; } else if (value == index) { _currentRect = middle; } else if (value < index) { _currentRect = previous == null ? middle : Rect.lerp(middle, previous, index - value); } else { _currentRect = next == null ? middle : Rect.lerp(middle, next, value - index); } } assert(_currentRect != null); final ImageConfiguration configuration = ImageConfiguration( size: _currentRect!.size, textDirection: _currentTextDirection, ); _painter!.paint(canvas, _currentRect!.topLeft, configuration); } static bool _tabOffsetsEqual(List? a, List? b) { if (a?.length != b?.length) return false; for (int i = 0; i < a!.length; i += 1) { if (a[i] != b![i]) return false; } return true; } @override bool shouldRepaint(_IndicatorPainter old) { return _needsPaint || controller != old.controller || indicator != old.indicator || tabKeys!.length != old.tabKeys!.length || (!_tabOffsetsEqual(_currentTabOffsets, old._currentTabOffsets)) || _currentTextDirection != old._currentTextDirection; } } class _ChangeAnimation extends Animation with AnimationWithParentMixin { _ChangeAnimation(this.controller); final TabController? controller; @override Animation get parent => controller!.animation!; @override double get value => _indexChangeProgress(controller!); } class _DragAnimation extends Animation with AnimationWithParentMixin { _DragAnimation(this.controller, this.index); final TabController? controller; final int index; @override Animation get parent => controller!.animation!; @override double get value { assert(!controller!.indexIsChanging); return (controller!.animation!.value - index.toDouble()) .abs() .clamp(0.0, 1.0); } } // This class, and TabBarScrollController, only exist to handle the case // where a scrollable TabBar has a non-zero initialIndex. In that case we can // only compute the scroll position's initial scroll offset (the "correct" // pixels value) after the TabBar viewport width and scroll limits are known. class _TabBarScrollPosition extends ScrollPositionWithSingleContext { _TabBarScrollPosition({ required super.physics, required super.context, super.oldPosition, this.tabBar, }) : super( initialPixels: null, ); final _TabBarState? tabBar; bool? _initialViewportDimensionWasZero; @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { bool result = true; if (_initialViewportDimensionWasZero != true) { // If the viewport never had a non-zero dimension, we just want to jump // to the initial scroll position to avoid strange scrolling effects in // release mode: In release mode, the viewport temporarily may have a // dimension of zero before the actual dimension is calculated. In that // scenario, setting the actual dimension would cause a strange scroll // effect without this guard because the super call below would starts a // ballistic scroll activity. _initialViewportDimensionWasZero = viewportDimension != 0.0; correctPixels(tabBar!._initialScrollOffset( viewportDimension, minScrollExtent, maxScrollExtent)); result = false; } return super.applyContentDimensions(minScrollExtent, maxScrollExtent) && result; } } // This class, and TabBarScrollPosition, only exist to handle the case // where a scrollable TabBar has a non-zero initialIndex. class _TabBarScrollController extends ScrollController { _TabBarScrollController(this.tabBar); final _TabBarState tabBar; @override ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition? oldPosition) { return _TabBarScrollPosition( physics: physics, context: context, oldPosition: oldPosition, tabBar: tabBar, ); } } /// A material design widget that displays a horizontal row of tabs. /// /// Typically created as the [AppBar.bottom] part of an [AppBar] and in /// conjunction with a [TabBarView]. /// /// If a [TabController] is not provided, then a [DefaultTabController] ancestor /// must be provided instead. The tab controller's [TabController.length] must /// equal the length of the [tabs] list and the length of the /// [TabBarView.children] list. /// /// Requires one of its ancestors to be a [Material] widget. /// /// Uses values from [TabBarTheme] if it is set in the current context. /// /// To see a sample implementation, visit the [TabController] documentation. /// /// See also: /// /// * [TabBarView], which displays page views that correspond to each tab. class TabBar extends StatefulWidget implements PreferredSizeWidget { /// Creates a material design tab bar. /// /// The [tabs] argument must not be null and its length must match the [controller]'s /// [TabController.length]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. /// /// The [indicatorWeight] parameter defaults to 2, and must not be null. /// /// The [indicatorPadding] parameter defaults to [EdgeInsets.zero], and must not be null. /// /// If [indicator] is not null, then [indicatorWeight], [indicatorPadding], and /// [indicatorColor] are ignored. const TabBar({ super.key, required this.tabs, this.controller, this.isScrollable = false, this.indicatorColor, this.indicatorWeight = 2.0, this.indicatorPadding = EdgeInsets.zero, this.indicator, this.indicatorSize, this.labelColor, this.labelStyle, this.labelPadding, this.unselectedLabelColor, this.unselectedLabelStyle, this.dragStartBehavior = DragStartBehavior.start, this.onTap, this.onDoubleTap, }); /// Typically a list of two or more [Tab] widgets. /// /// The length of this list must match the [controller]'s [TabController.length] /// and the length of the [TabBarView.children] list. final List tabs; /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController? controller; /// Whether this tab bar can be scrolled horizontally. /// /// If [isScrollable] is true, then each tab is as wide as needed for its label /// and the entire [TabBar] is scrollable. Otherwise each tab gets an equal /// share of the available space. final bool isScrollable; /// The color of the line that appears below the selected tab. /// /// If this parameter is null, then the value of the Theme's indicatorColor /// property is used. /// /// If [indicator] is specified, this property is ignored. final Color? indicatorColor; /// The thickness of the line that appears below the selected tab. /// /// The value of this parameter must be greater than zero and its default /// value is 2.0. /// /// If [indicator] is specified, this property is ignored. final double indicatorWeight; /// The horizontal padding for the line that appears below the selected tab. /// /// For [isScrollable] tab bars, specifying [kTabLabelPadding] will align /// the indicator with the tab's text for [Tab] widgets and all but the /// shortest [Tab.text] values. /// /// The [EdgeInsets.top] and [EdgeInsets.bottom] values of the /// [indicatorPadding] are ignored. /// /// The default value of [indicatorPadding] is [EdgeInsets.zero]. /// /// If [indicator] is specified, this property is ignored. final EdgeInsetsGeometry indicatorPadding; /// Defines the appearance of the selected tab indicator. /// /// If [indicator] is specified, the [indicatorColor], [indicatorWeight], /// and [indicatorPadding] properties are ignored. /// /// The default, underline-style, selected tab indicator can be defined with /// [UnderlineTabIndicator]. /// /// The indicator's size is based on the tab's bounds. If [indicatorSize] /// is [TabBarIndicatorSize.tab] the tab's bounds are as wide as the space /// occupied by the tab in the tab bar. If [indicatorSize] is /// [TabBarIndicatorSize.label], then the tab's bounds are only as wide as /// the tab widget itself. final Decoration? indicator; /// Defines how the selected tab indicator's size is computed. /// /// The size of the selected tab indicator is defined relative to the /// tab's overall bounds if [indicatorSize] is [TabBarIndicatorSize.tab] /// (the default) or relative to the bounds of the tab's widget if /// [indicatorSize] is [TabBarIndicatorSize.label]. /// /// The selected tab's location appearance can be refined further with /// the [indicatorColor], [indicatorWeight], [indicatorPadding], and /// [indicator] properties. final TabBarIndicatorSize? indicatorSize; /// The color of selected tab labels. /// /// Unselected tab labels are rendered with the same color rendered at 70% /// opacity unless [unselectedLabelColor] is non-null. /// /// If this parameter is null, then the color of the [ThemeData.primaryTextTheme]'s /// body2 text color is used. final Color? labelColor; /// The color of unselected tab labels. /// /// If this property is null, unselected tab labels are rendered with the /// [labelColor] with 70% opacity. final Color? unselectedLabelColor; /// The text style of the selected tab labels. /// /// If [unselectedLabelStyle] is null, then this text style will be used for /// both selected and unselected label styles. /// /// If this property is null, then the text style of the /// [ThemeData.primaryTextTheme]'s body2 definition is used. final TextStyle? labelStyle; /// The padding added to each of the tab labels. /// /// If this property is null, then kTabLabelPadding is used. final EdgeInsetsGeometry? labelPadding; /// The text style of the unselected tab labels /// /// If this property is null, then the [labelStyle] value is used. If [labelStyle] /// is null, then the text style of the [ThemeData.primaryTextTheme]'s /// body2 definition is used. final TextStyle? unselectedLabelStyle; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// An optional callback that's called when the [TabBar] is tapped. /// /// The callback is applied to the index of the tab where the tap occurred. /// /// This callback has no effect on the default handling of taps. It's for /// applications that want to do a little extra work when a tab is tapped, /// even if the tap doesn't change the TabController's index. TabBar [onTap] /// callbacks should not make changes to the TabController since that would /// interfere with the default tap handler. final ValueChanged? onTap; final ValueChanged? onDoubleTap; /// A size whose height depends on if the tabs have both icons and text. /// /// [AppBar] uses this this size to compute its own preferred size. @override Size get preferredSize { for (Widget item in tabs) { if (item is Tab) { final Tab tab = item; if (tab.text != null && tab.icon != null) { return Size.fromHeight(_kTextAndIconTabHeight + indicatorWeight); } } } return Size.fromHeight(_kTabHeight + indicatorWeight); } @override _TabBarState createState() => _TabBarState(); } class _TabBarState extends State { ScrollController? _scrollController; TabController? _controller; _IndicatorPainter? _indicatorPainter; int? _currentIndex; late double _tabStripWidth; List? _tabKeys; @override void initState() { super.initState(); // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find // the width of tab widget i. See _IndicatorPainter.indicatorRect(). _tabKeys = widget.tabs.map((Widget tab) => GlobalKey()).toList(); } Decoration? get _indicator { if (widget.indicator != null) return widget.indicator; final TabBarThemeData tabBarTheme = TabBarTheme.of(context); if (tabBarTheme.indicator != null) return tabBarTheme.indicator; Color color = widget.indicatorColor ?? TabBarTheme.of(context).indicatorColor ?? Colors.white; // ThemeData tries to avoid this by having indicatorColor avoid being the // primaryColor. However, it's possible that the tab bar is on a // Material that isn't the primaryColor. In that case, if the indicator // color ends up matching the material's color, then this overrides it. // When that happens, automatic transitions of the theme will likely look // ugly as the indicator color suddenly snaps to white at one end, but it's // not clear how to avoid that any further. // // The material's color might be null (if it's a transparency). In that case // there's no good way for us to find out what the color is so we don't. if (color == Material.of(context).color) color = Colors.white; return UnderlineTabIndicator( insets: widget.indicatorPadding, borderSide: BorderSide( width: widget.indicatorWeight, color: color, ), ); } // If the TabBar is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController newController = widget.controller ?? DefaultTabController.of(context); if (newController == _controller) return; if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); _controller!.removeListener(_handleTabControllerTick); } _controller = newController; if (_controller != null) { _controller!.animation!.addListener(_handleTabControllerAnimationTick); _controller!.addListener(_handleTabControllerTick); _currentIndex = _controller!.index; } } void _initIndicatorPainter() { _indicatorPainter = !_controllerIsValid ? null : _IndicatorPainter( controller: _controller!, indicator: _indicator!, indicatorSize: widget.indicatorSize ?? TabBarTheme.of(context).indicatorSize as TabBarIndicatorSize?, tabKeys: _tabKeys, old: _indicatorPainter, ); } @override void didChangeDependencies() { super.didChangeDependencies(); assert(debugCheckHasMaterial(context)); _updateTabController(); _initIndicatorPainter(); } @override void didUpdateWidget(TabBar oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) { _updateTabController(); _initIndicatorPainter(); } else if (widget.indicatorColor != oldWidget.indicatorColor || widget.indicatorWeight != oldWidget.indicatorWeight || widget.indicatorSize != oldWidget.indicatorSize || widget.indicator != oldWidget.indicator) { _initIndicatorPainter(); } if (widget.tabs.length > oldWidget.tabs.length) { final int delta = widget.tabs.length - oldWidget.tabs.length; _tabKeys!.addAll(List.generate(delta, (int n) => GlobalKey())); } else if (widget.tabs.length < oldWidget.tabs.length) { _tabKeys!.removeRange(widget.tabs.length, oldWidget.tabs.length); } } @override void dispose() { _indicatorPainter!.dispose(); if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); _controller!.removeListener(_handleTabControllerTick); } _controller = null; // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } int get maxTabIndex => _indicatorPainter!.maxTabIndex; double _tabScrollOffset( int? index, double viewportWidth, double minExtent, double maxExtent) { if (!widget.isScrollable) return 0.0; double tabCenter = _indicatorPainter!.centerOf(index!); switch (Directionality.of(context)) { case TextDirection.rtl: tabCenter = _tabStripWidth - tabCenter; break; case TextDirection.ltr: break; } return (tabCenter - viewportWidth / 2.0).clamp(minExtent, maxExtent); } double _tabCenteredScrollOffset(int? index) { final ScrollPosition position = _scrollController!.position; return _tabScrollOffset(index, position.viewportDimension, position.minScrollExtent, position.maxScrollExtent); } double _initialScrollOffset( double viewportWidth, double minExtent, double maxExtent) { return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent); } void _scrollToCurrentIndex() { final double offset = _tabCenteredScrollOffset(_currentIndex); _scrollController! .animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease); } void _scrollToControllerValue() { final double? leadingPosition = _currentIndex! > 0 ? _tabCenteredScrollOffset(_currentIndex! - 1) : null; final double middlePosition = _tabCenteredScrollOffset(_currentIndex); final double? trailingPosition = _currentIndex! < maxTabIndex ? _tabCenteredScrollOffset(_currentIndex! + 1) : null; final double index = _controller!.index.toDouble(); final double value = _controller!.animation!.value; double? offset; if (value == index - 1.0) { offset = leadingPosition ?? middlePosition; } else if (value == index + 1.0) { offset = trailingPosition ?? middlePosition; } else if (value == index) { offset = middlePosition; } else if (value < index) { offset = leadingPosition == null ? middlePosition : lerpDouble(middlePosition, leadingPosition, index - value); } else { offset = trailingPosition == null ? middlePosition : lerpDouble(middlePosition, trailingPosition, value - index); } _scrollController!.jumpTo(offset!); } void _handleTabControllerAnimationTick() { assert(mounted); if (!_controller!.indexIsChanging && widget.isScrollable) { // Sync the TabBar's scroll position with the TabBarView's PageView. _currentIndex = _controller!.index; _scrollToControllerValue(); } } void _handleTabControllerTick() { if (_controller!.index != _currentIndex) { _currentIndex = _controller!.index; if (widget.isScrollable) _scrollToCurrentIndex(); } setState(() { // Rebuild the tabs after a (potentially animated) index change // has completed. }); } // Called each time layout completes. void _saveTabOffsets( List? tabOffsets, TextDirection? textDirection, double? width) { _tabStripWidth = width!; _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection); } void _handleTap(int index) { assert(index >= 0 && index < widget.tabs.length); _controller!.animateTo(index); if (widget.onTap != null) { widget.onTap!(index); } } void _handleDoubleTap(int index) { assert(index >= 0 && index < widget.tabs.length); _controller!.animateTo(index); if (widget.onDoubleTap != null) { widget.onDoubleTap!(index); } } Widget _buildStyledTab( Widget? child, bool selected, Animation animation) { return _TabStyle( animation: animation, selected: selected, labelColor: widget.labelColor, unselectedLabelColor: widget.unselectedLabelColor, labelStyle: widget.labelStyle, unselectedLabelStyle: widget.unselectedLabelStyle, child: child, ); } @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); assert(() { if (_controller!.length != widget.tabs.length) { throw FlutterError( 'Controller\'s length property (${_controller!.length}) does not match the \n' 'number of tabs (${widget.tabs.length}) present in TabBar\'s tabs property.'); } return true; }()); final MaterialLocalizations localizations = MaterialLocalizations.of(context); if (_controller!.length == 0) { return Container( height: _kTabHeight + widget.indicatorWeight, ); } final TabBarThemeData tabBarTheme = TabBarTheme.of(context); List wrappedTabs = List.filled(widget.tabs.length, Container()); for (int i = 0; i < widget.tabs.length; i += 1) { wrappedTabs[i] = Center( heightFactor: 1.0, child: Padding( padding: widget.labelPadding ?? tabBarTheme.labelPadding ?? kTabLabelPadding, child: KeyedSubtree( key: _tabKeys![i], child: widget.tabs[i], ), ), ); } // If the controller was provided by DefaultTabController and we're part // of a Hero (typically the AppBar), then we will not be able to find the // controller during a Hero transition. See https://github.com/flutter/flutter/issues/213. if (_controller != null) { final int previousIndex = _controller!.previousIndex; if (_controller!.indexIsChanging) { // The user tapped on a tab, the tab controller's animation is running. assert(_currentIndex != previousIndex); final Animation animation = _ChangeAnimation(_controller); wrappedTabs[_currentIndex!] = _buildStyledTab(wrappedTabs[_currentIndex!], true, animation); wrappedTabs[previousIndex] = _buildStyledTab(wrappedTabs[previousIndex], false, animation); } else { // The user is dragging the TabBarView's PageView left or right. final int tabIndex = _currentIndex!; final Animation centerAnimation = _DragAnimation(_controller, tabIndex); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], true, centerAnimation); if (_currentIndex! > 0) { final int tabIndex = _currentIndex! - 1; final Animation previousAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex)); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, previousAnimation); } if (_currentIndex! < widget.tabs.length - 1) { final int tabIndex = _currentIndex! + 1; final Animation nextAnimation = ReverseAnimation(_DragAnimation(_controller, tabIndex)); wrappedTabs[tabIndex] = _buildStyledTab(wrappedTabs[tabIndex], false, nextAnimation); } } } // Add the tap handler to each tab. If the tab bar is not scrollable, // then give all of the tabs equal flexibility so that they each occupy // the same share of the tab bar's overall width. final int tabCount = widget.tabs.length; for (int index = 0; index < tabCount; index += 1) { wrappedTabs[index] = GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _handleTap(index); }, onDoubleTap: () { _handleDoubleTap(index); }, child: Padding( padding: EdgeInsets.only(bottom: widget.indicatorWeight), child: Stack( children: [ wrappedTabs[index], Semantics( selected: index == _currentIndex, label: localizations.tabLabel( tabIndex: index + 1, tabCount: tabCount), ), ], ), ), ); if (!widget.isScrollable) { wrappedTabs[index] = Expanded(child: wrappedTabs[index]); } } Widget tabBar = CustomPaint( painter: _indicatorPainter, child: _TabStyle( animation: kAlwaysDismissedAnimation, selected: false, labelColor: widget.labelColor, unselectedLabelColor: widget.unselectedLabelColor, labelStyle: widget.labelStyle, unselectedLabelStyle: widget.unselectedLabelStyle, child: _TabLabelBar( onPerformLayout: _saveTabOffsets, children: wrappedTabs, ), ), ); if (widget.isScrollable) { _scrollController ??= _TabBarScrollController(this); tabBar = SingleChildScrollView( dragStartBehavior: widget.dragStartBehavior, scrollDirection: Axis.horizontal, controller: _scrollController, child: tabBar, ); } return tabBar; } } /// A page view that displays the widget which corresponds to the currently /// selected tab. /// /// This widget is typically used in conjunction with a [TabBar]. /// /// If a [TabController] is not provided, then there must be a [DefaultTabController] /// ancestor. /// /// The tab controller's [TabController.length] must equal the length of the /// [children] list and the length of the [TabBar.tabs] list. /// /// To see a sample implementation, visit the [TabController] documentation. class TabBarView extends StatefulWidget { /// Creates a page view with one child per tab. /// /// The length of [children] must be the same as the [controller]'s length. const TabBarView({ super.key, required this.children, this.controller, this.physics, this.dragStartBehavior = DragStartBehavior.start, }); /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of [DefaultTabController.of] /// will be used. final TabController? controller; /// One widget per tab. /// /// Its length must match the length of the [TabBar.tabs] /// list, as well as the [controller]'s [TabController.length]. final List children; /// How the page view should respond to user input. /// /// For example, determines how the page view continues to animate after the /// user stops dragging the page view. /// /// The physics are modified to snap to page boundaries using /// [PageScrollPhysics] prior to being used. /// /// Defaults to matching platform conventions. final ScrollPhysics? physics; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; @override _TabBarViewState createState() => _TabBarViewState(); } final PageScrollPhysics _kTabBarViewPhysics = const PageScrollPhysics().applyTo(const ClampingScrollPhysics()); class _TabBarViewState extends State { TabController? _controller; PageController? _pageController; List? _children; List? _childrenWithKey; int? _currentIndex; int _warpUnderwayCount = 0; // If the TabBarView is rebuilt with a new tab controller, the caller should // dispose the old one. In that case the old controller's animation will be // null and should not be accessed. bool get _controllerIsValid => _controller?.animation != null; void _updateTabController() { final TabController newController = widget.controller ?? DefaultTabController.of(context); if (newController == _controller) return; if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = newController; if (_controller != null) { _controller!.animation!.addListener(_handleTabControllerAnimationTick); } } @override void initState() { super.initState(); _updateChildren(); } @override void didChangeDependencies() { super.didChangeDependencies(); _updateTabController(); _currentIndex = _controller?.index; _pageController = PageController(initialPage: _currentIndex ?? 0); } @override void didUpdateWidget(TabBarView oldWidget) { super.didUpdateWidget(oldWidget); if (widget.controller != oldWidget.controller) _updateTabController(); if (widget.children != oldWidget.children && _warpUnderwayCount == 0) { _updateChildren(); } } @override void dispose() { if (_controllerIsValid) { _controller!.animation!.removeListener(_handleTabControllerAnimationTick); } _controller = null; // We don't own the _controller Animation, so it's not disposed here. super.dispose(); } void _updateChildren() { _children = widget.children; _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children); } void _handleTabControllerAnimationTick() { if (_warpUnderwayCount > 0 || !_controller!.indexIsChanging) { return; // This widget is driving the controller's animation. } if (_controller!.index != _currentIndex) { _currentIndex = _controller!.index; _warpToCurrentIndex(); } } Future _warpToCurrentIndex() async { if (!mounted) return Future.value(); if (_pageController!.page == _currentIndex!.toDouble()) { return Future.value(); } final int previousIndex = _controller!.previousIndex; if ((_currentIndex! - previousIndex).abs() == 1) { return _pageController!.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease); } assert((_currentIndex! - previousIndex).abs() > 1); final int initialPage = _currentIndex! > previousIndex ? _currentIndex! - 1 : _currentIndex! + 1; final List? originalChildren = _childrenWithKey; setState(() { _warpUnderwayCount += 1; _childrenWithKey = List.from(_childrenWithKey!, growable: false); final Widget temp = _childrenWithKey![initialPage]; _childrenWithKey![initialPage] = _childrenWithKey![previousIndex]; _childrenWithKey![previousIndex] = temp; }); _pageController!.jumpToPage(initialPage); await _pageController!.animateToPage(_currentIndex!, duration: kTabScrollDuration, curve: Curves.ease); if (!mounted) return Future.value(); setState(() { _warpUnderwayCount -= 1; if (widget.children != _children) { _updateChildren(); } else { _childrenWithKey = originalChildren; } }); } // Called when the PageView scrolls bool _handleScrollNotification(ScrollNotification notification) { if (_warpUnderwayCount > 0) return false; if (notification.depth != 0) return false; _warpUnderwayCount += 1; if (notification is ScrollUpdateNotification && !_controller!.indexIsChanging) { if ((_pageController!.page! - _controller!.index).abs() > 1.0) { _controller!.index = _pageController!.page!.floor(); _currentIndex = _controller!.index; } _controller!.offset = (_pageController!.page! - _controller!.index).clamp(-1.0, 1.0); } else if (notification is ScrollEndNotification) { _controller!.index = _pageController!.page!.round(); _currentIndex = _controller!.index; } _warpUnderwayCount -= 1; return false; } @override Widget build(BuildContext context) { assert(() { if (_controller!.length != widget.children.length) { throw FlutterError( 'Controller\'s length property (${_controller!.length}) does not match the \n' 'number of tabs (${widget.children.length}) present in TabBar\'s tabs property.'); } return true; }()); return NotificationListener( onNotification: _handleScrollNotification, child: PageView( dragStartBehavior: widget.dragStartBehavior, controller: _pageController, physics: widget.physics == null ? _kTabBarViewPhysics : _kTabBarViewPhysics.applyTo(widget.physics), children: _childrenWithKey!, ), ); } } /// Displays a single circle with the specified border and background colors. /// /// Used by [TabPageSelector] to indicate the selected page. class TabPageSelectorIndicator extends StatelessWidget { /// Creates an indicator used by [TabPageSelector]. /// /// The [backgroundColor], [borderColor], and [size] parameters must not be null. const TabPageSelectorIndicator({ super.key, required this.backgroundColor, required this.borderColor, required this.size, }); /// The indicator circle's background color. final Color backgroundColor; /// The indicator circle's border color. final Color borderColor; /// The indicator circle's diameter. final double size; @override Widget build(BuildContext context) { return Container( width: size, height: size, margin: const EdgeInsets.all(4.0), decoration: BoxDecoration( color: backgroundColor, border: Border.all(color: borderColor), shape: BoxShape.circle, ), ); } } /// Displays a row of small circular indicators, one per tab. /// /// The selected tab's indicator is highlighted. Often used in conjunction with /// a [TabBarView]. /// /// If a [TabController] is not provided, then there must be a /// [DefaultTabController] ancestor. class TabPageSelector extends StatelessWidget { /// Creates a compact widget that indicates which tab has been selected. const TabPageSelector({ super.key, this.controller, this.indicatorSize = 12.0, this.color, this.selectedColor, }) : assert(indicatorSize > 0.0); /// This widget's selection and animation state. /// /// If [TabController] is not provided, then the value of /// [DefaultTabController.of] will be used. final TabController? controller; /// The indicator circle's diameter (the default value is 12.0). final double indicatorSize; /// The indicator circle's fill color for unselected pages. /// /// If this parameter is null, then the indicator is filled with [Colors.transparent]. final Color? color; /// The indicator circle's fill color for selected pages and border color /// for all indicator circles. /// /// If this parameter is null, then the indicator is filled with the theme's /// accent color, [ThemeData.colorScheme.secondary]. final Color? selectedColor; Widget _buildTabIndicator( int tabIndex, TabController tabController, ColorTween selectedColorTween, ColorTween previousColorTween, ) { Color? background; if (tabController.indexIsChanging) { // The selection's animation is animating from previousValue to value. final double t = 1.0 - _indexChangeProgress(tabController); if (tabController.index == tabIndex) { background = selectedColorTween.lerp(t); } else if (tabController.previousIndex == tabIndex) { background = previousColorTween.lerp(t); } else { background = selectedColorTween.begin; } } else { // The selection's offset reflects how far the TabBarView has / been dragged // to the previous page (-1.0 to 0.0) or the next page (0.0 to 1.0). final double offset = tabController.offset; if (tabController.index == tabIndex) { background = selectedColorTween.lerp(1.0 - offset.abs()); } else if (tabController.index == tabIndex - 1 && offset > 0.0) { background = selectedColorTween.lerp(offset); } else if (tabController.index == tabIndex + 1 && offset < 0.0) { background = selectedColorTween.lerp(-offset); } else { background = selectedColorTween.begin; } } return TabPageSelectorIndicator( backgroundColor: background!, borderColor: selectedColorTween.end!, size: indicatorSize, ); } @override Widget build(BuildContext context) { final Color fixColor = color ?? Colors.transparent; final Color fixSelectedColor = selectedColor ?? Theme.of(context).colorScheme.secondary; final ColorTween selectedColorTween = ColorTween(begin: fixColor, end: fixSelectedColor); final ColorTween previousColorTween = ColorTween(begin: fixSelectedColor, end: fixColor); final TabController tabController = controller ?? DefaultTabController.of(context); final Animation animation = CurvedAnimation( parent: tabController.animation!, curve: Curves.fastOutSlowIn, ); return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { return Semantics( child: Row( mainAxisSize: MainAxisSize.min, children: List.generate(tabController.length, (int tabIndex) { return _buildTabIndicator(tabIndex, tabController, selectedColorTween, previousColorTween); }).toList(), ), ); }, ); } } ================================================ FILE: lib/widget/gsy_title_bar.dart ================================================ import 'package:flutter/material.dart'; /// title 控件 /// Created by guoshuyu /// on 2018/7/24. class GSYTitleBar extends StatelessWidget { final String? title; final IconData? iconData; final ValueChanged? onRightIconPressed; final bool needRightLocalIcon; final Widget? rightWidget; final GlobalKey rightKey = GlobalKey(); GSYTitleBar(this.title, {super.key, this.iconData, this.onRightIconPressed, this.needRightLocalIcon = false, this.rightWidget}); @override Widget build(BuildContext context) { Widget? widget = rightWidget; if (rightWidget == null) { widget = (needRightLocalIcon) ? IconButton( icon: Icon( iconData, key: rightKey, size: 19.0, ), onPressed: () { RenderBox renderBox2 = rightKey.currentContext?.findRenderObject() as RenderBox; var position = renderBox2.localToGlobal(Offset.zero); var size = renderBox2.size; var centerPosition = Offset( position.dx + size.width / 2, position.dy + size.height / 2, ); onRightIconPressed?.call(centerPosition); }) : Container(); } return Row( children: [ Expanded( child: Text( title!, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), widget! ], ); } } ================================================ FILE: lib/widget/gsy_user_icon_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; /// 头像Icon /// Created by guoshuyu /// Date: 2018-07-30 class GSYUserIconWidget extends StatelessWidget { final String? image; final VoidCallback? onPressed; final double width; final double height; final EdgeInsetsGeometry? padding; const GSYUserIconWidget( {super.key, this.image, this.onPressed, this.width = 30.0, this.height = 30.0, this.padding}); @override Widget build(BuildContext context) { return RawMaterialButton( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: padding ?? const EdgeInsets.only(top: 4.0, right: 5.0, left: 5.0), constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0), onPressed: onPressed, child: ClipOval( child: FadeInImage( placeholder: const AssetImage( GSYICons.DEFAULT_USER_ICON, ), image: NetworkImage(image ?? GSYICons.DEFAULT_REMOTE_PIC), //预览图 fit: BoxFit.fitWidth, width: width, height: height, ), )); } } ================================================ FILE: lib/widget/markdown/gsy_markdown_widget.dart ================================================ // ignore_for_file: unnecessary_string_escapes import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/common/utils/common_utils.dart'; import 'package:gsy_github_app_flutter/widget/markdown/syntax_high_lighter.dart'; /// 代码详情 /// Created by guoshuyu /// Date: 2018-07-24 class GSYMarkdownWidget extends StatelessWidget { static const int DARK_WHITE = 0; static const int DARK_LIGHT = 1; static const int DARK_THEME = 2; final String? markdownData; final String baseUrl; final int style; final bool shrinkWrap; final bool scroll; const GSYMarkdownWidget( {super.key, this.markdownData = "", required this.baseUrl, this.shrinkWrap = false, this.scroll = true, this.style = DARK_WHITE}); _getCommonSheet(BuildContext context, Color codeBackground) { MarkdownStyleSheet markdownStyleSheet = MarkdownStyleSheet.fromTheme(Theme.of(context)); return markdownStyleSheet .copyWith( codeblockDecoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), color: codeBackground, border: Border.all(color: GSYColors.subTextColor, width: 0.3))) .copyWith( blockquoteDecoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), color: GSYColors.subTextColor, border: Border.all(color: GSYColors.subTextColor, width: 0.3)), blockquote: GSYConstant.smallTextWhite); } _getStyleSheetDark(BuildContext context) { return _getCommonSheet(context, const Color.fromRGBO(40, 44, 52, 1.00)) .copyWith( p: GSYConstant.smallTextWhite, h1: GSYConstant.largeLargeTextWhite, h2: GSYConstant.largeTextWhiteBold, h3: GSYConstant.normalTextMitWhiteBold, h4: GSYConstant.middleTextWhite, h5: GSYConstant.smallTextWhite, h6: GSYConstant.smallTextWhite, em: const TextStyle(fontStyle: FontStyle.italic), strong: GSYConstant.middleTextWhiteBold, code: GSYConstant.smallSubText, ); } _getStyleSheetWhite(BuildContext context) { return _getCommonSheet(context, const Color.fromRGBO(40, 44, 52, 1.00)) .copyWith( p: GSYConstant.smallText, h1: GSYConstant.largeLargeText, h2: GSYConstant.largeTextBold, h3: GSYConstant.normalTextBold, h4: GSYConstant.middleText, h5: GSYConstant.smallText, h6: GSYConstant.smallText, strong: GSYConstant.middleTextBold, code: GSYConstant.smallSubText, ); } _getStyleSheetTheme(BuildContext context) { return _getCommonSheet(context, const Color.fromRGBO(40, 44, 52, 1.00)) .copyWith( p: GSYConstant.smallTextWhite, h1: GSYConstant.largeLargeTextWhite, h2: GSYConstant.largeTextWhiteBold, h3: GSYConstant.normalTextMitWhiteBold, h4: GSYConstant.middleTextWhite, h5: GSYConstant.smallTextWhite, h6: GSYConstant.smallTextWhite, em: const TextStyle(fontStyle: FontStyle.italic), strong: GSYConstant.middleTextWhiteBold, code: GSYConstant.smallSubText, ); } _getBackgroundColor(BuildContext context) { Color background = GSYColors.white; switch (style) { case DARK_LIGHT: background = GSYColors.primaryLightValue; break; case DARK_THEME: background = Theme.of(context).primaryColor; break; } return background; } _getStyle(BuildContext context) { var styleSheet = _getStyleSheetWhite(context); switch (style) { case DARK_LIGHT: styleSheet = _getStyleSheetDark(context); break; case DARK_THEME: styleSheet = _getStyleSheetTheme(context); break; } return styleSheet; } /// 处理 Markdown 字符串,转换 img/image 标签为 Markdown 图片格式,并处理 URL。 /// /// 1. 将 替换为 ![alt](src) 格式。 /// 2. 为非 http/https 开头的 src 添加 baseUrl。 /// 3. 为最终的 URL 添加 ?raw=true 或 &raw=true (如果尚未存在)。 /// /// @param markdownInput 输入的 Markdown 字符串。 /// @param baseUrl 用于拼接相对路径的基础 URL。 /// @return 处理后的 Markdown 字符串。 String _processMarkdownImages(String markdownInput, String baseUrl) { // 确保 baseUrl 以 / 结尾,方便路径拼接 if (!baseUrl.endsWith('/')) { baseUrl += '/'; } final baseUri = Uri.parse(baseUrl); // 正则表达式匹配 标签,以及 Markdown 图片 ![]() // 捕获组: // Group 1: img/image 标签的 src // Group 2: img/image 标签的 alt (可选) // Group 3: Markdown 图片的 alt // Group 4: Markdown 图片的 url final pattern = RegExp( '' // 匹配 ... '|' // 匹配 ... (理论上 image 不标准,但以防万一) '|!\\[(.*?)\\]\\((.*?)\\)', // 匹配 ![alt](url) caseSensitive: false, // 忽略大小写 multiLine: true // 支持多行匹配 ); String result = markdownInput.replaceAllMapped(pattern, (match) { String? originalUrl; String? altText; // 检查是匹配了 img/image 标签还是 Markdown 图片 if (match.group(1) != null || match.group(3) != null) { // 匹配到 originalUrl = match.group(1) ?? match.group(3); // 获取 src altText = match.group(2) ?? match.group(4) ?? ''; // 获取 alt,如果不存在则为空字符串 } else if (match.group(6) != null) { // 匹配到 ![]() originalUrl = match.group(6); // 获取 url altText = match.group(5) ?? ''; // 获取 alt } if (originalUrl == null || originalUrl.isEmpty) { // 如果没有匹配到有效的 URL,返回原始匹配项 return match.group(0)!; } // --- 2. 处理 URL 添加 baseUrl --- String processedUrl; Uri parsedOriginalUrl; try { // 尝试解析原始 URL,看它是否已经是绝对路径 parsedOriginalUrl = Uri.parse(originalUrl.trim()); // 去除前后空格 if (parsedOriginalUrl.scheme == 'http' || parsedOriginalUrl.scheme == 'https') { processedUrl = originalUrl.trim(); } else { // 是相对路径,需要拼接 baseUrl // Uri.resolve 能正确处理 "./", "/", "filename" 等情况 processedUrl = baseUri.resolve(originalUrl.trim()).toString(); } } catch (e) { // 如果 URL 解析失败 (格式错误),尝试作为相对路径直接拼接 printLog( 'Warning: Could not parse URL "$originalUrl". Treating as relative path. Error: $e'); try { processedUrl = baseUri.resolve(originalUrl.trim()).toString(); } catch (resolveError) { printLog( 'Error: Could not resolve relative path "$originalUrl" with base "$baseUrl". Keeping original. Error: $resolveError'); processedUrl = originalUrl.trim(); // 拼接也失败,保留原始值 } } // --- 3. 处理 raw=true --- Uri finalUri; try { finalUri = Uri.parse(processedUrl); Map queryParameters = Map.from(finalUri.queryParametersAll); // 使用 All 支持同名参数,虽然这里不一定需要 // 检查 'raw' 参数是否已经是 'true' if (queryParameters['raw']?.contains('true') != true) { queryParameters['raw'] = 'true'; // 添加或覆盖 raw 参数 finalUri = finalUri.replace(queryParameters: queryParameters); processedUrl = finalUri.toString(); } // 注意: Uri.replace 会自动处理 ? 和 & 的添加 } catch (e) { printLog( 'Warning: Could not parse final URL "$processedUrl" for adding raw=true. Skipping. Error: $e'); // 解析失败,无法添加 raw=true,保留之前的 processedUrl } // 返回 Markdown 格式的图片 return '![$altText]($processedUrl)'; }); return result; } @override Widget build(BuildContext context) { return Container( color: _getBackgroundColor(context), padding: const EdgeInsets.all(5.0), child: Markdown( styleSheet: _getStyle(context), syntaxHighlighter: GSYHighlighter(), shrinkWrap: shrinkWrap, physics: scroll ? null : const NeverScrollableScrollPhysics(), data: _processMarkdownImages(markdownData!, baseUrl), imageBuilder: (Uri? uri, String? title, String? alt) { if (uri == null || uri.toString().isEmpty) return const SizedBox(); final StringList parts = uri.toString().split('#'); double? width; double? height; if (parts.length == 2) { final StringList dimensions = parts.last.split('x'); if (dimensions.length == 2) { var [ws, hs] = dimensions; width = double.parse(ws); height = double.parse(hs); } } return kDefaultImageBuilder(uri, "", width, height); }, onTapLink: (String text, String? href, String title) { CommonUtils.gsyLaunchUrl(context, href); }, ), ); } } class GSYHighlighter extends SyntaxHighlighter { @override TextSpan format(String source) { String showSource = source.replaceAll("<", "<"); showSource = showSource.replaceAll(">", ">"); return DartSyntaxHighlighter().format(showSource); } } Widget kDefaultImageBuilder( Uri uri, String? imageDirectory, double? width, double? height, ) { if (uri.scheme.isEmpty) { return const SizedBox(); } if (uri.scheme == 'http' || uri.scheme == 'https') { return FutureBuilder( future: _isUrlPointingToSvgDio(uri.toString()), builder: (c, snapshot) { if (snapshot.hasData) { if (snapshot.data == true) { return SvgPicture.network( uri.toString(), // Add fallback dimensions width: width ?? 24.0, height: height ?? 24.0, allowDrawingOutsideViewBox: false, placeholderBuilder: (BuildContext context) => SizedBox( child: Center( child: SpinKitRipple(color: Theme.of(context).primaryColor), ), ), ); } else { return Image.network(uri.toString(), width: width, height: height); } } else { return const SizedBox(); } }, ); } else if (uri.scheme == 'data') { return _handleDataSchemeUri(uri, width, height); } else if (uri.scheme == "resource") { return Image.asset(uri.path, width: width, height: height); } else { Uri fileUri = imageDirectory != null ? Uri.parse(imageDirectory + uri.toString()) : uri; if (fileUri.scheme == 'http' || fileUri.scheme == 'https') { return Image.network(fileUri.toString(), width: width, height: height); } else { return Image.file(File.fromUri(fileUri), width: width, height: height); } } } Widget _handleDataSchemeUri( Uri uri, final double? width, final double? height) { final String mimeType = uri.data!.mimeType; if (mimeType.startsWith('image/')) { return Image.memory( uri.data!.contentAsBytes(), width: width, height: height, ); } else if (mimeType.startsWith('text/')) { return Text(uri.data!.contentAsString()); } return const SizedBox(); } /// 使用 Dio 检查给定 URL 指向的资源是否可能是 SVG。 /// /// 它优先尝试检查 Content-Type header (通过 HEAD 或 GET 请求)。 /// 如果 Content-Type 不明确,它会下载响应体的一小部分并检查其内容 /// 是否以 SVG 的 XML 标记开头。 /// /// [urlString]: 要检查的 URL 字符串。 /// [timeout]: 整个检查过程的超时设置 (连接和接收)。 /// /// 返回 Future,如果资源被认为是 SVG,则为 true,否则为 false。 Future _isUrlPointingToSvgDio(String urlString, {Duration timeout = const Duration(seconds: 15)}) async { Uri uri; try { uri = Uri.parse(urlString); // Dio 通常处理 scheme,但基础验证有帮助 if (!uri.isAbsolute || !['http', 'https'].contains(uri.scheme)) { throw const FormatException("URL 必须是绝对的 http 或 https URL"); } } catch (e) { printLog("无效的 URL 格式: $urlString - $e"); return false; } // 创建 Dio 实例 // 可以为 Dio 实例配置全局选项,或在每个请求中指定 final dio = Dio(BaseOptions( connectTimeout: timeout, // 连接超时 receiveTimeout: timeout, // 接收数据超时 // 不自动跟随重定向,如果需要检查原始响应头的话。 // 但通常我们关心最终资源,所以让它跟随重定向(默认行为) // followRedirects: false, // validateStatus: (status) { // // 接受所有状态码,以便我们可以在下面处理它们 // return status != null && status < 500; // 例如,只处理非服务器错误 // }, )); try { // --- 1. 尝试 HEAD 请求获取 Headers (更高效) --- try { // 注意:并非所有服务器都正确支持或允许 HEAD 请求 final headResponse = await dio.head(urlString); // 超时由 Dio 配置处理 // Dio 默认只认为 2xx 是成功,这里 statusCode 检查可省略,除非自定义了 validateStatus // if (headResponse.statusCode != null && headResponse.statusCode! >= 200 && headResponse.statusCode! < 300) { final contentType = headResponse.headers .value(Headers.contentTypeHeader); // 获取 Content-Type printLog("HEAD Content-Type for $urlString: $contentType"); if (contentType != null && contentType.toLowerCase().contains('image/svg+xml')) { return true; // 明确是 SVG } // 如果 Content-Type 不明确,我们仍需尝试 GET // } } on DioException catch (e) { // HEAD 请求失败很常见 (例如 405 Method Not Allowed),不应阻止尝试 GET if (e.type == DioExceptionType.badResponse && e.response?.statusCode == 405) { printLog( "HEAD request received 405 (Method Not Allowed) for $urlString. Falling back to GET."); } else { printLog( "HEAD request failed for $urlString: ${e.type} - ${e.message}. Falling back to GET."); } // 继续执行 GET 请求 } // --- 2. 如果 HEAD 不可行或 Content-Type 不确定,使用 GET 请求 --- final response = await dio.get( urlString, options: Options( responseType: ResponseType.bytes, // **重要**: 获取原始字节用于嗅探 ), ); // 再次检查状态码 (虽然 Dio 默认会为非 2xx 抛出异常,除非配置了 validateStatus) if (response.statusCode != null && response.statusCode! >= 200 && response.statusCode! < 300) { // 2.1 再次检查 GET 请求的 Content-Type Header final contentType = response.headers.value(Headers.contentTypeHeader); printLog("GET Content-Type for $urlString: $contentType"); if (contentType != null && contentType.toLowerCase().contains('image/svg+xml')) { return true; // 明确是 SVG } // 2.2 Content-Type 不明确? 进行内容嗅探 (Content Sniffing) if (response.data is List) { final bodyBytes = response.data as List; if (bodyBytes.isEmpty) { printLog("Response body is empty for $urlString."); return false; // 空内容不是 SVG } const bytesToCheck = 200; // 检查开头的字节数 final startBytes = bodyBytes.length > bytesToCheck ? bodyBytes.sublist(0, bytesToCheck) : bodyBytes; try { // 尝试用 UTF-8 解码开头部分 final bodyStart = utf8.decode(startBytes, allowMalformed: true).trimLeft(); // 检查是否以 50 ? 50 : bodyStart.length)}'"); } } catch (e) { printLog( "Error decoding or checking response body start for $urlString: $e"); // 解码错误可能意味着它不是基于文本的 SVG return false; } } else { // 这通常不应该发生,因为我们设置了 ResponseType.bytes printLog( "Unexpected response data type: ${response.data?.runtimeType} for $urlString"); return false; } } else { // 如果自定义了 validateStatus,需要在这里处理非 2xx 状态码 printLog( "GET request failed or returned non-2xx status: ${response.statusCode} for $urlString"); return false; } } on DioException catch (e) { // 处理 Dio 的特定异常 printLog("DioException caught while checking URL $urlString:"); printLog(" Type: ${e.type}"); if (e.response != null) { printLog(" Status Code: ${e.response?.statusCode}"); printLog(" Status Message: ${e.response?.statusMessage}"); } if (e.message != null) { printLog(" Message: ${e.message}"); } return false; } catch (e) { // 捕获其他任何意外错误 printLog("Unexpected error checking URL $urlString: $e"); return false; } // 如果所有检查都未通过 return false; } ================================================ FILE: lib/widget/markdown/syntax_high_lighter.dart ================================================ import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:string_scanner/string_scanner.dart'; import 'package:flutter/material.dart'; class SyntaxHighlighterStyle { SyntaxHighlighterStyle( {this.baseStyle, this.numberStyle, this.commentStyle, this.keywordStyle, this.stringStyle, this.punctuationStyle, this.classStyle, this.constantStyle}); //123 static SyntaxHighlighterStyle defaultStyle() { return SyntaxHighlighterStyle( baseStyle: const TextStyle(color: Color.fromRGBO(212, 212, 212, 1.0)), numberStyle: TextStyle(color: Colors.blue[800]), commentStyle: const TextStyle(color: Color.fromRGBO(124, 126, 120, 1.0)), keywordStyle: const TextStyle(color: Color.fromRGBO(228, 125, 246, 1.0)), stringStyle: const TextStyle(color: Color.fromRGBO(150, 190, 118, 1.0)), punctuationStyle: const TextStyle(color: Color.fromRGBO(212, 212, 212, 1.0)), classStyle: const TextStyle(color: Color.fromRGBO(150, 190, 118, 1.0)), constantStyle: TextStyle(color: Colors.brown[500])); } final TextStyle? baseStyle; final TextStyle? numberStyle; final TextStyle? commentStyle; final TextStyle? keywordStyle; final TextStyle? stringStyle; final TextStyle? punctuationStyle; final TextStyle? classStyle; final TextStyle? constantStyle; } abstract class SyntaxCostomHighlighter { TextSpan format(String src); } class DartSyntaxHighlighter extends SyntaxCostomHighlighter { DartSyntaxHighlighter([this._style]) { _spans = <_HighlightSpan>[]; _style ??= SyntaxHighlighterStyle.defaultStyle(); } SyntaxHighlighterStyle? _style; static const List _kKeywords = [ 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'default', 'deferred', 'do', 'dynamic', 'else', 'enum', 'export', 'external', 'extends', 'factory', 'false', 'final', 'finally', 'for', 'get', 'if', 'implements', 'import', 'in', 'is', 'library', 'new', 'null', 'operator', 'part', 'rethrow', 'return', 'set', 'static', 'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var', 'void', 'while', 'with', 'yield', 'print', 'function', 'public', 'protected', 'private', 'namespace', 'using', 'extends', 'let', 'export', 'default', 'import', 'from', 'PureCommponent', 'constructor', 'render', '\$sudo', 'console', 'instanceof' ]; static const List _kBuiltInTypes = [ 'int', 'double', 'num', 'bool' ]; late String _src; late StringScanner _scanner; late List<_HighlightSpan> _spans; @override TextSpan format(String src) { _src = src; _scanner = StringScanner(_src); if (_generateSpans()) { // Successfully parsed the code List formattedText = []; int currentPosition = 0; for (_HighlightSpan span in _spans) { if (currentPosition != span.start) { formattedText .add(TextSpan(text: _src.substring(currentPosition, span.start))); } formattedText.add(TextSpan( style: span.textStyle(_style), text: span.textForSpan(_src))); currentPosition = span.end; } if (currentPosition != _src.length) { formattedText .add(TextSpan(text: _src.substring(currentPosition, _src.length))); } return TextSpan(style: _style!.baseStyle, children: formattedText); } else { // Parsing failed, return with only basic formatting return TextSpan(style: _style!.baseStyle, text: src); } } bool _generateSpans() { int lastLoopPosition = _scanner.position; try { while (!_scanner.isDone) { // Skip White space _scanner.scan(RegExp(r"\s+")); // Block comments if (_scanner.scan(RegExp(r"/\*(.|\n)*\*/"))) { _spans.add(_HighlightSpan(_HighlightType.comment, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Line comments if (_scanner.scan("//")) { int startComment = _scanner.lastMatch!.start; bool eof = false; int endComment; if (_scanner.scan(RegExp(r".*\n"))) { endComment = _scanner.lastMatch!.end - 1; } else { eof = true; endComment = _src.length; } _spans.add( _HighlightSpan(_HighlightType.comment, startComment, endComment)); if (eof) break; continue; } if (_scanner.scan("#")) { int startComment = _scanner.lastMatch!.start; bool eof = false; int endComment; if (_scanner.scan(RegExp(r".*\n"))) { endComment = _scanner.lastMatch!.end - 1; } else { eof = true; endComment = _src.length; } _spans.add( _HighlightSpan(_HighlightType.comment, startComment, endComment)); if (eof) break; continue; } // Raw r"String" if (_scanner.scan(RegExp(r'r".*"'))) { _spans.add(_HighlightSpan(_HighlightType.string, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Raw r'String' if (_scanner.scan(RegExp(r"r'.*'"))) { _spans.add(_HighlightSpan(_HighlightType.string, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Multiline """String""" if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) { _spans.add(_HighlightSpan(_HighlightType.string, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Multiline '''String''' if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) { _spans.add(_HighlightSpan(_HighlightType.string, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // "String" if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) { _spans.add(_HighlightSpan(_HighlightType.string, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // 'String' if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) { _spans.add(_HighlightSpan(_HighlightType.string, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Double if (_scanner.scan(RegExp(r"\d+\.\d+"))) { _spans.add(_HighlightSpan(_HighlightType.number, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Integer if (_scanner.scan(RegExp(r"\d+"))) { _spans.add(_HighlightSpan(_HighlightType.number, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Punctuation if (_scanner.scan(RegExp(r"[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]"))) { _spans.add(_HighlightSpan(_HighlightType.punctuation, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } //中文 if (_scanner.scan(RegExp(r"[\u4e00-\u9fa5]"))) { _spans.add(_HighlightSpan(_HighlightType.punctuation, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Metadata if (_scanner.scan(RegExp(r"@\w+"))) { _spans.add(_HighlightSpan(_HighlightType.keyword, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); continue; } // Words if (_scanner.scan(RegExp(r"\w+"))) { _HighlightType? type; String word = _scanner.lastMatch![0]!; if (word.startsWith("_")) word = word.substring(1); if (_kKeywords.contains(word)) { type = _HighlightType.keyword; } else if (_kBuiltInTypes.contains(word)) { type = _HighlightType.keyword; } else if (_firstLetterIsUpperCase(word)) { type = _HighlightType.klass; } else if (word.length >= 2 && word.startsWith("k") && _firstLetterIsUpperCase(word.substring(1))) { type = _HighlightType.constant; } if (type != null) { _spans.add(_HighlightSpan( type, _scanner.lastMatch!.start, _scanner.lastMatch!.end)); } } // Check if this loop did anything if (lastLoopPosition == _scanner.position) { // Failed to parse this file, abort gracefully if (_spans.isNotEmpty) { _spans.add(_HighlightSpan(_HighlightType.punctuation, lastLoopPosition, _scanner.string.length - 1)); _simplify(); return true; } return false; } lastLoopPosition = _scanner.position; } } catch (e) { printLog(e); } _simplify(); return true; } void _simplify() { for (int i = _spans.length - 2; i >= 0; i -= 1) { if (_spans[i].type == _spans[i + 1].type && _spans[i].end == _spans[i + 1].start) { _spans[i] = _HighlightSpan(_spans[i].type, _spans[i].start, _spans[i + 1].end); _spans.removeAt(i + 1); } } } bool _firstLetterIsUpperCase(String str) { if (str.isNotEmpty) { String first = str.substring(0, 1); return first == first.toUpperCase(); } return false; } } enum _HighlightType { number, comment, keyword, string, punctuation, klass, constant } class _HighlightSpan { _HighlightSpan(this.type, this.start, this.end); final _HighlightType type; final int start; final int end; String textForSpan(String src) { return src.substring(start, end); } TextStyle? textStyle(SyntaxHighlighterStyle? style) { if (type == _HighlightType.number) { return style!.numberStyle; } else if (type == _HighlightType.comment) { return style!.commentStyle; } else if (type == _HighlightType.keyword) { return style!.keywordStyle; } else if (type == _HighlightType.string) { return style!.stringStyle; } else if (type == _HighlightType.punctuation) { return style!.punctuationStyle; } else if (type == _HighlightType.klass) { return style!.classStyle; } else if (type == _HighlightType.constant) { return style!.constantStyle; } else { return style!.baseStyle; } } } ================================================ FILE: lib/widget/menu/flutter_radial_menu.dart ================================================ library flutter_radial_menu; export 'src/radial_menu.dart'; export 'src/radial_menu_item.dart'; ================================================ FILE: lib/widget/menu/src/arc_progress_indicator.dart ================================================ import 'dart:math' as Math; import 'package:flutter/material.dart'; /// Draws an [ActionIcon] and [_ArcProgressPainter] that represent an active action. /// As the provided [Animation] progresses the ActionArc grows into a full /// circle and the ActionIcon moves along it. class ArcProgressIndicator extends StatelessWidget { // required final Animation controller; final double radius; // optional final double startAngle; final double? width; /// The color to use when filling the arc. /// /// Defaults to the accent color of the current theme. final Color? color; final IconData? icon; final Color? iconColor; final double? iconSize; // private final Animation _progress; ArcProgressIndicator({super.key, required this.controller, required this.radius, this.startAngle = 0.0, this.width, this.color, this.icon, this.iconColor, this.iconSize, }) : _progress = Tween(begin: 0.0, end: 1.0).animate(controller); @override Widget build(BuildContext context) { TextPainter? iconPainter; final ThemeData theme = Theme.of(context); final Color iconColor = this.iconColor ?? theme.colorScheme.secondary; final double? iconSize = this.iconSize ?? IconTheme.of(context).size; if (icon != null) { iconPainter = TextPainter( textDirection: Directionality.of(context), text: TextSpan( text: String.fromCharCode(icon!.codePoint), style: TextStyle( inherit: false, color: iconColor, fontSize: iconSize, fontFamily: icon!.fontFamily, package: icon!.fontPackage, ), ), )..layout(); } return CustomPaint( painter: _ArcProgressPainter( controller: _progress, color: color ?? theme.colorScheme.secondary, radius: radius, width: width ?? iconSize! * 2, startAngle: startAngle, icon: iconPainter, ), ); } } class _ArcProgressPainter extends CustomPainter { // required final Animation controller; final Color color; final double radius; final double width; // optional final double startAngle; final TextPainter? icon; _ArcProgressPainter({ required this.controller, required this.color, required this.radius, required this.width, this.startAngle = 0.0, this.icon, }) : super(repaint: controller); @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = color ..strokeWidth = width ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; final double sweepAngle = controller.value * 2 * Math.pi; canvas.drawArc( Offset.zero & size, startAngle, sweepAngle, false, paint, ); if (icon != null) { double angle = startAngle + sweepAngle; Offset offset = Offset( (size.width / 2 - icon!.size.width / 2) + radius * Math.cos(angle), (size.height / 2 - icon!.size.height / 2) + radius * Math.sin(angle), ); icon!.paint(canvas, offset); } } @override bool shouldRepaint(_ArcProgressPainter other) { return controller.value != other.controller.value || color != other.color || radius != other.radius || width != other.width || startAngle != other.startAngle || icon != other.icon; } } ================================================ FILE: lib/widget/menu/src/radial_menu.dart ================================================ import 'dart:async'; import 'dart:math' as Math; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/widget/menu/src/arc_progress_indicator.dart'; import 'package:gsy_github_app_flutter/widget/menu/src/radial_menu_button.dart'; import 'package:gsy_github_app_flutter/widget/menu/src/radial_menu_center_button.dart'; import 'package:gsy_github_app_flutter/widget/menu/src/radial_menu_item.dart'; const double _radiansPerDegree = Math.pi / 180; const double _startAngle = -90.0 * _radiansPerDegree; typedef ItemAngleCalculator = double Function(int index); /// A radial menu for selecting from a list of items. /// /// A radial menu lets the user select from a number of items. It displays a /// button that opens the menu, showing its items arranged in an arc. Selecting /// an item triggers the animation of a progress bar drawn at the specified /// [radius] around the central menu button. /// /// The type `T` is the type of the values the radial menu represents. All the /// entries in a given menu must represent values with consistent types. /// Typically, an enum is used. Each [RadialMenuItem] in [items] must be /// specialized with that same type argument. /// /// Requires one of its ancestors to be a [Material] widget. /// /// See also: /// /// * [RadialMenuItem], the widget used to represent the [items]. /// * [RadialMenuCenterButton], the button used to open and close the menu. class RadialMenu extends StatefulWidget { /// Creates a dropdown button. /// /// The [items] must have distinct values. /// /// The [radius], [menuAnimationDuration], and [progressAnimationDuration] /// arguments must not be null (they all have defaults, so do not need to be /// specified). const RadialMenu({ super.key, required this.items, required this.onSelected, this.radius = 50.0, this.menuAnimationDuration = const Duration(milliseconds: 1000), this.progressAnimationDuration = const Duration(milliseconds: 1000), }) ; /// The list of possible items to select among. final List> items; /// Called when the user selects an item. final ValueChanged? onSelected; /// The radius of the arc used to lay out the items and draw the progress bar. /// /// Defaults to 100.0. final double radius; /// Duration of the menu opening/closing animation. /// /// Defaults to 1000 milliseconds. final Duration menuAnimationDuration; /// Duration of the action activation progress arc animation. /// /// Defaults to 1000 milliseconds. final Duration progressAnimationDuration; @override RadialMenuState createState() => RadialMenuState(); } class RadialMenuState extends State with TickerProviderStateMixin { late AnimationController _menuAnimationController; late AnimationController _progressAnimationController; bool _isOpen = false; int? _activeItemIndex; // todo: xqwzts: allow users to pass in their own calculator as a param. // and change this to the default: radialItemAngleCalculator. double calculateItemAngle(int index) { double itemSpacing = 360.0 / widget.items.length; return _startAngle + index * itemSpacing * _radiansPerDegree; } @override void initState() { super.initState(); _menuAnimationController = AnimationController( duration: widget.menuAnimationDuration, vsync: this, ); _progressAnimationController = AnimationController( duration: widget.progressAnimationDuration, vsync: this, ); } @override void dispose() { _menuAnimationController.dispose(); _progressAnimationController.dispose(); super.dispose(); } void _openMenu() { _menuAnimationController.forward(); setState(() => _isOpen = true); } void _closeMenu() { _menuAnimationController.reverse(); setState(() => _isOpen = false); } Future _activate(int itemIndex) async { //setState(() => _activeItemIndex = itemIndex); //await _progressAnimationController.forward().orCancel; _closeMenu(); if (widget.onSelected != null) { widget.onSelected!(widget.items[itemIndex].value); } } /// Resets the menu to its initial (closed) state. void reset() { _menuAnimationController.reset(); _progressAnimationController.reverse(); setState(() { _isOpen = false; _activeItemIndex = null; }); } Widget _buildActionButton(int index) { final RadialMenuItem item = widget.items[index]; return LayoutId( id: '${_RadialMenuLayout.actionButton}$index', child: RadialMenuButton( backgroundColor: item.backgroundColor, onPressed: () => _activate(index), child: item, ), ); } Widget _buildActiveAction(int index) { final RadialMenuItem item = widget.items[index]; return LayoutId( id: '${_RadialMenuLayout.activeAction}$index', child: ArcProgressIndicator( controller: _progressAnimationController.view, radius: widget.radius, color: item.backgroundColor, icon: item.child is Icon ? (item.child as Icon).icon : null, iconColor: item.iconColor, startAngle: calculateItemAngle(index), ), ); } Widget _buildCenterButton() { return LayoutId( id: _RadialMenuLayout.menuButton, child: RadialMenuCenterButton( openCloseAnimationController: _menuAnimationController.view, activateAnimationController: _progressAnimationController.view, isOpen: _isOpen, onPressed: _isOpen ? _closeMenu : _openMenu, ), ); } @override Widget build(BuildContext context) { final List children = []; for (int i = 0; i < widget.items.length; i++) { if (_activeItemIndex != i) { children.add(_buildActionButton(i)); } } if (_activeItemIndex != null) { children.add(_buildActiveAction(_activeItemIndex!)); } children.add(_buildCenterButton()); return AnimatedBuilder( animation: _menuAnimationController, builder: (BuildContext context, Widget? child) { return CustomMultiChildLayout( delegate: _RadialMenuLayout( itemCount: widget.items.length, radius: widget.radius, calculateItemAngle: calculateItemAngle, controller: _menuAnimationController.view, ), children: children, ); }, ); } } class _RadialMenuLayout extends MultiChildLayoutDelegate { static const String menuButton = 'menuButton'; static const String actionButton = 'actionButton'; static const String activeAction = 'activeAction'; final int itemCount; final double radius; final ItemAngleCalculator calculateItemAngle; final Animation controller; final Animation _progress; _RadialMenuLayout({ required this.itemCount, required this.radius, required this.calculateItemAngle, required this.controller, }) : _progress = Tween(begin: 0.0, end: radius).animate( CurvedAnimation(curve: Curves.elasticInOut, parent: controller)); late Offset center; @override void performLayout(Size size) { center = Offset(size.width, size.height); if (hasChild(menuButton)) { Size menuButtonSize; menuButtonSize = layoutChild(menuButton, BoxConstraints.loose(size)); // place the menubutton in the center positionChild( menuButton, Offset( center.dx - menuButtonSize.width + 1, center.dy - menuButtonSize.height + 1, ), ); } for (int i = 0; i < itemCount; i++) { final String actionButtonId = '$actionButton$i'; final String actionArcId = '$activeAction$i'; if (hasChild(actionArcId)) { final Size arcSize = layoutChild( actionArcId, BoxConstraints.expand( width: _progress.value * 2, height: _progress.value * 2, ), ); positionChild( actionArcId, Offset( center.dx - arcSize.width / 2, center.dy - arcSize.height / 2, ), ); } if (hasChild(actionButtonId)) { final Size buttonSize = layoutChild(actionButtonId, BoxConstraints.loose(size)); final double itemAngle = calculateItemAngle(0); positionChild( actionButtonId, Offset( (center.dx - buttonSize.width) + (_progress.value) * Math.cos(itemAngle), (center.dy - buttonSize.height) + (_progress.value * (i + 1)) * Math.sin(itemAngle), ), ); } } } @override bool shouldRelayout(_RadialMenuLayout oldDelegate) => itemCount != oldDelegate.itemCount || radius != oldDelegate.radius || calculateItemAngle != oldDelegate.calculateItemAngle || controller != oldDelegate.controller || _progress != oldDelegate._progress; } ================================================ FILE: lib/widget/menu/src/radial_menu_button.dart ================================================ import 'package:flutter/material.dart'; class RadialMenuButton extends StatelessWidget { const RadialMenuButton({super.key, required this.child, this.backgroundColor, this.onPressed, }); final Widget child; final Color? backgroundColor; final VoidCallback? onPressed; @override Widget build(BuildContext context) { final Color color = backgroundColor ?? Theme.of(context).primaryColor; return Semantics( button: true, enabled: true, child: Material( type: MaterialType.circle, color: color, child: InkWell( onTap: onPressed, child: child, ), ), ); } } ================================================ FILE: lib/widget/menu/src/radial_menu_center_button.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/widget/menu/src/radial_menu_button.dart'; const double _defaultButtonSize = 48.0; /// The button at the center of a [RadialMenu] which controls its open/closed /// state. class RadialMenuCenterButton extends StatelessWidget { /// Drives the opening/closing animation of the [RadialMenu]. final Animation openCloseAnimationController; /// Drives the animation when an item in the [RadialMenu] is pressed. final Animation activateAnimationController; /// Called when the user presses this button. final VoidCallback onPressed; /// The opened/closed state of the menu. /// /// Determines which of [closedColor] or [openedColor] should be used as the /// background color of the button. final bool isOpen; /// The color to use when painting the icon. /// /// Defaults to [Colors.black]. final Color iconColor; /// Background color when it is in its closed state. /// /// Defaults to [Colors.white]. final Color closedColor; /// Background color when it is in its opened state. /// /// Defaults to [Colors.grey]. final Color openedColor; /// The size of the button. /// /// Defaults to 48.0. final double size; /// The animation progress for the [AnimatedIcon] in the center of the button. final Animation _progress; /// The scale factor applied to the button. /// /// Animates from 1.0 to 0.0 when an an item is pressed in the menu and /// [activateAnimationController] progresses. final Animation _scale; RadialMenuCenterButton({super.key, required this.openCloseAnimationController, required this.activateAnimationController, required this.onPressed, required this.isOpen, this.iconColor = Colors.black, this.closedColor = Colors.white, this.openedColor = Colors.grey, this.size = _defaultButtonSize, }) : _progress = Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: openCloseAnimationController, curve: const Interval( 0.0, 0.5, curve: Curves.ease, ), ), ), _scale = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation( parent: activateAnimationController, curve: Curves.elasticIn, ), ); @override Widget build(BuildContext context) { final AnimatedIcon animatedIcon = AnimatedIcon( color: iconColor, icon: AnimatedIcons.menu_close, progress: _progress, ); final Widget child = SizedBox( width: size, height: size, child: Center( child: animatedIcon, ), ); final Color color = isOpen ? openedColor : closedColor; return ScaleTransition( scale: _scale, child: RadialMenuButton( backgroundColor: color, onPressed: onPressed, child: child, ), ); } } ================================================ FILE: lib/widget/menu/src/radial_menu_item.dart ================================================ import 'package:flutter/material.dart'; const double _defaultButtonSize = 48.0; /// An item in a [RadialMenu]. /// /// The type `T` is the type of the value the entry represents. All the entries /// in a given menu must represent values with consistent types. class RadialMenuItem extends StatelessWidget { /// Creates a circular action button for an item in a [RadialMenu]. /// /// The [child] argument is required. const RadialMenuItem({ super.key, required this.child, this.value, this.tooltip, this.size = _defaultButtonSize, this.backgroundColor, this.iconColor, // this.iconSize: 24.0, }) : assert(child != null); /// The widget below this widget in the tree. /// /// Typically an [Icon] widget. final Widget? child; /// The value to return if the user selects this menu item. /// /// Eventually returned in a call to [RadialMenu.onSelected]. final T? value; /// Text that describes the action that will occur when the button is pressed. /// /// This text is displayed when the user long-presses on the button and is /// used for accessibility. final String? tooltip; /// The color to use when filling the button. /// /// Defaults to the primary color of the current theme. final Color? backgroundColor; /// The size of the button. /// /// Defaults to 48.0. final double size; /// The color to use when painting the child icon. /// /// Defaults to the primary icon theme color. final Color? iconColor; @override Widget build(BuildContext context) { final Color? iconColor = this.iconColor ?? Theme.of(context).primaryIconTheme.color; Widget? result; if (child != null) { result = Center( child: IconTheme.merge( data: IconThemeData( color: iconColor, ), child: child ?? Container(), ), ); } if (tooltip != null) { result = Tooltip( message: tooltip!, child: result, ); } result = SizedBox( width: size, height: size, child: result, ); return result; } } ================================================ FILE: lib/widget/mole_widget.dart ================================================ import 'dart:async'; import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:simple_animations/simple_animations.dart'; import 'package:supercharged/supercharged.dart'; class Mole extends StatefulWidget { const Mole({super.key}); @override _MoleState createState() => _MoleState(); } class _MoleState extends State { final List particles = []; bool _moleIsVisible = false; @override void initState() { _restartMole(); Future.delayed(1200.milliseconds, () { _hitMole(); }); super.initState(); } @override Widget build(BuildContext context) { return SizedBox( width: 100, height: 100, child: _buildMole(), ); } Widget _buildMole() { _manageParticleLifecycle(); return LoopAnimationBuilder( tween: ConstantTween(1), duration: const Duration(seconds: 1), builder: (context, child, value) { return Stack( clipBehavior: Clip.none, children: [ if (_moleIsVisible) GestureDetector(onTap: () => _hitMole(), child: _mole()), ...particles.map((it) => it.buildWidget()) ], ); }, ); } Widget _mole() { return Container( decoration: BoxDecoration( color: GSYColors.primaryValue, borderRadius: BorderRadius.circular(50)), ); } _hitMole() { // ignore: unused_local_variable for (var i in Iterable.generate(50)) { particles.add(MoleParticle()); } } void _restartMole() async { var respawnTime = (2000 + Random().nextInt(8000)).milliseconds; await Future.delayed(respawnTime); _setMoleVisible(true); var timeVisible = (500 + Random().nextInt(1500)).milliseconds; await Future.delayed(timeVisible); _setMoleVisible(false); _restartMole(); } _manageParticleLifecycle() { particles.removeWhere((particle) { return particle.progress() == 1; }); } void _setMoleVisible(bool visible) { setState(() { _moleIsVisible = visible; }); } @override void setState(fn) { if (mounted) { super.setState(fn); } } } enum _MoleProps { x, y, scale } class MoleParticle { late Animatable tween; late Duration startTime; final duration = 600.milliseconds; MoleParticle() { final random = Random(); double x = (100 + 200) * random.nextDouble() * (random.nextBool() ? 1 : -1); double y = (100 + 200) * random.nextDouble() * (random.nextBool() ? 1 : -1); tween = MovieTween() ..tween(_MoleProps.x, Tween(begin: 0.0, end: x), duration: 2.seconds) ..tween(_MoleProps.y, Tween(begin: 0.0, end: y), duration: 2.seconds) ..tween(_MoleProps.scale, Tween(begin: 1.0, end: 0.0), duration: 2.seconds); startTime = DateTime.now().duration(); } Widget buildWidget() { final Movie values = tween.transform(progress()); var alpha = (255 * progress()).toInt(); return Positioned( left: values.get(_MoleProps.x), top: values.get(_MoleProps.y), child: Transform.scale( scale: values.get(_MoleProps.scale), child: Container( width: 100, height: 100, decoration: BoxDecoration( color: GSYColors.primaryValue.withAlpha(alpha), borderRadius: BorderRadius.circular(50)), ), ), ); } double progress() { return ((DateTime.now().duration() - startTime) / duration) .clamp(0.0, 1.0) .toDouble(); } } ================================================ FILE: lib/widget/never_overscroll_indicator.dart ================================================ import 'package:flutter/material.dart'; ///去除ScrollView的Material效果 class NeverOverScrollIndicator extends StatelessWidget { final bool needOverload; final Widget? child; const NeverOverScrollIndicator({super.key, this.child, this.needOverload = true}); @override Widget build(BuildContext context) { return ScrollConfiguration( behavior: NeverOverScrollBehavior(needOverload: needOverload), child: child!, ); } } class NeverOverScrollBehavior extends ScrollBehavior { final bool needOverload; const NeverOverScrollBehavior({this.needOverload = true}); @override Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { return child; } @override ScrollPhysics getScrollPhysics(BuildContext context) { switch (getPlatform(context)) { case TargetPlatform.iOS: if (needOverload) { return const BouncingScrollPhysics(); } return const ClampingScrollPhysics(); case TargetPlatform.android: case TargetPlatform.fuchsia: default: return const ClampingScrollPhysics(); } } } ================================================ FILE: lib/widget/only_share_widget.dart ================================================ import 'package:flutter/material.dart'; ///往下共享环境配置 class OnlyShareInstanceWidget extends StatelessWidget { const OnlyShareInstanceWidget({super.key, this.value, this.child}); @override Widget build(BuildContext context) { return _OnlyShareInstanceModel(value: value, child: child!); } static V? of(BuildContext context) { final _OnlyShareInstanceModel? inheritedConfig = context.dependOnInheritedWidgetOfExactType<_OnlyShareInstanceModel>(); return inheritedConfig?.value; } final T? value; final Widget? child; } class _OnlyShareInstanceModel extends InheritedWidget { const _OnlyShareInstanceModel({required this.value, required super.child}); final T? value; ///不需要刷新数据,直接返回false @override bool updateShouldNotify(_OnlyShareInstanceModel oldWidget) => false; } ================================================ FILE: lib/widget/particle/particle_model.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:simple_animations/simple_animations.dart'; import 'package:supercharged/supercharged.dart'; enum ParticleOffsetProps { x, y } class ParticleModel { late MovieTween tween; late double size; late Duration duration; late Duration startTime; Random random; ParticleModel(this.random) { restart(); shuffle(); } restart() { final startPosition = Offset(-0.2 + 1.4 * random.nextDouble(), 1.2); final endPosition = Offset(-0.2 + 1.4 * random.nextDouble(), -0.2); tween = MovieTween() ..tween(ParticleOffsetProps.x, Tween(begin: startPosition.dx, end: endPosition.dx), duration: 2.seconds) ..tween(ParticleOffsetProps.y, Tween(begin: startPosition.dy, end: endPosition.dy), duration: 2.seconds); duration = 3000.milliseconds + random.nextInt(6000).milliseconds; startTime = DateTime.now().duration(); size = 0.2 + random.nextDouble() * 0.4; } void shuffle() { startTime -= (random.nextDouble() * duration.inMilliseconds) .round() .milliseconds; } checkIfParticleNeedsToBeRestarted() { if (progress() == 1.0) { restart(); } } double progress() { return ((DateTime.now().duration() - startTime) / duration) .clamp(0.0, 1.0) .toDouble(); } } ================================================ FILE: lib/widget/particle/particle_painter.dart ================================================ import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/widget/particle/particle_model.dart'; import 'package:simple_animations/simple_animations.dart'; class ParticlePainter extends CustomPainter { List particles; ParticlePainter(this.particles); @override void paint(Canvas canvas, Size size) { final paint = Paint()..color = Colors.white.withAlpha(50); for (var particle in particles) { final progress = particle.progress(); final Movie animation = particle.tween.transform(progress); final position = Offset( animation.get(ParticleOffsetProps.x) * size.width, animation.get(ParticleOffsetProps.y) * size.height, ); canvas.drawCircle(position, size.width * 0.2 * particle.size, paint); } } @override bool shouldRepaint(CustomPainter oldDelegate) => true; } ================================================ FILE: lib/widget/particle/particle_widget.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/widget/particle/particle_model.dart'; import 'package:gsy_github_app_flutter/widget/particle/particle_painter.dart'; import 'package:simple_animations/simple_animations.dart'; import 'package:supercharged/supercharged.dart'; class ParticlesWidget extends StatefulWidget { final int numberOfParticles; const ParticlesWidget(this.numberOfParticles, {super.key}); @override _ParticlesWidgetState createState() => _ParticlesWidgetState(); } class _ParticlesWidgetState extends State with WidgetsBindingObserver { final Random random = Random(); final List particles = []; @override void initState() { widget.numberOfParticles.times(() => particles.add(ParticleModel(random))); WidgetsBinding.instance.addObserver(this); super.initState(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // 从后台切回来时重置粒子 if (state == AppLifecycleState.resumed) { for (var particle in particles) { particle.restart(); particle.shuffle(); } } } @override Widget build(BuildContext context) { return LoopAnimationBuilder( tween: ConstantTween(1), duration: const Duration(seconds: 1), builder: (context, child, dynamic _) { _simulateParticles(); return CustomPaint( painter: ParticlePainter(particles), ); }, ); } _simulateParticles() { for (var particle in particles) { particle.checkIfParticleNeedsToBeRestarted(); } } } ================================================ FILE: lib/widget/pull/custom_bouncing_scroll_physics.dart ================================================ import 'dart:math'; import 'package:flutter/material.dart'; ///自定义弹性滑动模块 class CustomBouncingScrollPhysics extends ScrollPhysics { final double refreshHeight; const CustomBouncingScrollPhysics( {super.parent, this.refreshHeight = 140}); @override CustomBouncingScrollPhysics applyTo(ScrollPhysics? ancestor) { return CustomBouncingScrollPhysics(parent: buildParent(ancestor)); } double frictionFactor(double overscrollFraction) => 0.52 * pow(1 - overscrollFraction, 2); @override double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { assert(offset != 0.0); assert(position.minScrollExtent <= position.maxScrollExtent); if (!position.outOfRange) return offset; /// -2 是因为有时候会只到 iosRefreshHeight - 1 /// 会导致触发重新又从0开始的问题 /*if (position.pixels.abs() >= (refreshHeight - 2)) { return 0; }*/ final double overscrollPastStart = max(position.minScrollExtent - position.pixels, 0.0); final double overscrollPastEnd = max(position.pixels - position.maxScrollExtent, 0.0); final double overscrollPast = max(overscrollPastStart, overscrollPastEnd); final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) || (overscrollPastEnd > 0.0 && offset > 0.0); final double friction = easing ? frictionFactor( (overscrollPast - offset.abs()) / position.viewportDimension) : frictionFactor(overscrollPast / position.viewportDimension); final double direction = offset.sign; return direction * _applyFriction(overscrollPast, offset.abs(), friction); } static double _applyFriction( double extentOutside, double absDelta, double gamma) { assert(absDelta > 0); double total = 0.0; if (extentOutside > 0) { final double deltaToLimit = extentOutside / gamma; if (absDelta < deltaToLimit) return absDelta * gamma; total += extentOutside; absDelta -= deltaToLimit; } return total + absDelta; } @override double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0; @override Simulation? createBallisticSimulation( ScrollMetrics position, double velocity) { final Tolerance tolerance = toleranceFor(position); if (velocity.abs() >= tolerance.velocity || position.outOfRange) { return BouncingScrollSimulation( spring: spring, position: position.pixels, velocity: velocity * 0.91, leadingExtent: position.minScrollExtent, trailingExtent: position.maxScrollExtent, tolerance: tolerance, ); } return null; } @override double get minFlingVelocity => 50.0 * 2.0; @override double carriedMomentum(double existingVelocity) { return existingVelocity.sign * min(0.000816 * pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0); } @override double get dragStartDistanceMotionThreshold => 3.5; } ================================================ FILE: lib/widget/pull/gsy_flare_mutli_pull_controller.dart ================================================ import 'package:flare_flutter/flare.dart'; import 'package:flare_flutter/flare_controller.dart'; mixin GSYFlarePullMutliController implements FlareController { late ActorAnimation? _loadingAnimation; late ActorAnimation? _successAnimation; late ActorAnimation? _pullAnimation; late ActorAnimation? _cometAnimation; double pulledExtentFlare = 0; bool _isSurround = false; final double _refreshTriggerPullDistance = 140; double _successTime = 0.0; double _loadingTime = 0.0; double _cometTime = 0.0; @override void initialize(FlutterActorArtboard artboard) { _pullAnimation = artboard.getAnimation("pull"); _successAnimation = artboard.getAnimation("success"); _loadingAnimation = artboard.getAnimation("loading"); _cometAnimation = artboard.getAnimation("idle comet"); } @override void setViewTransform(Mat2D viewTransform) {} @override bool advance(FlutterActorArtboard artboard, double elapsed) { double animationPosition = pulledExtentFlare / _refreshTriggerPullDistance; animationPosition *= animationPosition; _cometTime += elapsed; _cometAnimation?.apply(_cometTime % _cometAnimation!.duration, artboard, 1.0); _pullAnimation?.apply( _pullAnimation!.duration * animationPosition, artboard, 1.0); if (_isSurround) { _successTime += elapsed; if (_successTime >= _successAnimation!.duration) { _loadingTime += elapsed; } } else { _successTime = _loadingTime = 0.0; } if (_successTime >= _successAnimation!.duration) { _loadingAnimation?.apply( _loadingTime % _loadingAnimation!.duration, artboard, 1.0); } else if (_successTime > 0.0) { _successAnimation?.apply(_successTime, artboard, 1.0); } return true; } void onRefreshing() { _isSurround = true; } void onRefreshEnd() { _isSurround = false; } bool get getPlayAuto; } ================================================ FILE: lib/widget/pull/gsy_flare_pull_controller.dart ================================================ import 'package:flare_flutter/flare.dart'; import 'package:flare_flutter/flare_controller.dart'; mixin GSYFlarePullController implements FlareController { late ActorAnimation? _pullAnimation; double pulledExtentFlare = 0; final double _speed = 2.0; double _rockTime = 0.0; @override void initialize(FlutterActorArtboard artboard) { _pullAnimation = artboard.getAnimation("Earth Moving"); } @override void setViewTransform(Mat2D viewTransform) {} @override bool advance(FlutterActorArtboard artboard, double elapsed) { if (getPlayAuto) { _rockTime += elapsed * _speed; _pullAnimation?.apply(_rockTime % _pullAnimation!.duration, artboard, 1.0); return true; } var pullExtent = (pulledExtentFlare > refreshTriggerPullDistance) ? pulledExtentFlare - refreshTriggerPullDistance : pulledExtentFlare; double animationPosition = pullExtent / refreshTriggerPullDistance; animationPosition *= animationPosition; _rockTime = _pullAnimation!.duration * animationPosition; _pullAnimation?.apply(_rockTime, artboard, 1.0); return true; } bool get getPlayAuto; double get refreshTriggerPullDistance => 140; } ================================================ FILE: lib/widget/pull/gsy_pull_load_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; ///通用下上刷新控件 class GSYPullLoadWidget extends StatefulWidget { ///item渲染 final IndexedWidgetBuilder itemBuilder; ///加载更多回调 final RefreshCallback? onLoadMore; ///下拉刷新回调 final RefreshCallback? onRefresh; ///控制器,比如数据和一些配置 final GSYPullLoadWidgetControl? control; final Key? refreshKey; const GSYPullLoadWidget( this.control, this.itemBuilder, this.onRefresh, this.onLoadMore, {super.key, this.refreshKey}); @override _GSYPullLoadWidgetState createState() => _GSYPullLoadWidgetState(); } class _GSYPullLoadWidgetState extends State { final ScrollController _scrollController = ScrollController(); @override void initState() { widget.control?.needLoadMore.addListener(() { ///延迟两秒等待确认 try { Future.delayed(const Duration(seconds: 2), () { // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member _scrollController.notifyListeners(); }); } catch (e) { printLog(e); } }); ///增加滑动监听 _scrollController.addListener(() { ///判断当前滑动位置是不是到达底部,触发加载更多回调 if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { if (widget.control?.needLoadMore.value == true) { widget.onLoadMore?.call(); } } }); super.initState(); } ///根据配置状态返回实际列表数量 ///实际上这里可以根据你的需要做更多的处理 ///比如多个头部,是否需要空页面,是否需要显示加载更多。 _getListCount() { ///是否需要头部 if (widget.control!.needHeader) { ///如果需要头部,用Item 0 的 Widget 作为ListView的头部 ///列表数量大于0时,因为头部和底部加载更多选项,需要对列表数据总数+2 return (widget.control!.dataList.isNotEmpty) ? widget.control!.dataList.length + 2 : widget.control!.dataList.length + 1; } else { ///如果不需要头部,在没有数据时,固定返回数量1用于空页面呈现 if (widget.control!.dataList.isEmpty) { return 1; } ///如果有数据,因为部加载更多选项,需要对列表数据总数+1 return (widget.control!.dataList.isNotEmpty) ? widget.control!.dataList.length + 1 : widget.control!.dataList.length; } } ///根据配置状态返回实际列表渲染Item _getItem(int index) { if (!widget.control!.needHeader && index == widget.control!.dataList.length && widget.control!.dataList.isNotEmpty) { ///如果不需要头部,并且数据不为0,当index等于数据长度时,渲染加载更多Item(因为index是从0开始) return _buildProgressIndicator(); } else if (widget.control!.needHeader && index == _getListCount() - 1 && widget.control!.dataList.isNotEmpty) { ///如果需要头部,并且数据不为0,当index等于实际渲染长度 - 1时,渲染加载更多Item(因为index是从0开始) return _buildProgressIndicator(); } else if (!widget.control!.needHeader && widget.control!.dataList.isEmpty) { ///如果不需要头部,并且数据为0,渲染空页面 return _buildEmpty(); } else { ///回调外部正常渲染Item,如果这里有需要,可以直接返回相对位置的index return widget.itemBuilder(context, index); } } @override Widget build(BuildContext context) { return RefreshIndicator( ///GlobalKey,用户外部获取RefreshIndicator的State,做显示刷新 key: widget.refreshKey, ///下拉刷新触发,返回的是一个Future onRefresh: widget.onRefresh ?? () async {}, child: ListView.builder( ///保持ListView任何情况都能滚动,解决在RefreshIndicator的兼容问题。 physics: const AlwaysScrollableScrollPhysics(), ///根据状态返回子孔健 itemBuilder: (context, index) { return _getItem(index); }, ///根据状态返回数量 itemCount: _getListCount(), ///滑动监听 controller: _scrollController, ), ); } ///空页面 Widget _buildEmpty() { return SizedBox( height: MediaQuery.sizeOf(context).height - 100, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () {}, child: const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 70.0, height: 70.0), ), Text(context.l10n.app_empty, style: GSYConstant.normalText), ], ), ); } ///上拉加载更多 Widget _buildProgressIndicator() { ///是否需要显示上拉加载更多的loading Widget bottomWidget = (widget.control!.needLoadMore.value) ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ ///loading框 SpinKitRotatingCircle(color: Theme.of(context).primaryColor), Container( width: 5.0, ), ///加载中文本 Text( context.l10n.load_more_text, style: const TextStyle( color: Color(0xFF121917), fontSize: 14.0, fontWeight: FontWeight.bold, ), ) ]) /// 不需要加载 : Container(); return Padding( padding: const EdgeInsets.all(20.0), child: Center( child: bottomWidget, ), ); } } class GSYPullLoadWidgetControl { ///数据,对齐增减,不能替换 List dataList = []; ///是否需要加载更多 ValueNotifier needLoadMore = ValueNotifier(false); ///是否需要头部 bool needHeader = false; } ================================================ FILE: lib/widget/pull/gsy_pull_new_load_widget.dart ================================================ import 'package:flare_flutter/flare_actor.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_refresh_sliver.dart' as IOS; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'custom_bouncing_scroll_physics.dart'; import 'gsy_flare_pull_controller.dart'; const double iosRefreshHeight = 140; const double iosRefreshIndicatorExtent = 100; ///通用下上刷新控件 class GSYPullLoadWidget extends StatefulWidget { ///item渲染 final IndexedWidgetBuilder itemBuilder; ///加载更多回调 final RefreshCallback? onLoadMore; ///下拉刷新回调 final RefreshCallback? onRefresh; ///控制器,比如数据和一些配置 final GSYPullLoadWidgetControl control; final ScrollController? scrollController; final bool userIos; ///刷新key final Key? refreshKey; const GSYPullLoadWidget( this.control, this.itemBuilder, this.onRefresh, this.onLoadMore, {super.key, this.refreshKey, this.scrollController, this.userIos = false}); @override _GSYPullLoadWidgetState createState() => _GSYPullLoadWidgetState(); } class _GSYPullLoadWidgetState extends State with GSYFlarePullController { //with GSYFlarePullMutliController { final GlobalKey sliverRefreshKey = GlobalKey(); ScrollController? _scrollController; bool isRefreshing = false; bool isLoadMoring = false; @override ValueNotifier isActive = ValueNotifier(true); @override void initState() { _scrollController = widget.scrollController ?? ScrollController(); ///增加滑动监听 _scrollController!.addListener(() { ///判断当前滑动位置是不是到达底部,触发加载更多回调 if (_scrollController!.position.pixels == _scrollController!.position.maxScrollExtent) { if (widget.control.needLoadMore) { handleLoadMore(); } } }); widget.control.addListener(() { setState(() {}); try { Future.delayed(const Duration(seconds: 2), () { // ignore: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member _scrollController!.notifyListeners(); }); } catch (e) { printLog(e); } }); super.initState(); } ///根据配置状态返回实际列表数量 ///实际上这里可以根据你的需要做更多的处理 ///比如多个头部,是否需要空页面,是否需要显示加载更多。 _getListCount() { ///是否需要头部 if (widget.control.needHeader) { ///如果需要头部,用Item 0 的 Widget 作为ListView的头部 ///列表数量大于0时,因为头部和底部加载更多选项,需要对列表数据总数+2 return (widget.control.dataList!.isNotEmpty) ? widget.control.dataList!.length + 2 : widget.control.dataList!.length + 1; } else { ///如果不需要头部,在没有数据时,固定返回数量1用于空页面呈现 if (widget.control.dataList!.isEmpty) { return 1; } ///如果有数据,因为部加载更多选项,需要对列表数据总数+1 return (widget.control.dataList!.isNotEmpty) ? widget.control.dataList!.length + 1 : widget.control.dataList!.length; } } ///根据配置状态返回实际列表渲染Item _getItem(int index) { if (!widget.control.needHeader && index == widget.control.dataList!.length && widget.control.dataList!.isNotEmpty) { ///如果不需要头部,并且数据不为0,当index等于数据长度时,渲染加载更多Item(因为index是从0开始) return _buildProgressIndicator(); } else if (widget.control.needHeader && index == _getListCount() - 1 && widget.control.dataList!.isNotEmpty) { ///如果需要头部,并且数据不为0,当index等于实际渲染长度 - 1时,渲染加载更多Item(因为index是从0开始) return _buildProgressIndicator(); } else if (!widget.control.needHeader && widget.control.dataList!.isEmpty) { ///如果不需要头部,并且数据为0,渲染空页面 return _buildEmpty(); } else { ///回调外部正常渲染Item,如果这里有需要,可以直接返回相对位置的index return widget.itemBuilder(context, index); } } _lockToAwait() async { ///if loading, lock to await doDelayed() async { await Future.delayed(const Duration(seconds: 1)).then((_) async { if (widget.control.isLoading) { return await doDelayed(); } else { return null; } }); } await doDelayed(); } @protected Future handleRefresh() async { if (widget.control.isLoading) { if (isRefreshing) { return; } ///if loading, lock to await await _lockToAwait(); } widget.control.isLoading = true; isRefreshing = true; await widget.onRefresh?.call(); isRefreshing = false; widget.control.isLoading = false; return; } @protected Future handleLoadMore() async { if (widget.control.isLoading) { if (isLoadMoring) { return; } ///if loading, lock to await await _lockToAwait(); } isLoadMoring = true; widget.control.isLoading = true; await widget.onLoadMore?.call(); isLoadMoring = false; widget.control.isLoading = false; return; } @override Widget build(BuildContext context) { if (widget.userIos) { ///用ios模式的下拉刷新 return NotificationListener( onNotification: (ScrollNotification notification) { ///通知 CupertinoSliverRefreshControl 当前的拖拽状态 sliverRefreshKey.currentState!.notifyScrollNotification(notification); return false; }, child: CustomScrollView( controller: _scrollController, ///回弹效果 physics: const CustomBouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics(), refreshHeight: iosRefreshHeight), slivers: [ ///控制显示刷新的 CupertinoSliverRefreshControl IOS.CupertinoSliverRefreshControl( key: sliverRefreshKey, refreshIndicatorExtent: iosRefreshIndicatorExtent, refreshTriggerPullDistance: iosRefreshHeight, onRefresh: handleRefresh, builder: buildSimpleRefreshIndicator, ), SliverSafeArea( sliver: SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return _getItem(index); }, childCount: _getListCount(), ), ), ), ], ), ); } return RefreshIndicator( ///GlobalKey,用户外部获取RefreshIndicator的State,做显示刷新 key: widget.refreshKey, ///下拉刷新触发,返回的是一个Future onRefresh: handleRefresh, child: ListView.builder( ///保持ListView任何情况都能滚动,解决在RefreshIndicator的兼容问题。 physics: const AlwaysScrollableScrollPhysics(), ///根据状态返回子孔健 itemBuilder: (context, index) { return _getItem(index); }, ///根据状态返回数量 itemCount: _getListCount(), ///滑动监听 controller: _scrollController, ), ); } ///空页面 Widget _buildEmpty() { return SizedBox( height: MediaQuery.sizeOf(context).height - 100, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () {}, child: const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 70.0, height: 70.0), ), Text(context.l10n.app_empty, style: GSYConstant.normalText), ], ), ); } ///上拉加载更多 Widget _buildProgressIndicator() { ///是否需要显示上拉加载更多的loading Widget bottomWidget = (widget.control.needLoadMore) ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ ///loading框 SpinKitRotatingCircle(color: Theme.of(context).primaryColor), Container( width: 5.0, ), ///加载中文本 Text( context.l10n.load_more_text, style: const TextStyle( color: Color(0xFF121917), fontSize: 14.0, fontWeight: FontWeight.bold, ), ) ]) /// 不需要加载 : Container(); return Padding( padding: const EdgeInsets.all(20.0), child: Center( child: bottomWidget, ), ); } bool playAuto = false; @override bool get getPlayAuto => playAuto; @override double get refreshTriggerPullDistance => iosRefreshHeight; Widget buildSimpleRefreshIndicator( BuildContext? context, IOS.RefreshIndicatorMode? refreshState, double? pulledExtent, double? refreshTriggerPullDistance, double? refreshIndicatorExtent, ) { pulledExtentFlare = pulledExtent! * 0.6; playAuto = refreshState == IOS.RefreshIndicatorMode.refresh; /*if(refreshState == IOS.RefreshIndicatorMode.refresh) { onRefreshing(); } else { onRefreshEnd(); }*/ return Align( alignment: Alignment.bottomCenter, child: Container( color: Colors.black, width: MediaQuery.sizeOf(context!).width, ///动态大小处理 height: pulledExtent > iosRefreshHeight ? pulledExtent : iosRefreshHeight, child: FlareActor( //"static/file/Space-Demo.flr", "static/file/loading_world_now.flr", alignment: Alignment.topCenter, fit: BoxFit.cover, controller: this, animation: "Earth Moving" //animation: "idle" ), ), ); } } class GSYPullLoadWidgetControl extends ChangeNotifier { ///数据,对齐增减,不能替换 final List _dataList = []; List? get dataList => _dataList; set dataList(List? value) { _dataList.clear(); if (value != null) { _dataList.addAll(value); notifyListeners(); } } addList(List? value) { if (value != null) { _dataList.addAll(value); notifyListeners(); } } ///是否需要加载更多 bool _needLoadMore = true; set needLoadMore(value) { _needLoadMore = value; notifyListeners(); } get needLoadMore => _needLoadMore; ///是否需要头部 bool _needHeader = true; set needHeader(value) { _needHeader = value; notifyListeners(); } get needHeader => _needHeader; ///是否加载中 bool isLoading = false; } ================================================ FILE: lib/widget/pull/gsy_refresh_sliver.dart ================================================ // Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:gsy_github_app_flutter/provider/app_state_provider.dart'; class _CupertinoSliverRefresh extends SingleChildRenderObjectWidget { const _CupertinoSliverRefresh({ this.refreshIndicatorLayoutExtent = 0.0, this.hasLayoutExtent = false, super.child, }) : assert(refreshIndicatorLayoutExtent >= 0.0); // The amount of space the indicator should occupy in the sliver in a // resting state when in the refreshing mode. final double refreshIndicatorLayoutExtent; // _RenderCupertinoSliverRefresh will paint the child in the available // space either way but this instructs the _RenderCupertinoSliverRefresh // on whether to also occupy any layoutExtent space or not. final bool hasLayoutExtent; @override _RenderCupertinoSliverRefresh createRenderObject(BuildContext context) { return _RenderCupertinoSliverRefresh( refreshIndicatorExtent: refreshIndicatorLayoutExtent, hasLayoutExtent: hasLayoutExtent, ); } @override void updateRenderObject( BuildContext context, covariant _RenderCupertinoSliverRefresh renderObject, ) { renderObject ..refreshIndicatorLayoutExtent = refreshIndicatorLayoutExtent ..hasLayoutExtent = hasLayoutExtent; } } // RenderSliver object that gives its child RenderBox object space to paint // in the overscrolled gap and may or may not hold that overscrolled gap // around the RenderBox depending on whether [layoutExtent] is set. // // The [layoutExtentOffsetCompensation] field keeps internal accounting to // prevent scroll position jumps as the [layoutExtent] is set and unset. class _RenderCupertinoSliverRefresh extends RenderSliver with RenderObjectWithChildMixin { _RenderCupertinoSliverRefresh({ required double refreshIndicatorExtent, required bool hasLayoutExtent, RenderBox? child, }) : assert(refreshIndicatorExtent >= 0.0), _refreshIndicatorExtent = refreshIndicatorExtent, _hasLayoutExtent = hasLayoutExtent { this.child = child; } // The amount of layout space the indicator should occupy in the sliver in a // resting state when in the refreshing mode. double get refreshIndicatorLayoutExtent => _refreshIndicatorExtent; double _refreshIndicatorExtent; set refreshIndicatorLayoutExtent(double value) { assert(value >= 0.0); if (value == _refreshIndicatorExtent) return; _refreshIndicatorExtent = value; markNeedsLayout(); } // The child box will be laid out and painted in the available space either // way but this determines whether to also occupy any // [SliverGeometry.layoutExtent] space or not. bool get hasLayoutExtent => _hasLayoutExtent; bool _hasLayoutExtent; set hasLayoutExtent(bool value) { if (value == _hasLayoutExtent) return; _hasLayoutExtent = value; markNeedsLayout(); } // This keeps track of the previously applied scroll offsets to the scrollable // so that when [refreshIndicatorLayoutExtent] or [hasLayoutExtent] changes, // the appropriate delta can be applied to keep everything in the same place // visually. double layoutExtentOffsetCompensation = 0.0; @override void performLayout() { // Only pulling to refresh from the top is currently supported. assert(constraints.axisDirection == AxisDirection.down); assert(constraints.growthDirection == GrowthDirection.forward); // The new layout extent this sliver should now have. final double layoutExtent = (_hasLayoutExtent ? 1.0 : 0.0) * _refreshIndicatorExtent; // If the new layoutExtent instructive changed, the SliverGeometry's // layoutExtent will take that value (on the next performLayout run). Shift // the scroll offset first so it doesn't make the scroll position suddenly jump. if (layoutExtent != layoutExtentOffsetCompensation) { geometry = SliverGeometry( scrollOffsetCorrection: layoutExtent - layoutExtentOffsetCompensation, ); layoutExtentOffsetCompensation = layoutExtent; // Return so we don't have to do temporary accounting and adjusting the // child's constraints accounting for this one transient frame using a // combination of existing layout extent, new layout extent change and // the overlap. return; } final bool active = constraints.overlap < 0.0 || layoutExtent > 0.0; final double overscrolledExtent = constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0; // Layout the child giving it the space of the currently dragged overscroll // which may or may not include a sliver layout extent space that it will // keep after the user lets go during the refresh process. child!.layout( constraints.asBoxConstraints( maxExtent: layoutExtent // Plus only the overscrolled portion immediately preceding this // sliver. + overscrolledExtent, ), parentUsesSize: true, ); if (active) { geometry = SliverGeometry( scrollExtent: layoutExtent, paintOrigin: -overscrolledExtent - constraints.scrollOffset, paintExtent: max( // Check child size (which can come from overscroll) because // layoutExtent may be zero. Check layoutExtent also since even // with a layoutExtent, the indicator builder may decide to not // build anything. max(child!.size.height, layoutExtent) - constraints.scrollOffset, 0.0, ), maxPaintExtent: max( max(child!.size.height, layoutExtent) - constraints.scrollOffset, 0.0, ), layoutExtent: max(layoutExtent - constraints.scrollOffset, 0.0), ); } else { // If we never started overscrolling, return no geometry. geometry = SliverGeometry.zero; } } @override void paint(PaintingContext paintContext, Offset offset) { if (constraints.overlap < 0.0 || constraints.scrollOffset + child!.size.height > 0) { paintContext.paintChild(child!, offset); } } // Nothing special done here because this sliver always paints its child // exactly between paintOrigin and paintExtent. @override void applyPaintTransform(RenderObject child, Matrix4 transform) {} } /// The current state of the refresh control. /// /// Passed into the [RefreshControlIndicatorBuilder] builder function so /// users can show different UI in different modes. enum RefreshIndicatorMode { /// Initial state, when not being overscrolled into, or after the overscroll /// is canceled or after done and the sliver retracted away. inactive, /// While being overscrolled but not far enough yet to trigger the refresh. drag, /// Dragged far enough that the onRefresh callback will run and the dragged /// displacement is not yet at the final refresh resting state. armed, /// While the onRefresh task is running. refresh, /// While the indicator is animating away after refreshing. done, } /// Signature for a builder that can create a different widget to show in the /// refresh indicator space depending on the current state of the refresh /// control and the space available. /// /// The `refreshTriggerPullDistance` and `refreshIndicatorExtent` parameters are /// the same values passed into the [CupertinoSliverRefreshControl]. /// /// The `pulledExtent` parameter is the currently available space either from /// overscrolling or as held by the sliver during refresh. typedef RefreshControlIndicatorBuilder = Widget Function( BuildContext context, RefreshIndicatorMode? refreshState, double pulledExtent, double refreshTriggerPullDistance, double refreshIndicatorExtent, ); /// A callback function that's invoked when the [CupertinoSliverRefreshControl] is /// pulled a `refreshTriggerPullDistance`. Must return a [Future]. Upon /// completion of the [Future], the [CupertinoSliverRefreshControl] enters the /// [RefreshIndicatorMode.done] state and will start to go away. typedef RefreshCallback = Future Function(); /// A sliver widget implementing the iOS-style pull to refresh content control. /// /// When inserted as the first sliver in a scroll view or behind other slivers /// that still lets the scrollable overscroll in front of this sliver (such as /// the [CupertinoSliverNavigationBar], this widget will: /// /// * Let the user draw inside the overscrolled area via the passed in [builder]. /// * Trigger the provided [onRefresh] function when overscrolled far enough to /// pass [refreshTriggerPullDistance]. /// * Continue to hold [refreshIndicatorExtent] amount of space for the [builder] /// to keep drawing inside of as the [Future] returned by [onRefresh] processes. /// * Scroll away once the [onRefresh] [Future] completes. /// /// The [builder] function will be informed of the current [RefreshIndicatorMode] /// when invoking it, except in the [RefreshIndicatorMode.inactive] state when /// no space is available and nothing needs to be built. The [builder] function /// will otherwise be continuously invoked as the amount of space available /// changes from overscroll, as the sliver scrolls away after the [onRefresh] /// task is done, etc. /// /// Only one refresh can be triggered until the previous refresh has completed /// and the indicator sliver has retracted at least 90% of the way back. /// /// Can only be used in downward-scrolling vertical lists that overscrolls. In /// other words, refreshes can't be triggered with [Scrollable]s using /// [ClampingScrollPhysics] which is the default on Android. To allow overscroll /// on Android, use an overscrolling physics such as [BouncingScrollPhysics]. /// This can be done via: /// /// * Providing a [BouncingScrollPhysics] (possibly in combination with a /// [AlwaysScrollableScrollPhysics]) while constructing the scrollable. /// * By inserting a [ScrollConfiguration] with [BouncingScrollPhysics] above /// the scrollable. /// * By using [CupertinoApp], which always uses a [ScrollConfiguration] /// with [BouncingScrollPhysics] regardless of platform. /// /// In a typical application, this sliver should be inserted between the app bar /// sliver such as [CupertinoSliverNavigationBar] and your main scrollable /// content's sliver. /// /// See also: /// /// * [CustomScrollView], a typical sliver holding scroll view this control /// should go into. /// * /// * [RefreshIndicator], a Material Design version of the pull-to-refresh /// paradigm. This widget works differently than [RefreshIndicator] because /// instead of being an overlay on top of the scrollable, the /// [CupertinoSliverRefreshControl] is part of the scrollable and actively occupies /// scrollable space. class CupertinoSliverRefreshControl extends StatefulWidget { /// Create a new refresh control for inserting into a list of slivers. /// /// The [refreshTriggerPullDistance] and [refreshIndicatorExtent] arguments /// must not be null and must be >= 0. /// /// The [builder] argument may be null, in which case no indicator UI will be /// shown but the [onRefresh] will still be invoked. By default, [builder] /// shows a [CupertinoActivityIndicator]. /// /// The [onRefresh] argument will be called when pulled far enough to trigger /// a refresh. const CupertinoSliverRefreshControl({ super.key, this.refreshTriggerPullDistance = _defaultRefreshTriggerPullDistance, this.refreshIndicatorExtent = _defaultRefreshIndicatorExtent, this.builder = buildSimpleRefreshIndicator, this.onRefresh, }) : assert(refreshTriggerPullDistance > 0.0), assert(refreshIndicatorExtent >= 0.0), assert( refreshTriggerPullDistance >= refreshIndicatorExtent, 'The refresh indicator cannot take more space in its final state ' 'than the amount initially created by overscrolling.', ); /// The amount of overscroll the scrollable must be dragged to trigger a reload. /// /// Must not be null, must be larger than 0.0 and larger than /// [refreshIndicatorExtent]. Defaults to 100px when not specified. /// /// When overscrolled past this distance, [onRefresh] will be called if not /// null and the [builder] will build in the [RefreshIndicatorMode.armed] state. final double refreshTriggerPullDistance; /// The amount of space the refresh indicator sliver will keep holding while /// [onRefresh]'s [Future] is still running. /// /// Must not be null and must be positive, but can be 0.0, in which case the /// sliver will start retracting back to 0.0 as soon as the refresh is started. /// Defaults to 60px when not specified. /// /// Must be smaller than [refreshTriggerPullDistance], since the sliver /// shouldn't grow further after triggering the refresh. final double refreshIndicatorExtent; /// A builder that's called as this sliver's size changes, and as the state /// changes. /// /// A default simple Twitter-style pull-to-refresh indicator is provided if /// not specified. /// /// Can be set to null, in which case nothing will be drawn in the overscrolled /// space. /// /// Will not be called when the available space is zero such as before any /// overscroll. final RefreshControlIndicatorBuilder? builder; /// Callback invoked when pulled by [refreshTriggerPullDistance]. /// /// If provided, must return a [Future] which will keep the indicator in the /// [RefreshIndicatorMode.refresh] state until the [Future] completes. /// /// Can be null, in which case a single frame of [RefreshIndicatorMode.armed] /// state will be drawn before going immediately to the [RefreshIndicatorMode.done] /// where the sliver will start retracting. final RefreshCallback? onRefresh; static const double _defaultRefreshTriggerPullDistance = 100.0; static const double _defaultRefreshIndicatorExtent = 60.0; /// Retrieve the current state of the CupertinoSliverRefreshControl. The same as the /// state that gets passed into the [builder] function. Used for testing. @visibleForTesting static RefreshIndicatorMode? state(BuildContext context) { final CupertinoSliverRefreshControlState state = context .findAncestorStateOfType()!; return state.refreshState; } /// Builds a simple refresh indicator that fades in a bottom aligned down /// arrow before the refresh is triggered, a [CupertinoActivityIndicator] /// during the refresh and fades the [CupertinoActivityIndicator] away when /// the refresh is done. static Widget buildSimpleRefreshIndicator( BuildContext? context, RefreshIndicatorMode? refreshState, double? pulledExtent, double? refreshTriggerPullDistance, double? refreshIndicatorExtent, ) { const Curve opacityCurve = Interval(0.4, 0.8, curve: Curves.easeInOut); return Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 16.0), child: refreshState == RefreshIndicatorMode.drag ? Opacity( opacity: opacityCurve.transform( min(pulledExtent! / refreshTriggerPullDistance!, 1.0), ), child: const Icon( CupertinoIcons.down_arrow, color: CupertinoColors.inactiveGray, size: 36.0, ), ) : Opacity( opacity: opacityCurve.transform( min(pulledExtent! / refreshIndicatorExtent!, 1.0), ), child: const CupertinoActivityIndicator(radius: 14.0), ), ), ); } @override CupertinoSliverRefreshControlState createState() => CupertinoSliverRefreshControlState(); } class CupertinoSliverRefreshControlState extends State { // Reset the state from done to inactive when only this fraction of the // original `refreshTriggerPullDistance` is left. static const double _inactiveResetOverscrollFraction = 0.1; RefreshIndicatorMode? refreshState; // [Future] returned by the widget's `onRefresh`. Future? refreshTask; // The amount of space available from the inner indicator box's perspective. // // The value is the sum of the sliver's layout extent and the overscroll // (which partially gets transferred into the layout extent when the refresh // triggers). // // The value of latestIndicatorBoxExtent doesn't change when the sliver scrolls // away without retracting; it is independent from the sliver's scrollOffset. double latestIndicatorBoxExtent = 0.0; bool hasSliverLayoutExtent = false; bool needRefresh = false; bool draging = false; bool get vibrationEnable => ProviderScope.containerOf( context, listen: false, ).read(appVibrationStateProvider); @override void initState() { super.initState(); refreshState = RefreshIndicatorMode.inactive; } // A state machine transition calculator. Multiple states can be transitioned // through per single call. RefreshIndicatorMode? transitionNextState() { RefreshIndicatorMode? nextState; void goToDone() { nextState = RefreshIndicatorMode.done; // Either schedule the RenderSliver to re-layout on the next frame // when not currently in a frame or schedule it on the next frame. if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { setState(() => hasSliverLayoutExtent = false); } else { SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { setState(() => hasSliverLayoutExtent = false); }); } } switch (refreshState) { case null: case RefreshIndicatorMode.inactive: if (latestIndicatorBoxExtent <= 0) { return RefreshIndicatorMode.inactive; } else { nextState = RefreshIndicatorMode.drag; } continue drag; drag: case RefreshIndicatorMode.drag: if (latestIndicatorBoxExtent == 0) { return RefreshIndicatorMode.inactive; } else if (latestIndicatorBoxExtent < widget.refreshTriggerPullDistance) { return RefreshIndicatorMode.drag; } else { ///超过 refreshTriggerPullDistance 就可以进入准备刷新的装备状态 if (widget.onRefresh != null) { if (vibrationEnable) { HapticFeedback.mediumImpact(); } SchedulerBinding.instance.addPostFrameCallback(( Duration timestamp, ) { needRefresh = true; setState(() => hasSliverLayoutExtent = true); }); } return RefreshIndicatorMode.armed; } case RefreshIndicatorMode.armed: if (refreshState == RefreshIndicatorMode.armed && !needRefresh) { goToDone(); continue done; } ///当已经进去装备阶段,拖拽距离没到 refreshIndicatorExtent 的时候 ///继续返回 armed 状态,知道 latestIndicatorBoxExtent = refreshIndicatorExtent ///才进入刷新状态 if (latestIndicatorBoxExtent > widget.refreshIndicatorExtent) { return RefreshIndicatorMode.armed; } else { ///如果这时候手还在拖拽 if (draging) { goToDone(); continue done; } nextState = RefreshIndicatorMode.refresh; } continue refresh; refresh: case RefreshIndicatorMode.refresh: ///进入刷新状态,先判断是否达到刷新标准 if (needRefresh) { ///还没有触发外部刷新,触发一下 if (widget.onRefresh != null && refreshTask == null) { if (vibrationEnable) { HapticFeedback.mediumImpact(); } SchedulerBinding.instance.addPostFrameCallback(( Duration timestamp, ) { ///任务完成后清洗状态 refreshTask = widget.onRefresh!() ..whenComplete(() { if (mounted) { setState(() { refreshTask = null; needRefresh = false; }); refreshState = transitionNextState(); } }); setState(() => hasSliverLayoutExtent = true); }); } return RefreshIndicatorMode.refresh; } else { goToDone(); } continue done; done: case RefreshIndicatorMode.done: ///结束状态 if (latestIndicatorBoxExtent > widget.refreshTriggerPullDistance * _inactiveResetOverscrollFraction) { return RefreshIndicatorMode.done; } else { nextState = RefreshIndicatorMode.inactive; } break; } return nextState; } ///增加外部判断,处理手是不是还在拖拽,如果还在拖拽不触发刷新 void notifyScrollNotification(ScrollNotification notification) { if (notification is ScrollEndNotification) { if (refreshState == RefreshIndicatorMode.armed) { /// 放手了 draging = false; } } else if (notification is UserScrollNotification) { if (notification.direction != ScrollDirection.idle) { /// 手还在拖动 draging = true; } else { /// 放手了 draging = false; } } } @override Widget build(BuildContext context) { return _CupertinoSliverRefresh( refreshIndicatorLayoutExtent: widget.refreshIndicatorExtent, hasLayoutExtent: hasSliverLayoutExtent, child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { latestIndicatorBoxExtent = constraints.maxHeight; refreshState = transitionNextState(); if (widget.builder != null && latestIndicatorBoxExtent > 0) { var result = widget.builder?.call( context, refreshState, latestIndicatorBoxExtent, widget.refreshTriggerPullDistance, widget.refreshIndicatorExtent, ); return result!; } return Container(); }, ), ); } } ================================================ FILE: lib/widget/pull/nested/gsy_nested_pull_load_widget.dart ================================================ import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gsy_github_app_flutter/common/localization/extension.dart'; import 'package:gsy_github_app_flutter/common/style/gsy_style.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; import 'nested_refresh.dart'; ///通用下上刷新控件 class GSYNestedPullLoadWidget extends StatefulWidget { ///item渲染 final IndexedWidgetBuilder itemBuilder; ///加载更多回调 final RefreshCallback? onLoadMore; ///下拉刷新回调 final RefreshCallback? onRefresh; ///控制器,比如数据和一些配置 final GSYPullLoadWidgetControl control; final Key? refreshKey; final NestedScrollViewHeaderSliversBuilder? headerSliverBuilder; final ScrollController? scrollController; const GSYNestedPullLoadWidget( this.control, this.itemBuilder, this.onRefresh, this.onLoadMore, {super.key, this.refreshKey, this.headerSliverBuilder, this.scrollController}); @override _GSYNestedPullLoadWidgetState createState() => _GSYNestedPullLoadWidgetState(); } class _GSYNestedPullLoadWidgetState extends State { @override void initState() { super.initState(); } ///根据配置状态返回实际列表数量 ///实际上这里可以根据你的需要做更多的处理 ///比如多个头部,是否需要空页面,是否需要显示加载更多。 _getListCount() { ///是否需要头部 if (widget.control.needHeader) { ///如果需要头部,用Item 0 的 Widget 作为ListView的头部 ///列表数量大于0时,因为头部和底部加载更多选项,需要对列表数据总数+2 return (widget.control.dataList.isNotEmpty) ? widget.control.dataList.length + 2 : widget.control.dataList.length + 1; } else { ///如果不需要头部,在没有数据时,固定返回数量1用于空页面呈现 if (widget.control.dataList.isEmpty) { return 1; } ///如果有数据,因为部加载更多选项,需要对列表数据总数+1 return (widget.control.dataList.isNotEmpty) ? widget.control.dataList.length + 1 : widget.control.dataList.length; } } ///根据配置状态返回实际列表渲染Item _getItem(int index) { if (!widget.control.needHeader && index == widget.control.dataList.length && widget.control.dataList.isNotEmpty) { ///如果不需要头部,并且数据不为0,当index等于数据长度时,渲染加载更多Item(因为index是从0开始) return _buildProgressIndicator(); } else if (widget.control.needHeader && index == _getListCount() - 1 && widget.control.dataList.isNotEmpty) { ///如果需要头部,并且数据不为0,当index等于实际渲染长度 - 1时,渲染加载更多Item(因为index是从0开始) return _buildProgressIndicator(); } else if (!widget.control.needHeader && widget.control.dataList.isEmpty) { ///如果不需要头部,并且数据为0,渲染空页面 return _buildEmpty(); } else { ///回调外部正常渲染Item,如果这里有需要,可以直接返回相对位置的index return widget.itemBuilder(context, index); } } @override Widget build(BuildContext context) { return NestedScrollViewRefreshIndicator( ///GlobalKey,用户外部获取RefreshIndicator的State,做显示刷新 key: widget.refreshKey, ///下拉刷新触发,返回的是一个Future onRefresh: widget.onRefresh ?? () async {}, child: NestedScrollView( ///滑动监听 controller: widget.scrollController, physics: const AlwaysScrollableScrollPhysics(), headerSliverBuilder: widget.headerSliverBuilder!, body: NotificationListener( onNotification: (ScrollNotification p) { if (p.metrics.pixels >= p.metrics.maxScrollExtent) { if (widget.control.needLoadMore.value) { widget.onLoadMore?.call(); } } return false; }, child: ListView.builder( itemBuilder: (_, index) { return _getItem(index); }, ///根据状态返回数量 itemCount: _getListCount(), ), ), ), ); } ///空页面 Widget _buildEmpty() { return SizedBox( height: MediaQuery.sizeOf(context).height - 100, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( onPressed: () {}, child: const Image( image: AssetImage(GSYICons.DEFAULT_USER_ICON), width: 70.0, height: 70.0), ), Text(context.l10n.app_empty, style: GSYConstant.normalText), ], ), ); } ///上拉加载更多 Widget _buildProgressIndicator() { ///是否需要显示上拉加载更多的loading Widget bottomWidget = (widget.control.needLoadMore.value) ? Row(mainAxisAlignment: MainAxisAlignment.center, children: [ ///loading框 SpinKitRotatingCircle(color: Theme.of(context).primaryColor), Container( width: 5.0, ), ///加载中文本 Text( context.l10n.load_more_text, style: const TextStyle( color: Color(0xFF121917), fontSize: 14.0, fontWeight: FontWeight.bold, ), ) ]) /// 不需要加载 : Container(); return Padding( padding: const EdgeInsets.all(20.0), child: Center( child: bottomWidget, ), ); } } ================================================ FILE: lib/widget/pull/nested/gsy_sliver_header_delegate.dart ================================================ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; ///动态头部处理 class GSYSliverHeaderDelegate extends SliverPersistentHeaderDelegate { GSYSliverHeaderDelegate( {required this.minHeight, required this.maxHeight, required this.snapConfig, required this.vSyncs, this.child, this.builder, this.changeSize = false}); final double minHeight; final double maxHeight; final Widget? child; final Builder? builder; final TickerProvider vSyncs; final bool changeSize; final FloatingHeaderSnapConfiguration snapConfig; AnimationController? animationController; @override double get minExtent => minHeight; @override double get maxExtent => math.max(maxHeight, minHeight); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { if (builder != null) { return builder!(context, shrinkOffset, overlapsContent); } return child!; } @override TickerProvider get vsync => vSyncs; @override bool shouldRebuild(GSYSliverHeaderDelegate oldDelegate) { return true; } @override FloatingHeaderSnapConfiguration get snapConfiguration => snapConfig; } typedef Builder = Widget Function( BuildContext context, double shrinkOffset, bool overlapsContent); ================================================ FILE: lib/widget/pull/nested/nested_refresh.dart ================================================ // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math' as math; import 'package:flutter/material.dart'; // The over-scroll distance that moves the indicator to its maximum // displacement, as a percentage of the scrollable's container extent. const double _kDragContainerExtentPercentage = 0.25; // How much the scroll's drag gesture can overshoot the RefreshIndicator's // displacement; max displacement = _kDragSizeFactorLimit * displacement. const double _kDragSizeFactorLimit = 1.5; // When the scroll ends, the duration of the refresh indicator's animation // to the RefreshIndicator's displacement. const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150); // The duration of the ScaleTransition that starts when the refresh action // has completed. const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200); /// The signature for a function that's called when the user has dragged a /// [RefreshIndicator] far enough to demonstrate that they want the app to /// refresh. The returned [Future] must complete when the refresh operation is /// finished. /// /// Used by [RefreshIndicator.onRefresh]. /// // The state machine moves through these modes only when the scrollable // identified by scrollableKey has been scrolled to its min or max limit. enum _RefreshIndicatorMode { drag, // Pointer is down. armed, // Dragged far enough that an up event will run the onRefresh callback. snap, // Animating to the indicator's final "displacement". refresh, // Running the refresh callback. done, // Animating the indicator's fade-out after refreshing. canceled, // Animating the indicator's fade-out after not arming. } /// Used to configure how [NestedScrollViewRefreshIndicator] can be triggered. enum RefreshIndicatorTriggerMode { /// The indicator can be triggered regardless of the scroll position /// of the [Scrollable] when the drag starts. anywhere, /// The indicator can only be triggered if the [Scrollable] is at the edge /// when the drag starts. onEdge, } /// A widget that supports the Material "swipe to refresh" idiom. /// /// When the child's [Scrollable] descendant overscrolls, an animated circular /// progress indicator is faded into view. When the scroll ends, if the /// indicator has been dragged far enough for it to become completely opaque, /// the [onRefresh] callback is called. The callback is expected to update the /// scrollable's contents and then complete the [Future] it returns. The refresh /// indicator disappears after the callback's [Future] has completed. /// /// The trigger mode is configured by [NestedScrollViewRefreshIndicator.triggerMode]. /// /// ## Troubleshooting /// /// ### Refresh indicator does not show up /// /// The [NestedScrollViewRefreshIndicator] will appear if its scrollable descendant can be /// overscrolled, i.e. if the scrollable's content is bigger than its viewport. /// To ensure that the [NestedScrollViewRefreshIndicator] will always appear, even if the /// scrollable's content fits within its viewport, set the scrollable's /// [Scrollable.physics] property to [AlwaysScrollableScrollPhysics]: /// /// ```dart /// ListView( /// physics: const AlwaysScrollableScrollPhysics(), /// children: ... /// ) /// ``` /// /// A [NestedScrollViewRefreshIndicator] can only be used with a vertical scroll view. /// /// See also: /// /// * /// * [NestedScrollViewRefreshIndicatorState], can be used to programmatically show the refresh indicator. /// * [RefreshProgressIndicator], widget used by [NestedScrollViewRefreshIndicator] to show /// the inner circular progress spinner during refreshes. /// * [CupertinoSliverRefreshControl], an iOS equivalent of the pull-to-refresh pattern. /// Must be used as a sliver inside a [CustomScrollView] instead of wrapping /// around a [ScrollView] because it's a part of the scrollable instead of /// being overlaid on top of it. class NestedScrollViewRefreshIndicator extends StatefulWidget { /// Creates a refresh indicator. /// /// The [onRefresh], [child], and [notificationPredicate] arguments must be /// non-null. The default /// [displacement] is 40.0 logical pixels. /// /// The [semanticsLabel] is used to specify an accessibility label for this widget. /// If it is null, it will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]. /// An empty string may be passed to avoid having anything read by screen reading software. /// The [semanticsValue] may be used to specify progress on the widget. const NestedScrollViewRefreshIndicator({ super.key, required this.child, this.displacement = 40.0, this.edgeOffset = 0.0, required this.onRefresh, this.color, this.backgroundColor, this.notificationPredicate = NestScrollNotificationPredicate, this.semanticsLabel, this.semanticsValue, this.strokeWidth = 2.0, this.triggerMode = RefreshIndicatorTriggerMode.onEdge, }); /// The widget below this widget in the tree. /// /// The refresh indicator will be stacked on top of this child. The indicator /// will appear when child's Scrollable descendant is over-scrolled. /// /// Typically a [ListView] or [CustomScrollView]. final Widget child; /// The distance from the child's top or bottom [edgeOffset] where /// the refresh indicator will settle. During the drag that exposes the refresh /// indicator, its actual displacement may significantly exceed this value. /// /// In most cases, [displacement] distance starts counting from the parent's /// edges. However, if [edgeOffset] is larger than zero then the [displacement] /// value is calculated from that offset instead of the parent's edge. final double displacement; /// The offset where [RefreshProgressIndicator] starts to appear on drag start. /// /// Depending whether the indicator is showing on the top or bottom, the value /// of this variable controls how far from the parent's edge the progress /// indicator starts to appear. This may come in handy when, for example, the /// UI contains a top [Widget] which covers the parent's edge where the progress /// indicator would otherwise appear. /// /// By default, the edge offset is set to 0. /// /// See also: /// /// * [displacement], can be used to change the distance from the edge that /// the indicator settles. final double edgeOffset; /// A function that's called when the user has dragged the refresh indicator /// far enough to demonstrate that they want the app to refresh. The returned /// [Future] must complete when the refresh operation is finished. final RefreshCallback onRefresh; /// The progress indicator's foreground color. The current theme's /// [ColorScheme.primary] by default. final Color? color; /// The progress indicator's background color. The current theme's /// [ThemeData.canvasColor] by default. final Color? backgroundColor; /// A check that specifies whether a [ScrollNotification] should be /// handled by this widget. /// /// By default, checks whether `notification.depth == 0`. Set it to something /// else for more complicated layouts. final ScrollNotificationPredicate notificationPredicate; /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel} /// /// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel] /// if it is null. final String? semanticsLabel; /// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue} final String? semanticsValue; /// Defines `strokeWidth` for `NestedScrollViewRefreshIndicator`. /// /// By default, the value of `strokeWidth` is 2.0 pixels. final double strokeWidth; /// Defines how this [NestedScrollViewRefreshIndicator] can be triggered when users overscroll. /// /// The [NestedScrollViewRefreshIndicator] can be pulled out in two cases, /// 1, Keep dragging if the scrollable widget at the edge with zero scroll position /// when the drag starts. /// 2, Keep dragging after overscroll occurs if the scrollable widget has /// a non-zero scroll position when the drag starts. /// /// If this is [RefreshIndicatorTriggerMode.anywhere], both of the cases above can be triggered. /// /// If this is [RefreshIndicatorTriggerMode.onEdge], only case 1 can be triggered. /// /// Defaults to [RefreshIndicatorTriggerMode.onEdge]. final RefreshIndicatorTriggerMode triggerMode; @override NestedScrollViewRefreshIndicatorState createState() => NestedScrollViewRefreshIndicatorState(); } /// Contains the state for a [NestedScrollViewRefreshIndicator]. This class can be used to /// programmatically show the refresh indicator, see the [show] method. class NestedScrollViewRefreshIndicatorState extends State with TickerProviderStateMixin { late AnimationController _positionController; late AnimationController _scaleController; late Animation _positionFactor; late Animation _scaleFactor; late Animation _value; late Animation _valueColor; _RefreshIndicatorMode? _mode; late Future _pendingRefreshFuture; bool? _isIndicatorAtTop; double? _dragOffset; static final Animatable _threeQuarterTween = Tween(begin: 0.0, end: 0.75); static final Animatable _kDragSizeFactorLimitTween = Tween(begin: 0.0, end: _kDragSizeFactorLimit); static final Animatable _oneToZeroTween = Tween(begin: 1.0, end: 0.0); @override void initState() { super.initState(); _positionController = AnimationController(vsync: this); _positionFactor = _positionController.drive(_kDragSizeFactorLimitTween); _value = _positionController.drive( _threeQuarterTween); // The "value" of the circular progress indicator during a drag. _scaleController = AnimationController(vsync: this); _scaleFactor = _scaleController.drive(_oneToZeroTween); } @override void didChangeDependencies() { final ThemeData theme = Theme.of(context); _valueColor = _positionController.drive( ColorTween( begin: (widget.color ?? theme.colorScheme.primary).withValues(alpha: 0.0), end: (widget.color ?? theme.colorScheme.primary).withValues(alpha: 1.0), ).chain(CurveTween( curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit), )), ); super.didChangeDependencies(); } @override void didUpdateWidget(covariant NestedScrollViewRefreshIndicator oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.color != widget.color) { final ThemeData theme = Theme.of(context); _valueColor = _positionController.drive( ColorTween( begin: (widget.color ?? theme.colorScheme.primary).withValues(alpha: 0.0), end: (widget.color ?? theme.colorScheme.primary).withValues(alpha: 1.0), ).chain(CurveTween( curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit), )), ); } } @override void dispose() { _positionController.dispose(); _scaleController.dispose(); super.dispose(); } bool _shouldStart(ScrollNotification notification) { // If the notification.dragDetails is null, this scroll is not triggered by // user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll. // In this case, we don't want to trigger the refresh indicator. return ((notification is ScrollStartNotification && notification.dragDetails != null) || (notification is ScrollUpdateNotification && notification.dragDetails != null && widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) && notification.metrics.extentBefore == 0.0 && _mode == null && _start(notification.metrics.axisDirection); } bool _handleScrollNotification(ScrollNotification notification) { if (!widget.notificationPredicate(notification)) return false; if (_shouldStart(notification)) { setState(() { _mode = _RefreshIndicatorMode.drag; }); return false; } bool? indicatorAtTopNow; switch (notification.metrics.axisDirection) { case AxisDirection.down: indicatorAtTopNow = true; break; case AxisDirection.up: indicatorAtTopNow = false; break; case AxisDirection.left: case AxisDirection.right: indicatorAtTopNow = null; break; } if (indicatorAtTopNow != _isIndicatorAtTop) { if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) { _dismiss(_RefreshIndicatorMode.canceled); } } else if (notification is ScrollUpdateNotification) { if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) { if (notification.metrics.extentBefore > 0.0) { _dismiss(_RefreshIndicatorMode.canceled); } else { _dragOffset = _dragOffset! - notification.scrollDelta!; _checkDragOffset(notification.metrics.viewportDimension); } } if (_mode == _RefreshIndicatorMode.armed && notification.dragDetails == null) { // On iOS start the refresh when the Scrollable bounces back from the // overscroll (ScrollNotification indicating this don't have dragDetails // because the scroll activity is not directly triggered by a drag). _show(); } } else if (notification is OverscrollNotification) { if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) { _dragOffset = _dragOffset! - notification.overscroll; _checkDragOffset(notification.metrics.viewportDimension); } } else if (notification is ScrollEndNotification) { switch (_mode) { case _RefreshIndicatorMode.armed: _show(); break; case _RefreshIndicatorMode.drag: _dismiss(_RefreshIndicatorMode.canceled); break; default: // do nothing break; } } return false; } bool _handleGlowNotification(OverscrollIndicatorNotification notification) { if (notification.depth != 0 || !notification.leading) return false; if (_mode == _RefreshIndicatorMode.drag) { notification.disallowIndicator(); return true; } return false; } bool _start(AxisDirection direction) { assert(_mode == null); assert(_isIndicatorAtTop == null); assert(_dragOffset == null); switch (direction) { case AxisDirection.down: _isIndicatorAtTop = true; break; case AxisDirection.up: _isIndicatorAtTop = false; break; case AxisDirection.left: case AxisDirection.right: _isIndicatorAtTop = null; // we do not support horizontal scroll views. return false; } _dragOffset = 0.0; _scaleController.value = 0.0; _positionController.value = 0.0; return true; } void _checkDragOffset(double containerExtent) { assert(_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed); double newValue = _dragOffset! / (containerExtent * _kDragContainerExtentPercentage); if (_mode == _RefreshIndicatorMode.armed) { newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit); } _positionController.value = newValue.clamp(0.0, 1.0); // this triggers various rebuilds if (_mode == _RefreshIndicatorMode.drag && _valueColor.value!.a == 1.0) { _mode = _RefreshIndicatorMode.armed; } } // Stop showing the refresh indicator. Future _dismiss(_RefreshIndicatorMode newMode) async { await Future.value(); // This can only be called from _show() when refreshing and // _handleScrollNotification in response to a ScrollEndNotification or // direction change. assert(newMode == _RefreshIndicatorMode.canceled || newMode == _RefreshIndicatorMode.done); setState(() { _mode = newMode; }); switch (_mode) { case _RefreshIndicatorMode.done: await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration); break; case _RefreshIndicatorMode.canceled: await _positionController.animateTo(0.0, duration: _kIndicatorScaleDuration); break; default: assert(false); } if (mounted && _mode == newMode) { _dragOffset = null; _isIndicatorAtTop = null; setState(() { _mode = null; }); } } void _show() { assert(_mode != _RefreshIndicatorMode.refresh); assert(_mode != _RefreshIndicatorMode.snap); final Completer completer = Completer(); _pendingRefreshFuture = completer.future; _mode = _RefreshIndicatorMode.snap; _positionController .animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration) .then((void value) { if (mounted && _mode == _RefreshIndicatorMode.snap) { setState(() { // Show the indeterminate progress indicator. _mode = _RefreshIndicatorMode.refresh; }); final Future refreshResult = widget.onRefresh(); refreshResult.whenComplete(() { if (mounted && _mode == _RefreshIndicatorMode.refresh) { completer.complete(); _dismiss(_RefreshIndicatorMode.done); } }); } }); } /// Show the refresh indicator and run the refresh callback as if it had /// been started interactively. If this method is called while the refresh /// callback is running, it quietly does nothing. /// /// Creating the [NestedScrollViewRefreshIndicator] with a [GlobalKey] /// makes it possible to refer to the [NestedScrollViewRefreshIndicatorState]. /// /// The future returned from this method completes when the /// [NestedScrollViewRefreshIndicator.onRefresh] callback's future completes. /// /// If you await the future returned by this function from a [State], you /// should check that the state is still [mounted] before calling [setState]. /// /// When initiated in this manner, the refresh indicator is independent of any /// actual scroll view. It defaults to showing the indicator at the top. To /// show it at the bottom, set `atTop` to false. Future show({bool atTop = true}) { if (_mode != _RefreshIndicatorMode.refresh && _mode != _RefreshIndicatorMode.snap) { if (_mode == null) _start(atTop ? AxisDirection.down : AxisDirection.up); _show(); } return _pendingRefreshFuture; } @override Widget build(BuildContext context) { assert(debugCheckHasMaterialLocalizations(context)); final Widget child = NotificationListener( onNotification: _handleScrollNotification, child: NotificationListener( onNotification: _handleGlowNotification, child: widget.child, ), ); assert(() { if (_mode == null) { assert(_dragOffset == null); assert(_isIndicatorAtTop == null); } else { assert(_dragOffset != null); assert(_isIndicatorAtTop != null); } return true; }()); final bool showIndeterminateIndicator = _mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.done; return Stack( children: [ child, if (_mode != null) Positioned( top: _isIndicatorAtTop! ? widget.edgeOffset : null, bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null, left: 0.0, right: 0.0, child: SizeTransition( axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0, sizeFactor: _positionFactor, // this is what brings it down child: Container( padding: _isIndicatorAtTop! ? EdgeInsets.only(top: widget.displacement) : EdgeInsets.only(bottom: widget.displacement), alignment: _isIndicatorAtTop! ? Alignment.topCenter : Alignment.bottomCenter, child: ScaleTransition( scale: _scaleFactor, child: AnimatedBuilder( animation: _positionController, builder: (BuildContext context, Widget? child) { return RefreshProgressIndicator( semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context) .refreshIndicatorSemanticLabel, semanticsValue: widget.semanticsValue, value: showIndeterminateIndicator ? null : _value.value, valueColor: _valueColor, backgroundColor: widget.backgroundColor, strokeWidth: widget.strokeWidth, ); }, ), ), ), ), ), ], ); } } bool NestScrollNotificationPredicate(ScrollNotification notification) { return true; } ================================================ FILE: lib/widget/state/gsy_list_state.dart ================================================ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gsy_github_app_flutter/common/config/config.dart'; import 'package:gsy_github_app_flutter/common/logger.dart'; import 'package:gsy_github_app_flutter/widget/pull/gsy_pull_load_widget.dart'; /// 上下拉刷新列表的通用State /// Created by guoshuyu /// Date: 2018-07-20 mixin GSYListState on State, AutomaticKeepAliveClientMixin { bool isShow = false; bool isLoading = false; int page = 1; bool isRefreshing = false; bool isLoadMoring = false; final List dataList = []; final GSYPullLoadWidgetControl pullLoadWidgetControl = GSYPullLoadWidgetControl(); final GlobalKey refreshIndicatorKey = GlobalKey(); _lockToAwait() async { ///if loading, lock to await doDelayed() async { await Future.delayed(const Duration(seconds: 1)).then((_) async { if (isLoading) { return await doDelayed(); } else { return null; } }); } await doDelayed(); } showRefreshLoading() { Future.delayed(const Duration(seconds: 0), () { refreshIndicatorKey.currentState!.show().then((e) {}); return true; }); } @protected resolveRefreshResult(res) { if (res != null && res.result) { pullLoadWidgetControl.dataList.clear(); if (isShow) { setState(() { pullLoadWidgetControl.dataList.addAll(res.data); }); } } } @protected Future handleRefresh() async { if (isLoading) { if (isRefreshing) { return; } await _lockToAwait(); } isLoading = true; isRefreshing = true; page = 1; try { var res = await requestRefresh(); resolveRefreshResult(res); resolveDataResult(res); if (res.next != null) { var resNext = await res.next(); resolveRefreshResult(resNext); resolveDataResult(resNext); } } catch (e) { printLog(e); } isLoading = false; isRefreshing = false; return; } @protected Future onLoadMore() async { if (isLoading) { if (isLoadMoring) { return; } await _lockToAwait(); } isLoading = true; isLoadMoring = true; page++; var res = await requestLoadMore(); if (res != null && res.result) { if (isShow) { setState(() { pullLoadWidgetControl.dataList.addAll(res.data); }); } } resolveDataResult(res); isLoading = false; isLoadMoring = false; return; } @protected resolveDataResult(res) { if (isShow) { setState(() { pullLoadWidgetControl.needLoadMore.value = (res != null && res.data != null && res.data.length >= Config.PAGE_SIZE); }); } } @protected clearData() { if (isShow) { setState(() { pullLoadWidgetControl.dataList.clear(); }); } } ///下拉刷新数据 @protected requestRefresh() async {} ///上拉更多请求数据 @protected requestLoadMore() async {} ///是否需要第一次进入自动刷新 @protected bool get isRefreshFirst; ///是否需要头部 @protected bool get needHeader => false; ///是否需要保持 @override bool get wantKeepAlive => true; List get getDataList => dataList; @override void initState() { isShow = true; super.initState(); pullLoadWidgetControl.needHeader = needHeader; pullLoadWidgetControl.dataList = getDataList; if (pullLoadWidgetControl.dataList.isEmpty && isRefreshFirst) { showRefreshLoading(); } } @override void dispose() { isShow = false; isLoading = false; super.dispose(); } } ================================================ FILE: pubspec.yaml ================================================ name: gsy_github_app_flutter description: Github Application version: 1.7.0+32 environment: sdk: '>=3.8.0 <4.0.0' dependency_overrides: web_socket_channel: ^3.0.1 test_api: 0.7.7 matcher: 0.12.17 #dependency_overrides: #analyzer: 1.1.0 #web_socket_channel: 2.0.0 #pub_semver: 1.4.2 #glob: 2.0.0 #crypto: 3.0.0 #args: 1.0.0 #string_scanner: 1.1.0 #convert: 1.0.0 #rxdart: 0.25.0 #http: 0.13.0 #gql_http_link: 0.3.2 #http_parser: 4.0.0 # fix inappwebview R8 issue https://github.com/pichillilorenzo/flutter_inappwebview/issues/2193 # flutter_inappwebview_android: # git: # url: https://github.com/holzgeist/flutter_inappwebview # path: flutter_inappwebview_android # ref: d89b1d32638b49dfc58c4b7c84153be0c269d057 dependencies: flutter: sdk: flutter intl: any # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.4 fluttertoast: 8.2.10 share_plus: 12.0.1 flutter_spinkit: 5.2.1 sqflite: 2.3.3+1 pub_semver: 2.1.4 flutter_svg: 2.1.0 flutter_slidable: 3.1.2 dio: 5.9.0 #fconsole: 2.2.1 path_provider: 2.1.4 webview_flutter: 4.10.0 # 高版本目前和👆兼容有问题 flutter_inappwebview: 6.1.5 #bezier: ^1.1.5 flare_flutter: git: url: https://gitee.com/CarGuo/Flare-Flutter.git ref: d9c4ca55cbea3f283491679847ff1e9b4bc74192 path: flare_flutter/ rive: 0.13.13 # 固定版本 easy_refresh: 3.4.0 signals: 6.3.0 flutter_redux: 0.10.0 rxdart: 0.27.1 graphql: 5.2.2 #flutter_cache_manager: 3.0.0-nullsafety.0 provider: 6.1.5+1 permission_handler: 11.3.1 json_annotation: 4.9.0 package_info_plus: 8.0.2 connectivity_plus: 6.0.5 # flutter_markdown 已停止维护,使用 flutter_markdown_plus 替代 flutter_markdown_plus: 1.0.6 device_info_plus: 10.1.2 url_launcher: 6.3.2 google_fonts: 6.2.1 android_intent_plus: 5.1.0 shared_preferences: 2.5.4 built_value: 8.12.0 simple_animations: 5.2.0 supercharged: 2.1.1 animations: 2.1.1 lottie: 3.3.2 auto_size_text: 3.0.0 flutter_localizations: sdk: flutter photo_view: 0.15.0 # git: # url: https://github.com/CarSmallGuo/photo_view.git # ref: master redux: any webview_flutter_android: any webview_flutter_wkwebview: any string_scanner: any meta: any flutter_riverpod: 3.0.3 riverpod_annotation: 3.0.3 # error handle talker_flutter: 5.1.9 talker_dio_logger: 5.1.9 dev_dependencies: riverpod_generator: 3.0.3 built_value_generator: 8.12.0 build_runner: ^2.4.14 json_serializable: 6.11.1 flutter_lints: 5.0.0 # flutter_test: # sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec # The following section is specific to Flutter. flutter: # 针对多语言 l10n generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.io/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.io/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.io/custom-fonts/#from-packages assets: - static/images/ - static/file/ fonts: - family: wxcIconFont fonts: - asset: static/font/iconfont.ttf - family: google_kavivanar fonts: - asset: static/font/google_kavivanar.ttf - family: Akronim fonts: - asset: static/font/akronim.ttf ================================================ FILE: static/file/rejection.json ================================================ {"v":"5.0.0","fr":25,"ip":0,"op":87,"w":1080,"h":1080,"nm":"rejection v2","ddd":0,"assets":[{"id":"comp_17","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":54,"s":[10],"e":[0]},{"t":110}],"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[60,60,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Group 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[60,-28.25,0],"ix":2},"a":{"a":0,"k":[0,-88.25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":60,"s":[0],"e":[100]},{"t":110}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":10,"s":[0],"e":[100]},{"t":60}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Group 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[96.696,5.459,0],"ix":2},"a":{"a":0,"k":[36.696,-54.542,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.259,0.275,0.294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-12,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":58.947,"s":[0],"e":[100]},{"t":108.947265625}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":8.947,"s":[0],"e":[100]},{"t":58.947265625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Group 6","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[131.789,41.551,0],"ix":2},"a":{"a":0,"k":[71.789,-18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-24,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":57.895,"s":[0],"e":[100]},{"t":107.89453125}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":7.895,"s":[0],"e":[100]},{"t":57.89453125}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Group 8","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[131.789,78.449,0],"ix":2},"a":{"a":0,"k":[71.789,18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.259,0.275,0.294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":56.842,"s":[0],"e":[100]},{"t":106.841796875}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":6.842,"s":[0],"e":[100]},{"t":56.841796875}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Group 10","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[96.696,114.542,0],"ix":2},"a":{"a":0,"k":[36.696,54.541,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.027,0.02,0.02,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-48,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":55.789,"s":[0],"e":[100]},{"t":105.7890625}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":5.789,"s":[0],"e":[100]},{"t":55.7890625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Group 12","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[60,148.25,0],"ix":2},"a":{"a":0,"k":[0,88.25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.071,0.075,0.071,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-60,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":54.737,"s":[0],"e":[100]},{"t":104.7373046875}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":4.737,"s":[0],"e":[100]},{"t":54.7373046875}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Group 14","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[23.304,114.542,0],"ix":2},"a":{"a":0,"k":[-36.696,54.542,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-72,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":53.685,"s":[0],"e":[100]},{"t":103.6845703125}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":3.685,"s":[0],"e":[100]},{"t":53.6845703125}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Group 16","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-11.789,78.449,0],"ix":2},"a":{"a":0,"k":[-71.789,18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.071,0.098,0.09,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-84,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":52.632,"s":[0],"e":[100]},{"t":102.6318359375}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":2.632,"s":[0],"e":[100]},{"t":52.6318359375}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Group 18","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-11.789,41.551,0],"ix":2},"a":{"a":0,"k":[-71.789,-18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-96,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 18","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":51.579,"s":[0],"e":[100]},{"t":101.5791015625}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":1.579,"s":[0],"e":[100]},{"t":51.5791015625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Group 20","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[23.304,5.458,0],"ix":2},"a":{"a":0,"k":[-36.696,-54.542,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.071,0.098,0.09,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-108,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 20","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":50.526,"s":[0],"e":[100]},{"t":100.5263671875}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":0.526,"s":[0],"e":[100]},{"t":50.5263671875}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-186,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":86,"op":87,"st":86,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":86,"op":87,"st":86,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-186,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":87,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":87,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-186,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":25,"st":-86,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":25,"st":-86,"bm":0}]} ================================================ FILE: static/file/rejection2.json ================================================ {"v":"5.0.0","fr":25,"ip":0,"op":87,"w":1080,"h":1080,"nm":"rejection v2","ddd":0,"assets":[{"id":"comp_17","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":54,"s":[10],"e":[0]},{"t":110}],"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[60,60,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Group 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[60,-28.25,0],"ix":2},"a":{"a":0,"k":[0,-88.25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":60,"s":[0],"e":[100]},{"t":110}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":10,"s":[0],"e":[100]},{"t":60}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Group 4","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[96.696,5.459,0],"ix":2},"a":{"a":0,"k":[36.696,-54.542,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.259,0.275,0.294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-12,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":58.947,"s":[0],"e":[100]},{"t":108.947265625}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":8.947,"s":[0],"e":[100]},{"t":58.947265625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Group 6","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[131.789,41.551,0],"ix":2},"a":{"a":0,"k":[71.789,-18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-24,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":57.895,"s":[0],"e":[100]},{"t":107.89453125}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":7.895,"s":[0],"e":[100]},{"t":57.89453125}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Group 8","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[131.789,78.449,0],"ix":2},"a":{"a":0,"k":[71.789,18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.259,0.275,0.294,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-36,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 8","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":56.842,"s":[0],"e":[100]},{"t":106.841796875}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":6.842,"s":[0],"e":[100]},{"t":56.841796875}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Group 10","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[96.696,114.542,0],"ix":2},"a":{"a":0,"k":[36.696,54.541,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.027,0.02,0.02,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-48,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 10","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":55.789,"s":[0],"e":[100]},{"t":105.7890625}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":5.789,"s":[0],"e":[100]},{"t":55.7890625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Group 12","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[60,148.25,0],"ix":2},"a":{"a":0,"k":[0,88.25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.071,0.075,0.071,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-60,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 12","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":54.737,"s":[0],"e":[100]},{"t":104.7373046875}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":4.737,"s":[0],"e":[100]},{"t":54.7373046875}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Group 14","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[23.304,114.542,0],"ix":2},"a":{"a":0,"k":[-36.696,54.542,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-72,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 14","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":53.685,"s":[0],"e":[100]},{"t":103.6845703125}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":3.685,"s":[0],"e":[100]},{"t":53.6845703125}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Group 16","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-11.789,78.449,0],"ix":2},"a":{"a":0,"k":[-71.789,18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.071,0.098,0.09,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-84,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 16","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":52.632,"s":[0],"e":[100]},{"t":102.6318359375}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":2.632,"s":[0],"e":[100]},{"t":52.6318359375}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Group 18","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-11.789,41.551,0],"ix":2},"a":{"a":0,"k":[-71.789,-18.449,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.141,0.161,0.18,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-96,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 18","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":51.579,"s":[0],"e":[100]},{"t":101.5791015625}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":1.579,"s":[0],"e":[100]},{"t":51.5791015625}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Group 20","parent":1,"sr":1,"ks":{"o":{"a":0,"k":80,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[23.304,5.458,0],"ix":2},"a":{"a":0,"k":[-36.696,-54.542,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":353,"ix":7},"os":{"a":0,"k":0,"ix":9},"ix":1,"nm":"Polystar Path 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[0.071,0.098,0.09,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-108,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 20","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":50.526,"s":[0],"e":[100]},{"t":100.5263671875}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.8],"y":[0]},"n":["0p2_1_0p8_0"],"t":0.526,"s":[0],"e":[100]},{"t":50.5263671875}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":111,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-186,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":86,"op":87,"st":86,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":86,"op":87,"st":86,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-186,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":87,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":87,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-186,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":25,"st":-86,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"rejection lines","refId":"comp_17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[540,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":1080,"h":1080,"ip":0,"op":25,"st":-86,"bm":0}]} ================================================ FILE: static/file/search.json ================================================ {"v":"5.5.8","fr":24,"ip":0,"op":35,"w":480,"h":480,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":0,"k":[265,260,0],"ix":2},"a":{"a":0,"k":[0,19.75,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,19.75],[0,66.19]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":2,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":7,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":27,"s":[25]},{"t":32,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":2,"s":[18]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[9]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[9]},{"t":32,"s":[18]}],"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":2,"s":[229.645,224.645,0],"to":[-2.542,-2.458,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":7,"s":[214.395,209.895,0],"to":[0,0,0],"ti":[-2.542,-2.458,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":12,"s":[229.645,224.645,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":22,"s":[229.645,224.645,0],"to":[-2.542,-2.458,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[214.395,209.895,0],"to":[0,0,0],"ti":[-2.542,-2.458,0]},{"t":32,"s":[229.645,224.645,0]}],"ix":2},"a":{"a":0,"k":[-24.742,-25.176,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":2,"s":[{"i":[[-27.614,0],[0,-27.614],[27.614,0],[0,27.614]],"o":[[27.614,0],[0,27.614],[-27.614,0],[0,-27.614]],"v":[[0,-50],[50,0],[0,50],[-50,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[{"i":[[27.69,0],[0,-27.614],[-27.69,0],[0,27.614]],"o":[[-27.69,0],[0,27.614],[27.69,0],[0,-27.614]],"v":[[0.265,-50],[-49.872,0],[0.265,50],[50.402,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":22,"s":[{"i":[[27.69,0],[0,-27.614],[-27.69,0],[0,27.614]],"o":[[-27.69,0],[0,27.614],[27.69,0],[0,-27.614]],"v":[[0.265,-50],[-49.872,0],[0.265,50],[50.402,0]],"c":true}]},{"t":32,"s":[{"i":[[-27.614,0],[0,-27.614],[27.614,0],[0,27.614]],"o":[[27.614,0],[0,27.614],[-27.614,0],[0,-27.614]],"v":[[0,-50],[50,0],[0,50],[-50,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":2,"s":[18]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[9]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[9]},{"t":32,"s":[18]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-24.742,-25.176],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.958,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} ================================================ FILE: static/file/user.json ================================================ {"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":540,"h":540,"nm":"profile in out","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 2 Outlines","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.19,"y":0},"t":0,"s":[234.822,3.142,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":0},"o":{"x":0.19,"y":0.19},"t":38,"s":[234.822,-41.858,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.81,"y":1},"o":{"x":1,"y":0},"t":84,"s":[234.822,-41.858,0],"to":[0,0,0],"ti":[0,0,0]},{"t":114,"s":[234.822,3.142,0]}],"ix":2},"a":{"a":0,"k":[167.133,167.134,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.17,0.17,0.17],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":7,"s":[100,100,100]},{"i":{"x":[0.83,0.83,0.83],"y":[1,1,1]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":107,"s":[100,100,100]},{"t":114,"s":[0,0,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.19,"y":0},"t":0,"s":[{"i":[[0,-24.981],[24.981,0],[0,24.981],[-24.981,0]],"o":[[0,24.981],[-24.981,0],[0,-24.981],[24.981,0]],"v":[[45.232,0],[0,45.232],[-45.232,0],[0,-45.232]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.19,"y":0},"t":38,"s":[{"i":[[0,-43.98],[43.981,0],[0,43.98],[-43.98,0]],"o":[[0,43.98],[-43.98,0],[0,-43.98],[43.981,0]],"v":[[79.633,-0.001],[-0.001,79.633],[-79.633,-0.001],[-0.001,-79.633]],"c":true}]},{"i":{"x":0.81,"y":1},"o":{"x":1,"y":0},"t":84,"s":[{"i":[[0,-43.98],[43.981,0],[0,43.98],[-43.98,0]],"o":[[0,43.98],[-43.98,0],[0,-43.98],[43.981,0]],"v":[[79.633,-0.001],[-0.001,79.633],[-79.633,-0.001],[-0.001,-79.633]],"c":true}]},{"t":114,"s":[{"i":[[0,-24.981],[24.981,0],[0,24.981],[-24.981,0]],"o":[[0,24.981],[-24.981,0],[0,-24.981],[24.981,0]],"v":[[45.232,0],[0,45.232],[-45.232,0],[0,-45.232]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":35,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[167.133,167.134],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":240,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"1 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[315.5,379.496,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.78,"y":0.78},"o":{"x":0.167,"y":0.167},"t":38,"s":[270.5,379.496,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.78,"y":1},"o":{"x":1,"y":0},"t":84,"s":[270.5,379.496,0],"to":[0,0,0],"ti":[0,0,0]},{"t":114,"s":[220.5,379.496,0]}],"ix":2},"a":{"a":0,"k":[234.822,147.004,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-31.906,0],[0,0],[0,-31.907],[0,0]],"o":[[0,0],[0,-31.907],[0,0],[31.906,0],[0,0],[0,0]],"v":[[-147.322,59.504],[-147.322,-1.732],[-89.551,-59.504],[89.551,-59.504],[147.322,-1.732],[147.322,59.504]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.02],"y":[1]},"o":{"x":[0.42],"y":[0]},"t":0,"s":[0]},{"t":7,"s":[35]}],"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[234.822,147.004],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.78],"y":[1]},"o":{"x":[1],"y":[0]},"t":84,"s":[0]},{"t":114,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.08],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":0,"s":[0]},{"t":38,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} ================================================ FILE: static/font/demo.css ================================================ *{margin: 0;padding: 0;list-style: none;} /* KISSY CSS Reset 理念:1. reset 的目的不是清除浏览器的默认样式,这仅是部分工作。清除和重置是紧密不可分的。 2. reset 的目的不是让默认样式在所有浏览器下一致,而是减少默认样式有可能带来的问题。 3. reset 期望提供一套普适通用的基础样式。但没有银弹,推荐根据具体需求,裁剪和修改后再使用。 特色:1. 适应中文;2. 基于最新主流浏览器。 维护:玉伯, 正淳 */ /** 清除内外边距 **/ body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* structural elements 结构元素 */ dl, dt, dd, ul, ol, li, /* list elements 列表元素 */ pre, /* text formatting elements 文本格式元素 */ form, fieldset, legend, button, input, textarea, /* form elements 表单元素 */ th, td /* table elements 表格元素 */ { margin: 0; padding: 0; } /** 设置默认字体 **/ body, button, input, select, textarea /* for ie */ { font: 12px/1.5 tahoma, arial, \5b8b\4f53, sans-serif; } h1, h2, h3, h4, h5, h6 { font-size: 100%; } address, cite, dfn, em, var { font-style: normal; } /* 将斜体扶正 */ code, kbd, pre, samp { font-family: courier new, courier, monospace; } /* 统一等宽字体 */ small { font-size: 12px; } /* 小于 12px 的中文很难阅读,让 small 正常化 */ /** 重置列表元素 **/ ul, ol { list-style: none; } /** 重置文本格式元素 **/ a { text-decoration: none; } a:hover { text-decoration: underline; } /** 重置表单元素 **/ legend { color: #000; } /* for ie6 */ fieldset, img { border: 0; } /* img 搭车:让链接里的 img 无边框 */ button, input, select, textarea { font-size: 100%; } /* 使得表单元素在 ie 下能继承字体大小 */ /* 注:optgroup 无法扶正 */ /** 重置表格元素 **/ table { border-collapse: collapse; border-spacing: 0; } /* 清除浮动 */ .ks-clear:after, .clear:after { content: '\20'; display: block; height: 0; clear: both; } .ks-clear, .clear { *zoom: 1; } .main { padding: 30px 100px; width: 960px; margin: 0 auto; } .main h1{font-size:36px; color:#333; text-align:left;margin-bottom:30px; border-bottom: 1px solid #eee;} .helps{margin-top:40px;} .helps pre{ padding:20px; margin:10px 0; border:solid 1px #e7e1cd; background-color: #fffdef; overflow: auto; } .icon_lists{ width: 100% !important; } .icon_lists li{ float:left; width: 100px; height:180px; text-align: center; list-style: none !important; } .icon_lists .icon{ font-size: 42px; line-height: 100px; margin: 10px 0; color:#333; -webkit-transition: font-size 0.25s ease-out 0s; -moz-transition: font-size 0.25s ease-out 0s; transition: font-size 0.25s ease-out 0s; } .icon_lists .icon:hover{ font-size: 100px; } .markdown { color: #666; font-size: 14px; line-height: 1.8; } .highlight { line-height: 1.5; } .markdown img { vertical-align: middle; max-width: 100%; } .markdown h1 { color: #404040; font-weight: 500; line-height: 40px; margin-bottom: 24px; } .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6 { color: #404040; margin: 1.6em 0 0.6em 0; font-weight: 500; clear: both; } .markdown h1 { font-size: 28px; } .markdown h2 { font-size: 22px; } .markdown h3 { font-size: 16px; } .markdown h4 { font-size: 14px; } .markdown h5 { font-size: 12px; } .markdown h6 { font-size: 12px; } .markdown hr { height: 1px; border: 0; background: #e9e9e9; margin: 16px 0; clear: both; } .markdown p, .markdown pre { margin: 1em 0; } .markdown > p, .markdown > blockquote, .markdown > .highlight, .markdown > ol, .markdown > ul { width: 80%; } .markdown ul > li { list-style: circle; } .markdown > ul li, .markdown blockquote ul > li { margin-left: 20px; padding-left: 4px; } .markdown > ul li p, .markdown > ol li p { margin: 0.6em 0; } .markdown ol > li { list-style: decimal; } .markdown > ol li, .markdown blockquote ol > li { margin-left: 20px; padding-left: 4px; } .markdown code { margin: 0 3px; padding: 0 5px; background: #eee; border-radius: 3px; } .markdown pre { border-radius: 6px; background: #f7f7f7; padding: 20px; } .markdown pre code { border: none; background: #f7f7f7; margin: 0; } .markdown strong, .markdown b { font-weight: 600; } .markdown > table { border-collapse: collapse; border-spacing: 0px; empty-cells: show; border: 1px solid #e9e9e9; width: 95%; margin-bottom: 24px; } .markdown > table th { white-space: nowrap; color: #333; font-weight: 600; } .markdown > table th, .markdown > table td { border: 1px solid #e9e9e9; padding: 8px 16px; text-align: left; } .markdown > table th { background: #F7F7F7; } .markdown blockquote { font-size: 90%; color: #999; border-left: 4px solid #e9e9e9; padding-left: 0.8em; margin: 1em 0; font-style: italic; } .markdown blockquote p { margin: 0; } .markdown .anchor { opacity: 0; transition: opacity 0.3s ease; margin-left: 8px; } .markdown .waiting { color: #ccc; } .markdown h1:hover .anchor, .markdown h2:hover .anchor, .markdown h3:hover .anchor, .markdown h4:hover .anchor, .markdown h5:hover .anchor, .markdown h6:hover .anchor { opacity: 1; display: inline-block; } .markdown > br, .markdown > p > br { clear: both; } .hljs { display: block; background: white; padding: 0.5em; color: #333333; overflow-x: auto; } .hljs-comment, .hljs-meta { color: #969896; } .hljs-string, .hljs-variable, .hljs-template-variable, .hljs-strong, .hljs-emphasis, .hljs-quote { color: #df5000; } .hljs-keyword, .hljs-selector-tag, .hljs-type { color: #a71d5d; } .hljs-literal, .hljs-symbol, .hljs-bullet, .hljs-attribute { color: #0086b3; } .hljs-section, .hljs-name { color: #63a35c; } .hljs-tag { color: #333333; } .hljs-title, .hljs-attr, .hljs-selector-id, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo { color: #795da3; } .hljs-addition { color: #55a532; background-color: #eaffea; } .hljs-deletion { color: #bd2c00; background-color: #ffecec; } .hljs-link { text-decoration: underline; } pre{ background: #fff; } ================================================ FILE: static/font/demo_fontclass.html ================================================ IconFont

IconFont 图标

  • 提醒
    .icon-tixing
  • add
    .icon-add
  • 分享
    .icon-fenxiang
  • 朋友圈
    .icon-pengyouquan
  • 账户1
    .icon-zhanghu1
  • 团队
    .icon-tuandui
  • code
    .icon-code
  • 位置
    .icon-weizhi
  • .icon-xing
  • 搜索
    .icon-sousuo
  • 问号
    .icon-wenhao
  • 版本更新
    .icon-banbengengxin
  • .icon-icon
  • 地球
    .icon-tansuob
  • 键盘
    .icon-jianpan
  • 时间
    .icon-shijian
  • 眼睛
    .icon-yanjing
  • 书签
    .icon-labelb
  • 眼睛
    .icon-yanjing1
  • 提醒
    .icon-tixing1
  • 星星
    .icon-xingxing
  • 简介
    .icon-jianjie
  • next
    .icon-next
  • 返回
    .icon-fanhui
  • at
    .icon-at
  • 日访问趋势
    .icon-rifangwenqushi
  • .icon-ren
  • 星星
    .icon-star
  • .icon-xing1
  • 清理缓存
    .icon-qinglihuancun
  • .icon-fl-jia
  • 链接
    .icon-lianjie
  • 添加
    .icon-tianjia
  • 文件
    .icon-wenjian
  • 文件
    .icon-wenjian1
  • GitHub
    .icon-GitHub
  • 我的
    .icon-gaiicon-
  • 评论
    .icon-pinglun
  • more
    .icon-more

font-class引用


font-class是unicode使用方式的一种变种,主要是解决unicode书写不直观,语意不明确的问题。

与unicode使用方式相比,具有如下特点:

  • 兼容性良好,支持ie8+,及所有现代浏览器。
  • 相比于unicode语意明确,书写更直观。可以很容易分辨这个icon是什么。
  • 因为使用class来定义图标,所以当要替换图标时,只需要修改class里面的unicode引用。
  • 不过因为本质上还是使用的字体,所以多色图标还是不支持的。

使用步骤如下:

第一步:引入项目下面生成的fontclass代码:

<link rel="stylesheet" type="text/css" href="./iconfont.css">

第二步:挑选相应图标并获取类名,应用于页面:

<i class="iconfont icon-xxx"></i>

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

================================================ FILE: static/font/demo_symbol.html ================================================ IconFont

IconFont 图标

  • 提醒
    #icon-tixing
  • add
    #icon-add
  • 分享
    #icon-fenxiang
  • 朋友圈
    #icon-pengyouquan
  • 账户1
    #icon-zhanghu1
  • 团队
    #icon-tuandui
  • code
    #icon-code
  • 位置
    #icon-weizhi
  • #icon-xing
  • 搜索
    #icon-sousuo
  • 问号
    #icon-wenhao
  • 版本更新
    #icon-banbengengxin
  • #icon-icon
  • 地球
    #icon-tansuob
  • 键盘
    #icon-jianpan
  • 时间
    #icon-shijian
  • 眼睛
    #icon-yanjing
  • 书签
    #icon-labelb
  • 眼睛
    #icon-yanjing1
  • 提醒
    #icon-tixing1
  • 星星
    #icon-xingxing
  • 简介
    #icon-jianjie
  • next
    #icon-next
  • 返回
    #icon-fanhui
  • at
    #icon-at
  • 日访问趋势
    #icon-rifangwenqushi
  • #icon-ren
  • 星星
    #icon-star
  • #icon-xing1
  • 清理缓存
    #icon-qinglihuancun
  • #icon-fl-jia
  • 链接
    #icon-lianjie
  • 添加
    #icon-tianjia
  • 文件
    #icon-wenjian
  • 文件
    #icon-wenjian1
  • GitHub
    #icon-GitHub
  • 我的
    #icon-gaiicon-
  • 评论
    #icon-pinglun
  • more
    #icon-more

symbol引用


这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇文章 这种用法其实是做了一个svg的集合,与另外两种相比具有如下特点:

  • 支持多色图标了,不再受单色限制。
  • 通过一些技巧,支持像字体那样,通过font-size,color来调整样式。
  • 兼容性较差,支持 ie9+,及现代浏览器。
  • 浏览器渲染svg的性能一般,还不如png。

使用步骤如下:

第一步:引入项目下面生成的symbol代码:

<script src="./iconfont.js"></script>

第二步:加入通用css代码(引入一次就行):

<style type="text/css">
.icon {
   width: 1em; height: 1em;
   vertical-align: -0.15em;
   fill: currentColor;
   overflow: hidden;
}
</style>

第三步:挑选相应图标并获取类名,应用于页面:

<svg class="icon" aria-hidden="true">
  <use xlink:href="#icon-xxx"></use>
</svg>
        
================================================ FILE: static/font/demo_unicode.html ================================================ IconFont

IconFont 图标

  • 提醒
    &#xe600;
  • add
    &#xe662;
  • 分享
    &#xe67e;
  • 朋友圈
    &#xe684;
  • 账户1
    &#xe666;
  • 团队
    &#xe63e;
  • code
    &#xea77;
  • 位置
    &#xe7e6;
  • &#xe698;
  • 搜索
    &#xe61c;
  • 问号
    &#xe72d;
  • 版本更新
    &#xe63c;
  • &#xe62f;
  • 地球
    &#xe66a;
  • 键盘
    &#xe60e;
  • 时间
    &#xe62d;
  • 眼睛
    &#xe681;
  • 书签
    &#xe61a;
  • 眼睛
    &#xe629;
  • 提醒
    &#xe661;
  • 星星
    &#xe642;
  • 简介
    &#xe6c4;
  • next
    &#xe610;
  • 返回
    &#xe78a;
  • at
    &#xe6b0;
  • 日访问趋势
    &#xe818;
  • &#xe604;
  • 星星
    &#xe643;
  • &#xe630;
  • 清理缓存
    &#xe615;
  • &#xe624;
  • 链接
    &#xe670;
  • 添加
    &#xe67b;
  • 文件
    &#xe68a;
  • 文件
    &#xe793;
  • GitHub
    &#xea0a;
  • 我的
    &#xe6d0;
  • 评论
    &#xe6ba;
  • more
    &#xe674;

unicode引用


unicode是字体在网页端最原始的应用方式,特点是:

  • 兼容性最好,支持ie6+,及所有现代浏览器。
  • 支持按字体的方式去动态调整图标大小,颜色等等。
  • 但是因为是字体,所以不支持多色。只能使用平台里单色的图标,就算项目里有多色图标也会自动去色。

注意:新版iconfont支持多色图标,这些多色图标在unicode模式下将不能使用,如果有需求建议使用symbol的引用方式

unicode使用步骤如下:

第一步:拷贝项目下面生成的font-face

@font-face {
  font-family: 'iconfont';
  src: url('iconfont.eot');
  src: url('iconfont.eot?#iefix') format('embedded-opentype'),
  url('iconfont.woff') format('woff'),
  url('iconfont.ttf') format('truetype'),
  url('iconfont.svg#iconfont') format('svg');
}

第二步:定义使用iconfont的样式

.iconfont{
  font-family:"iconfont" !important;
  font-size:16px;font-style:normal;
  -webkit-font-smoothing: antialiased;
  -webkit-text-stroke-width: 0.2px;
  -moz-osx-font-smoothing: grayscale;
}

第三步:挑选相应图标并获取字体编码,应用于页面

<i class="iconfont">&#x33;</i>

"iconfont"是你项目下的font-family。可以通过编辑项目查看,默认是"iconfont"。

================================================ FILE: static/font/iconfont.css ================================================ @font-face {font-family: "iconfont"; src: url('iconfont.eot?t=1522803601095'); /* IE9*/ src: url('iconfont.eot?t=1522803601095#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAABtYAAsAAAAAJ8AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAAQwAAAFZW8UxsY21hcAAAAYAAAAGpAAAEsszHhhtnbHlmAAADLAAAFM0AABwoGCy/GWhlYWQAABf8AAAAMQAAADYRIs0daGhlYQAAGDAAAAAeAAAAJAgOA/NobXR4AAAYUAAAACYAAACkpFv//WxvY2EAABh4AAAAVAAAAFSHyo4QbWF4cAAAGMwAAAAfAAAAIAFCAJZuYW1lAAAY7AAAAUUAAAJtPlT+fXBvc3QAABo0AAABIgAAAaXbFF3peJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkYWKcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBwYKl6VMzf8b2CIYW5kaAYKM4LkAN0qC/YAeJzN1M1KG2EYxfF/NLU2tan9SJumae1H2io06AWIFN2WUnoHunDhyoWgIAiCq6wKbrwM19JV8QK8ijOSVe/AnjcnSxcVXHSGX8gM5M3Mc84McAeYtM9Wh4nf1PyN2i+frY3OT9IYna/Xfvr4K1/8mxn2hOpqalYtddRVTwvqa0nLWtGaNrSpLW1rR/s60KGONNCJTnWmc11U/WpQHVe6bA8bw92rK696/WqLo9VWtX7D1W5rq/lur9+/jfbv/Pjnvaw2QZdZ2szxhPu84SmPaPKaB9zlGa9oedZTPOQt87xzGo896feefodPPOejp/6CHvd4yQemveDULd7rjUfzn2wz5aP2Z3w0b3tjvkQRnrz7Fc4ANcNpoDHnglrhhFAnnBXqhlNDvXB+aCGcJOqHM0WLQfmfpXDOaDmcOFoJZ49Wwy1Aa+E+oPWgXNdGlKdVm+G2oK1wb9B2uEFoJ8pMtB9uFTqI8rTrMNw0dBTljaBBuH3oJMpbQKdBmc1ZuJvoPNxSdBHuK1U/SkrVINxhquNwm6kU7jWX7XDDGTbCXWe4G0z/BUU42MUAAAB4nHVZC3wbxZnfb2Z3VruSdrXa1a5fkizJ0vodW7IkHCeSnTghTwIOcR4UkgBtIalztFyBhHKYKyRwLTko0FIKRwoBAu31V8oFWhrCs9CGcgQClBBoEtrSF216l6NX0nhz36zsNKVXazTPb0Yz33yP/zcWJEE4cZjuonWCKbQKvcKwcKYgAOuAtEbikHL7ukkHxFJSzLE06mbclJxJd9OZ4KSZZedLfTmHyUwHDRJQSOVLbjdxodhXIQOQt+MA9Y0NS6PZpii9CdQ6N3Gdt4DcA7FkpkmvdHnzO6tWvtkMXB6KRuuj0S8GmCQFCBF1DcYcW5EUlXnbJb0htivZRpIQqncbFq0MNzdG117ftyGedRSA8XEwG5u1B6pGg4Hpcw22Ga2XI+FAXUM402LB5T8P1pmheO5nAv5RPOtOegddJqjCNGG6IGTzGsHjkELWxkoqnStXKO69nCv2lfJ2zGJygloJApY9ABXoy6UZuVW2WkvJxj7XEokWACXEJiwtHDR62hPx7hmpSP/Z/QE5U+luSnb0eKudCDFsO+PUJbNJTMtVIyQ7RnLR8rUXj8zsCDbG2/v72/XGULCjOvKJC1YuIBcbDthpxxEI7vUn9EHaJgQFRxCkbnArUE6Ao4GZKWZimVghVigWiLDziCQd2ennyYfOfghT21Qbc/hP3vMQXimu9yS9kg4K7XhuJrulolu2HVwTV9ZAthw5x0v8hVLZYTEs/THy7rTszwB+l2obGgUYHcJc3O8Wq0sBllaLuXcIWVrxG+9B/d2yTB6IAhkdGhwlZHQwatzRDHx8hEDya6FQZSmnrizld8FwP0/Rp3A/oqAIEaFeaBZc3JmJvwoUNwKpUrEv50pJMHGHaZmmWMyynTLFneXL8L+3r9ma9ca+vPbmHNFgtnZar/cHaXj7zLtWdbbb3q5IOf/jB7HR233Q+7D5hvNgPtDklnO9X5WmL71l+hWBy6CpfxAC3pP9laVfGjjhvT19EFS+L873LfT3dD3yPcM1QZBtwSkJ5ZxAa3dwyk3Ifg7H93m/lCSo3/ca1DHm/eq1c++fpW6/cnCEkAsWLrwQYGToyvuUofvpOsn75b4p4n1Qf7wIQ/2f2xEKrZ7NyS5cOHt1KLTjc/1DyJ8TJ07sE4F2CSmhKpwnXCxcLmxB/iBLugG1k8n488yW83aBM6RUxj3lS8UKZP0tFjM4jImzjI/bBRRppJH8+Zl0PoPCbSdRax1UU5zipyJOrolDgp+w8JeWVPuF4lSbMtNMLem5ND0QT7mSSERJZ/1j09Pz0vHpcZFIxFuthpWm+BazRS/NByK6i3N6WkfdHiJ7VCW+orIosSgel0Sxrtvqr2uvi1cTIvHY0DJxdMbgcsLO/lBLmoOj0rIZgyuI2+3Nsdp1qSVO/DZZcUe6WufmTLN+KNd5Xg/THSCSZKiy5C78QBSlpkoOZCl27sA2bKiGxHJ6k96/JG11mtOnu0N1QKkSN4dHu3ADkoknkh4hK8ZH8KYGB1fch0Zo+The3kjVmj9TVdyqxQb9sYFR34a8IvaQ9wRLaBH68UZklnYh11eGUj4JtqUjRy0uwdOQeWUUZ85kBVh6GhJVweVZKY9absHaONMl7wOUhiCyLy5Jhw9LsOh1EpZbUJNqZQA+ixRToxKfgdQ4k4HKdJ7hkM4OH2I6+dhEMiATlYQD5HCt4l3r/bFGcOgwzsRZ2Nan5Pxq+gU6judoRjnnJsB2bNwXKhe/a7xqeoqoE2HHQbElTUVoDqc1SIok0yIe3DHjTIAzZ/g5Hcf2zM/0Msm7VtPhSibmPzNzx8GJ9znBEkKWIFntd++h79DzUetR4cCUqJmVTJo1wXsATttb773p/aR+L5y2g671Di/xTngPo41fApiS/r5RL8ZFAfdtCtghcb6yFjdHyqUWx47KnPm5ZplZjt1cLvXR31l56/nN3o+u2hK+5gZYd+MV5sdfML2nLvL2XFStXgSliypjpvnC2tCG62Bs6xa2+Z+83Vuet/Jk3+bNy/Yu27x55Y8FtFBTtirgc2sYbz0B3PmlmXMKj2jNIvgKiLybOalX3VxbLZbOFU/aDV8zGeyFxtZGTNN2T0jSxO5afu9+Udx/by3XotDdFEsClEZKEG2KklXDw6sIdDTaaYheEmtsbGtsfO7kzN0TdODk1Hv3T7wL0XDJrTZYuUhbudgWjkbDwysJOWe4pb8h1m6VwtGaT6zxMyh0Cov/2uJBbiZwT+hbCNx5pqb+hbxtIUIo51wUmzTSYB+OFGpnR6K+0gDE0LrYDhHe8g6hrDa/tR9SkuQd9FpB0ZTUZfMv/ILWHN40Ory+cZOsKPIOoASOgxJW0p+df+EXtWZt4/LhdU3+2AOUEAA6zrxDb+33DqK+pPa/Bc3eWYRu4+P/OON0IFetXbqewJzpuDohxNvDFIV9dsY8oFet4QNz+/naBKhIKZcj7g9/SJ8j5wiIgBRwZQXoIu/cI5fDJ0gDWeWdd+QK7w7S4PPnebz76UKTMF9YhjfPDWiVM6TE7SdB8yrHuC2WSzmad/yaRtwcwiLUJ37TOW4JXD4NU9E3wCgyMCUpyFGyxTgnRESAzGAqEWTBWJ1lqwANysCCiLp17VlOn64EQlHHqOsNyqLFRKqgxQ0G41FNi7UwdBOybIX67/DuOEUWLm1dHYw3iVaDKsWsQERTg+GWZqk4Xck3ZGaRb7dqbjQcMqOUUKU+OrdeiaghQlko3Cghi0LBQCykNxtaRJwxx1u360NJ+nCXnyPvQj6Yeoo8IOiohY7QiJqYQe/dgaiqIJSFAeQSginXqHBMaDsGqkbKSEGmWEDsUoBUMeUWYhkTv7x0sO/Ukj50/JtKCCCk0LP90jr+DK1MfGJsbNHYGNzqLYL/KPn1r46NpcbGHt+wAfPvjY2txPtNK+GwMvFTJUy6Jv4HLhrzjm3wjo19dQOwMaTfcLv/ncKCV9MjdIWPr9rQkuQEF4UeRZ8Jpyqymcq5uaLhA0IDRdxihO71foNCaO99BWyU6d+88uhRSTr66GNHRfHoEu+YpshGQ/SXYcMYR6JX9taI9r7i/Ybe5pMh8WOPHvXqQQ0EoQ/pwt77gFMmcRFu7jmyFffVgJx1BdwU8vMkUMN95VrcUkvZFh0myv2keOrQqeCknxDhugkr9MFt/+oFgh/e/BXvYDAI6S/fA3VK0DtwS88gwGDPNJ5Pm70SYOXs2WgdVs7+N+Wezt6HVfnOBe33lgI7537y35n6w3mVXcpdpNpTm9RTnUamyDEXpmzkM2gjBSEu5DiWU8AXdTmHShAzLI5DUCPy5RL/5LLdqEPckLiQJlc/+2xdf/bzw8NugYiW9uq++OLEls1BWVMnnsoVoLAwn89S6n362WfrO4H/FVzvv4KqmQjv29fYtHlLUIujD0IyTHHBV3D6DLkePYzNvQRM3ifJnerSkGW1EIYIb/8cyG9fe+239K1fzTkHLeScWp5obZ3R2oraeTSqvxmJvJk66n0KTzzMmTU8e+U70DrQikmYPP8zdDcdQr9mozb04p1Nre8ap8iTa5x6Rwa6hhj3JWhocxk4kcjlBlw3vvu4JB3f/QTPn/j666L4+te3vSGKb2xL5AB8gj/5xUAOyLyThDjp+AvS69smJ7wukRvd+Mtx18+mbN779Hu0ishF4DYp63LfpIOkEbRllCVIFe+okK+AS9Fz0Z2iEfJOXMrMOotd6p0IRgNMebyTBCwn+N4vVDUcC0Dn4wEGEyGdZH8dDIWCv84S3XECO08IIVsDaeNGSbdDIOwMYAzDLe2Jcfoo+poIykg7yjV6UkhxNiA3UsgUzhtp0r2kc30ltBV+keJdmeLfNsoprpEZmpn4boVHIRUyrxZlTLx9wLAs40DEsiJE8IRaaZhW5O/V6TgZqUxkJ5c5UBkhX7JSFqaJbK08UCveNtMmAGY+P8epTa8RUHWlNEK7Msp8Xw3dycDADoUUBfZ4pylmgwIHFcVLKuOq0qDCi/BDtSGqeCmlUfHicu1unkb5mY0YA3kCroJxUIo+MTEKpy32huDJs7znyEPwfZh+hlf1qmdBuYalnqRPo87NEVb68saDWQxgC/kEiVkaQciR4Z25ot9pO4VyQeZSWcBQASP7DAb5MQxwsVmGk/UK4WP0i/N+sOHib26cPf/qB9csvfOKea2zR9cMsOii8siqdHV5qbuqEmWshygRp6fSO3rF3MXjq/J9q69dcsHWAe+FREcywqKJulR3XJPN5MiiubMuv+/p7RuHe9bcvHb+hoXZrky8b2x5/oLF09zGnp8bbbmmj4/Ou2RRrrT6mgVnjq/qqZZhQ7qvEtdSTdFU7/Q6M5cwJ2UYdftJMoJ20ddrGwPXNA9nazALWxl+QhSmPgxwC/laMIsH7KvFuyhh9JIUWhF6V6gpuEOEztkdQNbp0VTnVC0c4t2zOkG8K6whaWeKjEBXKhS8VxK3hMJImA4ERlH2Z/OxgLJMnuxOhYP3oBfvwqn+Nn2sisAFI7iFwvKaFS+5tVALN8TDtcnACw0m4i5E4H7CqC3ml36GulEzHmmoGQ3fgNCTNgTB5jdYGEEWVUUKTEb3jTgHP8w7uB/xEg6AKIo4IIVEOczm/PT6LYfmyd6KJesIWbdkyXpC1v+6476LLtreMXPbqnPuXh+ycTXVVGmAWRoNUtVURFUMx8OI4RhfFKvYoSBFkCJFgJkWZTRsh9ZfDXD1epIm62vLYj6x87wNABvOmz4HxFk1mX0NZbYT7SReKEZHsuPKZbdsUsmhWYmWszBx6bHvdB3+aet3j31qi/fiz3rBBKf/HW8PefXd1CN/+swVx77lQtV7r/dN78XbtkLfO22T8cx36B60bXGOP5BTMuMPGm6uhEYXWezU+FVCF8Tcmr9EefE56NglHEKfVAvvyRF5htN1UNPe7XKqsvGS1nVEZ+symfWS/n6Xtufm5iFdG2oOPx5Wt6pStxq7tjd/bUztlrAZfhxI5ER39MZQ6MZo94lI8Ln2eASMDwxMEIm3P/d82+5gcHcb0401PaX7HXaZolzGnPtLPWsMnU3hks/To/RqYYWwQbgBT9LnFhGBfOSTzqT9dzcm1x4C/E+MB3D2pJ9F9O2nfCFfKJU/8pkJpWK2r4wwvejnaBpiBY7zs9ATw5X4C5PLX5n4l+MfN+Wk6X8TQISoBLWGulxTc7a1f0YmE43qcd2ySr3TervSvREzGGUUdYtFQ9FIb7oLu3VVNS0rruvRaCYzY7qbbW5ynQY9GGAopxMFRWKbQ6J4DkKhzYGQtwAeIyy0sHWwdah1QZihq38Mv9tNE4xoMtnWWuhqj1u6oUaYiDEBEDmg6w3WzEx759zTM5nWTOb0uZ3tmZlWo67LGDX4SWQR1dCteHtXvq0tmYwaYJr/HGT0Xlw/FNgeCnySeMeBqVAoQB7/ICiKB97xZQovZBcF9Jd9eBMugnf+9BLnQN5Fq8OfqTQ/wEfDb5cwsM9ldTD4c2GxEONW1aA7/0kL9qra12RDDdWFVCPgXSeB5Ir42xthIwuLrgTe6TffJomHNh0UJYCbL3mQBQLsrH8JhKkk0XDA+5akST0MJDjT+7YErEfSDlxznfecHHj11YAMM3zs8RrdRk2M4LqEjwnn8ihlGmB0avG98XdM9ALdtRcJHrXK/CmNDxX8Zx1EYAUuVbyOUpD0lcfiSM3tqwUv/nMTyhSxfWxwp1iZFelojszSTSUSSWuxNuOBBGuMNHdE+tPN9zef2dF6elMyTie+SulwswiKgURWu3F/kjXpze1GP6RS9yeRal5jPEHEitMR6UjRyL1Lq7P0ZEdkVsS1tGxUVyEawcXikY6kPnBm21k4J5NoTJ7RBnXDzW0+RdT4CAHayMbE4ralVbubL+XbhafpdzF+C3JMCqci+0yMh68o5z6SKBQrNT3giAL+/MQxUfrz9584JknHBl4HM27CG5ppam/41dd1DJz6xWNP+ONPfP+4576hWZb2htX0F7omaxIXvkofJj/2Y6Usx/LA3CxChWzJkRAmSCwrdYGRdn1A7EjZUgK1WmLy3/SQtCR6mxFQbFJl2Ihg4gbRexDOghEUEByQqazLcAlcIuuKJHtbRPb/9qIY0deYzX7E2IuSw/aKv9i169GJl8irkq5J4ksviZKmS6+SO8g+UddF3sHLfeTkm+0axBuGYCHGzwsVYQFHK2mG+N7m7/9Fg78RlGMI3PhrWIwj3xQHvRUwT8F1Tg3DpT5aJ83eId0wdCLoxlONLTD4yMTvYI0WOKRCsd0T2osAxXXbOIWfwTjPvfGP1Klg6BODfuchaGl85BEQ1EMBbeKQP7+dPNVe9L5uJAxM439V4Bl1PONVYgP67NnCPIz4zxCWoOdeIZwnrBYuFD4urBPWC5cIn558J4mi6yi6LIfSJOdKaHljqDUn34hR2/psB+FgBfsnfQ3mMfTz5VKUv6+wXMnOODx3HcbxY8mWedOpFXbMYem+vFO2eR8JH/TeZPQHd5sGrJ9HzBwkE5LblltVL0aD59wkQlM9C5hzF1avry6cawZYfROIN31Mi4Fh3v0DKnlvThwgYTEUaJKCgZDSIEuEyKrUJEVUhYmt1KJWO1gWtALWCH9tefPgw94fy9QInm/Mk7tS4KjAumh/uCGjSutu7ZjbpC4oU6XHMHoUWl6gNs3tuGUdC/XkbgoatAzqwwehdcISmcKaaCgQhowYoAGgupiQdBWdjMFYWApQWZNlmWpU9nW19haVQjsmgP8I6dZeJn3O1eASx0OEe3U4BQKRrUOLybZNm7aRxUO374lIic40iIceeuiQCOnOhBTZ400jF8xdsJqQ1QvmXgD/4I6fseluSu/edMa42/WH7XYdpO2bnhbFp2+y01BnbzsKmQWrAVYvOP18Qs6fwp0byBE/dqrticmlmP9PCQ4ze9GtVjlA602Cf/fgfe1lcWapLkXCQcV2GhsjEStSrzXrRvzOl0XxZfJ7uucri6/rbWkACERUnYoSFWUWREUHIv7o9q/s+ct7wDfgDf/dJSzUCw2oeQIUOch10Br3dYP0dxt0WUtPT0tdrq4u593Cq47rOK53a7anJ+tXYXwZ9MzrAbuts81e9nfq+NP/BzQjeVIAAAB4nGNgZGBgAOIjpTqn4/ltvjJwszCAwLVXLhNh9P///2tZDJgbgVwOBiaQKABnpw1HAAAAeJxjYGRgYG7438AQw+L1HwhYDBiAIihAEwCrnwcIAAB4nGNhYGBgfsnAwMKADf//z+KFxseqDg/mxWImIT2SqHwASBsH9wAAAAAAAAB2AMwA9AE8AYgBzAKYAvwDOANYA5IEAAR8BJAFGgV8BcgGJAZqBqQG9gc2B54HxAfeCFgIrgk4CWQJyAqOCuILcAuwDBYMiA1GDZQN0A4UeJxjYGRgYNBk6GLgZwABJiDmAkIGhv9gPgMAG3AB2QB4nGWPTU7DMBCFX/oHpBKqqGCH5AViASj9EatuWFRq911036ZOmyqJI8et1ANwHo7ACTgC3IA78EgnmzaWx9+8eWNPANzgBx6O3y33kT1cMjtyDRe4F65TfxBukF+Em2jjVbhF/U3YxzOmwm10YXmD17hi9oR3YQ8dfAjXcI1P4Tr1L+EG+Vu4iTv8CrfQ8erCPuZeV7iNRy/2x1YvnF6p5UHFockikzm/gple75KFrdLqnGtbxCZTg6BfSVOdaVvdU+zXQ+ciFVmTqgmrOkmMyq3Z6tAFG+fyUa8XiR6EJuVYY/62xgKOcQWFJQ6MMUIYZIjK6Og7VWb0r7FDwl57Vj3N53RbFNT/c4UBAvTPXFO6stJ5Ok+BPV8bUnV0K27LnpQ0kV7NSRKyQl7WtlRC6gE2ZVeOEXpc0Yk/KGdI/wAJWm7IAAAAeJxtjtlywjAMRXPBxC4Uuu8L3Vs6wwNf0f6GQ0xsJlUgiaeBr68MPFZja7PvkaJWtLVu9L+N0EIbAh3EkFDYQxc97KOPAQ5wiCMc4wSnOMM5LnCJK1zjBre4wz2GeMAjnvCMF7ziDe/4wAifEZq4do2jrK3TVM0MNU5T1lsYylaFX3pNam25Y/1E1lyl3olpkZr417i1dSJI46rwlS+4RVYX/URTwnI+/CjctCBZa+IPiZwzfKFJVtaFVK40zQMg14nJE7UredJmpYkKPtyNcO6MINPU8UyT9a6l60HpOM947tIzsl0aElWty85G3V+yz53lraee4lk+ZorMtyQeEaKWLA5wtYuT+MvV3z5RmXZh9bFcBIon8VOUJor+AKNMcOMAAA==') format('woff'), url('iconfont.ttf?t=1522803601095') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ url('iconfont.svg?t=1522803601095#iconfont') format('svg'); /* iOS 4.1- */ } .iconfont { font-family:"iconfont" !important; font-size:16px; font-style:normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-tixing:before { content: "\e600"; } .icon-add:before { content: "\e662"; } .icon-fenxiang:before { content: "\e67e"; } .icon-pengyouquan:before { content: "\e684"; } .icon-zhanghu1:before { content: "\e666"; } .icon-tuandui:before { content: "\e63e"; } .icon-code:before { content: "\ea77"; } .icon-weizhi:before { content: "\e7e6"; } .icon-xing:before { content: "\e698"; } .icon-sousuo:before { content: "\e61c"; } .icon-wenhao:before { content: "\e72d"; } .icon-banbengengxin:before { content: "\e63c"; } .icon-icon:before { content: "\e62f"; } .icon-tansuob:before { content: "\e66a"; } .icon-jianpan:before { content: "\e60e"; } .icon-shijian:before { content: "\e62d"; } .icon-yanjing:before { content: "\e681"; } .icon-labelb:before { content: "\e61a"; } .icon-yanjing1:before { content: "\e629"; } .icon-tixing1:before { content: "\e661"; } .icon-xingxing:before { content: "\e642"; } .icon-jianjie:before { content: "\e6c4"; } .icon-next:before { content: "\e610"; } .icon-fanhui:before { content: "\e78a"; } .icon-at:before { content: "\e6b0"; } .icon-rifangwenqushi:before { content: "\e818"; } .icon-ren:before { content: "\e604"; } .icon-star:before { content: "\e643"; } .icon-xing1:before { content: "\e630"; } .icon-qinglihuancun:before { content: "\e615"; } .icon-fl-jia:before { content: "\e624"; } .icon-lianjie:before { content: "\e670"; } .icon-tianjia:before { content: "\e67b"; } .icon-wenjian:before { content: "\e68a"; } .icon-wenjian1:before { content: "\e793"; } .icon-GitHub:before { content: "\ea0a"; } .icon-gaiicon-:before { content: "\e6d0"; } .icon-pinglun:before { content: "\e6ba"; } .icon-more:before { content: "\e674"; } ================================================ FILE: static/font/iconfont.js ================================================ (function(window){var svgSprite='';var script=function(){var scripts=document.getElementsByTagName("script");return scripts[scripts.length-1]}();var shouldInjectCss=script.getAttribute("data-injectcss");var ready=function(fn){if(document.addEventListener){if(~["complete","loaded","interactive"].indexOf(document.readyState)){setTimeout(fn,0)}else{var loadFn=function(){document.removeEventListener("DOMContentLoaded",loadFn,false);fn()};document.addEventListener("DOMContentLoaded",loadFn,false)}}else if(document.attachEvent){IEContentLoaded(window,fn)}function IEContentLoaded(w,fn){var d=w.document,done=false,init=function(){if(!done){done=true;fn()}};var polling=function(){try{d.documentElement.doScroll("left")}catch(e){setTimeout(polling,50);return}init()};polling();d.onreadystatechange=function(){if(d.readyState=="complete"){d.onreadystatechange=null;init()}}}};var before=function(el,target){target.parentNode.insertBefore(el,target)};var prepend=function(el,target){if(target.firstChild){before(el,target.firstChild)}else{target.appendChild(el)}};function appendSvg(){var div,svg;div=document.createElement("div");div.innerHTML=svgSprite;svgSprite=null;svg=div.getElementsByTagName("svg")[0];if(svg){svg.setAttribute("aria-hidden","true");svg.style.position="absolute";svg.style.width=0;svg.style.height=0;svg.style.overflow="hidden";prepend(svg,document.body)}}if(shouldInjectCss&&!window.__iconfont__svg__cssinject__){window.__iconfont__svg__cssinject__=true;try{document.write("")}catch(e){console&&console.log(e)}}ready(appendSvg)})(window) ================================================ FILE: tool/ai/build_review_bundle.ps1 ================================================ param( [string]$BaseRef = "HEAD", [string]$OutFile = "build/ai/review-bundle.md" ) $ErrorActionPreference = "Stop" function Require-GitRepository { git rev-parse --show-toplevel *> $null if ($LASTEXITCODE -ne 0) { throw "Current directory is not a git repository." } } function Get-ChangedFiles { $files = git diff --name-only $BaseRef if ($LASTEXITCODE -ne 0) { throw "Failed to get changed files from git diff." } return @($files | Where-Object { $_ -and $_.Trim().Length -gt 0 }) } function Ensure-ParentDirectory([string]$Path) { $parent = Split-Path -Parent $Path if ($parent -and -not (Test-Path $parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } } function Format-BulletList([string[]]$Items, [string]$Fallback) { if (-not $Items -or $Items.Count -eq 0) { return "- $Fallback" } return (($Items | ForEach-Object { "- " + $_ }) -join ([Environment]::NewLine)) } Require-GitRepository $changedFiles = Get-ChangedFiles $diffStat = git diff --stat $BaseRef | Out-String $diffText = git diff --unified=3 $BaseRef | Out-String $shortHead = (git rev-parse --short HEAD).Trim() $featureHints = @() foreach ($file in $changedFiles) { switch -Regex ($file) { '^lib/page/repos/' { $featureHints += 'repos'; break } '^lib/page/trend/' { $featureHints += 'trend'; break } '^lib/page/notify/' { $featureHints += 'notify'; break } '^lib/page/issue/' { $featureHints += 'issue'; break } '^lib/page/search/' { $featureHints += 'search'; break } '^lib/page/user/' { $featureHints += 'user'; break } '^lib/page/home/' { $featureHints += 'home'; break } '^lib/page/dynamic/' { $featureHints += 'dynamic'; break } '^lib/page/release/' { $featureHints += 'release'; break } '^lib/page/push/' { $featureHints += 'push'; break } '^lib/page/debug/' { $featureHints += 'debug'; break } '^lib/common/net/' { $featureHints += 'common-net'; break } '^lib/redux/' { $featureHints += 'redux'; break } '^lib/provider/' { $featureHints += 'provider'; break } } } $featureHints = $featureHints | Sort-Object -Unique $changedFilesText = Format-BulletList -Items $changedFiles -Fallback "(none)" $featureHintsText = Format-BulletList -Items $featureHints -Fallback "general" $lines = @( "# Review Bundle", "", "- Head: $shortHead", "- Base ref: $BaseRef", "- Reviewer prompt: docs/05-ai/prompts/reviewer-system.md", "- Review harness: docs/05-ai/review-harness.md", "", "## Changed Files", "", $changedFilesText, "", "## Feature Hints", "", $featureHintsText, "", "## Recommended Docs", "", "- AGENTS.md", "- docs/CONTRIBUTING_AI.md", "- docs/04-quality/smoke-matrix.md", "- docs/05-ai/task-playbooks/fix-bug.md", "", "## Diff Stat", "", '```text', $diffStat.TrimEnd(), '```', "", "## Diff", "", '```diff', $diffText.TrimEnd(), '```' ) $bundle = ($lines -join ([Environment]::NewLine)) Ensure-ParentDirectory $OutFile Set-Content -Path $OutFile -Value $bundle -Encoding UTF8 Write-Output ('Review bundle written to ' + $OutFile)