├── .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 |
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 |
--------------------------------------------------------------------------------