├── .gitignore ├── .metadata ├── README.md ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── github │ │ │ │ └── qingmei2 │ │ │ │ └── flutter_rhine │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── art ├── home.png ├── login.png ├── profile.png └── repos.png ├── assets └── images │ ├── ic_fork_checked.png │ ├── ic_fork_unchecked.png │ ├── ic_github_cat_round.png │ ├── ic_issue_comment_fire.png │ ├── ic_issue_comment_fire_dark.png │ ├── ic_issue_comment_normal.png │ ├── ic_issue_comment_normal_dark.png │ ├── ic_issue_gray.png │ ├── ic_issue_tag.png │ ├── ic_issue_time.png │ ├── ic_location_on_gray.png │ ├── ic_nav_home_checked.png │ ├── ic_nav_home_normal.png │ ├── ic_nav_issue_checked.png │ ├── ic_nav_issue_normal.png │ ├── ic_nav_mine_checked.png │ ├── ic_nav_mine_normal.png │ ├── ic_nav_task_checked.png │ ├── ic_nav_task_normal.png │ ├── ic_open_in_new_white_24dp.png │ ├── ic_star_checked.png │ └── ic_star_unchecked.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── app │ ├── app.dart │ ├── app_reducer.dart │ ├── app_state.dart │ └── auth │ │ ├── auth_action.dart │ │ ├── auth_reducer.dart │ │ └── auth_state.dart ├── common │ ├── common.dart │ ├── constants │ │ ├── api.dart │ │ ├── assets.dart │ │ ├── colors.dart │ │ ├── config.dart │ │ └── constants.dart │ ├── errors │ │ └── errors.dart │ ├── model │ │ ├── event.dart │ │ ├── event.g.dart │ │ ├── issue.dart │ │ ├── issue.g.dart │ │ ├── login_request_model.dart │ │ ├── model.dart │ │ ├── repo.dart │ │ ├── repo.g.dart │ │ ├── user.dart │ │ ├── user.g.dart │ │ ├── user_introduction.dart │ │ └── user_introduction.g.dart │ ├── service │ │ ├── api_code.dart │ │ ├── interceptors │ │ │ ├── header_interceptor.dart │ │ │ ├── log_interceptor.dart │ │ │ ├── response_interceptor.dart │ │ │ └── token_interceptor.dart │ │ ├── service.dart │ │ └── service_manager.dart │ ├── utils │ │ ├── print_utils.dart │ │ ├── toast_utils.dart │ │ └── utils.dart │ └── widget │ │ ├── global_hide_footer.dart │ │ ├── global_progress_bar.dart │ │ └── widget.dart ├── main.dart ├── repository │ ├── event_repository.dart │ ├── issues_repository.dart │ ├── others │ │ ├── dao_result.dart │ │ └── sputils.dart │ ├── repos_repository.dart │ ├── repository.dart │ └── user_repository.dart ├── routers │ └── routes.dart └── ui │ ├── login │ ├── login.dart │ ├── login_action.dart │ ├── login_form.dart │ ├── login_middleware.dart │ ├── login_page.dart │ ├── login_reducer.dart │ └── login_state.dart │ └── main │ ├── home │ ├── main_events.dart │ ├── main_events_action.dart │ ├── main_events_item.dart │ ├── main_events_middleware.dart │ ├── main_events_page.dart │ ├── main_events_reducer.dart │ └── main_events_state.dart │ ├── issues │ ├── main_issues.dart │ ├── main_issues_action.dart │ ├── main_issues_item.dart │ ├── main_issues_middleware.dart │ ├── main_issues_page.dart │ ├── main_issues_reducer.dart │ └── main_issues_state.dart │ ├── main.dart │ ├── main_action.dart │ ├── main_page.dart │ ├── main_reducer.dart │ ├── main_state.dart │ ├── mine │ ├── main_profile.dart │ ├── main_profile_action.dart │ ├── main_profile_page.dart │ ├── main_profile_reducer.dart │ └── main_profile_state.dart │ └── repos │ ├── main_repo.dart │ ├── main_repo_action.dart │ ├── main_repo_item.dart │ ├── main_repo_middleware.dart │ ├── main_repo_page.dart │ ├── main_repo_reducer.dart │ └── main_repo_state.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | ignore.dart 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FlutterGitHubApp 2 | 3 | 使用`Flutter`开发的`Github`客户端,适用于`Android`和`iOS`平台,页面功能简单,易于上手学习`Flutter`。 4 | 5 | 项目提供了多种 **状态管理** 组件对应的实现方式,开发者只需切换对应的 **分支**,便可以根据自己感兴趣的模式进行学习开发: 6 | 7 | * [basic_provider ](https://github.com/qingmei2/FlutterGitHubApp/tree/basic_provider): `Google`官方推荐的 [provider](https://github.com/rrousselGit/provider) 库; 8 | * [basic_bloc_rxdart](https://github.com/qingmei2/FlutterGitHubApp/tree/basic_bloc_rxdart): 经典的 [bloc](https://github.com/felangel/bloc) 模式的实现案例,适用于复杂的业务场景开发; 9 | * [basic_redux](https://github.com/qingmei2/FlutterGitHubApp/tree/basic_redux): `Redux`模式的实现案例,适用于复杂的业务场景开发; 10 | 11 | * **master(默认分支)**: 该分支始终展示的是最新~~稳定版本~~的状态管理实践,目前展示的是 **redux** 模式的开发示例。 12 | 13 | ## 通知 14 | 15 | * 下载后,编译遇到错误? 16 | 17 | > 如果遇到 `Error: Error when reading 'lib/common/constants/ignore.dart': No such file or directory` 的错误,请参考下方【开始使用】,对项目进行配置。 18 | 19 | 20 | ## 屏幕截图 21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 | 29 | ## 开始使用 30 | 31 | * 1.直接通过git命令行进行clone: 32 | 33 | ```shell 34 | $ git clone https://github.com/qingmei2/FlutterGitHubApp.git 35 | ``` 36 | 37 | * 2.注册你的GithubApp 38 | 39 | 首先打开[这个链接](https://github.com/settings/applications/new),注册属于你的`OAuth Application`: 40 | 41 |
42 | 43 |
44 | 45 | 注册完成后,记住下面的`Client ID`和`Client Secret`,新建并配置到你的项目根目录的`lib\common\constants\ignore.dart`文件中: 46 | 47 |
48 | 49 |
50 | 51 | ```dart 52 | class Ignore { 53 | static const String clientId = 'xxxxxx'; 54 | static const String clientSecret ='xxxxxxx'; 55 | } 56 | ``` 57 | 58 | 大功告成,接下来点击编译并运行即可。:tada: :tada: :tada: 59 | 60 | 61 | ## 感谢 62 | 63 | :art: 项目中的UI设计部分参考了 [gitme](https://github.com/flutterchina/gitme). 64 | 65 | :star: 项目参考了 [GSYGithubAppFlutter](https://github.com/CarGuo/GSYGithubAppFlutter) 并对其部分代码进行了引用. 66 | 67 | ## 其他开源项目 68 | 69 | > [MVVM-Rhine: MVVM + Jetpack 架构组件的Github客户端。](https://github.com/qingmei2/MVVM-Rhine) 70 | 71 | > [MVI-Rhine: 基于Jetpack + MVVM, 更加响应式&函数式的编程实践](https://github.com/qingmei2/MVI-Rhine) 72 | 73 | > [RxImagePicker: 灵活的Android图片选择器,提供了知乎和微信主题的支持](https://github.com/qingmei2/RxImagePicker) 74 | 75 | ## License 76 | 77 | The FlutterGitHubApp: Apache License 78 | 79 | Copyright (c) 2019 qingmei2 80 | 81 | Licensed under the Apache License, Version 2.0 (the "License"); 82 | you may not use this file except in compliance with the License. 83 | You may obtain a copy of the License at 84 | 85 | http://www.apache.org/licenses/LICENSE-2.0 86 | 87 | Unless required by applicable law or agreed to in writing, software 88 | distributed under the License is distributed on an "AS IS" BASIS, 89 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 90 | See the License for the specific language governing permissions and 91 | limitations under the License. 92 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | sourceSets { 31 | main.java.srcDirs += 'src/main/kotlin' 32 | } 33 | lintOptions { 34 | disable 'InvalidPackage' 35 | } 36 | defaultConfig { 37 | applicationId "com.github.qingmei2.flutter_rhine" 38 | minSdkVersion 16 39 | targetSdkVersion 28 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 43 | } 44 | buildTypes { 45 | release { 46 | signingConfig signingConfigs.debug 47 | } 48 | } 49 | } 50 | 51 | flutter { 52 | source '../..' 53 | } 54 | 55 | dependencies { 56 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 57 | testImplementation 'junit:junit:4.12' 58 | androidTestImplementation 'androidx.test:runner:1.2.0' 59 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 60 | } 61 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 13 | 20 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/github/qingmei2/flutter_rhine/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.qingmei2.flutter_rhine 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.71' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.2.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableJetifier=true 4 | android.useAndroidX=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /art/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/art/home.png -------------------------------------------------------------------------------- /art/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/art/login.png -------------------------------------------------------------------------------- /art/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/art/profile.png -------------------------------------------------------------------------------- /art/repos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/art/repos.png -------------------------------------------------------------------------------- /assets/images/ic_fork_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_fork_checked.png -------------------------------------------------------------------------------- /assets/images/ic_fork_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_fork_unchecked.png -------------------------------------------------------------------------------- /assets/images/ic_github_cat_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_github_cat_round.png -------------------------------------------------------------------------------- /assets/images/ic_issue_comment_fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_comment_fire.png -------------------------------------------------------------------------------- /assets/images/ic_issue_comment_fire_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_comment_fire_dark.png -------------------------------------------------------------------------------- /assets/images/ic_issue_comment_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_comment_normal.png -------------------------------------------------------------------------------- /assets/images/ic_issue_comment_normal_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_comment_normal_dark.png -------------------------------------------------------------------------------- /assets/images/ic_issue_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_gray.png -------------------------------------------------------------------------------- /assets/images/ic_issue_tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_tag.png -------------------------------------------------------------------------------- /assets/images/ic_issue_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_issue_time.png -------------------------------------------------------------------------------- /assets/images/ic_location_on_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_location_on_gray.png -------------------------------------------------------------------------------- /assets/images/ic_nav_home_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_home_checked.png -------------------------------------------------------------------------------- /assets/images/ic_nav_home_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_home_normal.png -------------------------------------------------------------------------------- /assets/images/ic_nav_issue_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_issue_checked.png -------------------------------------------------------------------------------- /assets/images/ic_nav_issue_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_issue_normal.png -------------------------------------------------------------------------------- /assets/images/ic_nav_mine_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_mine_checked.png -------------------------------------------------------------------------------- /assets/images/ic_nav_mine_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_mine_normal.png -------------------------------------------------------------------------------- /assets/images/ic_nav_task_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_task_checked.png -------------------------------------------------------------------------------- /assets/images/ic_nav_task_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_nav_task_normal.png -------------------------------------------------------------------------------- /assets/images/ic_open_in_new_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_open_in_new_white_24dp.png -------------------------------------------------------------------------------- /assets/images/ic_star_checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_star_checked.png -------------------------------------------------------------------------------- /assets/images/ic_star_unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/assets/images/ic_star_unchecked.png -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 37 | # referring to absolute paths on developers' machines. 38 | system('rm -rf .symlinks') 39 | system('mkdir -p .symlinks/plugins') 40 | 41 | # Flutter Pods 42 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 43 | if generated_xcode_build_settings.empty? 44 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 45 | end 46 | generated_xcode_build_settings.map { |p| 47 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 48 | symlink = File.join('.symlinks', 'flutter') 49 | File.symlink(File.dirname(p[:path]), symlink) 50 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 51 | end 52 | } 53 | 54 | # Plugin Pods 55 | plugin_pods = parse_KV_file('../.flutter-plugins') 56 | plugin_pods.map { |p| 57 | symlink = File.join('.symlinks', 'plugins', p[:name]) 58 | File.symlink(p[:path], symlink) 59 | pod p[:name], :path => File.join(symlink, 'ios') 60 | } 61 | end 62 | 63 | post_install do |installer| 64 | installer.pods_project.targets.each do |target| 65 | target.build_configurations.each do |config| 66 | config.build_settings['ENABLE_BITCODE'] = 'NO' 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - android_intent (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - fluttertoast (0.0.2): 6 | - Flutter 7 | - shared_preferences (0.0.1): 8 | - Flutter 9 | 10 | DEPENDENCIES: 11 | - android_intent (from `.symlinks/plugins/android_intent/ios`) 12 | - Flutter (from `.symlinks/flutter/ios`) 13 | - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) 14 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 15 | 16 | EXTERNAL SOURCES: 17 | android_intent: 18 | :path: ".symlinks/plugins/android_intent/ios" 19 | Flutter: 20 | :path: ".symlinks/flutter/ios" 21 | fluttertoast: 22 | :path: ".symlinks/plugins/fluttertoast/ios" 23 | shared_preferences: 24 | :path: ".symlinks/plugins/shared_preferences/ios" 25 | 26 | SPEC CHECKSUMS: 27 | android_intent: 73316069cd1f6aac919df1c6e640cad190fd5e8b 28 | Flutter: 58dd7d1b27887414a370fcccb9e645c08ffd7a6a 29 | fluttertoast: b644586ef3b16f67fae9a1f8754cef6b2d6b634b 30 | shared_preferences: 1feebfa37bb57264736e16865e7ffae7fc99b523 31 | 32 | PODFILE CHECKSUM: aff02bfeed411c636180d6812254b2daeea14d09 33 | 34 | COCOAPODS: 1.7.1 35 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qingmei2/FlutterGitHubApp/061591fa0fcc821dc2bd2e2fbc2d79b2470d1259/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_rhine 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/app.dart: -------------------------------------------------------------------------------- 1 | export 'app_state.dart'; 2 | export 'app_reducer.dart'; 3 | 4 | export 'auth/auth_action.dart'; 5 | export 'auth/auth_reducer.dart'; 6 | export 'auth/auth_state.dart'; -------------------------------------------------------------------------------- /lib/app/app_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/app/auth/auth_reducer.dart'; 2 | import 'package:flutter_rhine/common/common.dart'; 3 | import 'package:flutter_rhine/ui/login/login_reducer.dart'; 4 | import 'package:flutter_rhine/ui/main/main.dart'; 5 | 6 | abstract class AppAction {} 7 | 8 | AppState appReducer(AppState preState, dynamic action) { 9 | final next = AppState( 10 | appUser: appAuthReducer(preState.appUser, action), 11 | loginState: loginReducer(preState.loginState, action), 12 | mainState: mainPageReducer(preState.mainState, action), 13 | ); 14 | if (Config.DEBUG) { 15 | print('[ Pre AppState: ]${preState.toString()}'); 16 | print('[ Next Action: ]${action.toString()}'); 17 | print('[ Next AppState: ]${next.toString()}'); 18 | } 19 | return next; 20 | } 21 | -------------------------------------------------------------------------------- /lib/app/app_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:flutter_rhine/ui/login/login_state.dart'; 3 | import 'package:flutter_rhine/ui/main/main.dart'; 4 | 5 | @immutable 6 | class AppState { 7 | final AppUser appUser; // 用户的信息数据 8 | final LoginState loginState; // 登录页面 9 | final MainPageState mainState; // 主页面 10 | 11 | AppState({this.appUser, this.loginState, this.mainState}); 12 | 13 | factory AppState.initial() { 14 | return AppState( 15 | loginState: LoginState.initial(), 16 | mainState: MainPageState.initial(), 17 | ); 18 | } 19 | 20 | AppState copyWith({ 21 | final AppUser appUser, 22 | final LoginState loginState, 23 | final MainPageState mainState, 24 | }) { 25 | return AppState( 26 | appUser: appUser ?? this.appUser, 27 | loginState: loginState ?? this.loginState, 28 | mainState: mainState ?? this.mainState, 29 | ); 30 | } 31 | 32 | @override 33 | bool operator ==(Object other) => 34 | identical(this, other) || 35 | other is AppState && 36 | runtimeType == other.runtimeType && 37 | appUser == other.appUser && 38 | loginState == other.loginState && 39 | mainState == other.mainState; 40 | 41 | @override 42 | int get hashCode => 43 | appUser.hashCode ^ loginState.hashCode ^ mainState.hashCode; 44 | 45 | @override 46 | String toString() { 47 | return 'AppState{appUser: $appUser, loginState: $loginState, mainState: $mainState}'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/app/auth/auth_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import '../app_reducer.dart'; 4 | 5 | abstract class AuthenticationAction extends AppAction {} 6 | 7 | class AuthenticationSuccessAction extends AuthenticationAction { 8 | final User user; 9 | final String token; 10 | 11 | AuthenticationSuccessAction(this.user, this.token) : assert(user != null); 12 | } 13 | 14 | class AuthenticationFailureAction extends AuthenticationAction {} 15 | 16 | class AuthenticationCancelAction extends AuthenticationAction {} 17 | 18 | class AuthenticationClearAction extends AuthenticationAction {} 19 | -------------------------------------------------------------------------------- /lib/app/auth/auth_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/app/auth/auth_action.dart'; 2 | import 'package:flutter_rhine/common/common.dart'; 3 | 4 | final appAuthReducer = combineReducers([ 5 | TypedReducer(_authenticationSuccess), 6 | TypedReducer(_authenticationClear), 7 | TypedReducer(_authenticationFailure), 8 | ]); 9 | 10 | AppUser _authenticationSuccess( 11 | AppUser user, 12 | AuthenticationSuccessAction action, 13 | ) { 14 | return AppUser(user: action.user, token: action.token); 15 | } 16 | 17 | AppUser _authenticationFailure( 18 | AppUser user, 19 | AuthenticationClearAction action, 20 | ) { 21 | return AppUser(user: null, token: null); 22 | } 23 | 24 | AppUser _authenticationClear( 25 | AppUser user, 26 | AuthenticationCancelAction action, 27 | ) { 28 | return AppUser(user: null, token: null); 29 | } 30 | -------------------------------------------------------------------------------- /lib/app/auth/auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | @immutable 4 | class AppUser { 5 | final User user; 6 | final String token; 7 | 8 | AppUser({ 9 | this.user, 10 | this.token, 11 | }); 12 | 13 | AppUser copyWith({ 14 | User user, 15 | String token, 16 | }) { 17 | return AppUser( 18 | user: user ?? this.user, 19 | token: token ?? this.token, 20 | ); 21 | } 22 | 23 | @override 24 | bool operator ==(Object other) => 25 | identical(this, other) || 26 | other is AppUser && 27 | runtimeType == other.runtimeType && 28 | user == other.user && 29 | token == other.token; 30 | 31 | @override 32 | int get hashCode => user.hashCode ^ token.hashCode; 33 | 34 | @override 35 | String toString() { 36 | return 'AppUser{user: $user, token: $token}'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/common.dart: -------------------------------------------------------------------------------- 1 | export 'package:flutter_redux/flutter_redux.dart'; 2 | export 'package:meta/meta.dart'; 3 | export 'package:redux/redux.dart'; 4 | export 'package:redux_epics/redux_epics.dart'; 5 | export 'package:rxdart/rxdart.dart'; 6 | export 'errors/errors.dart'; 7 | 8 | export '../app/app.dart'; 9 | export '../repository/repository.dart'; 10 | export '../routers/routes.dart'; 11 | export 'constants/constants.dart'; 12 | export 'model/model.dart'; 13 | export 'service/service.dart'; 14 | export 'utils/utils.dart'; 15 | export 'widget/widget.dart'; 16 | -------------------------------------------------------------------------------- /lib/common/constants/api.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/constants/config.dart'; 2 | 3 | class Api { 4 | static const String host = "https://api.github.com"; 5 | 6 | /// [POST]用户鉴权 7 | static const String authorization = "$host/authorizations"; 8 | 9 | /// [GET]我的用户信息 10 | static const String userInfo = "$host/user"; 11 | 12 | /// [GET]用户事件 13 | static String userEvents(final String username) => 14 | "$host/users/$username/received_events"; 15 | 16 | /// [GET]用户仓库 17 | static String userRepos(final String username) => 18 | '$host/users/$username/repos'; 19 | 20 | /// [GET]用户issues 21 | static const String userIssues = '$host/issues'; 22 | 23 | /// 处理分页参数 24 | /// [tab] 分隔符 25 | /// [page] 页数 26 | /// [pageSize] 每页请求数据量 27 | static getPageParams(tab, page, [pageSize = Config.PAGE_SIZE]) { 28 | if (page != null) { 29 | if (pageSize != null) { 30 | return "${tab}page=$page&per_page=$pageSize"; 31 | } else { 32 | return "${tab}page=$page"; 33 | } 34 | } else { 35 | return ""; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/common/constants/assets.dart: -------------------------------------------------------------------------------- 1 | const String imageHost = 'assets/images/'; 2 | 3 | /// LoginPage 4 | const String imageGithubCat = '${imageHost}ic_github_cat_round.png'; 5 | 6 | /// MainPage 7 | const String mainNavEventsNormal = '${imageHost}ic_nav_home_normal.png'; 8 | const String mainNavEventsPressed = '${imageHost}ic_nav_home_checked.png'; 9 | const String mainNavReposNormal = '${imageHost}ic_nav_task_normal.png'; 10 | const String mainNavReposPressed = '${imageHost}ic_nav_task_checked.png'; 11 | const String mainNavIssueNormal = '${imageHost}ic_nav_issue_normal.png'; 12 | const String mainNavIssuePressed = '${imageHost}ic_nav_issue_checked.png'; 13 | const String mainNavProfileNormal = '${imageHost}ic_nav_mine_normal.png'; 14 | const String mainNavProfilePressed = '${imageHost}ic_nav_mine_checked.png'; 15 | 16 | /// eventsPage 17 | const String eventsForkChecked = '${imageHost}ic_fork_checked.png'; 18 | const String eventsStarChecked = '${imageHost}ic_star_checked.png'; 19 | const String eventsStarUnChecked = '${imageHost}ic_star_checked.png'; 20 | 21 | /// reposPage 22 | const String reposFork = '${imageHost}ic_fork_unchecked.png'; 23 | const String reposIssue = '${imageHost}ic_issue_gray.png'; 24 | const String reposStar = '${imageHost}ic_star_unchecked.png'; 25 | 26 | /// issuesPage 27 | const String issuesCommentNormal = '${imageHost}ic_issue_comment_normal.png'; 28 | const String issuesCommentNormalDark = '${imageHost}ic_issue_comment_normal_dark.png'; 29 | const String issuesCommentFire = '${imageHost}ic_issue_comment_fire.png'; 30 | const String issuesCommentFireDark = '${imageHost}ic_issue_comment_fire_dark.png'; 31 | const String issuesTag = '${imageHost}ic_issue_tag.png'; 32 | const String issuesTime = '${imageHost}ic_issue_time.png'; 33 | 34 | /// profilePage 35 | const String mineShared = '${imageHost}ic_open_in_new_white_24dp.png'; 36 | const String mineLocationIcon = '${imageHost}ic_location_on_gray.png'; 37 | -------------------------------------------------------------------------------- /lib/common/constants/colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const Color colorPrimary = Color(0xff263238); 4 | const Color colorPrimaryDark = Color(0xff000a12); 5 | const Color colorAccent = Color(0xff263238); 6 | const Color colorPrimaryLight = Color(0xffcfd8dc); 7 | 8 | const Color colorPrimaryText = Color(0xff263238); 9 | const Color colorSecondaryTextGray = Color(0xffaaaaaa); 10 | const Color colorSecondaryTextBlueGray = Color(0xff607d8b); 11 | const Color colorDivider = Color(0xffcfd8dc); 12 | 13 | const Color colorSecondary = Color(0xff455a64); 14 | const Color colorSecondaryLight = Color(0xff718792); 15 | const Color colorSecondaryDark = Color(0xff1c313a); 16 | -------------------------------------------------------------------------------- /lib/common/constants/config.dart: -------------------------------------------------------------------------------- 1 | class Config { 2 | static const PAGE_SIZE = 20; 3 | static const DEBUG = true; 4 | static const USE_NATIVE_WEBVIEW = true; 5 | 6 | 7 | /// //////////////////////////////////////常量////////////////////////////////////// /// 8 | static const TOKEN_KEY = "token"; 9 | static const USER_NAME_KEY = "user-name"; 10 | static const PW_KEY = "user-pw"; 11 | static const USER_BASIC_CODE = "user-basic-code"; 12 | static const USER_INFO = "user-info"; 13 | static const LANGUAGE_SELECT = "language-select"; 14 | static const LANGUAGE_SELECT_NAME = "language-select-name"; 15 | static const REFRESH_LANGUAGE = "refreshLanguageApp"; 16 | static const THEME_COLOR = "theme-color"; 17 | static const LOCALE = "locale"; 18 | } -------------------------------------------------------------------------------- /lib/common/constants/constants.dart: -------------------------------------------------------------------------------- 1 | export 'api.dart'; 2 | export 'assets.dart'; 3 | export 'colors.dart'; 4 | export 'config.dart'; 5 | export 'ignore.dart'; 6 | -------------------------------------------------------------------------------- /lib/common/errors/errors.dart: -------------------------------------------------------------------------------- 1 | /// 自定义异常管理类 2 | /// 3 | /// 开发者实例化类似[EmptyListException]、[LoginFailureException]不应该直接通过new的 4 | /// 方式,而是使用[Errors]类提供的接口. 5 | abstract class Errors { 6 | factory Errors._() => null; 7 | 8 | static EmptyListException emptyListException() { 9 | return EmptyListException(); 10 | } 11 | 12 | static LoginFailureException loginFailureException() { 13 | return LoginFailureException(); 14 | } 15 | 16 | static NoMoreDataException noMoreDataException() { 17 | return NoMoreDataException(); 18 | } 19 | 20 | static EmptyInputException emptyInputException(final String message) { 21 | return EmptyInputException(message); 22 | } 23 | 24 | static NetworkRequestException networkException({ 25 | final String message, 26 | final int statusCode = 400, 27 | }) { 28 | return NetworkRequestException(message, statusCode); 29 | } 30 | } 31 | 32 | /// 服务器请求错误 33 | class NetworkRequestException implements Exception { 34 | final String message; 35 | final int statusCode; 36 | 37 | NetworkRequestException(this.message, this.statusCode); 38 | } 39 | 40 | /// 空数据集错误 41 | class EmptyListException implements Exception {} 42 | 43 | /// 没有更多了 44 | class NoMoreDataException implements Exception {} 45 | 46 | /// 用户账号密码错误 47 | class LoginFailureException implements Exception { 48 | final String message = '账号密码错误'; 49 | } 50 | 51 | /// 输入为空错误 52 | class EmptyInputException implements Exception { 53 | final String message; 54 | 55 | EmptyInputException(this.message); 56 | } 57 | -------------------------------------------------------------------------------- /lib/common/model/event.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'event.g.dart'; 4 | 5 | List getEventList(List list) { 6 | List result = []; 7 | list.forEach((item) { 8 | result.add(Event.fromJson(item)); 9 | }); 10 | return result; 11 | } 12 | 13 | @JsonSerializable() 14 | class Event extends Object { 15 | @JsonKey(name: 'id') 16 | String id; 17 | 18 | @JsonKey(name: 'type') 19 | String type; 20 | 21 | @JsonKey(name: 'actor') 22 | Actor actor; 23 | 24 | @JsonKey(name: 'repo') 25 | EventRepo repo; 26 | 27 | @JsonKey(name: 'payload') 28 | Payload payload; 29 | 30 | @JsonKey(name: 'public') 31 | bool public; 32 | 33 | @JsonKey(name: 'created_at') 34 | String createdAt; 35 | 36 | Event( 37 | this.id, 38 | this.type, 39 | this.actor, 40 | this.repo, 41 | this.payload, 42 | this.public, 43 | this.createdAt, 44 | ); 45 | 46 | factory Event.fromJson(Map srcJson) => 47 | _$EventFromJson(srcJson); 48 | 49 | Map toJson() => _$EventToJson(this); 50 | } 51 | 52 | @JsonSerializable() 53 | class Actor extends Object { 54 | @JsonKey(name: 'id') 55 | int id; 56 | 57 | @JsonKey(name: 'login') 58 | String login; 59 | 60 | @JsonKey(name: 'display_login') 61 | String displayLogin; 62 | 63 | @JsonKey(name: 'gravatar_id') 64 | String gravatarId; 65 | 66 | @JsonKey(name: 'url') 67 | String url; 68 | 69 | @JsonKey(name: 'avatar_url') 70 | String avatarUrl; 71 | 72 | Actor( 73 | this.id, 74 | this.login, 75 | this.displayLogin, 76 | this.gravatarId, 77 | this.url, 78 | this.avatarUrl, 79 | ); 80 | 81 | factory Actor.fromJson(Map srcJson) => 82 | _$ActorFromJson(srcJson); 83 | 84 | Map toJson() => _$ActorToJson(this); 85 | } 86 | 87 | @JsonSerializable() 88 | class EventRepo extends Object { 89 | @JsonKey(name: 'id') 90 | int id; 91 | 92 | @JsonKey(name: 'name') 93 | String name; 94 | 95 | @JsonKey(name: 'url') 96 | String url; 97 | 98 | EventRepo( 99 | this.id, 100 | this.name, 101 | this.url, 102 | ); 103 | 104 | factory EventRepo.fromJson(Map srcJson) => 105 | _$EventRepoFromJson(srcJson); 106 | 107 | Map toJson() => _$EventRepoToJson(this); 108 | } 109 | 110 | @JsonSerializable() 111 | class Payload extends Object { 112 | @JsonKey(name: 'action') 113 | String action; 114 | 115 | Payload( 116 | this.action, 117 | ); 118 | 119 | factory Payload.fromJson(Map srcJson) => 120 | _$PayloadFromJson(srcJson); 121 | 122 | Map toJson() => _$PayloadToJson(this); 123 | } 124 | -------------------------------------------------------------------------------- /lib/common/model/event.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'event.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Event _$EventFromJson(Map json) { 10 | return Event( 11 | json['id'] as String, 12 | json['type'] as String, 13 | json['actor'] == null 14 | ? null 15 | : Actor.fromJson(json['actor'] as Map), 16 | json['repo'] == null 17 | ? null 18 | : EventRepo.fromJson(json['repo'] as Map), 19 | json['payload'] == null 20 | ? null 21 | : Payload.fromJson(json['payload'] as Map), 22 | json['public'] as bool, 23 | json['created_at'] as String); 24 | } 25 | 26 | Map _$EventToJson(Event instance) => { 27 | 'id': instance.id, 28 | 'type': instance.type, 29 | 'actor': instance.actor, 30 | 'repo': instance.repo, 31 | 'payload': instance.payload, 32 | 'public': instance.public, 33 | 'created_at': instance.createdAt 34 | }; 35 | 36 | Actor _$ActorFromJson(Map json) { 37 | return Actor( 38 | json['id'] as int, 39 | json['login'] as String, 40 | json['display_login'] as String, 41 | json['gravatar_id'] as String, 42 | json['url'] as String, 43 | json['avatar_url'] as String); 44 | } 45 | 46 | Map _$ActorToJson(Actor instance) => { 47 | 'id': instance.id, 48 | 'login': instance.login, 49 | 'display_login': instance.displayLogin, 50 | 'gravatar_id': instance.gravatarId, 51 | 'url': instance.url, 52 | 'avatar_url': instance.avatarUrl 53 | }; 54 | 55 | EventRepo _$EventRepoFromJson(Map json) { 56 | return EventRepo( 57 | json['id'] as int, json['name'] as String, json['url'] as String); 58 | } 59 | 60 | Map _$EventRepoToJson(EventRepo instance) => { 61 | 'id': instance.id, 62 | 'name': instance.name, 63 | 'url': instance.url 64 | }; 65 | 66 | Payload _$PayloadFromJson(Map json) { 67 | return Payload(json['action'] as String); 68 | } 69 | 70 | Map _$PayloadToJson(Payload instance) => 71 | {'action': instance.action}; 72 | -------------------------------------------------------------------------------- /lib/common/model/login_request_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/constants/ignore.dart'; 2 | 3 | /// github login api params. 4 | class LoginRequestModel { 5 | List scopes; 6 | String note; 7 | String clientId; 8 | String clientSecret; 9 | 10 | LoginRequestModel._internal( 11 | this.scopes, this.note, this.clientId, this.clientSecret); 12 | 13 | factory LoginRequestModel() { 14 | return LoginRequestModel._internal( 15 | ['user', 'repo', 'gist', 'notifications'], 16 | Ignore.clientId, 17 | Ignore.clientId, 18 | Ignore.clientSecret); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/common/model/model.dart: -------------------------------------------------------------------------------- 1 | /// flutter packages pub run build_runner build 2 | /// 将在我们运行生成命令后自动生成 3 | /// refers: https://flutterchina.club/json/ 4 | 5 | export 'event.dart'; 6 | export 'issue.dart'; 7 | export 'repo.dart'; 8 | export 'user.dart'; 9 | export 'user_introduction.dart'; 10 | export 'login_request_model.dart'; -------------------------------------------------------------------------------- /lib/common/model/repo.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'repo.g.dart'; 4 | 5 | /// flutter packages pub run build_runner build 6 | List getRepoList(List list){ 7 | List result = []; 8 | list.forEach((item){ 9 | result.add(Repo.fromJson(item)); 10 | }); 11 | return result; 12 | } 13 | @JsonSerializable() 14 | class Repo extends Object { 15 | 16 | @JsonKey(name: 'id') 17 | int id; 18 | 19 | @JsonKey(name: 'node_id') 20 | String nodeId; 21 | 22 | @JsonKey(name: 'name') 23 | String name; 24 | 25 | @JsonKey(name: 'full_name') 26 | String fullName; 27 | 28 | @JsonKey(name: 'private') 29 | bool private; 30 | 31 | @JsonKey(name: 'owner') 32 | Owner owner; 33 | 34 | @JsonKey(name: 'html_url') 35 | String htmlUrl; 36 | 37 | @JsonKey(name: 'description') 38 | String description; 39 | 40 | @JsonKey(name: 'fork') 41 | bool fork; 42 | 43 | @JsonKey(name: 'url') 44 | String url; 45 | 46 | @JsonKey(name: 'forks_url') 47 | String forksUrl; 48 | 49 | @JsonKey(name: 'keys_url') 50 | String keysUrl; 51 | 52 | @JsonKey(name: 'collaborators_url') 53 | String collaboratorsUrl; 54 | 55 | @JsonKey(name: 'teams_url') 56 | String teamsUrl; 57 | 58 | @JsonKey(name: 'hooks_url') 59 | String hooksUrl; 60 | 61 | @JsonKey(name: 'issue_events_url') 62 | String issueEventsUrl; 63 | 64 | @JsonKey(name: 'events_url') 65 | String eventsUrl; 66 | 67 | @JsonKey(name: 'assignees_url') 68 | String assigneesUrl; 69 | 70 | @JsonKey(name: 'branches_url') 71 | String branchesUrl; 72 | 73 | @JsonKey(name: 'tags_url') 74 | String tagsUrl; 75 | 76 | @JsonKey(name: 'blobs_url') 77 | String blobsUrl; 78 | 79 | @JsonKey(name: 'git_tags_url') 80 | String gitTagsUrl; 81 | 82 | @JsonKey(name: 'git_refs_url') 83 | String gitRefsUrl; 84 | 85 | @JsonKey(name: 'trees_url') 86 | String treesUrl; 87 | 88 | @JsonKey(name: 'statuses_url') 89 | String statusesUrl; 90 | 91 | @JsonKey(name: 'languages_url') 92 | String languagesUrl; 93 | 94 | @JsonKey(name: 'stargazers_url') 95 | String stargazersUrl; 96 | 97 | @JsonKey(name: 'contributors_url') 98 | String contributorsUrl; 99 | 100 | @JsonKey(name: 'subscribers_url') 101 | String subscribersUrl; 102 | 103 | @JsonKey(name: 'subscription_url') 104 | String subscriptionUrl; 105 | 106 | @JsonKey(name: 'commits_url') 107 | String commitsUrl; 108 | 109 | @JsonKey(name: 'git_commits_url') 110 | String gitCommitsUrl; 111 | 112 | @JsonKey(name: 'comments_url') 113 | String commentsUrl; 114 | 115 | @JsonKey(name: 'issue_comment_url') 116 | String issueCommentUrl; 117 | 118 | @JsonKey(name: 'contents_url') 119 | String contentsUrl; 120 | 121 | @JsonKey(name: 'compare_url') 122 | String compareUrl; 123 | 124 | @JsonKey(name: 'merges_url') 125 | String mergesUrl; 126 | 127 | @JsonKey(name: 'archive_url') 128 | String archiveUrl; 129 | 130 | @JsonKey(name: 'downloads_url') 131 | String downloadsUrl; 132 | 133 | @JsonKey(name: 'issues_url') 134 | String issuesUrl; 135 | 136 | @JsonKey(name: 'pulls_url') 137 | String pullsUrl; 138 | 139 | @JsonKey(name: 'milestones_url') 140 | String milestonesUrl; 141 | 142 | @JsonKey(name: 'notifications_url') 143 | String notificationsUrl; 144 | 145 | @JsonKey(name: 'labels_url') 146 | String labelsUrl; 147 | 148 | @JsonKey(name: 'releases_url') 149 | String releasesUrl; 150 | 151 | @JsonKey(name: 'deployments_url') 152 | String deploymentsUrl; 153 | 154 | @JsonKey(name: 'created_at') 155 | String createdAt; 156 | 157 | @JsonKey(name: 'updated_at') 158 | String updatedAt; 159 | 160 | @JsonKey(name: 'pushed_at') 161 | String pushedAt; 162 | 163 | @JsonKey(name: 'git_url') 164 | String gitUrl; 165 | 166 | @JsonKey(name: 'ssh_url') 167 | String sshUrl; 168 | 169 | @JsonKey(name: 'clone_url') 170 | String cloneUrl; 171 | 172 | @JsonKey(name: 'svn_url') 173 | String svnUrl; 174 | 175 | @JsonKey(name: 'homepage') 176 | String homepage; 177 | 178 | @JsonKey(name: 'size') 179 | int size; 180 | 181 | @JsonKey(name: 'stargazers_count') 182 | int stargazersCount; 183 | 184 | @JsonKey(name: 'watchers_count') 185 | int watchersCount; 186 | 187 | @JsonKey(name: 'language') 188 | String language; 189 | 190 | @JsonKey(name: 'has_issues') 191 | bool hasIssues; 192 | 193 | @JsonKey(name: 'has_projects') 194 | bool hasProjects; 195 | 196 | @JsonKey(name: 'has_downloads') 197 | bool hasDownloads; 198 | 199 | @JsonKey(name: 'has_wiki') 200 | bool hasWiki; 201 | 202 | @JsonKey(name: 'has_pages') 203 | bool hasPages; 204 | 205 | @JsonKey(name: 'forks_count') 206 | int forksCount; 207 | 208 | @JsonKey(name: 'archived') 209 | bool archived; 210 | 211 | @JsonKey(name: 'disabled') 212 | bool disabled; 213 | 214 | @JsonKey(name: 'open_issues_count') 215 | int openIssuesCount; 216 | 217 | @JsonKey(name: 'forks') 218 | int forks; 219 | 220 | @JsonKey(name: 'open_issues') 221 | int openIssues; 222 | 223 | @JsonKey(name: 'watchers') 224 | int watchers; 225 | 226 | @JsonKey(name: 'default_branch') 227 | String defaultBranch; 228 | 229 | Repo(this.id,this.nodeId,this.name,this.fullName,this.private,this.owner,this.htmlUrl,this.description,this.fork,this.url,this.forksUrl,this.keysUrl,this.collaboratorsUrl,this.teamsUrl,this.hooksUrl,this.issueEventsUrl,this.eventsUrl,this.assigneesUrl,this.branchesUrl,this.tagsUrl,this.blobsUrl,this.gitTagsUrl,this.gitRefsUrl,this.treesUrl,this.statusesUrl,this.languagesUrl,this.stargazersUrl,this.contributorsUrl,this.subscribersUrl,this.subscriptionUrl,this.commitsUrl,this.gitCommitsUrl,this.commentsUrl,this.issueCommentUrl,this.contentsUrl,this.compareUrl,this.mergesUrl,this.archiveUrl,this.downloadsUrl,this.issuesUrl,this.pullsUrl,this.milestonesUrl,this.notificationsUrl,this.labelsUrl,this.releasesUrl,this.deploymentsUrl,this.createdAt,this.updatedAt,this.pushedAt,this.gitUrl,this.sshUrl,this.cloneUrl,this.svnUrl,this.homepage,this.size,this.stargazersCount,this.watchersCount,this.language,this.hasIssues,this.hasProjects,this.hasDownloads,this.hasWiki,this.hasPages,this.forksCount,this.archived,this.disabled,this.openIssuesCount,this.forks,this.openIssues,this.watchers,this.defaultBranch,); 230 | 231 | factory Repo.fromJson(Map srcJson) => _$RepoFromJson(srcJson); 232 | 233 | Map toJson() => _$RepoToJson(this); 234 | 235 | } 236 | 237 | 238 | @JsonSerializable() 239 | class Owner extends Object { 240 | 241 | @JsonKey(name: 'login') 242 | String login; 243 | 244 | @JsonKey(name: 'id') 245 | int id; 246 | 247 | @JsonKey(name: 'node_id') 248 | String nodeId; 249 | 250 | @JsonKey(name: 'avatar_url') 251 | String avatarUrl; 252 | 253 | @JsonKey(name: 'gravatar_id') 254 | String gravatarId; 255 | 256 | @JsonKey(name: 'url') 257 | String url; 258 | 259 | @JsonKey(name: 'html_url') 260 | String htmlUrl; 261 | 262 | @JsonKey(name: 'followers_url') 263 | String followersUrl; 264 | 265 | @JsonKey(name: 'following_url') 266 | String followingUrl; 267 | 268 | @JsonKey(name: 'gists_url') 269 | String gistsUrl; 270 | 271 | @JsonKey(name: 'starred_url') 272 | String starredUrl; 273 | 274 | @JsonKey(name: 'subscriptions_url') 275 | String subscriptionsUrl; 276 | 277 | @JsonKey(name: 'organizations_url') 278 | String organizationsUrl; 279 | 280 | @JsonKey(name: 'repos_url') 281 | String reposUrl; 282 | 283 | @JsonKey(name: 'events_url') 284 | String eventsUrl; 285 | 286 | @JsonKey(name: 'received_events_url') 287 | String receivedEventsUrl; 288 | 289 | @JsonKey(name: 'type') 290 | String type; 291 | 292 | @JsonKey(name: 'site_admin') 293 | bool siteAdmin; 294 | 295 | Owner(this.login,this.id,this.nodeId,this.avatarUrl,this.gravatarId,this.url,this.htmlUrl,this.followersUrl,this.followingUrl,this.gistsUrl,this.starredUrl,this.subscriptionsUrl,this.organizationsUrl,this.reposUrl,this.eventsUrl,this.receivedEventsUrl,this.type,this.siteAdmin,); 296 | 297 | factory Owner.fromJson(Map srcJson) => _$OwnerFromJson(srcJson); 298 | 299 | Map toJson() => _$OwnerToJson(this); 300 | 301 | } -------------------------------------------------------------------------------- /lib/common/model/repo.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'repo.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Repo _$RepoFromJson(Map json) { 10 | return Repo( 11 | json['id'] as int, 12 | json['node_id'] as String, 13 | json['name'] as String, 14 | json['full_name'] as String, 15 | json['private'] as bool, 16 | json['owner'] == null 17 | ? null 18 | : Owner.fromJson(json['owner'] as Map), 19 | json['html_url'] as String, 20 | json['description'] as String, 21 | json['fork'] as bool, 22 | json['url'] as String, 23 | json['forks_url'] as String, 24 | json['keys_url'] as String, 25 | json['collaborators_url'] as String, 26 | json['teams_url'] as String, 27 | json['hooks_url'] as String, 28 | json['issue_events_url'] as String, 29 | json['events_url'] as String, 30 | json['assignees_url'] as String, 31 | json['branches_url'] as String, 32 | json['tags_url'] as String, 33 | json['blobs_url'] as String, 34 | json['git_tags_url'] as String, 35 | json['git_refs_url'] as String, 36 | json['trees_url'] as String, 37 | json['statuses_url'] as String, 38 | json['languages_url'] as String, 39 | json['stargazers_url'] as String, 40 | json['contributors_url'] as String, 41 | json['subscribers_url'] as String, 42 | json['subscription_url'] as String, 43 | json['commits_url'] as String, 44 | json['git_commits_url'] as String, 45 | json['comments_url'] as String, 46 | json['issue_comment_url'] as String, 47 | json['contents_url'] as String, 48 | json['compare_url'] as String, 49 | json['merges_url'] as String, 50 | json['archive_url'] as String, 51 | json['downloads_url'] as String, 52 | json['issues_url'] as String, 53 | json['pulls_url'] as String, 54 | json['milestones_url'] as String, 55 | json['notifications_url'] as String, 56 | json['labels_url'] as String, 57 | json['releases_url'] as String, 58 | json['deployments_url'] as String, 59 | json['created_at'] as String, 60 | json['updated_at'] as String, 61 | json['pushed_at'] as String, 62 | json['git_url'] as String, 63 | json['ssh_url'] as String, 64 | json['clone_url'] as String, 65 | json['svn_url'] as String, 66 | json['homepage'] as String, 67 | json['size'] as int, 68 | json['stargazers_count'] as int, 69 | json['watchers_count'] as int, 70 | json['language'] as String, 71 | json['has_issues'] as bool, 72 | json['has_projects'] as bool, 73 | json['has_downloads'] as bool, 74 | json['has_wiki'] as bool, 75 | json['has_pages'] as bool, 76 | json['forks_count'] as int, 77 | json['archived'] as bool, 78 | json['disabled'] as bool, 79 | json['open_issues_count'] as int, 80 | json['forks'] as int, 81 | json['open_issues'] as int, 82 | json['watchers'] as int, 83 | json['default_branch'] as String); 84 | } 85 | 86 | Map _$RepoToJson(Repo instance) => { 87 | 'id': instance.id, 88 | 'node_id': instance.nodeId, 89 | 'name': instance.name, 90 | 'full_name': instance.fullName, 91 | 'private': instance.private, 92 | 'owner': instance.owner, 93 | 'html_url': instance.htmlUrl, 94 | 'description': instance.description, 95 | 'fork': instance.fork, 96 | 'url': instance.url, 97 | 'forks_url': instance.forksUrl, 98 | 'keys_url': instance.keysUrl, 99 | 'collaborators_url': instance.collaboratorsUrl, 100 | 'teams_url': instance.teamsUrl, 101 | 'hooks_url': instance.hooksUrl, 102 | 'issue_events_url': instance.issueEventsUrl, 103 | 'events_url': instance.eventsUrl, 104 | 'assignees_url': instance.assigneesUrl, 105 | 'branches_url': instance.branchesUrl, 106 | 'tags_url': instance.tagsUrl, 107 | 'blobs_url': instance.blobsUrl, 108 | 'git_tags_url': instance.gitTagsUrl, 109 | 'git_refs_url': instance.gitRefsUrl, 110 | 'trees_url': instance.treesUrl, 111 | 'statuses_url': instance.statusesUrl, 112 | 'languages_url': instance.languagesUrl, 113 | 'stargazers_url': instance.stargazersUrl, 114 | 'contributors_url': instance.contributorsUrl, 115 | 'subscribers_url': instance.subscribersUrl, 116 | 'subscription_url': instance.subscriptionUrl, 117 | 'commits_url': instance.commitsUrl, 118 | 'git_commits_url': instance.gitCommitsUrl, 119 | 'comments_url': instance.commentsUrl, 120 | 'issue_comment_url': instance.issueCommentUrl, 121 | 'contents_url': instance.contentsUrl, 122 | 'compare_url': instance.compareUrl, 123 | 'merges_url': instance.mergesUrl, 124 | 'archive_url': instance.archiveUrl, 125 | 'downloads_url': instance.downloadsUrl, 126 | 'issues_url': instance.issuesUrl, 127 | 'pulls_url': instance.pullsUrl, 128 | 'milestones_url': instance.milestonesUrl, 129 | 'notifications_url': instance.notificationsUrl, 130 | 'labels_url': instance.labelsUrl, 131 | 'releases_url': instance.releasesUrl, 132 | 'deployments_url': instance.deploymentsUrl, 133 | 'created_at': instance.createdAt, 134 | 'updated_at': instance.updatedAt, 135 | 'pushed_at': instance.pushedAt, 136 | 'git_url': instance.gitUrl, 137 | 'ssh_url': instance.sshUrl, 138 | 'clone_url': instance.cloneUrl, 139 | 'svn_url': instance.svnUrl, 140 | 'homepage': instance.homepage, 141 | 'size': instance.size, 142 | 'stargazers_count': instance.stargazersCount, 143 | 'watchers_count': instance.watchersCount, 144 | 'language': instance.language, 145 | 'has_issues': instance.hasIssues, 146 | 'has_projects': instance.hasProjects, 147 | 'has_downloads': instance.hasDownloads, 148 | 'has_wiki': instance.hasWiki, 149 | 'has_pages': instance.hasPages, 150 | 'forks_count': instance.forksCount, 151 | 'archived': instance.archived, 152 | 'disabled': instance.disabled, 153 | 'open_issues_count': instance.openIssuesCount, 154 | 'forks': instance.forks, 155 | 'open_issues': instance.openIssues, 156 | 'watchers': instance.watchers, 157 | 'default_branch': instance.defaultBranch 158 | }; 159 | 160 | Owner _$OwnerFromJson(Map json) { 161 | return Owner( 162 | json['login'] as String, 163 | json['id'] as int, 164 | json['node_id'] as String, 165 | json['avatar_url'] as String, 166 | json['gravatar_id'] as String, 167 | json['url'] as String, 168 | json['html_url'] as String, 169 | json['followers_url'] as String, 170 | json['following_url'] as String, 171 | json['gists_url'] as String, 172 | json['starred_url'] as String, 173 | json['subscriptions_url'] as String, 174 | json['organizations_url'] as String, 175 | json['repos_url'] as String, 176 | json['events_url'] as String, 177 | json['received_events_url'] as String, 178 | json['type'] as String, 179 | json['site_admin'] as bool); 180 | } 181 | 182 | Map _$OwnerToJson(Owner instance) => { 183 | 'login': instance.login, 184 | 'id': instance.id, 185 | 'node_id': instance.nodeId, 186 | 'avatar_url': instance.avatarUrl, 187 | 'gravatar_id': instance.gravatarId, 188 | 'url': instance.url, 189 | 'html_url': instance.htmlUrl, 190 | 'followers_url': instance.followersUrl, 191 | 'following_url': instance.followingUrl, 192 | 'gists_url': instance.gistsUrl, 193 | 'starred_url': instance.starredUrl, 194 | 'subscriptions_url': instance.subscriptionsUrl, 195 | 'organizations_url': instance.organizationsUrl, 196 | 'repos_url': instance.reposUrl, 197 | 'events_url': instance.eventsUrl, 198 | 'received_events_url': instance.receivedEventsUrl, 199 | 'type': instance.type, 200 | 'site_admin': instance.siteAdmin 201 | }; 202 | -------------------------------------------------------------------------------- /lib/common/model/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'user.g.dart'; 4 | 5 | @JsonSerializable() 6 | class User extends Object { 7 | 8 | @JsonKey(name: 'login') 9 | String login; 10 | 11 | @JsonKey(name: 'id') 12 | int id; 13 | 14 | @JsonKey(name: 'node_id') 15 | String nodeId; 16 | 17 | @JsonKey(name: 'token') 18 | String token; 19 | 20 | @JsonKey(name: 'avatar_url') 21 | String avatarUrl; 22 | 23 | @JsonKey(name: 'gravatar_id') 24 | String gravatarId; 25 | 26 | @JsonKey(name: 'url') 27 | String url; 28 | 29 | @JsonKey(name: 'html_url') 30 | String htmlUrl; 31 | 32 | @JsonKey(name: 'followers_url') 33 | String followersUrl; 34 | 35 | @JsonKey(name: 'following_url') 36 | String followingUrl; 37 | 38 | @JsonKey(name: 'gists_url') 39 | String gistsUrl; 40 | 41 | @JsonKey(name: 'starred_url') 42 | String starredUrl; 43 | 44 | @JsonKey(name: 'subscriptions_url') 45 | String subscriptionsUrl; 46 | 47 | @JsonKey(name: 'organizations_url') 48 | String organizationsUrl; 49 | 50 | @JsonKey(name: 'repos_url') 51 | String reposUrl; 52 | 53 | @JsonKey(name: 'events_url') 54 | String eventsUrl; 55 | 56 | @JsonKey(name: 'received_events_url') 57 | String receivedEventsUrl; 58 | 59 | @JsonKey(name: 'type') 60 | String type; 61 | 62 | @JsonKey(name: 'site_admin') 63 | bool siteAdmin; 64 | 65 | @JsonKey(name: 'name') 66 | String name; 67 | 68 | @JsonKey(name: 'blog') 69 | String blog; 70 | 71 | @JsonKey(name: 'location') 72 | String location; 73 | 74 | @JsonKey(name: 'email') 75 | String email; 76 | 77 | @JsonKey(name: 'bio') 78 | String bio; 79 | 80 | @JsonKey(name: 'public_repos') 81 | int publicRepos; 82 | 83 | @JsonKey(name: 'public_gists') 84 | int publicGists; 85 | 86 | @JsonKey(name: 'followers') 87 | int followers; 88 | 89 | @JsonKey(name: 'following') 90 | int following; 91 | 92 | @JsonKey(name: 'created_at') 93 | String createdAt; 94 | 95 | @JsonKey(name: 'updated_at') 96 | String updatedAt; 97 | 98 | @JsonKey(name: 'private_gists') 99 | int privateGists; 100 | 101 | @JsonKey(name: 'total_private_repos') 102 | int totalPrivateRepos; 103 | 104 | @JsonKey(name: 'owned_private_repos') 105 | int ownedPrivateRepos; 106 | 107 | @JsonKey(name: 'disk_usage') 108 | int diskUsage; 109 | 110 | @JsonKey(name: 'collaborators') 111 | int collaborators; 112 | 113 | @JsonKey(name: 'two_factor_authentication') 114 | bool twoFactorAuthentication; 115 | 116 | @JsonKey(name: 'plan') 117 | Plan plan; 118 | 119 | User(this.login,this.id,this.nodeId,this.avatarUrl,this.gravatarId,this.url,this.htmlUrl,this.followersUrl,this.followingUrl,this.gistsUrl,this.starredUrl,this.subscriptionsUrl,this.organizationsUrl,this.reposUrl,this.eventsUrl,this.receivedEventsUrl,this.type,this.siteAdmin,this.name,this.blog,this.location,this.email,this.bio,this.publicRepos,this.publicGists,this.followers,this.following,this.createdAt,this.updatedAt,this.privateGists,this.totalPrivateRepos,this.ownedPrivateRepos,this.diskUsage,this.collaborators,this.twoFactorAuthentication,this.plan,); 120 | 121 | factory User.fromJson(Map srcJson) => _$UserFromJson(srcJson); 122 | 123 | Map toJson() => _$UserToJson(this); 124 | } 125 | 126 | 127 | @JsonSerializable() 128 | class Plan extends Object { 129 | 130 | @JsonKey(name: 'name') 131 | String name; 132 | 133 | @JsonKey(name: 'space') 134 | int space; 135 | 136 | @JsonKey(name: 'collaborators') 137 | int collaborators; 138 | 139 | @JsonKey(name: 'private_repos') 140 | int privateRepos; 141 | 142 | Plan(this.name,this.space,this.collaborators,this.privateRepos,); 143 | 144 | factory Plan.fromJson(Map srcJson) => _$PlanFromJson(srcJson); 145 | 146 | Map toJson() => _$PlanToJson(this); 147 | 148 | } -------------------------------------------------------------------------------- /lib/common/model/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | User _$UserFromJson(Map json) { 10 | return User( 11 | json['login'] as String, 12 | json['id'] as int, 13 | json['node_id'] as String, 14 | json['avatar_url'] as String, 15 | json['gravatar_id'] as String, 16 | json['url'] as String, 17 | json['html_url'] as String, 18 | json['followers_url'] as String, 19 | json['following_url'] as String, 20 | json['gists_url'] as String, 21 | json['starred_url'] as String, 22 | json['subscriptions_url'] as String, 23 | json['organizations_url'] as String, 24 | json['repos_url'] as String, 25 | json['events_url'] as String, 26 | json['received_events_url'] as String, 27 | json['type'] as String, 28 | json['site_admin'] as bool, 29 | json['name'] as String, 30 | json['blog'] as String, 31 | json['location'] as String, 32 | json['email'] as String, 33 | json['bio'] as String, 34 | json['public_repos'] as int, 35 | json['public_gists'] as int, 36 | json['followers'] as int, 37 | json['following'] as int, 38 | json['created_at'] as String, 39 | json['updated_at'] as String, 40 | json['private_gists'] as int, 41 | json['total_private_repos'] as int, 42 | json['owned_private_repos'] as int, 43 | json['disk_usage'] as int, 44 | json['collaborators'] as int, 45 | json['two_factor_authentication'] as bool, 46 | json['plan'] == null 47 | ? null 48 | : Plan.fromJson(json['plan'] as Map)); 49 | } 50 | 51 | Map _$UserToJson(User instance) => { 52 | 'login': instance.login, 53 | 'id': instance.id, 54 | 'node_id': instance.nodeId, 55 | 'avatar_url': instance.avatarUrl, 56 | 'gravatar_id': instance.gravatarId, 57 | 'url': instance.url, 58 | 'html_url': instance.htmlUrl, 59 | 'followers_url': instance.followersUrl, 60 | 'following_url': instance.followingUrl, 61 | 'gists_url': instance.gistsUrl, 62 | 'starred_url': instance.starredUrl, 63 | 'subscriptions_url': instance.subscriptionsUrl, 64 | 'organizations_url': instance.organizationsUrl, 65 | 'repos_url': instance.reposUrl, 66 | 'events_url': instance.eventsUrl, 67 | 'received_events_url': instance.receivedEventsUrl, 68 | 'type': instance.type, 69 | 'site_admin': instance.siteAdmin, 70 | 'name': instance.name, 71 | 'blog': instance.blog, 72 | 'location': instance.location, 73 | 'email': instance.email, 74 | 'bio': instance.bio, 75 | 'public_repos': instance.publicRepos, 76 | 'public_gists': instance.publicGists, 77 | 'followers': instance.followers, 78 | 'following': instance.following, 79 | 'created_at': instance.createdAt, 80 | 'updated_at': instance.updatedAt, 81 | 'private_gists': instance.privateGists, 82 | 'total_private_repos': instance.totalPrivateRepos, 83 | 'owned_private_repos': instance.ownedPrivateRepos, 84 | 'disk_usage': instance.diskUsage, 85 | 'collaborators': instance.collaborators, 86 | 'two_factor_authentication': instance.twoFactorAuthentication, 87 | 'plan': instance.plan 88 | }; 89 | 90 | Plan _$PlanFromJson(Map json) { 91 | return Plan(json['name'] as String, json['space'] as int, 92 | json['collaborators'] as int, json['private_repos'] as int); 93 | } 94 | 95 | Map _$PlanToJson(Plan instance) => { 96 | 'name': instance.name, 97 | 'space': instance.space, 98 | 'collaborators': instance.collaborators, 99 | 'private_repos': instance.privateRepos 100 | }; 101 | -------------------------------------------------------------------------------- /lib/common/model/user_introduction.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'user_introduction.g.dart'; 4 | 5 | /// 用户的简单信息 6 | /// 该Model类用于其他接口中返回的某个属性的复用 7 | @JsonSerializable() 8 | class UserIntro extends Object { 9 | @JsonKey(name: 'login') 10 | String login; 11 | 12 | @JsonKey(name: 'id') 13 | int id; 14 | 15 | @JsonKey(name: 'node_id') 16 | String nodeId; 17 | 18 | @JsonKey(name: 'avatar_url') 19 | String avatarUrl; 20 | 21 | @JsonKey(name: 'gravatar_id') 22 | String gravatarId; 23 | 24 | @JsonKey(name: 'url') 25 | String url; 26 | 27 | @JsonKey(name: 'html_url') 28 | String htmlUrl; 29 | 30 | @JsonKey(name: 'followers_url') 31 | String followersUrl; 32 | 33 | @JsonKey(name: 'following_url') 34 | String followingUrl; 35 | 36 | @JsonKey(name: 'gists_url') 37 | String gistsUrl; 38 | 39 | @JsonKey(name: 'starred_url') 40 | String starredUrl; 41 | 42 | @JsonKey(name: 'subscriptions_url') 43 | String subscriptionsUrl; 44 | 45 | @JsonKey(name: 'organizations_url') 46 | String organizationsUrl; 47 | 48 | @JsonKey(name: 'repos_url') 49 | String reposUrl; 50 | 51 | @JsonKey(name: 'events_url') 52 | String eventsUrl; 53 | 54 | @JsonKey(name: 'received_events_url') 55 | String receivedEventsUrl; 56 | 57 | @JsonKey(name: 'type') 58 | String type; 59 | 60 | @JsonKey(name: 'site_admin') 61 | bool siteAdmin; 62 | 63 | UserIntro( 64 | this.login, 65 | this.id, 66 | this.nodeId, 67 | this.avatarUrl, 68 | this.gravatarId, 69 | this.url, 70 | this.htmlUrl, 71 | this.followersUrl, 72 | this.followingUrl, 73 | this.gistsUrl, 74 | this.starredUrl, 75 | this.subscriptionsUrl, 76 | this.organizationsUrl, 77 | this.reposUrl, 78 | this.eventsUrl, 79 | this.receivedEventsUrl, 80 | this.type, 81 | this.siteAdmin, 82 | ); 83 | 84 | factory UserIntro.fromJson(Map srcJson) => 85 | _$UserIntroFromJson(srcJson); 86 | 87 | Map toJson() => _$UserIntroToJson(this); 88 | } 89 | -------------------------------------------------------------------------------- /lib/common/model/user_introduction.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user_introduction.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | UserIntro _$UserIntroFromJson(Map json) { 10 | return UserIntro( 11 | json['login'] as String, 12 | json['id'] as int, 13 | json['node_id'] as String, 14 | json['avatar_url'] as String, 15 | json['gravatar_id'] as String, 16 | json['url'] as String, 17 | json['html_url'] as String, 18 | json['followers_url'] as String, 19 | json['following_url'] as String, 20 | json['gists_url'] as String, 21 | json['starred_url'] as String, 22 | json['subscriptions_url'] as String, 23 | json['organizations_url'] as String, 24 | json['repos_url'] as String, 25 | json['events_url'] as String, 26 | json['received_events_url'] as String, 27 | json['type'] as String, 28 | json['site_admin'] as bool); 29 | } 30 | 31 | Map _$UserIntroToJson(UserIntro instance) => { 32 | 'login': instance.login, 33 | 'id': instance.id, 34 | 'node_id': instance.nodeId, 35 | 'avatar_url': instance.avatarUrl, 36 | 'gravatar_id': instance.gravatarId, 37 | 'url': instance.url, 38 | 'html_url': instance.htmlUrl, 39 | 'followers_url': instance.followersUrl, 40 | 'following_url': instance.followingUrl, 41 | 'gists_url': instance.gistsUrl, 42 | 'starred_url': instance.starredUrl, 43 | 'subscriptions_url': instance.subscriptionsUrl, 44 | 'organizations_url': instance.organizationsUrl, 45 | 'repos_url': instance.reposUrl, 46 | 'events_url': instance.eventsUrl, 47 | 'received_events_url': instance.receivedEventsUrl, 48 | 'type': instance.type, 49 | 'site_admin': instance.siteAdmin 50 | }; 51 | -------------------------------------------------------------------------------- /lib/common/service/api_code.dart: -------------------------------------------------------------------------------- 1 | class ApiCode { 2 | ///网络错误 3 | static const NETWORK_ERROR = -1; 4 | 5 | ///网络超时 6 | static const NETWORK_TIMEOUT = -2; 7 | 8 | ///网络返回数据格式化 9 | static const NETWORK_JSON_EXCEPTION = -3; 10 | 11 | static const SUCCESS = 200; 12 | } 13 | -------------------------------------------------------------------------------- /lib/common/service/interceptors/header_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class HeaderInterceptors extends InterceptorsWrapper { 4 | @override 5 | onRequest(RequestOptions options) { 6 | // 超时 7 | options.connectTimeout = 15000; 8 | return options; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/common/service/interceptors/log_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_rhine/common/constants/config.dart'; 3 | 4 | class LogsInterceptors extends InterceptorsWrapper { 5 | @override 6 | onRequest(RequestOptions options) { 7 | if (Config.DEBUG) { 8 | print("请求url:${options.path}"); 9 | print('请求头: ' + options.headers.toString()); 10 | if (options.data != null) { 11 | print('请求参数: ' + options.data.toString()); 12 | } 13 | } 14 | return options; 15 | } 16 | 17 | @override 18 | onResponse(Response response) { 19 | if (Config.DEBUG) { 20 | if (response != null) { 21 | print('返回参数: ' + response.toString()); 22 | } 23 | } 24 | return response; // continue 25 | } 26 | 27 | @override 28 | onError(DioError err) { 29 | if (Config.DEBUG) { 30 | print('请求异常: ' + err.toString()); 31 | print('请求异常信息: ' + err.response?.toString() ?? ""); 32 | } 33 | return err; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/common/service/interceptors/response_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_rhine/common/errors/errors.dart'; 3 | import 'package:flutter_rhine/repository/others/dao_result.dart'; 4 | 5 | class ResponseInterceptors extends InterceptorsWrapper { 6 | @override 7 | onResponse(Response response) { 8 | final RequestOptions option = response.request; 9 | try { 10 | if (option.contentType != null && 11 | option.contentType.primaryType == 'text') { 12 | return DataResult.success(response.data); 13 | } 14 | if (response.statusCode == 200 || response.statusCode == 201) { 15 | return DataResult.success(response.data); 16 | } 17 | } catch (e) { 18 | print(e.toString() + option.path); 19 | final networkError = 20 | NetworkRequestException('network error', response.statusCode); 21 | return DataResult.failure(networkError); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/common/service/interceptors/token_interceptor.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_rhine/common/constants/config.dart'; 3 | import 'package:flutter_rhine/repository/others/dao_result.dart'; 4 | import 'package:flutter_rhine/repository/others/sputils.dart'; 5 | 6 | class TokenInterceptor extends InterceptorsWrapper { 7 | String _token; 8 | 9 | @override 10 | onRequest(RequestOptions options) async { 11 | if (_token == null) { 12 | var authorizationCode = await getAuthorization(); 13 | if (authorizationCode != null) { 14 | _token = authorizationCode; 15 | } 16 | } 17 | options.headers["Authorization"] = _token; 18 | return options; 19 | } 20 | 21 | @override 22 | onResponse(Response response) async { 23 | try { 24 | final DataResult res = response.data; 25 | final dynamic responseJson = res.data; 26 | 27 | if (response.statusCode == 201 && responseJson['token'] != null) { 28 | _token = 'token' + responseJson['token']; 29 | await SpUtils.save(Config.TOKEN_KEY, _token); 30 | } 31 | } catch (e) { 32 | print(e); 33 | } 34 | return response; 35 | } 36 | 37 | clearAuthorization() { 38 | this._token = null; 39 | SpUtils.remove(Config.TOKEN_KEY); 40 | } 41 | 42 | getAuthorization() async { 43 | final String token = await SpUtils.get(Config.TOKEN_KEY); 44 | if (token == null) { 45 | final String basic = await SpUtils.get(Config.USER_BASIC_CODE); 46 | if (basic == null) { 47 | } else { 48 | return "Basic $basic"; 49 | } 50 | } else { 51 | this._token = token; 52 | return token; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/common/service/service.dart: -------------------------------------------------------------------------------- 1 | export 'api_code.dart'; 2 | export 'service_manager.dart'; -------------------------------------------------------------------------------- /lib/common/service/service_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_rhine/common/common.dart'; 5 | import 'package:flutter_rhine/common/service/api_code.dart'; 6 | 7 | import 'interceptors/header_interceptor.dart'; 8 | import 'interceptors/response_interceptor.dart'; 9 | import 'interceptors/token_interceptor.dart'; 10 | 11 | /// 全局的网络请求对象 12 | final ServiceManager serviceManager = ServiceManager(); 13 | 14 | class ServiceManager { 15 | static const CONTENT_TYPE_JSON = "application/json"; 16 | static const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"; 17 | 18 | final Dio _dio = Dio(); 19 | final TokenInterceptor _tokenInterceptor = TokenInterceptor(); 20 | 21 | ServiceManager() { 22 | _dio.interceptors.add(new HeaderInterceptors()); 23 | 24 | _dio.interceptors.add(new LogInterceptor()); 25 | 26 | _dio.interceptors.add(new ResponseInterceptors()); 27 | 28 | _dio.interceptors.add(_tokenInterceptor); 29 | } 30 | 31 | /// 发起网络请求 32 | /// [ url] 请求url 33 | /// [ params] 请求参数 34 | /// [ header] 外加头 35 | /// [ option] 配置 36 | Future> netFetch( 37 | final url, 38 | final params, 39 | final Map header, 40 | Options option, 41 | ) async { 42 | Map headers = new HashMap(); 43 | if (header != null) { 44 | headers.addAll(header); 45 | } 46 | 47 | if (option != null) { 48 | option.headers = headers; 49 | } else { 50 | option = new Options(method: "get"); 51 | option.headers = headers; 52 | } 53 | 54 | Response response; 55 | try { 56 | response = await _dio.request(url, data: params, options: option); 57 | } on DioError catch (e) { 58 | return _resultError(e); 59 | } 60 | if (response.data is DioError) { 61 | return _resultError(response.data); 62 | } 63 | return DataResult.success(response.data); 64 | } 65 | 66 | /// 生成请求失败对应的error 67 | /// [e] 网络请求失败的error 68 | DataResult _resultError(final DioError e) { 69 | Response errorResponse; 70 | if (e.response != null) { 71 | errorResponse = e.response; 72 | } else { 73 | errorResponse = new Response(statusCode: 400); 74 | } 75 | if (e.type == DioErrorType.CONNECT_TIMEOUT || 76 | e.type == DioErrorType.RECEIVE_TIMEOUT) { 77 | errorResponse.statusCode = ApiCode.NETWORK_TIMEOUT; 78 | } 79 | final Exception exception = Errors.networkException( 80 | message: e.message, 81 | statusCode: errorResponse.statusCode, 82 | ); 83 | 84 | return DataResult.failure(exception); 85 | } 86 | 87 | ///清除授权 88 | clearAuthorization() { 89 | _tokenInterceptor.clearAuthorization(); 90 | } 91 | 92 | ///获取授权token 93 | getAuthorization() async { 94 | return _tokenInterceptor.getAuthorization(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/common/utils/print_utils.dart: -------------------------------------------------------------------------------- 1 | import '../common.dart'; 2 | 3 | void printDebug(String message) { 4 | if (Config.DEBUG) print(message); 5 | } 6 | -------------------------------------------------------------------------------- /lib/common/utils/toast_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:fluttertoast/fluttertoast.dart'; 2 | 3 | void toast( 4 | final String message, { 5 | final Toast toastLength = Toast.LENGTH_SHORT, 6 | final ToastGravity gravity = ToastGravity.BOTTOM, 7 | }) { 8 | Fluttertoast.showToast( 9 | msg: message, 10 | toastLength: toastLength, 11 | gravity: gravity, 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/common/utils/utils.dart: -------------------------------------------------------------------------------- 1 | export 'toast_utils.dart'; -------------------------------------------------------------------------------- /lib/common/widget/global_hide_footer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | 4 | /// 不可见的footer 5 | class GlobalHideFooter extends RefreshFooter { 6 | @override 7 | State createState() { 8 | return _GlobalHideFooterState(); 9 | } 10 | 11 | GlobalHideFooter( 12 | GlobalKey key, 13 | ) : super(key: key); 14 | } 15 | 16 | class _GlobalHideFooterState extends RefreshFooterState { 17 | @override 18 | Widget build(BuildContext context) { 19 | return Container( 20 | width: .0, 21 | height: .0, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/common/widget/global_progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 全局的ProgressBar 4 | class ProgressBar extends StatelessWidget { 5 | final bool visibility; 6 | 7 | ProgressBar({ 8 | Key key, 9 | this.visibility = true, 10 | }) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Offstage( 15 | offstage: (!visibility), 16 | child: CircularProgressIndicator( 17 | strokeWidth: 3.0, 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/common/widget/widget.dart: -------------------------------------------------------------------------------- 1 | export 'global_hide_footer.dart'; 2 | export 'global_progress_bar.dart'; -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_rhine/common/common.dart'; 4 | import 'package:flutter_rhine/ui/login/login_middleware.dart'; 5 | import 'package:flutter_rhine/ui/login/login_page.dart'; 6 | import 'package:flutter_rhine/ui/main/main_page.dart'; 7 | import 'package:flutter_rhine/ui/main/repos/main_repo.dart'; 8 | 9 | import 'app/app_reducer.dart'; 10 | import 'ui/login/login.dart'; 11 | import 'ui/main/home/main_events.dart'; 12 | import 'ui/main/issues/main_issues_middleware.dart'; 13 | 14 | void main() { 15 | debugDefaultTargetPlatformOverride = TargetPlatform.iOS; 16 | 17 | runApp(App()); 18 | } 19 | 20 | class App extends StatelessWidget { 21 | App({Key key}) : super(key: key); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | final UserRepository userRepository = UserRepository(); 26 | 27 | final Store appStore = Store( 28 | appReducer, 29 | initialState: AppState.initial(), 30 | middleware: [ 31 | EpicMiddleware(LoginEpic(userRepository)), 32 | EpicMiddleware(MainEventsEpic()), 33 | EpicMiddleware(MainRepoEpic()), 34 | EpicMiddleware(MainIssuesEpic()), 35 | ], 36 | ); 37 | 38 | return StoreProvider( 39 | store: appStore, 40 | child: MaterialApp( 41 | title: 'Flutter-Rhine', 42 | debugShowCheckedModeBanner: Config.DEBUG, 43 | theme: ThemeData( 44 | primaryColor: colorPrimary, 45 | primaryColorDark: colorPrimaryDark, 46 | accentColor: colorAccent, 47 | ), 48 | routes: { 49 | AppRoutes.login: (context) { 50 | return _loginPage(appStore, userRepository); 51 | }, 52 | AppRoutes.main: (context) { 53 | return MainPage(userRepository: userRepository); 54 | }, 55 | }, 56 | home: _loginPage(appStore, userRepository), 57 | ), 58 | ); 59 | } 60 | 61 | Widget _loginPage( 62 | final Store appStore, 63 | final UserRepository userRepository, 64 | ) { 65 | return LoginPage( 66 | userRepository: userRepository, 67 | loginSuccessCallback: ( 68 | final BuildContext context, 69 | final User user, 70 | final String token, 71 | ) { 72 | toast('登录成功,跳转主页面'); 73 | Navigator.pop(context); 74 | Navigator.pushNamed(context, AppRoutes.main); 75 | }, 76 | loginCancelCallback: ( 77 | final BuildContext context, 78 | ) { 79 | toast('用户取消了登录'); 80 | }, 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/repository/event_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | class UserEventRepository { 4 | UserEventRepository._(); 5 | 6 | /// 拉取用户Event分页数据 7 | /// [username] 用户名 8 | /// [pageIndex] 分页索引 9 | static Future>> fetchEvents( 10 | final String username, final int pageIndex) async { 11 | var res = await serviceManager.netFetch( 12 | Api.userEvents(username) + Api.getPageParams('?', pageIndex), 13 | null, 14 | null, 15 | null); 16 | 17 | var resultData; 18 | if (res != null && res.result) { 19 | final List events = getEventList(res.data.data); 20 | 21 | resultData = DataResult.success(events); 22 | 23 | if (Config.DEBUG) { 24 | print("resultData events result " + resultData.result.toString()); 25 | print(resultData.data); 26 | print(res.data.toString()); 27 | } 28 | } else { 29 | resultData = DataResult.failure(Exception('获取用户事件失败')); 30 | } 31 | 32 | return resultData; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/repository/issues_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:flutter_rhine/common/model/issue.dart'; 3 | import 'package:flutter_rhine/common/service/service_manager.dart'; 4 | import 'package:flutter_rhine/repository/others/dao_result.dart'; 5 | 6 | /// 用户issues相关 7 | class IssuesRepository { 8 | IssuesRepository._(); 9 | 10 | static const String SORT_CREATED = 'created'; 11 | static const String SORT_UPDATED = 'updated'; 12 | static const String SORT_COMMENTS = 'comments'; 13 | 14 | static const String STATE_OPEN = 'open'; 15 | static const String STATE_CLOSED = 'closed'; 16 | static const String STATE_ALL = 'all'; 17 | 18 | /// 获取用户issues列表 19 | /// [sort] 排序规则,[SORT_CREATED]、[SORT_UPDATED]、[SORT_COMMENTS] 20 | /// [state] issue状态,[STATE_OPEN]、[STATE_CLOSED]、[STATE_ALL] 21 | /// [page] 页数 22 | /// [perPage] 分页请求数量 23 | static Future>> fetchIssues({ 24 | String sort = SORT_CREATED, 25 | String state = STATE_ALL, 26 | @required int page, 27 | int perPage = Config.PAGE_SIZE, 28 | }) async { 29 | _verifyIssuesApiParams(sort, state); 30 | 31 | final String url = Api.userIssues + 32 | Api.getPageParams('?', page, perPage) + 33 | '&sort=$sort&state=$state'; 34 | 35 | DataResult res = await serviceManager.netFetch(url, null, null, null); 36 | if (res != null && res.result) { 37 | List list = new List(); 38 | var data = res.data.data; 39 | if (data == null || data.length == 0) { 40 | return DataResult.failure(Errors.emptyListException()); 41 | } 42 | for (int i = 0; i < data.length; i++) { 43 | list.add(Issue.fromJson(data[i])); 44 | } 45 | return DataResult.success(list); 46 | } else { 47 | return DataResult.failure(Errors.networkException(statusCode: 400)); 48 | } 49 | } 50 | 51 | static void _verifyIssuesApiParams(final String sort, final String state) { 52 | if (![SORT_CREATED, SORT_UPDATED, SORT_COMMENTS].contains(sort)) { 53 | throw Exception('错误的sort参数,请使用SORT_CREATED、SORT_UPDATED、SORT_COMMENTS'); 54 | } 55 | if (![STATE_OPEN, STATE_CLOSED, STATE_ALL].contains(state)) { 56 | throw Exception( 57 | '错误的 common.state 参数,请使用SORT_CREATED、SORT_UPDATED、SORT_COMMENTS'); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/repository/others/dao_result.dart: -------------------------------------------------------------------------------- 1 | class DataResult { 2 | T data; 3 | bool result; 4 | Exception exception; 5 | 6 | DataResult( 7 | this.data, 8 | this.result, { 9 | this.exception, 10 | }); 11 | 12 | factory DataResult.failure(Exception exception) => 13 | DataResult(null, false, exception: exception); 14 | 15 | factory DataResult.success(T data) => 16 | DataResult(data, true, exception: null); 17 | 18 | @override 19 | String toString() { 20 | return 'DataResult{data: $data, result: $result, exception: $exception}'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/repository/others/sputils.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class SpUtils { 4 | 5 | SpUtils._(); 6 | 7 | static save(String key, value) async { 8 | final SharedPreferences prefs = await SharedPreferences.getInstance(); 9 | prefs.setString(key, value); 10 | } 11 | 12 | static get(String key) async { 13 | final SharedPreferences prefs = await SharedPreferences.getInstance(); 14 | return prefs.get(key); 15 | } 16 | 17 | static remove(String key) async { 18 | final SharedPreferences prefs = await SharedPreferences.getInstance(); 19 | prefs.remove(key); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/repository/repos_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | class UserRepoRepository { 4 | UserRepoRepository._(); 5 | 6 | static const String SORT_UPDATED = 'updated'; 7 | static const String SORT_CREATED = 'created'; 8 | static const String SORT_LETTER = 'full_name'; 9 | 10 | /// 拉取用户Repo分页数据 11 | /// [username] 用户名 12 | /// [sort] 排序方式,当该参数发生了改变,clear列表并刷新ui 13 | /// [pageIndex] 分页索引 14 | static Future>> fetchRepos({ 15 | final String username, 16 | final String sort = SORT_UPDATED, 17 | final int pageIndex, 18 | }) async { 19 | var res = await serviceManager.netFetch( 20 | Api.userRepos(username) + 21 | Api.getPageParams('?', pageIndex) + 22 | '&sort=$sort', 23 | null, 24 | null, 25 | null); 26 | 27 | DataResult> resultData; 28 | if (res != null && res.result) { 29 | final List repos = getRepoList(res.data.data); 30 | 31 | if (repos.length > 0) { 32 | resultData = DataResult.success(repos); 33 | } else { 34 | resultData = DataResult.failure(Errors.emptyListException()); 35 | } 36 | 37 | if (Config.DEBUG) { 38 | print("resultData events result " + resultData.result.toString()); 39 | print(resultData.data); 40 | print(res.data.toString()); 41 | } 42 | } else { 43 | resultData = DataResult.failure(Errors.networkException()); 44 | } 45 | 46 | return resultData; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/repository/repository.dart: -------------------------------------------------------------------------------- 1 | export 'package:flutter_rhine/repository/others/dao_result.dart'; 2 | 3 | export 'event_repository.dart'; 4 | export 'issues_repository.dart'; 5 | export 'user_repository.dart'; 6 | export 'repos_repository.dart'; 7 | -------------------------------------------------------------------------------- /lib/repository/user_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_rhine/common/common.dart'; 5 | import 'package:flutter_rhine/common/constants/constants.dart'; 6 | import 'package:flutter_rhine/common/model/user.dart'; 7 | import 'package:flutter_rhine/common/service/service_manager.dart'; 8 | import 'package:flutter_rhine/repository/others/dao_result.dart'; 9 | import 'package:flutter_rhine/repository/others/sputils.dart'; 10 | 11 | class UserRepository { 12 | /// 用户是否自动登录 13 | Future hasAutoLoginInfo() async { 14 | final String usernameTemp = await SpUtils.get(Config.USER_NAME_KEY) ?? ''; 15 | final String passwordTemp = await SpUtils.get(Config.PW_KEY) ?? ''; 16 | 17 | return usernameTemp != '' && passwordTemp != ''; 18 | } 19 | 20 | /// 获取用户自动登录信息 21 | Future> fetchAutoLoginInfo() async { 22 | final String usernameTemp = await SpUtils.get(Config.USER_NAME_KEY) ?? ''; 23 | final String passwordTemp = await SpUtils.get(Config.PW_KEY) ?? ''; 24 | 25 | return [usernameTemp, passwordTemp]; 26 | } 27 | 28 | /// 用户登录 29 | /// [username] 用户名 30 | /// [password] 登录密码 31 | Future> login( 32 | final String username, final String password) async { 33 | if (username == null || 34 | username == '' || 35 | password == null || 36 | password == '') { 37 | return DataResult.failure(Errors.emptyInputException('用户名密码不能为空')); 38 | } 39 | 40 | final String type = username + ":" + password; 41 | var bytes = utf8.encode(type); 42 | var base64Str = base64.encode(bytes); 43 | if (Config.DEBUG) { 44 | print("base64Str login " + base64Str); 45 | } 46 | 47 | await SpUtils.save(Config.USER_BASIC_CODE, base64Str); 48 | 49 | final Map requestParams = { 50 | "scopes": ['user', 'repo'], 51 | "note": "admin_script", 52 | "client_id": Ignore.clientId, 53 | "client_secret": Ignore.clientSecret 54 | }; 55 | 56 | var res = await serviceManager.netFetch(Api.authorization, 57 | json.encode(requestParams), null, new Options(method: "post")); 58 | 59 | if (res != null && res.result) { 60 | await SpUtils.save(Config.USER_NAME_KEY, username); 61 | await SpUtils.save(Config.PW_KEY, password); 62 | DataResult resultData = await getUserInfo(null); 63 | 64 | if (Config.DEBUG) { 65 | print("user result " + resultData.result.toString()); 66 | print(resultData.data); 67 | print(res.data.toString()); 68 | } 69 | return resultData; 70 | } else { 71 | return DataResult.failure(Errors.loginFailureException()); 72 | } 73 | } 74 | 75 | /// 获取用户登录信息 76 | /// [userName] 查询对应username的用户信息,该参数为null时,请求用户自己的用户信息 77 | /// [needDb] 是否需要将用户信息存储数据库 78 | Future> getUserInfo(final String userName, 79 | {final bool needDb = false}) async { 80 | final DataResult res = 81 | await serviceManager.netFetch(Api.userInfo, null, null, null); 82 | if (res != null && res.result) { 83 | final User user = User.fromJson(res.data.data); 84 | 85 | // 存入持久层 86 | if (needDb) SpUtils.save(Config.USER_INFO, json.encode(user.toJson())); 87 | 88 | return DataResult.success(user); 89 | } else { 90 | return DataResult.failure(Errors.loginFailureException()); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/routers/routes.dart: -------------------------------------------------------------------------------- 1 | abstract class AppRoutes { 2 | factory AppRoutes._() => null; 3 | 4 | /// 登录页 5 | static final login = '/login'; 6 | /// 主页面 7 | static final main = '/main'; 8 | } 9 | -------------------------------------------------------------------------------- /lib/ui/login/login.dart: -------------------------------------------------------------------------------- 1 | export 'login_action.dart'; 2 | export 'login_form.dart'; 3 | export 'login_middleware.dart'; 4 | export 'login_page.dart'; 5 | export 'login_reducer.dart'; 6 | export 'login_state.dart'; 7 | -------------------------------------------------------------------------------- /lib/ui/login/login_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | abstract class LoginAction {} 4 | 5 | /// 初始化事件 6 | class InitialAction extends LoginAction { 7 | final bool shouldAutoLogin; 8 | 9 | InitialAction({this.shouldAutoLogin}) : assert(shouldAutoLogin != null); 10 | } 11 | 12 | /// 释放事件 13 | class LoginDisposeAction extends LoginAction {} 14 | 15 | /// 用户点击登录事件 16 | class LoginClickedAction extends LoginAction { 17 | final String username; 18 | final String password; 19 | 20 | LoginClickedAction({this.username, this.password}); 21 | } 22 | 23 | /// 用户自动登录信息获取事件 24 | class AutoLoginInfoGetAction extends LoginAction { 25 | final String username; 26 | final String password; 27 | 28 | AutoLoginInfoGetAction({this.username, this.password}); 29 | } 30 | 31 | /// 登录中 32 | class LoginLoadingAction extends LoginAction {} 33 | 34 | /// 登录成功 35 | class LoginSuccessAction extends LoginAction { 36 | final User user; 37 | final String token; 38 | 39 | LoginSuccessAction(this.user, this.token); 40 | } 41 | 42 | /// 登录失败 43 | class LoginFailureAction extends LoginAction { 44 | final Exception exception; 45 | 46 | LoginFailureAction(this.exception); 47 | } 48 | -------------------------------------------------------------------------------- /lib/ui/login/login_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_rhine/common/common.dart'; 3 | import 'package:flutter_rhine/common/constants/constants.dart'; 4 | import 'package:flutter_rhine/common/widget/global_progress_bar.dart'; 5 | 6 | import 'login.dart'; 7 | 8 | class LoginForm extends StatefulWidget { 9 | final LoginSuccessCallback loginSuccessCallback; 10 | final LoginCancelCallback loginCancelCallback; 11 | 12 | LoginForm(this.loginSuccessCallback, this.loginCancelCallback); 13 | 14 | @override 15 | _LoginFormState createState() => 16 | _LoginFormState(loginSuccessCallback, loginCancelCallback); 17 | } 18 | 19 | class _LoginFormState extends State { 20 | final TextEditingController userNameController = TextEditingController(); 21 | final TextEditingController passwordController = TextEditingController(); 22 | 23 | final LoginSuccessCallback loginSuccessCallback; 24 | final LoginCancelCallback loginCancelCallback; 25 | 26 | _LoginFormState(this.loginSuccessCallback, this.loginCancelCallback); 27 | 28 | bool _isFirstLoad = true; 29 | 30 | @override 31 | void didChangeDependencies() { 32 | super.didChangeDependencies(); 33 | if (_isFirstLoad) { 34 | _isFirstLoad = false; 35 | StoreProvider.of(context) 36 | .dispatch(InitialAction(shouldAutoLogin: true)); 37 | } 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return StoreConnector( 43 | converter: (store) { 44 | final state = store.state.loginState; 45 | if (Config.DEBUG) print('onNext State: ${state.toString()}'); 46 | 47 | final User user = state.user; 48 | // 登录成功 49 | if (user != null && loginSuccessCallback != null) { 50 | loginSuccessCallback(context, user, user.token); 51 | } 52 | // 取消登录 53 | if (state.isLoginCancel) { 54 | loginCancelCallback(context); 55 | } 56 | // 异常事件 57 | final Exception error = state.error; 58 | if (error != null) { 59 | if (error is NetworkRequestException) toast(error.message); 60 | if (error is LoginFailureException) toast(error.message); 61 | if (error is EmptyInputException) toast(error.message); 62 | } 63 | return state; 64 | }, 65 | builder: (context, LoginState state) => Container( 66 | alignment: Alignment.topCenter, 67 | child: Padding( 68 | padding: EdgeInsets.fromLTRB(16.0, 38.0, 16.0, 8.0), 69 | child: Stack( 70 | alignment: Alignment.center, 71 | fit: StackFit.loose, 72 | children: [ 73 | SingleChildScrollView( 74 | child: Column( 75 | children: [ 76 | /// 顶部图标和标题 77 | Row( 78 | children: [ 79 | Image( 80 | image: AssetImage(imageGithubCat), 81 | width: 65.0, 82 | height: 65.0, 83 | fit: BoxFit.fitWidth, 84 | ), 85 | Container( 86 | margin: EdgeInsets.only(left: 32.0), 87 | child: Text( 88 | 'Sign into GitHub', 89 | style: TextStyle( 90 | fontSize: 24.0, 91 | fontWeight: FontWeight.normal, 92 | color: colorPrimaryText, 93 | ), 94 | maxLines: 1, 95 | textAlign: TextAlign.start, 96 | ), 97 | ), 98 | ], 99 | ), 100 | _usernameInput(), 101 | _passwordInput(), 102 | _signInButton() 103 | ], 104 | ), 105 | ), 106 | StoreConnector( 107 | converter: (store) => store.state.loginState.isLoading, 108 | builder: (context, visibility) => 109 | ProgressBar(visibility: visibility), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ), 115 | ); 116 | } 117 | 118 | /// 用户名输入框 119 | Widget _usernameInput() { 120 | final Store store = StoreProvider.of(context); 121 | final String username = store.state.loginState.username ?? ''; 122 | userNameController.text = username; 123 | return Container( 124 | margin: EdgeInsets.only(top: 24.0), 125 | child: TextField( 126 | controller: userNameController, 127 | keyboardType: TextInputType.text, 128 | decoration: InputDecoration( 129 | contentPadding: EdgeInsets.only(top: 10.0, bottom: 10.0), 130 | labelText: 'Username or email address', 131 | ), 132 | ), 133 | ); 134 | } 135 | 136 | /// 密码输入框 137 | Widget _passwordInput() { 138 | final Store store = StoreProvider.of(context); 139 | final String password = store.state.loginState.password ?? ''; 140 | passwordController.text = password; 141 | return Container( 142 | margin: EdgeInsets.only(top: 8.0), 143 | child: TextField( 144 | controller: passwordController, 145 | keyboardType: TextInputType.text, 146 | obscureText: true, 147 | decoration: InputDecoration( 148 | contentPadding: EdgeInsets.only(top: 10.0, bottom: 10.0), 149 | labelText: 'Password', 150 | ), 151 | ), 152 | ); 153 | } 154 | 155 | /// 登录按钮 156 | Widget _signInButton() { 157 | final Store store = StoreProvider.of(context); 158 | 159 | /// 登录按钮点击事件 160 | void _onLoginButtonClicked(Store store) { 161 | final String username = userNameController.text ?? ''; 162 | final String password = passwordController.text ?? ''; 163 | 164 | store 165 | .dispatch(LoginClickedAction(username: username, password: password)); 166 | } 167 | 168 | return Container( 169 | alignment: Alignment.center, 170 | margin: EdgeInsets.only(top: 32.0), 171 | width: double.infinity, 172 | child: ConstrainedBox( 173 | constraints: BoxConstraints( 174 | minWidth: double.infinity, 175 | minHeight: 50.0, 176 | ), 177 | child: FlatButton( 178 | onPressed: () => _onLoginButtonClicked(store), 179 | color: colorSecondaryDark, 180 | highlightColor: colorPrimary, 181 | shape: RoundedRectangleBorder( 182 | borderRadius: BorderRadius.all(Radius.circular(7.0)), 183 | ), 184 | child: Text( 185 | 'Sign in', 186 | style: TextStyle( 187 | color: Colors.white, 188 | fontSize: 18.0, 189 | fontWeight: FontWeight.bold, 190 | ), 191 | ), 192 | ), 193 | ), 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /lib/ui/login/login_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; import 'login.dart'; class LoginEpic implements EpicClass { final UserRepository userRepository; LoginEpic(this.userRepository) : assert(userRepository != null); @override Stream call(Stream actions, EpicStore store) { return Observable.merge([ Observable(actions) .ofType(TypeToken()) .take(1) .map((it) => it.shouldAutoLogin) .flatMap((it) => _loginInitial(it)), Observable(actions) .ofType(TypeToken()) .flatMap((it) => _loginClicked(it.username, it.password)), ]); } /// 用户进入界面初始化 /// [shouldAutoLogin] 是否自动登录 Stream _loginInitial( final bool shouldAutoLogin, ) async* { if (shouldAutoLogin) { final bool hasAutoLoginInfo = await userRepository.hasAutoLoginInfo(); if (hasAutoLoginInfo) { final List info = await userRepository.fetchAutoLoginInfo(); final String username = info[0]; final String password = info[1]; yield AutoLoginInfoGetAction(username: username, password: password); // 本地存有登录信息,自动登录 yield* _loginStateStream(username, password); if (Config.DEBUG) { print('用户进入界面初始化. username: $username, password: $password.'); } } else { if (Config.DEBUG) { print('没有登录信息,手动登录.'); } // 没有登录信息,手动登录 yield _loginFailure(Exception()); } } else { // 需要用户手动登录 yield _loginFailure(Exception()); } } /// 用户点击登录按钮 /// [username] 用户名 /// [password] 用户密码 Stream _loginClicked( final String username, final String password, ) async* { yield* _loginStateStream(username, password); } /// 用户登录 /// [username] 用户名 /// [password] 用户密码 Stream _loginStateStream( final String username, final String password, ) async* { print('login API request: username: $username, password: $password.'); yield LoginLoadingAction(); final DataResult loginResult = await userRepository.login(username, password); print('login API result: ${loginResult.toString()}'); if (loginResult.result) { final User user = loginResult.data; yield LoginSuccessAction(user, user.token); yield LoginDisposeAction(); yield AuthenticationSuccessAction(user, user.token); } else { yield _loginFailure( loginResult.exception ?? Errors.loginFailureException()); yield AuthenticationFailureAction(); } } /// 登录失败 /// [exception] 错误信息 LoginFailureAction _loginFailure(final Exception exception) { return LoginFailureAction(exception); } } -------------------------------------------------------------------------------- /lib/ui/login/login_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_rhine/common/common.dart'; 3 | import 'package:flutter_rhine/repository/repository.dart'; 4 | 5 | import 'login.dart'; 6 | 7 | /// 登录成功的回调函数 8 | /// [context] 上下文对象 9 | /// [user] 用户信息 10 | /// [token] 用户token 11 | typedef void LoginSuccessCallback( 12 | final BuildContext context, 13 | final User user, 14 | final String token, 15 | ); 16 | 17 | /// 取消登录的回调函数 18 | /// [context] 上下文对象 19 | typedef void LoginCancelCallback( 20 | final BuildContext context, 21 | ); 22 | 23 | @immutable 24 | class LoginPage extends StatelessWidget { 25 | final UserRepository userRepository; 26 | final LoginSuccessCallback loginSuccessCallback; 27 | final LoginCancelCallback loginCancelCallback; 28 | 29 | LoginPage({ 30 | Key key, 31 | @required this.userRepository, 32 | this.loginSuccessCallback, 33 | this.loginCancelCallback, 34 | }) : assert(userRepository != null), 35 | super(key: key); 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return StoreConnector( 40 | converter: (store) => store.state.loginState, 41 | builder: (BuildContext context, LoginState loginState) => Scaffold( 42 | appBar: AppBar( 43 | automaticallyImplyLeading: false, 44 | title: Container( 45 | alignment: Alignment.centerLeft, 46 | padding: EdgeInsets.only(left: 16.0), 47 | child: Text( 48 | 'Sign in', 49 | textAlign: TextAlign.start, 50 | ), 51 | ), 52 | ), 53 | body: LoginForm(loginSuccessCallback, loginCancelCallback), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/ui/login/login_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'login.dart'; 4 | 5 | final loginReducer = combineReducers([ 6 | TypedReducer(_clickLogin), 7 | TypedReducer(_autoLogin), 8 | TypedReducer(_loginLoading), 9 | TypedReducer(_loginSuccess), 10 | TypedReducer(_loginFailure), 11 | TypedReducer(_loginDispose), 12 | ]); 13 | 14 | LoginState _clickLogin(LoginState state, LoginClickedAction action) { 15 | return state.copyWith( 16 | username: action.username, 17 | password: action.password, 18 | user: null, 19 | isLoginCancel: null, 20 | isLoading: true, 21 | error: null, 22 | ); 23 | } 24 | 25 | LoginState _autoLogin(LoginState state, AutoLoginInfoGetAction action) { 26 | return state.copyWith( 27 | username: action.username, 28 | password: action.password, 29 | user: null, 30 | isLoginCancel: null, 31 | isLoading: true, 32 | error: null, 33 | ); 34 | } 35 | 36 | LoginState _loginLoading(LoginState state, LoginLoadingAction action) { 37 | return state.copyWith( 38 | user: null, isLoginCancel: null, isLoading: true, error: null); 39 | } 40 | 41 | LoginState _loginSuccess(LoginState state, LoginSuccessAction action) { 42 | return state.copyWith( 43 | user: action.user, isLoginCancel: null, isLoading: false, error: null); 44 | } 45 | 46 | LoginState _loginFailure(LoginState state, LoginFailureAction action) { 47 | return state.copyWith(isLoading: false, error: action.exception); 48 | } 49 | 50 | LoginState _loginDispose(LoginState state, LoginDisposeAction action) { 51 | return state.copyWith( 52 | user: null, 53 | isLoginCancel: false, 54 | isLoading: false, 55 | error: null, 56 | username: '', 57 | password: '', 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /lib/ui/login/login_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | @immutable 4 | class LoginState { 5 | final String username; 6 | final String password; 7 | final bool isLoading; 8 | 9 | /// 用户信息,当用户信息不为空时,视为登录成功,跳转login页面 10 | final User user; 11 | final bool isLoginCancel; 12 | 13 | final Exception error; 14 | 15 | LoginState({ 16 | this.username, 17 | this.password, 18 | this.isLoading = false, 19 | this.isLoginCancel = false, 20 | this.user, 21 | this.error, 22 | }); 23 | 24 | LoginState copyWith({ 25 | String username, 26 | String password, 27 | bool isLoading, 28 | User user, 29 | bool isLoginCancel, 30 | Exception error, 31 | }) { 32 | return LoginState( 33 | username: username ?? this.username, 34 | password: password ?? this.password, 35 | isLoading: isLoading ?? this.isLoading, 36 | isLoginCancel: isLoginCancel ?? this.isLoginCancel, 37 | user: user, 38 | error: error, 39 | ); 40 | } 41 | 42 | factory LoginState.initial() => 43 | LoginState(username: '', password: '', isLoading: false); 44 | 45 | @override 46 | String toString() { 47 | return 'LoginState{username: $username, password: $password, isLoading: $isLoading, user: $user, isLoginCancel: $isLoginCancel, error: $error}'; 48 | } 49 | 50 | @override 51 | bool operator ==(Object other) => 52 | identical(this, other) || 53 | other is LoginState && 54 | runtimeType == other.runtimeType && 55 | username == other.username && 56 | password == other.password && 57 | isLoading == other.isLoading && 58 | user == other.user && 59 | isLoginCancel == other.isLoginCancel && 60 | error == other.error; 61 | 62 | @override 63 | int get hashCode => 64 | username.hashCode ^ 65 | password.hashCode ^ 66 | isLoading.hashCode ^ 67 | user.hashCode ^ 68 | isLoginCancel.hashCode ^ 69 | error.hashCode; 70 | } 71 | -------------------------------------------------------------------------------- /lib/ui/main/home/main_events.dart: -------------------------------------------------------------------------------- 1 | export 'main_events_action.dart'; 2 | export 'main_events_item.dart'; 3 | export 'main_events_middleware.dart'; 4 | export 'main_events_page.dart'; 5 | export 'main_events_reducer.dart'; 6 | export 'main_events_state.dart'; -------------------------------------------------------------------------------- /lib/ui/main/home/main_events_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | abstract class MainEventsAction {} 5 | 6 | /// 第一次加载数据 7 | class MainEventsInitialAction extends MainEventsAction { 8 | final String username; 9 | 10 | MainEventsInitialAction({ 11 | @required this.username, 12 | }) : assert(username != null); 13 | } 14 | 15 | /// 加载更多 16 | class MainEventLoadNextPageAction extends MainEventsAction { 17 | final String username; 18 | final int currentPage; 19 | final List previousList; 20 | 21 | MainEventLoadNextPageAction({ 22 | @required this.username, 23 | @required this.currentPage, 24 | this.previousList = const [], 25 | }) : assert(username != null), 26 | assert(currentPage != null); 27 | } 28 | 29 | /// 加载中 30 | class MainEventsFirstLoadingAction extends MainEventsAction {} 31 | 32 | /// 加载分页数据成功 33 | class MainEventsPageLoadSuccessAction extends MainEventsAction { 34 | final List events; 35 | final int currentPage; 36 | 37 | MainEventsPageLoadSuccessAction( 38 | this.events, 39 | this.currentPage, 40 | ); 41 | } 42 | 43 | /// 加载分页数据失败 44 | class MainEventPageLoadFailureAction extends MainEventsAction { 45 | final Exception exception; 46 | 47 | MainEventPageLoadFailureAction( 48 | this.exception, 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /lib/ui/main/home/main_events_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:common_utils/common_utils.dart'; 2 | import 'package:flutter/gestures.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_rhine/common/constants/assets.dart'; 5 | import 'package:flutter_rhine/common/constants/colors.dart'; 6 | import 'package:flutter_rhine/common/model/event.dart'; 7 | 8 | class MainEventItem extends StatelessWidget { 9 | final Event event; 10 | 11 | final EventItemActionObserver observer; 12 | 13 | MainEventItem({Key key, @required this.event, this.observer}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final String timeLine = _transformEventTime(event.createdAt); 19 | final String imageAsset = _fetchEventImage(event); 20 | 21 | return Container( 22 | width: double.infinity, 23 | child: Column( 24 | mainAxisSize: MainAxisSize.min, 25 | mainAxisAlignment: MainAxisAlignment.start, 26 | crossAxisAlignment: CrossAxisAlignment.center, 27 | children: [ 28 | // 主要内容 29 | Flex( 30 | direction: Axis.horizontal, 31 | mainAxisSize: MainAxisSize.max, 32 | mainAxisAlignment: MainAxisAlignment.start, 33 | crossAxisAlignment: CrossAxisAlignment.center, 34 | children: [ 35 | // 头像 36 | Container( 37 | margin: EdgeInsets.only(top: 16.0, left: 16.0), 38 | child: ClipOval( 39 | child: Image( 40 | width: 40.0, 41 | height: 40.0, 42 | image: NetworkImage(event.actor.avatarUrl), 43 | ), 44 | ), 45 | ), 46 | // 详细事件和时间 47 | Expanded( 48 | flex: 1, 49 | child: Container( 50 | margin: EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), 51 | height: 60.0, 52 | child: Flex( 53 | direction: Axis.vertical, 54 | mainAxisAlignment: MainAxisAlignment.center, 55 | crossAxisAlignment: CrossAxisAlignment.start, 56 | children: [ 57 | Expanded( 58 | flex: 1, 59 | child: Container( 60 | margin: EdgeInsets.only(bottom: 5.0), 61 | child: _transformEventTitle(event), 62 | ), 63 | ), 64 | Text( 65 | timeLine, 66 | style: TextStyle( 67 | color: colorSecondaryTextGray, fontSize: 12.0), 68 | ), 69 | ], 70 | ), 71 | ), 72 | ), 73 | Container( 74 | margin: EdgeInsets.only(right: 16.0), 75 | child: Image( 76 | width: 20.0, 77 | height: 20.0, 78 | image: AssetImage(imageAsset), 79 | ), 80 | ), 81 | ], 82 | ), 83 | // 分割线 84 | Container( 85 | margin: EdgeInsets.only(left: 16.0, top: 12.0, right: 12.0), 86 | child: Divider( 87 | height: 1.0, 88 | color: colorDivider, 89 | ), 90 | ) 91 | ], 92 | ), 93 | ); 94 | } 95 | 96 | Widget _transformEventTitle(final Event event) { 97 | final String actor = event.actor.displayLogin; 98 | final String repo = event.repo.name; 99 | 100 | String eventType = event.type; 101 | switch (event.type) { 102 | case 'WatchEvent': 103 | eventType = 'starred'; 104 | break; 105 | case 'CreateEvent': 106 | eventType = 'created'; 107 | break; 108 | case 'ForkEvent': 109 | eventType = 'forked'; 110 | break; 111 | case 'PushEvent': 112 | eventType = 'pushed'; 113 | break; 114 | } 115 | 116 | final TapGestureRecognizer recognizerActor = TapGestureRecognizer(); 117 | recognizerActor.onTap = () { 118 | observer(EventItemAction(true, event.actor.url)); 119 | }; 120 | final TapGestureRecognizer recognizerRepo = TapGestureRecognizer(); 121 | recognizerRepo.onTap = () { 122 | observer(EventItemAction(false, event.repo.url)); 123 | }; 124 | 125 | return RichText( 126 | text: TextSpan( 127 | text: '', 128 | children: [ 129 | TextSpan( 130 | text: actor, 131 | style: TextStyle( 132 | fontWeight: FontWeight.bold, 133 | decoration: TextDecoration.underline, 134 | color: colorPrimaryText, 135 | ), 136 | recognizer: recognizerActor, 137 | ), 138 | TextSpan( 139 | text: ' ' + eventType + ' ', 140 | style: TextStyle( 141 | color: colorPrimaryText, 142 | ), 143 | ), 144 | TextSpan( 145 | text: repo, 146 | style: TextStyle( 147 | color: colorPrimaryText, 148 | fontWeight: FontWeight.bold, 149 | decoration: TextDecoration.underline, 150 | ), 151 | recognizer: recognizerRepo, 152 | ), 153 | ], 154 | ), 155 | ); 156 | } 157 | 158 | String _transformEventTime(final String createAt) { 159 | final int formatTimes = DateTime.parse(createAt).millisecondsSinceEpoch; 160 | 161 | setLocaleInfo('zh_normal', ZhInfo()); 162 | 163 | final int now = DateTime.now().millisecondsSinceEpoch; 164 | final String timeLine = TimelineUtil.format(formatTimes, 165 | locTimeMillis: now, locale: 'zh_normal', dayFormat: DayFormat.Full); 166 | 167 | return timeLine; 168 | } 169 | 170 | String _fetchEventImage(final Event event) { 171 | String asset = eventsForkChecked; 172 | switch (event.type) { 173 | case 'WatchEvent': 174 | asset = eventsStarChecked; 175 | break; 176 | default: 177 | asset = eventsForkChecked; 178 | break; 179 | } 180 | return asset; 181 | } 182 | } 183 | 184 | class EventItemAction { 185 | final bool isActorAction; 186 | final String url; 187 | 188 | EventItemAction(this.isActorAction, this.url); 189 | } 190 | 191 | typedef void EventItemActionObserver(EventItemAction action); 192 | -------------------------------------------------------------------------------- /lib/ui/main/home/main_events_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import '../main.dart'; 4 | 5 | class MainEventsEpic implements EpicClass { 6 | @override 7 | Stream call(Stream actions, EpicStore store) { 8 | return Observable.merge([ 9 | Observable(actions) 10 | .ofType(TypeToken()) 11 | .flatMap((event) => _pagingRequestStream(1, [], event.username)), 12 | Observable(actions) 13 | .ofType(TypeToken()) 14 | .flatMap((event) => _pagingRequestStream( 15 | event.currentPage + 1, 16 | event.previousList, 17 | event.username, 18 | )), 19 | ]); 20 | } 21 | 22 | /// 分页网络请求 23 | /// [newPageIndex] 新的分页索引 24 | /// [previousList] 之前的列表数据 25 | /// [username] 用户名 26 | Stream _pagingRequestStream( 27 | int newPageIndex, List previousList, String username) async* { 28 | final isFirstPage = newPageIndex == 1; 29 | if (isFirstPage) { 30 | yield MainEventsFirstLoadingAction(); 31 | } 32 | 33 | final DataResult> result = 34 | await UserEventRepository.fetchEvents(username, newPageIndex); 35 | 36 | if (result.result) { 37 | if (result.data.length > 0) { 38 | final List eventList = result.data; 39 | previousList.addAll(eventList); 40 | 41 | yield MainEventsPageLoadSuccessAction(previousList, newPageIndex); 42 | } else { 43 | if (isFirstPage) { 44 | yield MainEventPageLoadFailureAction(Errors.emptyListException()); 45 | } else { 46 | yield MainEventPageLoadFailureAction(Errors.noMoreDataException()); 47 | } 48 | } 49 | } else { 50 | if (isFirstPage) { 51 | yield MainEventPageLoadFailureAction(Exception('初始化网络请求失败')); 52 | } else { 53 | yield MainEventPageLoadFailureAction(Exception('请求更多数据失败')); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/ui/main/home/main_events_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | import 'package:flutter_rhine/common/common.dart'; 4 | import 'package:flutter_rhine/common/model/event.dart'; 5 | import 'package:flutter_rhine/common/widget/global_hide_footer.dart'; 6 | import 'package:flutter_rhine/common/widget/global_progress_bar.dart'; 7 | import 'package:flutter_rhine/repository/repository.dart'; 8 | import 'package:flutter_rhine/ui/main/home/main_events_item.dart'; 9 | import 'package:fluttertoast/fluttertoast.dart'; 10 | 11 | import 'main_events.dart'; 12 | 13 | class MainEventsPage extends StatelessWidget { 14 | final UserRepository userRepository; 15 | 16 | MainEventsPage({@required this.userRepository}) 17 | : assert(userRepository != null); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text('Home'), 24 | automaticallyImplyLeading: false, // 隐藏返回键 25 | ), 26 | body: MainEventForm(), 27 | ); 28 | } 29 | } 30 | 31 | class MainEventForm extends StatefulWidget { 32 | @override 33 | State createState() { 34 | return _MainEventsFormState(); 35 | } 36 | } 37 | 38 | class _MainEventsFormState extends State 39 | with AutomaticKeepAliveClientMixin { 40 | GlobalKey _footerKey = GlobalKey(); 41 | 42 | @override 43 | void didChangeDependencies() { 44 | super.didChangeDependencies(); 45 | final Store store = StoreProvider.of(context); 46 | final currentState = store.state; 47 | store.dispatch( 48 | MainEventsInitialAction(username: currentState.appUser.user.login)); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | super.build(context); 54 | return StoreConnector( 55 | converter: (store) => store.state.mainState.eventState, 56 | builder: (context, MainEventsState state) { 57 | if (state.isLoading) { 58 | return Center( 59 | child: ProgressBar(visibility: true), 60 | ); 61 | } 62 | 63 | final Exception error = state.error; 64 | if (error != null) { 65 | if (error is EmptyListException) { 66 | return Center( 67 | child: Text('内容为空'), 68 | ); 69 | } else if (error is NetworkRequestException) { 70 | return Center( 71 | child: Text('网络错误'), 72 | ); 73 | } 74 | } 75 | 76 | return _initExistDataList(state.events); 77 | }, 78 | ); 79 | } 80 | 81 | /// 有数据时的列表 82 | /// [renders] 事件列表 83 | Widget _initExistDataList(final List renders) { 84 | return EasyRefresh( 85 | refreshFooter: GlobalHideFooter(_footerKey), 86 | child: ListView.builder( 87 | itemCount: renders.length, 88 | itemBuilder: (context, index) { 89 | return MainEventItem( 90 | event: renders[index], 91 | observer: (EventItemAction action) { 92 | if (action.isActorAction) { 93 | // 用户名点击事件 94 | Fluttertoast.showToast( 95 | msg: '用户: ' + action.url, 96 | toastLength: Toast.LENGTH_SHORT, 97 | gravity: ToastGravity.BOTTOM, 98 | ); 99 | } else { 100 | // repo点击事件 101 | Fluttertoast.showToast( 102 | msg: '被点击的Repo: ' + action.url, 103 | toastLength: Toast.LENGTH_SHORT, 104 | gravity: ToastGravity.BOTTOM, 105 | ); 106 | } 107 | }, 108 | ); 109 | }, 110 | ), 111 | autoLoad: true, 112 | loadMore: () async { 113 | final Store store = StoreProvider.of(context); 114 | final currentState = store.state; 115 | store.dispatch(MainEventLoadNextPageAction( 116 | username: currentState.appUser.user.login, 117 | currentPage: currentState.mainState.eventState.currentPage, 118 | previousList: currentState.mainState.eventState.events, 119 | )); 120 | }, 121 | ); 122 | } 123 | 124 | @override 125 | bool get wantKeepAlive => true; 126 | } 127 | -------------------------------------------------------------------------------- /lib/ui/main/home/main_events_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'main_events.dart'; 4 | 5 | final mainEventsReducer = combineReducers([ 6 | TypedReducer( 7 | _mainEventsFirstLoadingReducer), 8 | TypedReducer( 9 | _mainEventsPageLoadSuccessAction), 10 | TypedReducer( 11 | _mainEventPageLoadFailureAction), 12 | ]); 13 | 14 | MainEventsState _mainEventsFirstLoadingReducer( 15 | MainEventsState preState, 16 | MainEventsFirstLoadingAction action, 17 | ) { 18 | return preState.copyWith( 19 | isLoading: true, 20 | error: null, 21 | ); 22 | } 23 | 24 | MainEventsState _mainEventsPageLoadSuccessAction( 25 | MainEventsState preState, 26 | MainEventsPageLoadSuccessAction action, 27 | ) { 28 | return preState.copyWith( 29 | isLoading: false, 30 | currentPage: action.currentPage, 31 | events: action.events, 32 | error: null, 33 | ); 34 | } 35 | 36 | MainEventsState _mainEventPageLoadFailureAction( 37 | MainEventsState preState, 38 | MainEventPageLoadFailureAction action, 39 | ) { 40 | return preState.copyWith( 41 | isLoading: false, 42 | error: action.exception, 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /lib/ui/main/home/main_events_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | @immutable 5 | class MainEventsState { 6 | final bool isLoading; 7 | final int currentPage; 8 | final List events; 9 | 10 | final Exception error; 11 | 12 | MainEventsState({ 13 | this.isLoading = false, 14 | this.currentPage = 0, 15 | this.events = const [], 16 | this.error, 17 | }); 18 | 19 | MainEventsState copyWith({ 20 | final bool isLoading, 21 | final int currentPage, 22 | final List events, 23 | final Exception error, 24 | }) { 25 | return MainEventsState( 26 | isLoading: isLoading ?? this.isLoading, 27 | currentPage: currentPage ?? this.currentPage, 28 | events: events ?? this.events, 29 | error: error ?? this.error, 30 | ); 31 | } 32 | 33 | @override 34 | bool operator ==(Object other) => 35 | identical(this, other) || 36 | other is MainEventsState && 37 | runtimeType == other.runtimeType && 38 | isLoading == other.isLoading && 39 | currentPage == other.currentPage && 40 | events == other.events && 41 | error == other.error; 42 | 43 | @override 44 | int get hashCode => 45 | isLoading.hashCode ^ 46 | currentPage.hashCode ^ 47 | events.hashCode ^ 48 | error.hashCode; 49 | 50 | @override 51 | String toString() { 52 | return 'MainEventsState{isLoading: $isLoading, currentPage: $currentPage, events: $events, error: $error}'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues.dart: -------------------------------------------------------------------------------- 1 | export 'main_issues_action.dart'; 2 | export 'main_issues_item.dart'; 3 | export 'main_issues_page.dart'; 4 | export 'main_issues_state.dart'; 5 | export 'main_issues_middleware.dart'; 6 | export 'main_issues_reducer.dart'; -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | abstract class MainIssuesAction {} 4 | 5 | /// 第一次加载数据 6 | class MainIssuesInitialAction extends MainIssuesAction { 7 | 8 | MainIssuesInitialAction(); 9 | } 10 | 11 | /// 加载更多 12 | class MainIssuesLoadNextPageAction extends MainIssuesAction { 13 | final int currentPage; 14 | final List previousList; 15 | 16 | MainIssuesLoadNextPageAction({ 17 | @required this.currentPage, 18 | this.previousList = const [], 19 | }) : assert(currentPage != null); 20 | } 21 | 22 | /// 加载中 23 | class MainIssuesFirstLoadingAction extends MainIssuesAction { 24 | MainIssuesFirstLoadingAction(); 25 | } 26 | 27 | /// 空列表状态 28 | class MainIssuesEmptyAction extends MainIssuesAction { 29 | MainIssuesEmptyAction(); 30 | } 31 | 32 | /// 加载分页数据成功 33 | class MainIssuesPageLoadSuccess extends MainIssuesAction { 34 | final List issues; 35 | final int currentPage; 36 | 37 | MainIssuesPageLoadSuccess({ 38 | this.issues, 39 | this.currentPage, 40 | }); 41 | } 42 | 43 | /// 加载分页数据失败 44 | class MainIssuesPageLoadFailure extends MainIssuesAction { 45 | final Exception exception; 46 | 47 | MainIssuesPageLoadFailure(this.exception); 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:common_utils/common_utils.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_rhine/common/constants/assets.dart'; 4 | import 'package:flutter_rhine/common/constants/colors.dart'; 5 | import 'package:flutter_rhine/common/model/issue.dart'; 6 | 7 | /// issue界面的Item 8 | class MainIssuesItem extends StatelessWidget { 9 | final Issue issue; 10 | 11 | MainIssuesItem({Key key, this.issue}) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | alignment: Alignment.topLeft, 17 | child: Column( 18 | children: [ 19 | Container( 20 | margin: EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), 21 | child: Flex( 22 | direction: Axis.horizontal, 23 | crossAxisAlignment: CrossAxisAlignment.center, 24 | children: [ 25 | ClipOval( 26 | child: Image( 27 | width: 40.0, 28 | height: 40.0, 29 | image: NetworkImage(issue.assignee.avatarUrl), 30 | ), 31 | ), 32 | Expanded( 33 | flex: 1, 34 | child: _itemContent(), 35 | ), 36 | Container( 37 | margin: EdgeInsets.only(left: 3.0), 38 | child: Offstage( 39 | offstage: issue.comments <= 0, // 没有评论不显示评论数量 40 | child: Row( 41 | children: [ 42 | Image( 43 | width: 16.0, 44 | height: 16.0, 45 | image: AssetImage(issue.comments > 5 // 评论大于5视为热评 46 | ? issuesCommentFireDark 47 | : issuesCommentNormalDark), 48 | ), 49 | Container( 50 | margin: EdgeInsets.only(left: 1.0), 51 | alignment: Alignment.center, 52 | child: Text( 53 | issue.comments.toString(), 54 | style: 55 | TextStyle(color: colorPrimary, fontSize: 12.0), 56 | ), 57 | ), 58 | ], 59 | ), 60 | ), 61 | ), 62 | ], 63 | ), 64 | ), 65 | // 分割线 66 | Container( 67 | margin: EdgeInsets.only(top: 16.0), 68 | child: Divider( 69 | height: 1.0, 70 | color: colorDivider, 71 | ), 72 | ) 73 | ], 74 | ), 75 | ); 76 | } 77 | 78 | Widget _itemContent() { 79 | final int labelsCount = issue.labels.length; 80 | 81 | return Container( 82 | alignment: Alignment.topLeft, 83 | margin: EdgeInsets.only(left: 16.0), 84 | child: Flex( 85 | direction: Axis.vertical, 86 | crossAxisAlignment: CrossAxisAlignment.start, 87 | mainAxisAlignment: MainAxisAlignment.start, 88 | children: [ 89 | // 标题 90 | RichText( 91 | textAlign: TextAlign.start, 92 | text: TextSpan( 93 | text: '', 94 | children: [ 95 | TextSpan( 96 | text: issue.title, 97 | style: TextStyle(color: colorPrimaryText, fontSize: 14.0), 98 | ), 99 | TextSpan( 100 | text: ' #${issue.number}', 101 | style: 102 | TextStyle(color: colorSecondaryTextGray, fontSize: 14.0), 103 | ), 104 | ], 105 | ), 106 | ), 107 | // tags 108 | Container( 109 | margin: EdgeInsets.only(top: 6.0, left: 1.0), 110 | child: Row( 111 | crossAxisAlignment: CrossAxisAlignment.center, 112 | children: [ 113 | Container( 114 | child: Image( 115 | width: 12.0, 116 | height: 12.0, 117 | image: AssetImage(issuesTime), 118 | )), 119 | Container( 120 | margin: EdgeInsets.only(left: 4.0), 121 | child: Text(_transformEventTime(issue.updatedAt), 122 | style: TextStyle( 123 | color: colorSecondaryTextGray, fontSize: 12.0)), 124 | ), 125 | Offstage( 126 | offstage: labelsCount <= 0, 127 | child: Container( 128 | margin: EdgeInsets.only(left: 8.0), 129 | child: Image( 130 | width: 11.0, 131 | height: 11.0, 132 | image: AssetImage(issuesTag), 133 | ), 134 | ), 135 | ), 136 | Offstage( 137 | offstage: labelsCount <= 0, 138 | child: Container( 139 | margin: EdgeInsets.only(left: 4.0), 140 | child: Text(labelsCount.toString(), 141 | style: TextStyle( 142 | color: colorSecondaryTextGray, fontSize: 12.0)), 143 | ), 144 | ), 145 | ], 146 | ), 147 | ) 148 | ], 149 | ), 150 | ); 151 | } 152 | 153 | String _transformEventTime(final String createAt) { 154 | final int formatTimes = DateTime.parse(createAt).millisecondsSinceEpoch; 155 | 156 | setLocaleInfo('zh_normal', ZhInfo()); 157 | 158 | final int now = DateTime.now().millisecondsSinceEpoch; 159 | final String timeLine = TimelineUtil.format(formatTimes, 160 | locTimeMillis: now, locale: 'zh_normal', dayFormat: DayFormat.Common); 161 | 162 | return timeLine; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'main_issues.dart'; 4 | 5 | class MainIssuesEpic implements EpicClass { 6 | @override 7 | Stream call(Stream actions, EpicStore store) { 8 | return Observable.merge([ 9 | Observable(actions).ofType(TypeToken()).flatMap( 10 | (action) => _pagingRequestStream(newPageIndex: 1, previousList: [])), 11 | Observable(actions) 12 | .ofType(TypeToken()) 13 | .flatMap((action) => _pagingRequestStream( 14 | newPageIndex: action.currentPage + 1, 15 | previousList: action.previousList, 16 | )), 17 | ]); 18 | } 19 | 20 | /// 分页网络请求 21 | /// [newPageIndex] 新的分页索引 22 | /// [sort] 排序规则,[IssuesRepository.SORT_CREATED]、[IssuesRepository.SORT_UPDATED]、[IssuesRepository.SORT_COMMENTS] 23 | /// [state] issue状态,[IssuesRepository.STATE_OPEN]、[IssuesRepository.STATE_CLOSED]、[IssuesRepository.STATE_ALL] 24 | /// [previousList] 之前的列表数据 25 | Stream _pagingRequestStream({ 26 | final int newPageIndex, 27 | final List previousList, 28 | final String sort = IssuesRepository.SORT_UPDATED, 29 | final String state = IssuesRepository.STATE_OPEN, 30 | }) async* { 31 | final isFirstPage = newPageIndex == 1; 32 | if (isFirstPage) { 33 | yield MainIssuesFirstLoadingAction(); 34 | } 35 | 36 | final DataResult> result = await IssuesRepository.fetchIssues( 37 | sort: sort, 38 | state: state, 39 | page: newPageIndex, 40 | ); 41 | 42 | if (result.result) { 43 | if (result.data.length > 0) { 44 | final List eventList = result.data; 45 | previousList.addAll(eventList); 46 | 47 | yield MainIssuesPageLoadSuccess( 48 | issues: previousList, 49 | currentPage: newPageIndex, 50 | ); 51 | } else { 52 | if (isFirstPage) { 53 | yield MainIssuesEmptyAction(); 54 | } else { 55 | yield MainIssuesPageLoadFailure(Errors.noMoreDataException()); 56 | } 57 | } 58 | } else { 59 | if (isFirstPage) { 60 | yield MainIssuesPageLoadFailure( 61 | Errors.networkException(message: '网络请求失败')); 62 | } else { 63 | yield MainIssuesPageLoadFailure( 64 | Errors.networkException(message: '请求更多数据失败')); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | import 'package:flutter_redux/flutter_redux.dart'; 4 | import 'package:flutter_rhine/app/app.dart'; 5 | import 'package:flutter_rhine/common/common.dart'; 6 | import 'package:flutter_rhine/common/model/issue.dart'; 7 | import 'package:flutter_rhine/common/widget/global_hide_footer.dart'; 8 | import 'package:flutter_rhine/common/widget/global_progress_bar.dart'; 9 | 10 | import 'main_issues.dart'; 11 | import 'main_issues_item.dart'; 12 | 13 | class MainIssuesPage extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Container( 17 | child: Scaffold( 18 | appBar: AppBar( 19 | title: Text('Issues'), 20 | automaticallyImplyLeading: false, 21 | ), 22 | body: MainIssuesForm(), 23 | ), 24 | ); 25 | } 26 | } 27 | 28 | class MainIssuesForm extends StatefulWidget { 29 | @override 30 | _MainIssuesFormState createState() => _MainIssuesFormState(); 31 | } 32 | 33 | class _MainIssuesFormState extends State 34 | with AutomaticKeepAliveClientMixin { 35 | final GlobalKey _footerKey = 36 | GlobalKey(); 37 | 38 | @override 39 | void didChangeDependencies() { 40 | super.didChangeDependencies(); 41 | final Store store = StoreProvider.of(context); 42 | store.dispatch(MainIssuesInitialAction()); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | super.build(context); 48 | return StoreConnector( 49 | converter: (store) => store.state.mainState.issueState, 50 | builder: (context, final MainIssuesState state) { 51 | final Exception error = state.error; 52 | if (error is EmptyListException) { 53 | return Center(child: Text('Empty page.')); 54 | } else if (error is NetworkRequestException) { 55 | return Center(child: Text('网络错误')); 56 | } else if (error is Exception) { 57 | return Center(child: Text(error.toString())); 58 | } 59 | 60 | if (state.isLoading) { 61 | return Center( 62 | child: ProgressBar(visibility: true), 63 | ); 64 | } 65 | 66 | final List issues = []; 67 | if (state.issues.length > 0) { 68 | issues.addAll(state.issues); 69 | } 70 | 71 | return Container( 72 | child: EasyRefresh( 73 | refreshFooter: GlobalHideFooter(_footerKey), 74 | child: ListView.builder( 75 | itemCount: issues.length, 76 | itemBuilder: (_, index) => 77 | MainIssuesItem(issue: issues[index])), 78 | ), 79 | ); 80 | }, 81 | ); 82 | } 83 | 84 | @override 85 | bool get wantKeepAlive => true; 86 | } 87 | -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'main_issues.dart'; 4 | 5 | final mainIssuesReducer = combineReducers([ 6 | TypedReducer( 7 | _mainIssuesFirstLoadingReducer), 8 | TypedReducer(_mainIssuesEmptyReducer), 9 | TypedReducer( 10 | _mainIssuesPageLoadSuccessReducer), 11 | TypedReducer( 12 | _mainIssuesPageLoadFailureReducer), 13 | ]); 14 | 15 | MainIssuesState _mainIssuesFirstLoadingReducer( 16 | MainIssuesState pre, 17 | MainIssuesFirstLoadingAction action, 18 | ) { 19 | return pre.copyWith(isLoading: true, error: null); 20 | } 21 | 22 | MainIssuesState _mainIssuesEmptyReducer( 23 | MainIssuesState pre, 24 | MainIssuesEmptyAction action, 25 | ) { 26 | return pre.copyWith(isLoading: false, error: Errors.emptyListException()); 27 | } 28 | 29 | MainIssuesState _mainIssuesPageLoadSuccessReducer( 30 | MainIssuesState pre, 31 | MainIssuesPageLoadSuccess action, 32 | ) { 33 | return pre.copyWith( 34 | isLoading: false, 35 | issues: action.issues, 36 | currentPage: action.currentPage, 37 | ); 38 | } 39 | 40 | MainIssuesState _mainIssuesPageLoadFailureReducer( 41 | MainIssuesState pre, 42 | MainIssuesPageLoadFailure action, 43 | ) { 44 | return pre.copyWith( 45 | isLoading: false, 46 | error: action.exception, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /lib/ui/main/issues/main_issues_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | @immutable 4 | class MainIssuesState { 5 | final bool isLoading; 6 | final int currentPage; 7 | final List issues; 8 | final String listSort; 9 | final String listState; 10 | 11 | final Exception error; 12 | 13 | MainIssuesState({ 14 | this.currentPage = 0, 15 | this.issues = const [], 16 | this.isLoading = false, 17 | this.listSort = IssuesRepository.SORT_UPDATED, 18 | this.listState = IssuesRepository.STATE_OPEN, 19 | this.error, 20 | }) : assert(currentPage != null); 21 | 22 | MainIssuesState copyWith({ 23 | final bool isLoading, 24 | final int currentPage, 25 | final List issues, 26 | final String listSort, 27 | final String listState, 28 | final Exception error, 29 | }) { 30 | return MainIssuesState( 31 | isLoading: isLoading ?? this.isLoading, 32 | currentPage: currentPage ?? this.currentPage, 33 | issues: issues ?? this.issues, 34 | listSort: listSort ?? this.listSort, 35 | listState: listState ?? this.listState, 36 | error: error ?? this.error, 37 | ); 38 | } 39 | 40 | @override 41 | bool operator ==(Object other) => 42 | identical(this, other) || 43 | other is MainIssuesState && 44 | runtimeType == other.runtimeType && 45 | isLoading == other.isLoading && 46 | currentPage == other.currentPage && 47 | issues == other.issues && 48 | listSort == other.listSort && 49 | listState == other.listState && 50 | error == other.error; 51 | 52 | @override 53 | int get hashCode => 54 | isLoading.hashCode ^ 55 | currentPage.hashCode ^ 56 | issues.hashCode ^ 57 | listSort.hashCode ^ 58 | listState.hashCode ^ 59 | error.hashCode; 60 | 61 | @override 62 | String toString() { 63 | return 'MainIssuesState{isLoading: $isLoading, currentPage: $currentPage, issues: $issues, listSort: $listSort, listState: $listState, error: $error}'; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/ui/main/main.dart: -------------------------------------------------------------------------------- 1 | export 'home/main_events.dart'; 2 | export 'issues/main_issues.dart'; 3 | export 'main_action.dart'; 4 | export 'main_page.dart'; 5 | export 'main_reducer.dart'; 6 | export 'main_state.dart'; 7 | export 'mine/main_profile.dart'; 8 | export 'repos/main_repo.dart'; -------------------------------------------------------------------------------- /lib/ui/main/main_action.dart: -------------------------------------------------------------------------------- 1 | import 'mine/main_profile.dart'; 2 | 3 | abstract class MainPageAction {} 4 | 5 | class MainSwipeViewPagerAction extends MainPageAction { 6 | final int currentItem; 7 | 8 | MainSwipeViewPagerAction(this.currentItem); 9 | } 10 | 11 | class MainProfileUpdateAction extends MainPageAction { 12 | final MainProfileState state; 13 | 14 | MainProfileUpdateAction(this.state); 15 | } 16 | -------------------------------------------------------------------------------- /lib/ui/main/main_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:android_intent/android_intent.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_rhine/common/common.dart'; 6 | import 'package:flutter_rhine/common/constants/assets.dart'; 7 | import 'package:flutter_rhine/common/constants/colors.dart'; 8 | import 'package:flutter_rhine/repository/repository.dart'; 9 | import 'package:flutter_rhine/ui/main/home/main_events_page.dart'; 10 | import 'package:flutter_rhine/ui/main/issues/main_issues_page.dart'; 11 | import 'package:flutter_rhine/ui/main/mine/main_profile_page.dart'; 12 | import 'package:flutter_rhine/ui/main/repos/main_repo_page.dart'; 13 | 14 | import 'main.dart'; 15 | 16 | class MainPage extends StatefulWidget { 17 | final UserRepository userRepository; 18 | 19 | MainPage({Key key, @required this.userRepository}) : super(key: key); 20 | 21 | @override 22 | State createState() { 23 | return _MainPageState(userRepository: userRepository); 24 | } 25 | } 26 | 27 | class _MainPageState extends State 28 | with SingleTickerProviderStateMixin { 29 | final UserRepository userRepository; 30 | 31 | final _pageController = PageController(); 32 | 33 | _MainPageState({@required this.userRepository}); 34 | 35 | final List> _bottomTabIcons = [ 36 | [ 37 | Image.asset(mainNavEventsNormal, width: 24.0, height: 24.0), 38 | Image.asset(mainNavEventsPressed, width: 24.0, height: 24.0), 39 | ], 40 | [ 41 | Image.asset(mainNavReposNormal, width: 24.0, height: 24.0), 42 | Image.asset(mainNavReposPressed, width: 24.0, height: 24.0), 43 | ], 44 | [ 45 | Image.asset(mainNavIssueNormal, width: 24.0, height: 24.0), 46 | Image.asset(mainNavIssuePressed, width: 24.0, height: 24.0), 47 | ], 48 | [ 49 | Image.asset(mainNavProfileNormal, width: 24.0, height: 24.0), 50 | Image.asset(mainNavProfilePressed, width: 24.0, height: 24.0), 51 | ] 52 | ]; 53 | 54 | final List _bottomTabTitles = [ 55 | 'home', 56 | 'repos', 57 | 'issues', 58 | 'me', 59 | ]; 60 | 61 | @override 62 | void didChangeDependencies() { 63 | super.didChangeDependencies(); 64 | } 65 | 66 | @override 67 | void dispose() { 68 | super.dispose(); 69 | _pageController.dispose(); 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return WillPopScope( 75 | onWillPop: () { 76 | return _dialogExitApp(context); 77 | }, 78 | child: StoreConnector( 79 | converter: (store) { 80 | final MainPageState state = store.state.mainState; 81 | if (_pageController.hasClients) { 82 | final int currentPosition = _pageController.page.toInt(); 83 | final int newPosition = state.currentPageIndex; 84 | if (currentPosition != newPosition) 85 | _pageController.jumpToPage(newPosition); 86 | } 87 | return state; 88 | }, 89 | builder: (BuildContext context, MainPageState state) => Scaffold( 90 | body: PageView( 91 | children: [ 92 | MainEventsPage(userRepository: userRepository), 93 | MainReposPage(), 94 | MainIssuesPage(), 95 | MainProfilePage() 96 | ], 97 | controller: _pageController, 98 | onPageChanged: _notifyViewPagerChanged, 99 | ), 100 | bottomNavigationBar: BottomNavigationBar( 101 | backgroundColor: colorPrimary, 102 | items: [ 103 | _bottomNavigationBarItem( 104 | state, MainPageState.TAB_INDEX_EVENTS), 105 | _bottomNavigationBarItem( 106 | state, MainPageState.TAB_INDEX_REPOS), 107 | _bottomNavigationBarItem( 108 | state, MainPageState.TAB_INDEX_ISSUES), 109 | _bottomNavigationBarItem( 110 | state, MainPageState.TAB_INDEX_PROFILE), 111 | ], 112 | currentIndex: state.currentPageIndex, 113 | iconSize: 24.0, 114 | type: BottomNavigationBarType.fixed, 115 | onTap: _notifyViewPagerChanged, 116 | ), 117 | ), 118 | ), 119 | ); 120 | } 121 | 122 | /// 不退出 123 | Future _dialogExitApp(BuildContext context) async { 124 | ///如果是 android 回到桌面 125 | if (Platform.isAndroid) { 126 | AndroidIntent intent = AndroidIntent( 127 | action: 'android.intent.action.MAIN', 128 | category: "android.intent.category.HOME", 129 | ); 130 | await intent.launch(); 131 | } 132 | 133 | return Future.value(false); 134 | } 135 | 136 | BottomNavigationBarItem _bottomNavigationBarItem( 137 | MainPageState state, int tabIndex) { 138 | return BottomNavigationBarItem( 139 | icon: _getTabIcon(state.currentPageIndex, tabIndex), 140 | title: _getTabTitle(state.currentPageIndex, tabIndex), 141 | ); 142 | } 143 | 144 | Image _getTabIcon(int currentPageIndex, int tabIndex) => 145 | (currentPageIndex == tabIndex) 146 | ? _bottomTabIcons[tabIndex][1] 147 | : _bottomTabIcons[tabIndex][0]; 148 | 149 | Text _getTabTitle(int currentPageIndex, int tabIndex) { 150 | final String title = _bottomTabTitles[tabIndex]; 151 | final Color textColor = 152 | (currentPageIndex == tabIndex) ? Colors.white : colorSecondaryTextGray; 153 | return Text(title, style: TextStyle(fontSize: 14.0, color: textColor)); 154 | } 155 | 156 | void _notifyViewPagerChanged(int newIndex) { 157 | StoreProvider.of(context) 158 | .dispatch(MainSwipeViewPagerAction(newIndex)); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/ui/main/main_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:flutter_rhine/ui/main/home/main_events_reducer.dart'; 3 | 4 | import 'main.dart'; 5 | 6 | final mainPageReducer = combineReducers([ 7 | TypedReducer( 8 | _mainSwipeViewPagerReducer), 9 | TypedReducer(_mainEventsReducer), 10 | TypedReducer(_mainRepoReducer), 11 | TypedReducer(_mainProfileReducer), 12 | TypedReducer(_mainIssuesReducer), 13 | ]); 14 | 15 | MainPageState _mainSwipeViewPagerReducer( 16 | MainPageState preState, 17 | MainSwipeViewPagerAction action, 18 | ) { 19 | return preState.currentPageIndex == action.currentItem 20 | ? preState 21 | : preState.copyWith(currentPageIndex: action.currentItem); 22 | } 23 | 24 | MainPageState _mainProfileReducer( 25 | MainPageState preState, 26 | MainProfileAction action, 27 | ) { 28 | final preProfileState = preState.profileState; 29 | final newProfileState = mainProfileReducer(preProfileState, action); 30 | return preState.copyWith(profileState: newProfileState); 31 | } 32 | 33 | MainPageState _mainEventsReducer( 34 | MainPageState preState, 35 | MainEventsAction action, 36 | ) { 37 | final preProfileState = preState.eventState; 38 | final newProfileState = mainEventsReducer(preProfileState, action); 39 | return preState.copyWith(eventState: newProfileState); 40 | } 41 | 42 | MainPageState _mainIssuesReducer( 43 | MainPageState preState, 44 | MainIssuesAction action, 45 | ) { 46 | final before = preState.issueState; 47 | final next = mainIssuesReducer(before, action); 48 | return preState.copyWith(issueState: next); 49 | } 50 | 51 | MainPageState _mainRepoReducer( 52 | MainPageState preState, 53 | MainReposAction action, 54 | ) { 55 | final before = preState.repoState; 56 | final next = mainRepoReducer(before, action); 57 | return preState.copyWith(repoState: next); 58 | } 59 | -------------------------------------------------------------------------------- /lib/ui/main/main_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'main.dart'; 4 | 5 | @immutable 6 | class MainPageState { 7 | static const int TAB_INDEX_EVENTS = 0; 8 | static const int TAB_INDEX_REPOS = 1; 9 | static const int TAB_INDEX_ISSUES = 2; 10 | static const int TAB_INDEX_PROFILE = 3; 11 | 12 | final int currentPageIndex; 13 | final MainEventsState eventState; 14 | final MainReposState repoState; 15 | final MainIssuesState issueState; 16 | final MainProfileState profileState; 17 | 18 | MainPageState({ 19 | this.currentPageIndex, 20 | this.eventState, 21 | this.repoState, 22 | this.issueState, 23 | this.profileState, 24 | }); 25 | 26 | factory MainPageState.initial() { 27 | return MainPageState( 28 | currentPageIndex: 0, 29 | eventState: MainEventsState(), 30 | repoState: MainReposState(), 31 | issueState: MainIssuesState(), 32 | profileState: MainProfileState(), 33 | ); 34 | } 35 | 36 | MainPageState copyWith({ 37 | final int currentPageIndex, 38 | final MainEventsState eventState, 39 | final MainReposState repoState, 40 | final MainIssuesState issueState, 41 | final MainProfileState profileState, 42 | }) { 43 | return MainPageState( 44 | currentPageIndex: currentPageIndex ?? this.currentPageIndex, 45 | eventState: eventState ?? this.eventState, 46 | repoState: repoState ?? this.repoState, 47 | issueState: issueState ?? this.issueState, 48 | profileState: profileState ?? this.profileState, 49 | ); 50 | } 51 | 52 | @override 53 | bool operator ==(Object other) => 54 | identical(this, other) || 55 | other is MainPageState && 56 | runtimeType == other.runtimeType && 57 | currentPageIndex == other.currentPageIndex && 58 | eventState == other.eventState && 59 | repoState == other.repoState && 60 | issueState == other.issueState && 61 | profileState == other.profileState; 62 | 63 | @override 64 | int get hashCode => 65 | currentPageIndex.hashCode ^ 66 | eventState.hashCode ^ 67 | repoState.hashCode ^ 68 | issueState.hashCode ^ 69 | profileState.hashCode; 70 | 71 | @override 72 | String toString() { 73 | return 'MainPageState{currentPageIndex: $currentPageIndex, eventState: $eventState, repoState: $repoState, issueState: $issueState, profileState: $profileState}'; 74 | } 75 | } 76 | 77 | abstract class MainPageStates { 78 | final int currentPageIndex; 79 | 80 | MainPageStates(this.currentPageIndex); 81 | 82 | static MainNormalState initial() { 83 | return MainNormalState(MainPageState.TAB_INDEX_EVENTS); 84 | } 85 | } 86 | 87 | class MainNormalState extends MainPageStates { 88 | MainNormalState(int newIndex) : super(newIndex); 89 | } 90 | -------------------------------------------------------------------------------- /lib/ui/main/mine/main_profile.dart: -------------------------------------------------------------------------------- 1 | export 'main_profile_reducer.dart'; 2 | export 'main_profile_action.dart'; 3 | export 'main_profile_page.dart'; 4 | export 'main_profile_state.dart'; 5 | -------------------------------------------------------------------------------- /lib/ui/main/mine/main_profile_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | abstract class MainProfileAction {} 4 | 5 | class MainProfileInitialAction extends MainProfileAction { 6 | final User user; 7 | 8 | MainProfileInitialAction(this.user) : assert(user != null); 9 | } 10 | -------------------------------------------------------------------------------- /lib/ui/main/mine/main_profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_rhine/common/common.dart'; 3 | import 'package:flutter_rhine/common/constants/assets.dart'; 4 | import 'package:flutter_rhine/common/constants/colors.dart'; 5 | 6 | import 'main_profile.dart'; 7 | 8 | class MainProfilePage extends StatelessWidget { 9 | MainProfilePage(); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return MainProfileForm(); 14 | } 15 | } 16 | 17 | class MainProfileForm extends StatefulWidget { 18 | MainProfileForm(); 19 | 20 | @override 21 | State createState() => _MainProfileFormState(); 22 | } 23 | 24 | class _MainProfileFormState extends State 25 | with AutomaticKeepAliveClientMixin { 26 | _MainProfileFormState(); 27 | 28 | @override 29 | void didChangeDependencies() { 30 | super.didChangeDependencies(); 31 | final store = StoreProvider.of(context); 32 | final currentUser = store.state.appUser.user; 33 | store.dispatch(MainProfileInitialAction(currentUser)); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | super.build(context); 39 | return StoreConnector( 40 | converter: (Store store) => store.state.appUser.user, 41 | builder: (context, final User user) => Container( 42 | width: double.infinity, 43 | height: double.infinity, 44 | color: colorPrimary, 45 | child: Stack( 46 | alignment: Alignment.center, 47 | textDirection: TextDirection.ltr, 48 | fit: StackFit.loose, 49 | children: [ 50 | MainProfileUserInfoLayer(user), 51 | ], 52 | ), 53 | ), 54 | ); 55 | } 56 | 57 | @override 58 | bool get wantKeepAlive => true; 59 | } 60 | 61 | /// 用户信息层 62 | class MainProfileUserInfoLayer extends StatelessWidget { 63 | final User user; 64 | 65 | MainProfileUserInfoLayer(this.user); 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Column( 70 | mainAxisAlignment: MainAxisAlignment.center, 71 | mainAxisSize: MainAxisSize.min, 72 | verticalDirection: VerticalDirection.down, 73 | crossAxisAlignment: CrossAxisAlignment.center, 74 | children: [ 75 | ClipOval( 76 | child: Image( 77 | width: 70.0, 78 | height: 70.0, 79 | image: NetworkImage(user?.avatarUrl ?? ''), 80 | ), 81 | ), 82 | Container( 83 | margin: EdgeInsets.only(top: 30.0), 84 | padding: EdgeInsets.only(left: 20.0, right: 20.0), 85 | child: Text( 86 | user?.login ?? "unknown", 87 | style: TextStyle( 88 | fontSize: 16.0, 89 | color: Colors.white, 90 | fontWeight: FontWeight.bold, 91 | ), 92 | ), 93 | ), 94 | Container( 95 | margin: EdgeInsets.only(top: 20.0), 96 | padding: EdgeInsets.only(left: 20.0, right: 20.0), 97 | child: Text( 98 | user?.bio ?? "no description.", 99 | style: TextStyle( 100 | fontSize: 14.0, 101 | color: colorSecondaryTextGray, 102 | ), 103 | ), 104 | ), 105 | Container( 106 | margin: EdgeInsets.only(top: 20.0), 107 | padding: EdgeInsets.only(left: 20.0, right: 20.0), 108 | child: Row( 109 | mainAxisSize: MainAxisSize.min, 110 | mainAxisAlignment: MainAxisAlignment.center, 111 | crossAxisAlignment: CrossAxisAlignment.center, 112 | children: [ 113 | Image( 114 | width: 15.0, 115 | height: 15.0, 116 | image: AssetImage(mineLocationIcon), 117 | ), 118 | Text( 119 | user?.location ?? 'no address.', 120 | style: TextStyle( 121 | fontSize: 14.0, 122 | color: colorSecondaryTextGray, 123 | ), 124 | ) 125 | ], 126 | ), 127 | ), 128 | ], 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /lib/ui/main/mine/main_profile_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:flutter_rhine/ui/main/main.dart'; 3 | 4 | final mainProfileReducer = combineReducers([ 5 | TypedReducer(_initialReducer), 6 | ]); 7 | 8 | MainProfileState _initialReducer( 9 | MainProfileState preState, MainProfileInitialAction action) { 10 | return preState.copyWith(user: action.user); 11 | } 12 | -------------------------------------------------------------------------------- /lib/ui/main/mine/main_profile_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | @immutable 4 | class MainProfileState { 5 | final User user; 6 | 7 | MainProfileState({ 8 | this.user, 9 | }); 10 | 11 | @override 12 | String toString() { 13 | return 'MainProfileState{user: $user}'; 14 | } 15 | 16 | MainProfileState copyWith({ 17 | final User user, 18 | }) { 19 | return MainProfileState( 20 | user: user ?? this.user, 21 | ); 22 | } 23 | 24 | @override 25 | bool operator ==(Object other) => 26 | identical(this, other) || 27 | other is MainProfileState && 28 | runtimeType == other.runtimeType && 29 | user == other.user; 30 | 31 | @override 32 | int get hashCode => user.hashCode; 33 | } 34 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo.dart: -------------------------------------------------------------------------------- 1 | export 'main_repo_action.dart'; 2 | export 'main_repo_item.dart'; 3 | export 'main_repo_middleware.dart'; 4 | export 'main_repo_page.dart'; 5 | export 'main_repo_reducer.dart'; 6 | export 'main_repo_state.dart'; 7 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | abstract class MainReposAction {} 5 | 6 | /// 第一次加载数据,默认按照更新时间排序 7 | class MainReposInitialAction extends MainReposAction { 8 | final String username; 9 | final String sortType = UserRepoRepository.SORT_UPDATED; 10 | 11 | MainReposInitialAction({ 12 | @required this.username, 13 | }) : assert(username != null); 14 | } 15 | 16 | /// sort排序规则修改 17 | class MainReposChangeFilterAction extends MainReposAction { 18 | final String username; 19 | final String sortType; 20 | 21 | MainReposChangeFilterAction({ 22 | @required this.username, 23 | @required this.sortType, 24 | }) : assert(sortType != null && username != null); 25 | } 26 | 27 | /// 加载更多 28 | class MainReposLoadNextPageAction extends MainReposAction { 29 | final String username; 30 | final List preList; 31 | final String sortType; 32 | final int currentPageIndex; 33 | 34 | MainReposLoadNextPageAction({ 35 | @required this.username, 36 | @required this.currentPageIndex, 37 | @required this.preList, 38 | @required this.sortType, 39 | }) : assert(username != null); 40 | } 41 | 42 | /// 加载中 43 | class MainReposLoadingAction extends MainReposAction {} 44 | 45 | /// 空列表状态 46 | class MainReposEmptyAction extends MainReposAction {} 47 | 48 | /// 加载分页数据成功 49 | class MainReposPageLoadSuccessAction extends MainReposAction { 50 | final List repos; 51 | final int currentPage; 52 | final String sortType; 53 | 54 | MainReposPageLoadSuccessAction({ 55 | this.repos, 56 | this.currentPage, 57 | this.sortType, 58 | }); 59 | } 60 | 61 | /// 加载分页数据失败 62 | class MainReposPageLoadFailureAction extends MainReposAction { 63 | final Exception exception; 64 | 65 | MainReposPageLoadFailureAction(this.exception); 66 | } 67 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_rhine/common/constants/assets.dart'; 3 | import 'package:flutter_rhine/common/constants/colors.dart'; 4 | import 'package:flutter_rhine/common/model/repo.dart'; 5 | 6 | class MainRepoPagedItem extends StatelessWidget { 7 | final Repo repo; 8 | 9 | final MainRepoActionObserver observer; 10 | 11 | MainRepoPagedItem({Key key, @required this.repo, this.observer}) 12 | : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return GestureDetector( 17 | child: Container( 18 | padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), 19 | child: Column( 20 | mainAxisSize: MainAxisSize.min, 21 | children: [ 22 | _itemTopLayout(), 23 | _itemRepoName(), 24 | _itemRepoDesc(), 25 | _itemRepoOthers(), 26 | Container( 27 | margin: EdgeInsets.only(top: 12.0), 28 | child: Divider( 29 | height: 1.0, 30 | color: colorDivider, 31 | ), 32 | ) 33 | ], 34 | ), 35 | ), 36 | onTap: () { 37 | observer(MainRepoItemsAction(repo.url)); 38 | }, 39 | ); 40 | } 41 | 42 | Widget _itemTopLayout() { 43 | return Flex( 44 | direction: Axis.horizontal, 45 | mainAxisSize: MainAxisSize.max, 46 | crossAxisAlignment: CrossAxisAlignment.center, 47 | children: [ 48 | // 头像 49 | Container( 50 | child: ClipOval( 51 | child: Image( 52 | width: 16.0, 53 | height: 16.0, 54 | image: NetworkImage(repo.owner.avatarUrl), 55 | ), 56 | ), 57 | ), 58 | // 仓库名 59 | Expanded( 60 | flex: 1, 61 | child: Container( 62 | alignment: Alignment.centerLeft, 63 | margin: EdgeInsets.only(left: 8.0), 64 | child: Text( 65 | repo.owner.login, 66 | maxLines: 1, 67 | style: TextStyle(fontSize: 12.0, color: colorPrimaryText), 68 | ), 69 | ), 70 | ), 71 | // 编程语言颜色 72 | ClipOval( 73 | child: Container( 74 | width: 7.0, 75 | height: 7.0, 76 | color: fetchLanguageColor(repo.language), 77 | ), 78 | ), 79 | // 编程语言 80 | Container( 81 | margin: EdgeInsets.only(left: 4.0), 82 | child: Text( 83 | repo.language ?? '', 84 | style: TextStyle(color: colorSecondaryTextGray, fontSize: 12.0), 85 | ), 86 | ), 87 | ], 88 | ); 89 | } 90 | 91 | Widget _itemRepoName() { 92 | return Container( 93 | alignment: Alignment.centerLeft, 94 | margin: EdgeInsets.only(top: 6.0), 95 | child: Text( 96 | repo.name, 97 | maxLines: 1, 98 | style: TextStyle( 99 | color: colorPrimaryText, 100 | fontWeight: FontWeight.bold, 101 | fontSize: 14.0, 102 | ), 103 | ), 104 | ); 105 | } 106 | 107 | Widget _itemRepoDesc() { 108 | return Container( 109 | alignment: Alignment.centerLeft, 110 | margin: EdgeInsets.only(top: 6.0), 111 | child: Text( 112 | repo.description ?? '(No description, website, or topics provided.)', 113 | maxLines: 2, 114 | style: TextStyle( 115 | color: colorSecondaryTextGray, 116 | fontSize: 12.0, 117 | ), 118 | ), 119 | ); 120 | } 121 | 122 | Widget _itemRepoOthers() { 123 | return Container( 124 | alignment: Alignment.centerLeft, 125 | margin: EdgeInsets.only(top: 6.0), 126 | child: Row( 127 | mainAxisAlignment: MainAxisAlignment.start, 128 | mainAxisSize: MainAxisSize.max, 129 | crossAxisAlignment: CrossAxisAlignment.center, 130 | children: [ 131 | // star 132 | Image( 133 | image: AssetImage(reposStar), 134 | width: 13.0, 135 | height: 13.0, 136 | ), 137 | Container( 138 | alignment: Alignment.center, 139 | margin: EdgeInsets.only(left: 2.0, top: 2.0), 140 | child: Text( 141 | repo.stargazersCount.toString(), 142 | style: TextStyle( 143 | fontSize: 12.0, 144 | color: colorSecondaryTextGray, 145 | ), 146 | ), 147 | ), 148 | // fork 149 | Container( 150 | alignment: Alignment.center, 151 | margin: EdgeInsets.only(left: 10.0), 152 | padding: EdgeInsets.only(top: 2.0), 153 | child: Image( 154 | image: AssetImage(reposFork), 155 | width: 13.0, 156 | height: 15.0, 157 | ), 158 | ), 159 | Container( 160 | alignment: Alignment.center, 161 | margin: EdgeInsets.only(top: 2.0), 162 | child: Text( 163 | repo.forksCount.toString(), 164 | style: TextStyle( 165 | fontSize: 12.0, 166 | color: colorSecondaryTextGray, 167 | ), 168 | ), 169 | ), 170 | // issue 171 | Container( 172 | alignment: Alignment.center, 173 | margin: EdgeInsets.only(top: 2.0, left: 10.0), 174 | child: Image( 175 | image: AssetImage(reposIssue), 176 | width: 13.0, 177 | height: 13.0, 178 | ), 179 | ), 180 | Container( 181 | alignment: Alignment.center, 182 | margin: EdgeInsets.only(left: 2.0, top: 2.0), 183 | child: Text( 184 | repo.openIssuesCount.toString(), 185 | style: TextStyle( 186 | fontSize: 12.0, 187 | color: colorSecondaryTextGray, 188 | ), 189 | ), 190 | ), 191 | ], 192 | ), 193 | ); 194 | } 195 | 196 | Color fetchLanguageColor(final String language) { 197 | if (language == null) return Colors.transparent; 198 | Color color; 199 | switch (language) { 200 | case 'Kotlin': 201 | color = Color(0xFFF18E33); 202 | break; 203 | case 'Java': 204 | color = Color(0xFFB07219); 205 | break; 206 | case 'JavaScript': 207 | color = Color(0xFFF1E05A); 208 | break; 209 | case 'Python': 210 | color = Color(0xFF3572A5); 211 | break; 212 | case 'HTML': 213 | color = Color(0xFFE34C26); 214 | break; 215 | case 'CSS': 216 | color = Color(0xFF563D7C); 217 | break; 218 | case 'C++': 219 | color = Color(0xFFF34B7D); 220 | break; 221 | case 'C': 222 | color = Color(0xFF555555); 223 | break; 224 | case 'ruby': 225 | color = Color(0xFF701516); 226 | break; 227 | case 'Dart': 228 | color = Color(0xFF4FB1AA); 229 | break; 230 | default: 231 | color = Color(0xFF455a64); 232 | } 233 | 234 | return color; 235 | } 236 | } 237 | 238 | class MainRepoItemsAction { 239 | final String repoUrl; 240 | 241 | MainRepoItemsAction(this.repoUrl); 242 | } 243 | 244 | typedef MainRepoActionObserver(MainRepoItemsAction nextAction); 245 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo_middleware.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'main_repo.dart'; 4 | 5 | class MainRepoEpic implements EpicClass { 6 | @override 7 | Stream call(Stream actions, EpicStore store) { 8 | return Observable.merge([ 9 | Observable(actions).ofType(TypeToken()).flatMap( 10 | (action) => 11 | _pagingRequestStream(1, [], action.username, action.sortType)), 12 | Observable(actions) 13 | .ofType(TypeToken()) 14 | .flatMap((action) => 15 | _pagingRequestStream(1, [], action.username, action.sortType)), 16 | Observable(actions) 17 | .ofType(TypeToken()) 18 | .flatMap((action) => _pagingRequestStream(action.currentPageIndex, 19 | action.preList, action.username, action.sortType)), 20 | ]); 21 | } 22 | 23 | /// 分页网络请求 24 | /// [newPageIndex] 新的分页索引 25 | /// [previousList] 之前的列表数据 26 | /// [username] 用户名 27 | /// [sort] 排序规则,应使用[UserRepoRepository.SORT_UPDATED]、[UserRepoRepository.SORT_CREATED]、[UserRepoRepository.SORT_LETTER] 28 | Stream _pagingRequestStream( 29 | int newPageIndex, 30 | List previousList, 31 | String username, 32 | String sort, 33 | ) async* { 34 | final isFirstPage = newPageIndex == 1; 35 | if (isFirstPage) { 36 | yield MainReposLoadingAction(); 37 | } 38 | 39 | final DataResult> result = await UserRepoRepository.fetchRepos( 40 | username: username, 41 | sort: sort, 42 | pageIndex: newPageIndex, 43 | ); 44 | 45 | if (result.result) { 46 | if (result.data.length > 0) { 47 | final List eventList = result.data; 48 | previousList.addAll(eventList); 49 | yield MainReposPageLoadSuccessAction( 50 | repos: previousList, 51 | currentPage: newPageIndex, 52 | sortType: sort, 53 | ); 54 | } else { 55 | if (isFirstPage) { 56 | yield MainReposEmptyAction(); 57 | } else { 58 | yield MainReposPageLoadFailureAction(Errors.noMoreDataException()); 59 | } 60 | } 61 | } else { 62 | if (isFirstPage) { 63 | yield MainReposPageLoadFailureAction(Errors.networkException()); 64 | } else { 65 | yield MainReposPageLoadFailureAction(Exception('请求更多数据失败')); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | import 'package:flutter_rhine/common/common.dart'; 4 | import 'package:flutter_rhine/common/widget/global_hide_footer.dart'; 5 | import 'package:flutter_rhine/common/widget/global_progress_bar.dart'; 6 | import 'package:flutter_rhine/repository/repository.dart'; 7 | import 'package:flutter_rhine/ui/main/repos/main_repo_item.dart'; 8 | import 'package:fluttertoast/fluttertoast.dart'; 9 | 10 | import 'main_repo.dart'; 11 | 12 | class MainReposPage extends StatefulWidget { 13 | @override 14 | State createState() { 15 | return _MainReposPageState(); 16 | } 17 | } 18 | 19 | class _MainReposPageState extends State 20 | with AutomaticKeepAliveClientMixin { 21 | final GlobalKey _footerKey = 22 | GlobalKey(); 23 | 24 | @override 25 | void didChangeDependencies() { 26 | super.didChangeDependencies(); 27 | final Store store = StoreProvider.of(context); 28 | store.dispatch( 29 | MainReposInitialAction(username: store.state.appUser.user.login)); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | super.build(context); 35 | return StoreConnector( 36 | converter: (store) => store.state, 37 | builder: (context, appState) => Scaffold( 38 | appBar: AppBar( 39 | title: Text('Repos'), 40 | automaticallyImplyLeading: false, 41 | actions: [ 42 | PopupMenuButton( 43 | onSelected: (newValue) { 44 | // 更新排序条件,刷新ui 45 | final Store store = 46 | StoreProvider.of(context); 47 | store.dispatch(MainReposChangeFilterAction( 48 | username: store.state.appUser.user.login, 49 | sortType: newValue, 50 | )); 51 | }, 52 | itemBuilder: (context) => >[ 53 | PopupMenuItem( 54 | value: UserRepoRepository.SORT_UPDATED, 55 | child: Text('Sort by Update'), 56 | ), 57 | PopupMenuItem( 58 | value: UserRepoRepository.SORT_CREATED, 59 | child: Text('Sort by Created'), 60 | ), 61 | PopupMenuItem( 62 | value: UserRepoRepository.SORT_LETTER, 63 | child: Text('Sort by FullName'), 64 | ), 65 | ], 66 | ), 67 | ], 68 | ), 69 | body: _repoList(), 70 | ), 71 | ); 72 | } 73 | 74 | Widget _repoList() { 75 | return StoreConnector( 76 | converter: (store) => store.state.mainState.repoState, 77 | builder: (context, MainReposState state) { 78 | final error = state.error; 79 | if (error is NetworkRequestException && state.currentPage <= 1) { 80 | return Center( 81 | child: Text('网络错误'), 82 | ); 83 | } else if (error is EmptyListException) { 84 | return Center( 85 | child: Text('Empty page.'), 86 | ); 87 | } 88 | 89 | if (state.isLoading) { 90 | return Center( 91 | child: ProgressBar(visibility: true), 92 | ); 93 | } 94 | 95 | return _initExistDataList(state.repos); 96 | }, 97 | ); 98 | } 99 | 100 | /// 有数据时的列表 101 | /// [renders] 事件列表 102 | Widget _initExistDataList(final List renders) { 103 | final store = StoreProvider.of(context); 104 | final username = store.state.appUser.user.login; 105 | final preState = store.state.mainState.repoState; 106 | final preList = preState.repos; 107 | final prePageIndex = preState.currentPage; 108 | final preSort = preState.sortType; 109 | 110 | return EasyRefresh( 111 | refreshFooter: GlobalHideFooter(_footerKey), 112 | child: ListView.builder( 113 | itemCount: renders.length, 114 | itemBuilder: (context, index) { 115 | return MainRepoPagedItem( 116 | repo: renders[index], 117 | observer: (MainRepoItemsAction action) { 118 | // repo的点击事件 119 | Fluttertoast.showToast( 120 | msg: '被点击的Repo: ${action.repoUrl}', 121 | toastLength: Toast.LENGTH_SHORT, 122 | gravity: ToastGravity.BOTTOM); 123 | }, 124 | ); 125 | }, 126 | ), 127 | autoLoad: true, 128 | loadMore: () async { 129 | store.dispatch(MainReposLoadNextPageAction( 130 | username: username, 131 | currentPageIndex: prePageIndex, 132 | preList: preList, 133 | sortType: preSort)); 134 | }, 135 | ); 136 | } 137 | 138 | @override 139 | bool get wantKeepAlive => true; 140 | } 141 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo_reducer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | 3 | import 'main_repo.dart'; 4 | 5 | final mainRepoReducer = combineReducers([ 6 | TypedReducer( 7 | _mainReposLoadingReducer), 8 | TypedReducer(_mainReposEmptyReducer), 9 | TypedReducer( 10 | _mainReposPageLoadSuccessReducer), 11 | TypedReducer( 12 | _mainReposPageLoadFailureReducer), 13 | ]); 14 | 15 | MainReposState _mainReposLoadingReducer( 16 | final MainReposState pre, 17 | final MainReposLoadingAction action, 18 | ) { 19 | return pre.copyWith( 20 | isLoading: true, 21 | error: null, 22 | ); 23 | } 24 | 25 | MainReposState _mainReposEmptyReducer( 26 | final MainReposState pre, 27 | final MainReposEmptyAction action, 28 | ) { 29 | return pre.copyWith( 30 | isLoading: false, 31 | repos: [], 32 | error: Errors.emptyListException(), 33 | ); 34 | } 35 | 36 | MainReposState _mainReposPageLoadSuccessReducer( 37 | final MainReposState pre, 38 | final MainReposPageLoadSuccessAction action, 39 | ) { 40 | return pre.copyWith( 41 | isLoading: false, 42 | repos: action.repos, 43 | sortType: action.sortType, 44 | currentPage: action.currentPage, 45 | error: null, 46 | ); 47 | } 48 | 49 | MainReposState _mainReposPageLoadFailureReducer( 50 | final MainReposState pre, 51 | final MainReposPageLoadFailureAction action, 52 | ) { 53 | return pre.copyWith( 54 | isLoading: false, 55 | error: action.exception, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /lib/ui/main/repos/main_repo_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_rhine/common/common.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | @immutable 5 | class MainReposState { 6 | final bool isLoading; 7 | final int currentPage; 8 | final List repos; 9 | final String sortType; 10 | 11 | final Exception error; 12 | 13 | MainReposState({ 14 | this.currentPage = 0, 15 | this.repos = const [], 16 | this.isLoading = false, 17 | this.sortType = UserRepoRepository.SORT_UPDATED, 18 | this.error, 19 | }) : assert(currentPage != null); 20 | 21 | MainReposState copyWith({ 22 | final bool isLoading, 23 | final int currentPage, 24 | final List repos, 25 | final String sortType, 26 | final Exception error, 27 | }) { 28 | return MainReposState( 29 | isLoading: isLoading ?? this.isLoading, 30 | currentPage: currentPage ?? this.currentPage, 31 | repos: repos ?? this.repos, 32 | sortType: sortType ?? this.sortType, 33 | error: error ?? this.error, 34 | ); 35 | } 36 | 37 | @override 38 | String toString() { 39 | return 'MainReposState{isLoading: $isLoading, currentPage: $currentPage, repos: $repos, sortType: $sortType, error: $error}'; 40 | } 41 | 42 | @override 43 | bool operator ==(Object other) => 44 | identical(this, other) || 45 | other is MainReposState && 46 | runtimeType == other.runtimeType && 47 | isLoading == other.isLoading && 48 | currentPage == other.currentPage && 49 | repos == other.repos && 50 | sortType == other.sortType && 51 | error == other.error; 52 | 53 | @override 54 | int get hashCode => 55 | isLoading.hashCode ^ 56 | currentPage.hashCode ^ 57 | repos.hashCode ^ 58 | sortType.hashCode ^ 59 | error.hashCode; 60 | } 61 | 62 | abstract class MainReposStates { 63 | final bool isLoading; 64 | final int currentPage; 65 | final List repos; 66 | final String sortType; 67 | 68 | MainReposStates({ 69 | @required this.currentPage, 70 | this.repos, 71 | this.isLoading = false, 72 | this.sortType = UserRepoRepository.SORT_UPDATED, 73 | }) : assert(currentPage != null); 74 | } 75 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_rhine 2 | description: The Flutter Architecture in Android & iOS. 3 | 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.1.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | cupertino_icons: ^0.1.2 14 | dio: ^2.0.7 15 | flutter_swiper: ^1.1.4 16 | flutter_easyrefresh: ^1.2.7 17 | fluttertoast: ^3.0.1 18 | fluro: ^1.4.0 19 | shared_preferences: ^0.5.3 20 | json_annotation: 2.4.0 21 | 22 | android_intent: 0.3.0 23 | 24 | # 时间戳转换 25 | common_utils: 1.1.1 26 | 27 | flutter_bloc: 0.18.0 28 | 29 | flutter_redux: 0.5.3 30 | redux_epics: 0.10.6 31 | rxdart: 0.22.0 32 | 33 | dev_dependencies: 34 | build_runner: ^1.0.0 35 | json_serializable: ^3.0.0 36 | 37 | flutter_test: 38 | sdk: flutter 39 | 40 | flutter: 41 | uses-material-design: true 42 | assets: 43 | - assets/images/ 44 | 45 | # An image asset can refer to one or more resolution-specific "variants", see 46 | # https://flutter.dev/assets-and-images/#resolution-aware. 47 | 48 | # For details regarding adding assets from package dependencies, see 49 | # https://flutter.dev/assets-and-images/#from-packages 50 | 51 | # To add custom fonts to your application, add a fonts section here, 52 | # in this "flutter" section. Each entry in this list should have a 53 | # "family" key with the font family name, and a "fonts" key with a 54 | # list giving the asset and other descriptors for the font. For 55 | # example: 56 | # fonts: 57 | # - family: Schyler 58 | # fonts: 59 | # - asset: fonts/Schyler-Regular.ttf 60 | # - asset: fonts/Schyler-Italic.ttf 61 | # style: italic 62 | # - family: Trajan Pro 63 | # fonts: 64 | # - asset: fonts/TrajanPro.ttf 65 | # - asset: fonts/TrajanPro_Bold.ttf 66 | # weight: 700 67 | # 68 | # For details regarding fonts from package dependencies, 69 | # see https://flutter.dev/custom-fonts/#from-packages 70 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter common.widget test. 2 | // 3 | // To perform an interaction with a common.widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the common.widget 6 | // tree, read text, and verify that the values of common.widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_rhine/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(App()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------