├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── 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-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── Podfile.lock └── Podfile ├── images ├── circle.png ├── start.png ├── license.png ├── loading.gif ├── app_welcome.png ├── issue_open.png ├── issue_closed.png ├── pull_request.png ├── git_repo_forked.png └── flutter_github_preview.png ├── android ├── gradle.properties ├── .gitignore ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── 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 │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── idisfkj │ │ │ │ │ └── flutter_github │ │ │ │ │ ├── Constants.kt │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── lib ├── main.dart ├── common │ ├── colors.dart │ ├── user_manager.dart │ ├── constants.dart │ └── dialog.dart ├── ui │ ├── base │ │ ├── vm_s_contract.dart │ │ ├── base_page.dart │ │ ├── base_widget.dart │ │ ├── base_vm.dart │ │ └── base_state.dart │ ├── home │ │ ├── notification │ │ │ ├── notification_change_model.dart │ │ │ ├── notification_vm.dart │ │ │ └── notification.dart │ │ ├── search │ │ │ ├── search_vm.dart │ │ │ └── search.dart │ │ ├── user │ │ │ ├── user_vm.dart │ │ │ └── user.dart │ │ └── home.dart │ ├── repos │ │ ├── repos.dart │ │ └── repos_vm.dart │ ├── webview │ │ ├── webview_vm.dart │ │ └── webview.dart │ ├── app.dart │ ├── followers │ │ ├── followers_vm.dart │ │ └── followers.dart │ ├── welcome │ │ └── welcome.dart │ └── login │ │ ├── login_vm.dart │ │ └── login.dart ├── http │ ├── http.dart │ └── interceptors.dart ├── model │ ├── search_model.dart │ ├── followers_model.dart │ ├── notification_request_url_model.dart │ ├── user_model.dart │ └── notification_model.dart ├── widget │ ├── text_with_side.dart │ ├── followers_item_view.dart │ └── repository_item_view.dart └── routes │ └── app_routes.dart ├── .metadata ├── .gitignore ├── test └── widget_test.dart ├── pubspec.yaml ├── README.md └── pubspec.lock /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/circle.png -------------------------------------------------------------------------------- /images/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/start.png -------------------------------------------------------------------------------- /images/license.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/license.png -------------------------------------------------------------------------------- /images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/loading.gif -------------------------------------------------------------------------------- /images/app_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/app_welcome.png -------------------------------------------------------------------------------- /images/issue_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/issue_open.png -------------------------------------------------------------------------------- /images/issue_closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/issue_closed.png -------------------------------------------------------------------------------- /images/pull_request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/pull_request.png -------------------------------------------------------------------------------- /images/git_repo_forked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/git_repo_forked.png -------------------------------------------------------------------------------- /images/flutter_github_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/images/flutter_github_preview.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | import 'ui/app.dart'; 4 | 5 | void main() { 6 | runApp(GithubApp()); 7 | } 8 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/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/idisfkj/flutter_github/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/idisfkj/flutter_github/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/common/colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | final color_666 = Color.fromARGB(255, 102, 102, 102); 4 | final color_999 = Color.fromARGB(255, 153, 153, 153); 5 | final colorEAEAEA = Color.fromARGB(255, 234, 234, 234); 6 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/ui/base/vm_s_contract.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class VMSContract { 4 | ValueChanged getShowLoadingCallback() => (isShow) {}; 5 | 6 | ValueChanged getLoadingShowContentCallback() => (isShow) {}; 7 | 8 | VoidCallback notifyStateChanged() => () {}; 9 | } 10 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/common/user_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_github/common/constants.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | clearUserInfo() async { 5 | SharedPreferences prefs = await SharedPreferences.getInstance(); 6 | await prefs.remove(SP_AUTHORIZATION); 7 | await prefs.remove(SP_ACCESS_TOKEN); 8 | } 9 | -------------------------------------------------------------------------------- /.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: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/ui/base/base_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/ui/base/base_state.dart'; 3 | 4 | abstract class BasePage 5 | extends StatefulWidget { 6 | const BasePage(); 7 | 8 | @override 9 | S createState() { 10 | return createBaseState(); 11 | } 12 | 13 | S createBaseState(); 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/idisfkj/flutter_github/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.idisfkj.flutter_github 2 | 3 | /** 4 | * Created by idisfkj on 2020-02-19. 5 | * Email: idisfkj@gmail.com. 6 | */ 7 | 8 | object Constants { 9 | const val AUTHORIZATION_CODE = "code" 10 | const val METHOD_CHANNEL_NAME = "app.channel.shared.data" 11 | const val CALL_LOGIN_CODE = "getLoginCode" 12 | } -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/ui/home/notification/notification_change_model.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/cupertino.dart'; 3 | 4 | class NotificationChangeModel extends ChangeNotifier { 5 | 6 | NotificationChangeModel(this._unread); 7 | 8 | bool get unread => _unread; 9 | 10 | bool _unread; 11 | 12 | void changeUnread(bool unread) { 13 | _unread = unread; 14 | notifyListeners(); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /lib/http/http.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_github/http/interceptors.dart'; 3 | 4 | const String API_GITHUB_BASE_URL = 'https://api.github.com'; 5 | const String GITHUB_BASE_URL = 'https://github.com'; 6 | 7 | BaseOptions options = BaseOptions( 8 | baseUrl: API_GITHUB_BASE_URL, 9 | connectTimeout: 5000, 10 | receiveTimeout: 3000, 11 | ); 12 | 13 | var dio = Dio(options)..interceptors.add(AppInterceptors()); 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/model/search_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_github/model/notification_model.dart'; 2 | 3 | class SearchModel { 4 | int totalCount; 5 | bool incompleteResult; 6 | List items; 7 | 8 | SearchModel({this.totalCount, this.incompleteResult, this.items}); 9 | 10 | factory SearchModel.fromJson(Map json) => SearchModel( 11 | totalCount: json['total_count'], 12 | incompleteResult: json['incomplete_result'], 13 | items: repositoryListFromJson(json['items']), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/model/followers_model.dart: -------------------------------------------------------------------------------- 1 | List followersModelListFromJson(dynamic list) => 2 | List.from(list.map((x) => FollowersModel.fromJson(x))); 3 | 4 | class FollowersModel { 5 | String login; 6 | String avatar_url; 7 | String html_url; 8 | 9 | FollowersModel({this.login, this.avatar_url, this.html_url}); 10 | 11 | factory FollowersModel.fromJson(Map json) => FollowersModel( 12 | login: json['login'], 13 | avatar_url: json['avatar_url'], 14 | html_url: json['html_url'], 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/widget/text_with_side.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class TextWithSide extends StatelessWidget { 4 | final double sideDistance; 5 | final Widget side; 6 | final Text text; 7 | 8 | const TextWithSide( 9 | {Key key, this.sideDistance, @required this.side, @required this.text}) 10 | : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Row( 15 | children: [ 16 | side, 17 | Padding( 18 | padding: EdgeInsets.only(left: sideDistance), 19 | child: text, 20 | ), 21 | ], 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/model/notification_request_url_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_github/model/user_model.dart'; 2 | 3 | class NotificationRequestUrlModel { 4 | String url; 5 | String html_url; 6 | UserModel userModel; 7 | String body; 8 | 9 | NotificationRequestUrlModel( 10 | {this.url, this.html_url, this.userModel, this.body}); 11 | 12 | factory NotificationRequestUrlModel.fromJson(Map json) { 13 | return NotificationRequestUrlModel( 14 | url: json['url'], 15 | html_url: json['html_url'], 16 | // userModel: UserModel.fromJson(json['userModel']), 17 | body: json['body'], 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/common/constants.dart: -------------------------------------------------------------------------------- 1 | const String SP_USER_NAME = 'username'; 2 | const String SP_PASSWORD = 'password'; 3 | const String SP_AUTHORIZATION = 'authorization'; 4 | const String SP_ACCESS_TOKEN = "token"; 5 | 6 | const String AUTHORIZATION_CODE = 'code'; 7 | const String CLIENT_ID = 'a36f72d09eb4c9ee207a'; 8 | const String CLIENT_SECRET = 'c11cfe68bfc7a8c4753bfdbf036a94ff943b972c'; 9 | const String REDIRECT_URI = 'github://login'; 10 | const String URL_AUTHORIZATION = 11 | 'https://github.com/login/oauth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI&scope=user%20repo%20notifications%20'; 12 | 13 | const String METHOD_CHANNEL_NAME = 'app.channel.shared.data'; 14 | const String CALL_LOGIN_CODE = 'getLoginCode'; 15 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 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 | -------------------------------------------------------------------------------- /.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 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | -------------------------------------------------------------------------------- /lib/ui/base/base_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class BaseWidget extends StatelessWidget { 5 | final bool showLoading; 6 | final Widget contentWidget; 7 | final bool loadingShowContent; 8 | 9 | BaseWidget( 10 | {Key key, 11 | @required this.contentWidget, 12 | this.showLoading = true, 13 | this.loadingShowContent = false}) 14 | : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Stack( 19 | children: [ 20 | Visibility( 21 | visible: !showLoading || loadingShowContent, 22 | child: contentWidget, 23 | ), 24 | Visibility( 25 | visible: showLoading, 26 | child: Center( 27 | child: Image.asset('images/loading.gif'), 28 | ), 29 | ), 30 | ], 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/routes/app_routes.dart: -------------------------------------------------------------------------------- 1 | class AppRoutes { 2 | final String routeName; 3 | final String pageTitle; 4 | final String pageType; 5 | 6 | const AppRoutes(this.routeName, {this.pageTitle, this.pageType}); 7 | } 8 | 9 | class PageType { 10 | static const String followers = 'followers'; 11 | static const String following = 'following'; 12 | } 13 | 14 | const AppRoutes welcomeRoute = AppRoutes('/'); 15 | 16 | const AppRoutes loginRoute = AppRoutes('/login'); 17 | 18 | const AppRoutes homeRoute = AppRoutes('/home'); 19 | 20 | const AppRoutes repositoryRoute = 21 | AppRoutes('/repository', pageTitle: 'repository'); 22 | 23 | const AppRoutes followersRoute = AppRoutes('/followers', 24 | pageTitle: 'followers', pageType: PageType.followers); 25 | const AppRoutes followingRoute = AppRoutes('/following', 26 | pageTitle: 'following', pageType: PageType.following); 27 | 28 | const AppRoutes webViewRoute = AppRoutes('/webview', pageTitle: 'WebView'); 29 | -------------------------------------------------------------------------------- /lib/ui/base/base_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_github/ui/base/vm_s_contract.dart'; 3 | 4 | abstract class BaseVM { 5 | final BuildContext _context; 6 | 7 | BuildContext get context => _context; 8 | 9 | BaseVM(this._context); 10 | 11 | ValueChanged _showLoading; 12 | ValueChanged _loadingShowContent; 13 | VoidCallback _notifyStateChanged; 14 | 15 | ValueChanged get showLoading => _showLoading; 16 | 17 | ValueChanged get loadingShowContent => _loadingShowContent; 18 | 19 | VoidCallback get notifyStateChanged => _notifyStateChanged; 20 | 21 | void setupContract(VMSContract contract) { 22 | _showLoading = contract.getShowLoadingCallback(); 23 | _loadingShowContent = contract.getLoadingShowContentCallback(); 24 | _notifyStateChanged = contract.notifyStateChanged(); 25 | } 26 | 27 | void init(); 28 | 29 | void dispose() {} 30 | 31 | void didChangeDependencies() {} 32 | } 33 | -------------------------------------------------------------------------------- /lib/ui/home/search/search_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_github/http/http.dart'; 4 | import 'package:flutter_github/model/search_model.dart'; 5 | import 'package:flutter_github/ui/base/base_vm.dart'; 6 | import 'package:toast/toast.dart'; 7 | 8 | class SearchVM extends BaseVM { 9 | SearchModel _searchModel; 10 | 11 | SearchVM(BuildContext context) : super(context); 12 | 13 | SearchModel get searchModel => _searchModel; 14 | 15 | @override 16 | void init() { 17 | loadingShowContent(true); 18 | search('android-api-analysis'); 19 | } 20 | 21 | search(String query) async { 22 | showLoading(true); 23 | try { 24 | Response response = 25 | await dio.get('/search/repositories', queryParameters: {'q': query}); 26 | _searchModel = SearchModel.fromJson(response.data); 27 | } on DioError catch (e) { 28 | _searchModel = null; 29 | Toast.show('search error ${e.message}', context); 30 | } 31 | showLoading(false); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/http/interceptors.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter_github/common/constants.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | class AppInterceptors extends InterceptorsWrapper { 6 | @override 7 | Future onRequest(RequestOptions options) async { 8 | SharedPreferences prefs = await SharedPreferences.getInstance(); 9 | String authorization = prefs.getString(SP_AUTHORIZATION); 10 | String token = prefs.getString(SP_ACCESS_TOKEN); 11 | print('authorization: $authorization, token: $token'); 12 | options.headers['Authorization'] = 13 | authorization != null && authorization.isNotEmpty 14 | ? 'Basic ' + authorization 15 | : 'token ' + token; 16 | print('onRequest: ${options.headers}'); 17 | return options; 18 | } 19 | 20 | @override 21 | Future onResponse(Response response) { 22 | print('onResponse ${response.headers}'); 23 | return super.onResponse(response); 24 | } 25 | 26 | @override 27 | Future onError(DioError err) { 28 | print('Network request onError ${err.request.uri}, ${err.message}'); 29 | return super.onError(err); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_github/ui/app.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | import 'package:flutter_github/main.dart'; 13 | 14 | void main() { 15 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 16 | // Build our app and trigger a frame. 17 | await tester.pumpWidget(GithubApp()); 18 | 19 | // Verify that our counter starts at 0. 20 | expect(find.text('0'), findsOneWidget); 21 | expect(find.text('1'), findsNothing); 22 | 23 | // Tap the '+' icon and trigger a frame. 24 | await tester.tap(find.byIcon(Icons.add)); 25 | await tester.pump(); 26 | 27 | // Verify that our counter has incremented. 28 | expect(find.text('0'), findsNothing); 29 | expect(find.text('1'), findsOneWidget); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/repos/repos.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_github/ui/base/base_page.dart'; 4 | import 'package:flutter_github/ui/base/base_state.dart'; 5 | import 'package:flutter_github/ui/repos/repos_vm.dart'; 6 | import 'package:flutter_github/widget/repository_item_view.dart'; 7 | 8 | class RepositoryPage extends BasePage { 9 | @override 10 | _RepositoryState createBaseState() => _RepositoryState(); 11 | } 12 | 13 | class _RepositoryState extends BaseState { 14 | @override 15 | RepositoryVM createVM() => RepositoryVM(context); 16 | 17 | @override 18 | Widget createContentWidget() { 19 | return RefreshIndicator( 20 | onRefresh: vm.handlerRefresh, 21 | child: Scrollbar( 22 | child: ListView.builder( 23 | itemCount: vm.list?.length ?? 0, 24 | itemBuilder: (BuildContext context, int index) { 25 | return RepositoryItemView(vm.list[index]); 26 | }, 27 | ), 28 | ), 29 | ); 30 | } 31 | 32 | @override 33 | PreferredSizeWidget createAppBar() { 34 | return AppBar( 35 | title: Text('Repository'), 36 | centerTitle: true, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/ui/repos/repos_vm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter_github/http/http.dart'; 6 | import 'package:flutter_github/model/notification_model.dart'; 7 | import 'package:flutter_github/ui/base/base_vm.dart'; 8 | import 'package:toast/toast.dart'; 9 | 10 | class RepositoryVM extends BaseVM { 11 | List _list; 12 | 13 | RepositoryVM(BuildContext context) : super(context); 14 | 15 | List get list => _list; 16 | 17 | Completer _completer; 18 | 19 | @override 20 | void init() { 21 | _getRepos(); 22 | } 23 | 24 | _getRepos({bool isRefresh = false}) async { 25 | try { 26 | Response response = await dio.get('/user/repos', 27 | queryParameters: {'vivibility': 'all', 'sort': 'pushed'}); 28 | _list = repositoryListFromJson(response.data); 29 | } on DioError catch (e) { 30 | Toast.show('getRepos Error ${e.message}', context); 31 | } 32 | if (isRefresh) _completer.complete('refreshing completed'); 33 | showLoading(false); 34 | } 35 | 36 | Future handlerRefresh() { 37 | _completer = Completer(); 38 | _getRepos(isRefresh: true); 39 | return _completer.future.then((content) { 40 | Toast.show(content, context); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/ui/webview/webview_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_github/http/http.dart'; 4 | import 'package:flutter_github/model/notification_request_url_model.dart'; 5 | import 'package:flutter_github/ui/base/base_vm.dart'; 6 | import 'package:toast/toast.dart'; 7 | 8 | class WebViewVM extends BaseVM { 9 | String _requestUrl; 10 | 11 | set requestUrl(String requestUrl) { 12 | _requestUrl = requestUrl; 13 | } 14 | 15 | String _htmlUrl; 16 | 17 | String get htmlUrl => _htmlUrl; 18 | 19 | WebViewVM(BuildContext context) : super(context); 20 | 21 | @override 22 | void didChangeDependencies() { 23 | loadingShowContent(true); 24 | if (!(_requestUrl?.isEmpty ?? true) && ModalRoute.of(context).isActive) { 25 | _getNotificationRequestUrl(); 26 | } 27 | } 28 | 29 | @override 30 | void init() {} 31 | 32 | _getNotificationRequestUrl() async { 33 | try { 34 | if (_requestUrl.startsWith(API_GITHUB_BASE_URL)) { 35 | _requestUrl = _requestUrl.substring(API_GITHUB_BASE_URL.length + 1); 36 | } 37 | Response response = await dio.get('/$_requestUrl'); 38 | _htmlUrl = NotificationRequestUrlModel.fromJson(response.data)?.html_url ?? ''; 39 | notifyStateChanged(); 40 | } on DioError catch (e) { 41 | Toast.show('getNotificationRequestUrl error ${e.message}', context); 42 | showLoading(false); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/idisfkj/flutter_github/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.idisfkj.flutter_github 2 | 3 | import android.content.Intent 4 | import android.text.TextUtils 5 | import androidx.annotation.NonNull 6 | import io.flutter.embedding.android.FlutterActivity 7 | import io.flutter.embedding.engine.FlutterEngine 8 | import io.flutter.plugin.common.MethodChannel 9 | import io.flutter.plugins.GeneratedPluginRegistrant 10 | 11 | class MainActivity : FlutterActivity() { 12 | 13 | private var mAuthorizationCode: String? = null 14 | 15 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 16 | GeneratedPluginRegistrant.registerWith(flutterEngine) 17 | setupMethodChannel() 18 | } 19 | 20 | override fun onNewIntent(intent: Intent) { 21 | super.onNewIntent(intent) 22 | getExtra(intent) 23 | } 24 | 25 | private fun getExtra(intent: Intent?) { 26 | // from author login 27 | mAuthorizationCode = intent?.data?.getQueryParameter(Constants.AUTHORIZATION_CODE) 28 | } 29 | 30 | private fun setupMethodChannel() { 31 | MethodChannel(flutterEngine?.dartExecutor, Constants.METHOD_CHANNEL_NAME).setMethodCallHandler { call, result -> 32 | if (call.method == Constants.CALL_LOGIN_CODE && !TextUtils.isEmpty(mAuthorizationCode)) { 33 | result.success(mAuthorizationCode) 34 | mAuthorizationCode = null 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | var paramsMap: Dictionary = [:] 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | GeneratedPluginRegistrant.register(with: self) 12 | 13 | let controller: FlutterViewController = window?.rootViewController as! FlutterViewController 14 | let methodChannel = FlutterMethodChannel.init(name: "app.channel.shared.data", binaryMessenger: controller.binaryMessenger) 15 | 16 | methodChannel.setMethodCallHandler { (call, result) in 17 | if "getLoginCode" == call.method && !self.paramsMap.isEmpty { 18 | result(self.paramsMap["code"]) 19 | self.paramsMap.removeAll() 20 | } 21 | } 22 | 23 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 24 | } 25 | override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 26 | let absoluteString = url.absoluteURL.absoluteString 27 | let urlComponents = NSURLComponents(string: absoluteString) 28 | let queryItems = urlComponents?.queryItems 29 | for item in queryItems! { 30 | paramsMap[item.name] = item.value 31 | } 32 | return true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/ui/home/user/user_vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_github/common/dialog.dart'; 5 | import 'package:flutter_github/common/user_manager.dart'; 6 | import 'package:flutter_github/http/http.dart'; 7 | import 'package:flutter_github/model/user_model.dart'; 8 | import 'package:flutter_github/routes/app_routes.dart'; 9 | import 'package:flutter_github/ui/base/base_vm.dart'; 10 | import 'package:toast/toast.dart'; 11 | 12 | class UserVM extends BaseVM { 13 | UserModel _userModel; 14 | 15 | UserVM(BuildContext context) : super(context); 16 | 17 | UserModel get userModel => _userModel; 18 | 19 | @override 20 | void init() { 21 | _getUser(); 22 | } 23 | 24 | _getUser() async { 25 | try { 26 | Response response = await dio.get('/user'); 27 | _userModel = UserModel.fromJson(response.data); 28 | } on DioError catch (e) { 29 | // Unauthorized 30 | if (e.response != null && e.response.statusCode == 401) { 31 | clearUserInfo(); 32 | Navigator.of(context).pushReplacementNamed(loginRoute.routeName); 33 | Toast.show( 34 | 'The authorized login has expired, please login again.', context, 35 | duration: 3); 36 | } else { 37 | Toast.show('getUser error: ${e.message}', context); 38 | } 39 | } 40 | showLoading(false); 41 | } 42 | 43 | logout() { 44 | showLogoutDialog(context); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/ui/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/ui/followers/followers.dart'; 3 | import 'package:flutter_github/ui/home/home.dart'; 4 | import 'package:flutter_github/ui/login/login.dart'; 5 | import 'package:flutter_github/routes/app_routes.dart'; 6 | import 'package:flutter_github/ui/repos/repos.dart'; 7 | import 'package:flutter_github/ui/webview/webview.dart'; 8 | import 'welcome/welcome.dart'; 9 | 10 | class GithubApp extends StatefulWidget { 11 | @override 12 | _GithubAppState createState() { 13 | return _GithubAppState(); 14 | } 15 | } 16 | 17 | class _GithubAppState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return MaterialApp( 21 | title: 'Flutter Github', 22 | theme: ThemeData.light(), 23 | initialRoute: welcomeRoute.routeName, 24 | routes: { 25 | welcomeRoute.routeName: (BuildContext context) => WelcomePage(), 26 | loginRoute.routeName: (BuildContext context) => LoginPage(), 27 | homeRoute.routeName: (BuildContext context) => HomePage(), 28 | repositoryRoute.routeName: (BuildContext context) => RepositoryPage(), 29 | followersRoute.routeName: (BuildContext context) => 30 | FollowersPage(followersRoute.pageType), 31 | followingRoute.routeName: (BuildContext context) => 32 | FollowersPage(followingRoute.pageType), 33 | webViewRoute.routeName: (BuildContext context) => WebViewPage(), 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/ui/followers/followers_vm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter_github/http/http.dart'; 6 | import 'package:flutter_github/model/followers_model.dart'; 7 | import 'package:flutter_github/routes/app_routes.dart'; 8 | import 'package:flutter_github/ui/base/base_vm.dart'; 9 | import 'package:toast/toast.dart'; 10 | 11 | class FollowersVM extends BaseVM { 12 | final String _pageType; 13 | 14 | Completer _completer; 15 | List _list; 16 | 17 | FollowersVM(BuildContext context, this._pageType) : super(context); 18 | 19 | List get list => _list; 20 | String _requestPath; 21 | 22 | @override 23 | void init() { 24 | _requestPath = 25 | _pageType == PageType.followers ? '/user/followers' : '/user/following'; 26 | _getFollowers(); 27 | } 28 | 29 | _getFollowers({bool isRefresh = false}) async { 30 | try { 31 | Response response = await dio.get(_requestPath); 32 | _list = followersModelListFromJson(response.data); 33 | } on DioError catch (e) { 34 | Toast.show('getFollowers error ${e.message}', context); 35 | } 36 | if (isRefresh) { 37 | _completer.complete('refreshing completed'); 38 | } 39 | showLoading(false); 40 | } 41 | 42 | Future handlerRefresh() { 43 | _completer = Completer(); 44 | _getFollowers(isRefresh: true); 45 | return _completer.future.then((content) { 46 | Toast.show(content, context); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/common/dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/common/user_manager.dart'; 3 | import 'package:flutter_github/routes/app_routes.dart'; 4 | 5 | showLogoutDialog(BuildContext context) { 6 | showDialog( 7 | context: context, 8 | barrierDismissible: false, 9 | builder: (BuildContext context) { 10 | return AlertDialog( 11 | shape: RoundedRectangleBorder( 12 | borderRadius: BorderRadius.circular(5.0), 13 | ), 14 | title: Center( 15 | child: Text( 16 | 'Logout', 17 | style: TextStyle( 18 | fontSize: 22.0, 19 | fontWeight: FontWeight.bold, 20 | color: Colors.black87), 21 | ), 22 | ), 23 | content: const Text( 24 | 'Are you sure to logout?', 25 | style: TextStyle(fontSize: 16.0, color: Colors.grey), 26 | ), 27 | actions: [ 28 | FlatButton( 29 | child: const Text( 30 | 'CONFIRM', 31 | style: TextStyle(fontSize: 16.0), 32 | ), 33 | onPressed: () { 34 | clearUserInfo(); 35 | Navigator.pop(context); 36 | Navigator.pushReplacementNamed(context, loginRoute.routeName); 37 | }, 38 | ), 39 | FlatButton( 40 | child: const Text( 41 | 'CANCEL', 42 | style: TextStyle(fontSize: 16.0), 43 | ), 44 | onPressed: () { 45 | Navigator.pop(context); 46 | }, 47 | ) 48 | ], 49 | ); 50 | }, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/widget/followers_item_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/common/colors.dart'; 3 | 4 | class FollowersItemView extends StatelessWidget { 5 | final GestureTapCallback tapCallback; 6 | final String avatarUrl; 7 | final String name; 8 | 9 | const FollowersItemView( 10 | {Key key, this.avatarUrl, this.name, this.tapCallback}) 11 | : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | padding: EdgeInsets.symmetric(horizontal: 15.0), 17 | child: GestureDetector( 18 | behavior: HitTestBehavior.opaque, 19 | onTap: tapCallback, 20 | child: Column( 21 | children: [ 22 | Row( 23 | children: [ 24 | FadeInImage.assetNetwork( 25 | placeholder: 'images/app_welcome.png', 26 | image: avatarUrl, 27 | width: 80.0, 28 | height: 80.0, 29 | ), 30 | Expanded( 31 | child: Padding( 32 | padding: EdgeInsets.only(left: 15.0), 33 | child: Text( 34 | name, 35 | overflow: TextOverflow.ellipsis, 36 | maxLines: 1, 37 | style: TextStyle( 38 | color: Colors.grey[600], 39 | fontSize: 20.0, 40 | fontWeight: FontWeight.bold, 41 | ), 42 | ), 43 | ), 44 | ) 45 | ], 46 | ), 47 | Padding( 48 | padding: EdgeInsets.symmetric(vertical: 15.0), 49 | child: Divider( 50 | thickness: 1.0, 51 | color: colorEAEAEA, 52 | height: 1.0, 53 | endIndent: 0.0, 54 | ), 55 | ), 56 | ], 57 | ), 58 | ), 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_github 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLName 27 | github_app 28 | CFBundleURLSchemes 29 | 30 | github 31 | 32 | 33 | 34 | CFBundleVersion 35 | $(FLUTTER_BUILD_NUMBER) 36 | LSRequiresIPhoneOS 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIMainStoryboardFile 41 | Main 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | UIViewControllerBasedStatusBarAppearance 56 | 57 | io.flutter.embedded_views_preview 58 | YES 59 | 60 | 61 | -------------------------------------------------------------------------------- /lib/ui/welcome/welcome.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_github/common/constants.dart'; 5 | import 'package:flutter_github/routes/app_routes.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | 8 | class WelcomePage extends StatefulWidget { 9 | @override 10 | _WelcomeState createState() { 11 | return _WelcomeState(); 12 | } 13 | } 14 | 15 | class _WelcomeState extends State { 16 | Timer _timer; 17 | 18 | void _goToLogin() async { 19 | SharedPreferences prefs = await SharedPreferences.getInstance(); 20 | String authorization = prefs.getString(SP_AUTHORIZATION); 21 | String token = prefs.getString(SP_ACCESS_TOKEN); 22 | if ((authorization != null && authorization.isNotEmpty) || 23 | (token != null && token.isNotEmpty)) { 24 | Navigator.pushReplacementNamed(context, homeRoute.routeName); 25 | } else { 26 | Navigator.pushReplacementNamed(context, loginRoute.routeName); 27 | } 28 | } 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | _timer = Timer(const Duration(milliseconds: 1500), () { 34 | _goToLogin(); 35 | }); 36 | } 37 | 38 | @override 39 | void dispose() { 40 | _timer.cancel(); 41 | _timer = null; 42 | super.dispose(); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | final size = MediaQuery.of(context).size; 48 | final width = size.width; 49 | final height = size.height; 50 | return WillPopScope( 51 | child: Scaffold( 52 | appBar: PreferredSize( 53 | preferredSize: Size.fromHeight(kToolbarHeight), 54 | child: SafeArea( 55 | top: true, 56 | child: Offstage(), 57 | ), 58 | ), 59 | body: Container( 60 | width: width, 61 | height: height, 62 | child: GestureDetector( 63 | child: Image.asset('images/app_welcome.png'), 64 | ))), 65 | onWillPop: () { 66 | return Future.value(false); 67 | }, 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/ui/followers/followers.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/routes/app_routes.dart'; 3 | import 'package:flutter_github/ui/base/base_page.dart'; 4 | import 'package:flutter_github/ui/base/base_state.dart'; 5 | import 'package:flutter_github/ui/base/base_vm.dart'; 6 | import 'package:flutter_github/ui/followers/followers_vm.dart'; 7 | import 'package:flutter_github/ui/webview/webview.dart'; 8 | import 'package:flutter_github/widget/followers_item_view.dart'; 9 | 10 | class FollowersPage extends BasePage { 11 | final String _pageType; 12 | 13 | FollowersPage(this._pageType); 14 | 15 | @override 16 | BaseState createBaseState() => 17 | _FollowersState(this._pageType); 18 | } 19 | 20 | class _FollowersState extends BaseState { 21 | final String _pageType; 22 | 23 | _FollowersState(this._pageType); 24 | 25 | @override 26 | PreferredSizeWidget createAppBar() { 27 | return AppBar( 28 | title: Text(_pageType == PageType.followers 29 | ? followersRoute.pageTitle 30 | : followingRoute.pageTitle), 31 | centerTitle: true, 32 | ); 33 | } 34 | 35 | @override 36 | Widget createContentWidget() { 37 | return RefreshIndicator( 38 | onRefresh: vm.handlerRefresh, 39 | child: Scrollbar( 40 | child: ListView.builder( 41 | padding: EdgeInsets.only(top: 15.0), 42 | itemCount: vm.list?.length ?? 0, 43 | itemBuilder: (BuildContext context, int index) { 44 | final item = vm.list[index]; 45 | return FollowersItemView( 46 | avatarUrl: item.avatar_url, 47 | name: item.login, 48 | tapCallback: () { 49 | Navigator.push(context, MaterialPageRoute(builder: (_) { 50 | return WebViewPage(); 51 | }, settings: RouteSettings(arguments: {WebViewPage.ARGS_TITLE: item.login, WebViewPage.ARGS_URL: item.html_url}))); 52 | }, 53 | ); 54 | }), 55 | ), 56 | ); 57 | } 58 | 59 | @override 60 | FollowersVM createVM() => FollowersVM(context, _pageType); 61 | } 62 | -------------------------------------------------------------------------------- /lib/ui/base/base_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/ui/base/base_vm.dart'; 3 | import 'package:flutter_github/ui/base/base_widget.dart'; 4 | import 'package:flutter_github/ui/base/vm_s_contract.dart'; 5 | 6 | abstract class BaseState 7 | extends State implements VMSContract { 8 | VM _vm; 9 | bool _showLoading = true; 10 | bool _loadingShowContent = false; 11 | 12 | bool get showLoading => _showLoading; 13 | 14 | bool get loadingShowContent => _loadingShowContent; 15 | 16 | VM get vm => _vm; 17 | 18 | VM createVM(); 19 | 20 | @override 21 | getShowLoadingCallback() { 22 | return (isShow) { 23 | setState(() { 24 | _showLoading = isShow; 25 | }); 26 | }; 27 | } 28 | 29 | @override 30 | getLoadingShowContentCallback() { 31 | return (isShow) { 32 | setState(() { 33 | _loadingShowContent = isShow; 34 | }); 35 | }; 36 | } 37 | 38 | @override 39 | notifyStateChanged() { 40 | return () { 41 | setState(() {}); 42 | }; 43 | } 44 | 45 | @override 46 | void didChangeDependencies() { 47 | super.didChangeDependencies(); 48 | _vm.didChangeDependencies(); 49 | } 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | _vm = createVM() 55 | ..setupContract(this) 56 | ..init(); 57 | } 58 | 59 | PreferredSizeWidget createAppBar() { 60 | return PreferredSize( 61 | preferredSize: Size.fromHeight(kToolbarHeight), 62 | child: SafeArea( 63 | child: Offstage(), 64 | ), 65 | ); 66 | } 67 | 68 | Widget createContentWidget(); 69 | 70 | @override 71 | Widget build(BuildContext context) { 72 | return Scaffold( 73 | //防止因键盘弹出造成bottom overlowed by X pixels 74 | resizeToAvoidBottomPadding: false, 75 | appBar: createAppBar(), 76 | body: BaseWidget( 77 | contentWidget: createContentWidget(), 78 | showLoading: _showLoading, 79 | loadingShowContent: _loadingShowContent, 80 | ), 81 | ); 82 | } 83 | 84 | @override 85 | void dispose() { 86 | _vm.dispose(); 87 | super.dispose(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/ui/home/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_github/ui/home/notification/notification.dart'; 5 | import 'package:flutter_github/ui/home/search/search.dart'; 6 | import 'package:flutter_github/ui/home/user/user.dart'; 7 | 8 | class HomePage extends StatefulWidget { 9 | @override 10 | _HomePageState createState() { 11 | return _HomePageState(); 12 | } 13 | } 14 | 15 | class _HomePageState extends State 16 | with SingleTickerProviderStateMixin { 17 | int _selectedIndex = 0; 18 | 19 | static const List _tabPages = [ 20 | SearchTabPage(), 21 | NotificationTabPage(), 22 | UserTabPage() 23 | ]; 24 | 25 | _onTabClick(int index) { 26 | setState(() { 27 | _selectedIndex = index; 28 | }); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Scaffold( 34 | appBar: PreferredSize( 35 | child: AppBar( 36 | brightness: Theme.of(context).platform == TargetPlatform.android 37 | ? Brightness.dark 38 | : Brightness.light, 39 | backgroundColor: Colors.white, 40 | elevation: 0, 41 | ), 42 | preferredSize: Size.fromHeight(0), 43 | ), 44 | body: IndexedStack( 45 | index: _selectedIndex, 46 | children: _tabPages, 47 | ), 48 | bottomNavigationBar: BottomNavigationBar( 49 | items: const [ 50 | BottomNavigationBarItem( 51 | icon: Icon(Icons.search), title: Text('Search')), 52 | BottomNavigationBarItem( 53 | icon: Icon(Icons.notifications), title: Text('Notifications')), 54 | BottomNavigationBarItem(icon: Icon(Icons.person), title: Text('User')) 55 | ], 56 | currentIndex: _selectedIndex, 57 | selectedItemColor: Color.fromARGB(255, 3, 169, 244), 58 | unselectedItemColor: Color.fromARGB(255, 191, 191, 191), 59 | iconSize: 30.0, 60 | selectedFontSize: 16.0, 61 | unselectedFontSize: 14.0, 62 | onTap: _onTabClick, 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/ui/webview/webview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/ui/base/base_page.dart'; 3 | import 'package:flutter_github/ui/base/base_state.dart'; 4 | import 'package:flutter_github/ui/webview/webview_vm.dart'; 5 | import 'package:webview_flutter/webview_flutter.dart'; 6 | 7 | class WebViewPage extends BasePage<_WebViewState> { 8 | static const String ARGS_TITLE = "title"; 9 | static const String ARGS_REQUEST_URL = "request_Url"; 10 | static const String ARGS_URL = "url"; 11 | 12 | @override 13 | _WebViewState createBaseState() => _WebViewState(); 14 | } 15 | 16 | class _WebViewState extends BaseState { 17 | String _url; 18 | String _title; 19 | bool _canBack; 20 | 21 | WebViewController _controller; 22 | 23 | @override 24 | void didChangeDependencies() { 25 | Map arguments = ModalRoute.of(context).settings.arguments; 26 | _title = arguments[WebViewPage.ARGS_TITLE]; 27 | _url = arguments[WebViewPage.ARGS_URL]; 28 | vm.requestUrl = arguments[WebViewPage.ARGS_REQUEST_URL]; 29 | super.didChangeDependencies(); 30 | } 31 | 32 | @override 33 | PreferredSizeWidget createAppBar() { 34 | return AppBar( 35 | backgroundColor: Colors.black87, 36 | title: Text(_title), 37 | centerTitle: true, 38 | ); 39 | } 40 | 41 | @override 42 | Widget createContentWidget() { 43 | if ((_url?.isEmpty ?? true) && (vm.htmlUrl?.isEmpty ?? true)) { 44 | return Offstage(); 45 | } 46 | return WillPopScope( 47 | child: WebView( 48 | initialUrl: vm.htmlUrl ?? _url, 49 | javascriptMode: JavascriptMode.unrestricted, 50 | gestureNavigationEnabled: true, 51 | onWebViewCreated: (WebViewController controller) { 52 | _controller = controller; 53 | }, 54 | onPageFinished: (url) { 55 | vm.showLoading(false); 56 | _controller.canGoBack().then((canBack) { 57 | _canBack = canBack; 58 | }); 59 | }, 60 | ), 61 | onWillPop: () async { 62 | if (_canBack) _controller.goBack(); 63 | return !_canBack; 64 | }, 65 | ); 66 | } 67 | 68 | @override 69 | WebViewVM createVM() => WebViewVM(context); 70 | } 71 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - shared_preferences (0.0.1): 4 | - Flutter 5 | - shared_preferences_macos (0.0.1): 6 | - Flutter 7 | - shared_preferences_web (0.0.1): 8 | - Flutter 9 | - url_launcher (0.0.1): 10 | - Flutter 11 | - url_launcher_macos (0.0.1): 12 | - Flutter 13 | - url_launcher_web (0.0.1): 14 | - Flutter 15 | - webview_flutter (0.0.1): 16 | - Flutter 17 | 18 | DEPENDENCIES: 19 | - Flutter (from `Flutter`) 20 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 21 | - shared_preferences_macos (from `.symlinks/plugins/shared_preferences_macos/ios`) 22 | - shared_preferences_web (from `.symlinks/plugins/shared_preferences_web/ios`) 23 | - url_launcher (from `.symlinks/plugins/url_launcher/ios`) 24 | - url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`) 25 | - url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`) 26 | - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) 27 | 28 | EXTERNAL SOURCES: 29 | Flutter: 30 | :path: Flutter 31 | shared_preferences: 32 | :path: ".symlinks/plugins/shared_preferences/ios" 33 | shared_preferences_macos: 34 | :path: ".symlinks/plugins/shared_preferences_macos/ios" 35 | shared_preferences_web: 36 | :path: ".symlinks/plugins/shared_preferences_web/ios" 37 | url_launcher: 38 | :path: ".symlinks/plugins/url_launcher/ios" 39 | url_launcher_macos: 40 | :path: ".symlinks/plugins/url_launcher_macos/ios" 41 | url_launcher_web: 42 | :path: ".symlinks/plugins/url_launcher_web/ios" 43 | webview_flutter: 44 | :path: ".symlinks/plugins/webview_flutter/ios" 45 | 46 | SPEC CHECKSUMS: 47 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 48 | shared_preferences: 430726339841afefe5142b9c1f50cb6bd7793e01 49 | shared_preferences_macos: f3f29b71ccbb56bf40c9dd6396c9acf15e214087 50 | shared_preferences_web: 141cce0c3ed1a1c5bf2a0e44f52d31eeb66e5ea9 51 | url_launcher: a1c0cc845906122c4784c542523d8cacbded5626 52 | url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 53 | url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c 54 | webview_flutter: bec7599de6bfbe8008a739aa3ebd7b364ea9d0cd 55 | 56 | PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83 57 | 58 | COCOAPODS: 1.8.4 59 | -------------------------------------------------------------------------------- /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 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.idisfkj.flutter_github" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 38 | 41 | 42 | 43 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_github 2 | description: Awesome Github flutter application. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.2.2 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | http: 0.12.0+4 23 | dio: 3.0.7 24 | shared_preferences: 0.5.6+1 25 | url_launcher: 5.4.1 26 | toast: 0.1.5 27 | webview_flutter: 0.3.19+8 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^0.1.2 32 | provider: ^3.0.0 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | 38 | 39 | # For information on the generic Dart part of this file, see the 40 | # following page: https://dart.dev/tools/pub/pubspec 41 | 42 | # The following section is specific to Flutter. 43 | flutter: 44 | 45 | # The following line ensures that the Material Icons font is 46 | # included with your application, so that you can use the icons in 47 | # the material Icons class. 48 | uses-material-design: true 49 | 50 | # To add assets to your application, add an assets section, like this: 51 | assets: 52 | - images/app_welcome.png 53 | - images/loading.gif 54 | - images/issue_closed.png 55 | - images/issue_open.png 56 | - images/pull_request.png 57 | - images/circle.png 58 | - images/git_repo_forked.png 59 | - images/license.png 60 | - images/start.png 61 | # - images/a_dot_burr.jpeg 62 | # - images/a_dot_ham.jpeg 63 | 64 | # An image asset can refer to one or more resolution-specific "variants", see 65 | # https://flutter.dev/assets-and-images/#resolution-aware. 66 | 67 | # For details regarding adding assets from package dependencies, see 68 | # https://flutter.dev/assets-and-images/#from-packages 69 | 70 | # To add custom fonts to your application, add a fonts section here, 71 | # in this "flutter" section. Each entry in this list should have a 72 | # "family" key with the font family name, and a "fonts" key with a 73 | # list giving the asset and other descriptors for the font. For 74 | # example: 75 | # fonts: 76 | # - family: Schyler 77 | # fonts: 78 | # - asset: fonts/Schyler-Regular.ttf 79 | # - asset: fonts/Schyler-Italic.ttf 80 | # style: italic 81 | # - family: Trajan Pro 82 | # fonts: 83 | # - asset: fonts/TrajanPro.ttf 84 | # - asset: fonts/TrajanPro_Bold.ttf 85 | # weight: 700 86 | # 87 | # For details regarding fonts from package dependencies, 88 | # see https://flutter.dev/custom-fonts/#from-packages 89 | -------------------------------------------------------------------------------- /lib/ui/home/notification/notification_vm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_github/http/http.dart'; 7 | import 'package:flutter_github/model/notification_model.dart'; 8 | import 'package:flutter_github/ui/base/base_vm.dart'; 9 | import 'package:flutter_github/ui/webview/webview.dart'; 10 | import 'package:provider/provider.dart'; 11 | import 'package:toast/toast.dart'; 12 | 13 | import 'notification_change_model.dart'; 14 | 15 | class NotificationVM extends BaseVM { 16 | List _notifications; 17 | 18 | List get notifications => _notifications; 19 | 20 | NotificationVM(BuildContext context) : super(context); 21 | 22 | Completer _completer; 23 | 24 | static const String PULL_REQUEST = 'images/pull_request.png'; 25 | static const String ISSUE_OPEN = 'images/issue_open.png'; 26 | static const String ISSUE_CLOSED = 'images/issue_closed.png'; 27 | 28 | @override 29 | void init() { 30 | _getNotification(); 31 | } 32 | 33 | Future handlerRefresh() { 34 | _completer = Completer(); 35 | _getNotification(isRefresh: true); 36 | return _completer.future.then((content) { 37 | Toast.show(content, context); 38 | }); 39 | } 40 | 41 | _getNotification({bool isRefresh = false, bool all = true, bool participating = false, String since = '', String before = ''}) async { 42 | try { 43 | Response response = await dio.get('/notifications', queryParameters: {'all': true, 'participating': false, 'since': since, 'before': before}); 44 | _notifications = List.from(response.data.map((x) => NotificationModel.fromJson(x))); 45 | } on DioError catch (e) { 46 | Toast.show('getNotification error: ${e.message}', context); 47 | } 48 | if (isRefresh) { 49 | _completer.complete('refresing completed'); 50 | } 51 | showLoading(false); 52 | } 53 | 54 | String getTypeFlagSrc(String type) { 55 | if (type == "PullRequest") { 56 | return PULL_REQUEST; 57 | } else if (type == "Issue") { 58 | return ISSUE_OPEN; 59 | } 60 | return ISSUE_CLOSED; 61 | } 62 | 63 | contentTap(int index, BuildContext context) { 64 | NotificationModel item = _notifications[index]; 65 | if (item.unread) _markThreadRead(index, context); 66 | Navigator.push( 67 | context, 68 | MaterialPageRoute( 69 | builder: (_) { 70 | return WebViewPage(); 71 | }, 72 | settings: RouteSettings( 73 | arguments: {WebViewPage.ARGS_TITLE: item.subject?.title ?? '', WebViewPage.ARGS_REQUEST_URL: item.subject?.url ?? ''}))); 74 | } 75 | 76 | _markThreadRead(int index, BuildContext context) async { 77 | try { 78 | Response response = await dio.patch('/notifications/threads/${_notifications[index].id}'); 79 | if (response != null && response.statusCode >= 200 && response.statusCode < 300) { 80 | _notifications[index].unread = false; 81 | // 使用Provider进行通知子widget进行更新,避免widget树全局更新 82 | Provider.of(context, listen: false).changeUnread(false); 83 | // notifyStateChanged(); 84 | } 85 | } on DioError catch (e) { 86 | Toast.show('makThreadRead error: ${e.message}', context); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flutter_github 2 | 3 | [![License](https://img.shields.io/badge/license-Apache%202-green.svg)](https://www.apache.org/licenses/LICENSE-2.0) 4 | [![Platform](https://img.shields.io/badge/platform-android%20%7C%20ios-brightgreen)](https://flutter.dev/) 5 | [![Language](https://img.shields.io/badge/language-dart-ff69b4)](https://dart.dev/) 6 | [![Author](https://img.shields.io/badge/Author-idisfkj-orange.svg)](https://idisfkj.github.io/archives/) 7 | [![Rating](https://img.shields.io/chrome-web-store/stars/nimelepbpejjlbmoobocpfnjhihnpked.svg)]() 8 | 9 | 在Android原生Github客户端[AwesomeGithub](https://github.com/idisfkj/AwesomeGithub)上同步开发出的基于Flutter的跨平台客户端。 10 | 11 | Flutter Github客户端,同时支持Android与IOS,支持账户密码与认证登陆。使用dart语言进行开发,项目架构是基于Model/State/ViewModel的MSVM; 12 | 使用Navigator进行页面的跳转;网络框架使用了dio;通过MethodChannel实现与客户端的通信;使用Provider进行全局变量共享,优化页面的局部刷新。 13 | 14 | 这主要是一个学习项目,如有疑问欢迎来一起讨论,当然如果有帮助的话,请不要吝啬你的Star😄 15 | 16 | ![flutter_github_preview](https://github.com/idisfkj/flutter_github/raw/master/images/flutter_github_preview.png) 17 | 18 | > 温馨提示:GitHub提供的OpenApi可能不稳定,如果登录失败或者成功之后页面无数据,请尝试使用科学上网或者稍等再尝试。 19 | 20 | ### Doing 21 | 下面是与该项目相关的技术总结,欢迎一起来讨论👏 22 | 23 | - [x] [Flutter StatelessWidget](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247484222&idx=1&sn=d11adb51b2488310d0e99e85edad3929&chksm=e8e0faaedf9773b89b3db238ea978ab285055456ef127e550cad576cfe43e6ee646f3fb45b8e&token=288527406&lang=zh_CN#rd) 24 | - [x] [Flutter StatefulWidget](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247484232&idx=1&sn=008d1782cefdd8555f2b95681b33f27a&chksm=e8e0fad8df9773ce71e2914bf4bb8510d8e6b16911cf3aef45eca95a939e0c297a162b061545&token=288527406&lang=zh_CN#rd) 25 | - [x] [Flutter InheritedWidget](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247484244&idx=1&sn=08aadb3de199382bce2c9cd8ebb9fa1b&chksm=e8e0fac4df9773d2557bbe430577000814edb33fbb0ffb6060bd927056019b5ae50790afa3bd&token=288527406&lang=zh_CN#rd) 26 | - [x] [Flutter Provider](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247484324&idx=1&sn=0f0fbf7af29369de207fae9188a2dcf8&chksm=e8e0fa34df977322113a3d47296a2a21a775fad419df8c802b42a33d1a8884bf68316b351eb0&token=288527406&lang=zh_CN#rd) 27 | - [x] [Flutter Navigator](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247484403&idx=1&sn=469720c4cfadba6275756493209dec84&chksm=e8e0fa63df9773751eccc7233916be35e1d2edf3f8b6cb826ba30a54fe8d911fa728ee47da09&token=288527406&lang=zh_CN#rd) 28 | - [x] [Flutter MethodChannel](https://mp.weixin.qq.com/s?__biz=MzIzNTc5NDY4Nw==&mid=2247484504&idx=1&sn=78375eb5172ab358c7f0d01836927b71&chksm=e8e0fdc8df9774de74df60877d2d57a4ddf3a474e7fcd82824221dc099f53aa8b08d97aae1da&token=1979725139&lang=zh_CN#rd) 29 | - [ ] Flutter Dio 30 | - [ ] Flutter Dialog 31 | - [ ] Flutter MSVM 32 | - [ ] Flutter ValueNotifier 33 | - [ ] Flutter WebView 34 | 35 | ### Android纯原生版直通车 36 | 37 | [AwesomeGithub](https://github.com/idisfkj/AwesomeGithub) 38 | 39 | ### Pubspec.yaml 40 | 41 | ``` 42 | version: 1.0.0+1 43 | 44 | environment: 45 | sdk: ">=2.2.2 <3.0.0" 46 | 47 | dependencies: 48 | flutter: 49 | sdk: flutter 50 | http: 0.12.0+4 51 | dio: 3.0.7 52 | shared_preferences: 0.5.6+1 53 | url_launcher: 5.4.1 54 | toast: 0.1.5 55 | webview_flutter: 0.3.19+8 56 | 57 | # The following adds the Cupertino Icons font to your application. 58 | # Use with the CupertinoIcons class for iOS style icons. 59 | cupertino_icons: ^0.1.2 60 | 61 | ``` 62 | 63 | ## 加入我们 64 | 65 | 如需了解更多可以扫描下方二维码,加入我们:Android补给站。让我们与志同道合的你一起成长。 66 | 67 | ![关注](https://github.com/idisfkj/android-api-analysis/raw/master/image/wx.jpg) 68 | 69 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |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 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | use_modular_headers! 38 | 39 | # Flutter Pod 40 | 41 | copied_flutter_dir = File.join(__dir__, 'Flutter') 42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 48 | 49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 50 | unless File.exist?(generated_xcode_build_settings_path) 51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 52 | end 53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 55 | 56 | unless File.exist?(copied_framework_path) 57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 58 | end 59 | unless File.exist?(copied_podspec_path) 60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 61 | end 62 | end 63 | 64 | # Keep pod path relative so it can be checked into Podfile.lock. 65 | pod 'Flutter', :path => 'Flutter' 66 | 67 | # Plugin Pods 68 | 69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 70 | # referring to absolute paths on developers' machines. 71 | system('rm -rf .symlinks') 72 | system('mkdir -p .symlinks/plugins') 73 | plugin_pods = parse_KV_file('../.flutter-plugins') 74 | plugin_pods.each do |name, path| 75 | symlink = File.join('.symlinks', 'plugins', name) 76 | File.symlink(path, symlink) 77 | pod name, :path => File.join(symlink, 'ios') 78 | end 79 | end 80 | 81 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 82 | install! 'cocoapods', :disable_input_output_paths => true 83 | 84 | post_install do |installer| 85 | installer.pods_project.targets.each do |target| 86 | target.build_configurations.each do |config| 87 | config.build_settings['ENABLE_BITCODE'] = 'NO' 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/ui/login/login_vm.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:flutter_github/common/constants.dart'; 8 | import 'package:flutter_github/common/user_manager.dart'; 9 | import 'package:flutter_github/http/http.dart'; 10 | import 'package:flutter_github/model/user_model.dart'; 11 | import 'package:flutter_github/routes/app_routes.dart'; 12 | import 'package:flutter_github/ui/base/base_vm.dart'; 13 | import 'package:shared_preferences/shared_preferences.dart'; 14 | import 'package:toast/toast.dart'; 15 | import 'package:url_launcher/url_launcher.dart'; 16 | 17 | class LoginVM extends BaseVM { 18 | String username = ''; 19 | String password = ''; 20 | 21 | LoginVM(BuildContext context) : super(context); 22 | 23 | @override 24 | void init() { 25 | showLoading(false); 26 | loadingShowContent(true); 27 | } 28 | 29 | signInOnPress() { 30 | if (username.trim().isEmpty || password.trim().isEmpty) { 31 | return null; 32 | } else { 33 | return () { 34 | FocusScope.of(context).requestFocus(FocusNode()); 35 | _saveUserInfo(); 36 | }; 37 | } 38 | } 39 | 40 | _saveUserInfo() async { 41 | SharedPreferences prefs = await SharedPreferences.getInstance(); 42 | await prefs.setString(SP_USER_NAME, username); 43 | await prefs.setString(SP_PASSWORD, password); 44 | await prefs.setString(SP_AUTHORIZATION, 45 | base64Encode(ascii.encode(username + ':' + password))); 46 | _getUser(); 47 | } 48 | 49 | _getUser() async { 50 | showLoading(true); 51 | try { 52 | Response response = await dio.get('/user'); 53 | UserModel userModel = UserModel.fromJson(response.data); 54 | SharedPreferences prefs = await SharedPreferences.getInstance(); 55 | await prefs.setString(SP_USER_NAME, userModel.login); 56 | Toast.show('login success! hi ${userModel.name}', context); 57 | Navigator.of(context).pushReplacementNamed(homeRoute.routeName); 58 | } on DioError catch (e) { 59 | showLoading(false); 60 | clearUserInfo(); 61 | Toast.show('getUser error: ${e.message}', context); 62 | } 63 | } 64 | 65 | authorization() { 66 | return () async { 67 | FocusScope.of(context).requestFocus(FocusNode()); 68 | if (await canLaunch(URL_AUTHORIZATION)) { 69 | // 为设置forceSafariVC,IOS 默认会打开APP内部WebView 70 | // 而APP内部WebView不支持重定向跳转到APP 71 | await launch(URL_AUTHORIZATION, forceSafariVC: false); 72 | } else { 73 | throw 'Can not launch $URL_AUTHORIZATION)'; 74 | } 75 | }; 76 | } 77 | 78 | callLoginCode(AppLifecycleState state) async { 79 | if (state == AppLifecycleState.resumed) { 80 | final platform = const MethodChannel(METHOD_CHANNEL_NAME); 81 | final code = await platform.invokeMethod(CALL_LOGIN_CODE); 82 | if (code != null) { 83 | _getAccessTokenFromCode(code); 84 | } 85 | } 86 | } 87 | 88 | _getAccessTokenFromCode(String code) async { 89 | showLoading(true); 90 | try { 91 | Dio dio = Dio(); 92 | dio.options.baseUrl = GITHUB_BASE_URL; 93 | dio.options.connectTimeout = 5000; 94 | dio.options.receiveTimeout = 3000; 95 | Response response = await dio.post('/login/oauth/access_token', 96 | data: FormData.fromMap({ 97 | 'client_id': CLIENT_ID, 98 | 'client_secret': CLIENT_SECRET, 99 | 'code': code, 100 | 'redirect_uri': REDIRECT_URI 101 | })); 102 | final token = response.data.toString().split("=")[1].split("&")[0]; 103 | SharedPreferences prefs = await SharedPreferences.getInstance(); 104 | await prefs.setString(SP_ACCESS_TOKEN, token); 105 | await prefs.setString(SP_AUTHORIZATION, ''); 106 | _getUser(); 107 | } on DioError catch (e) { 108 | showLoading(false); 109 | Toast.show('getAccessTokenFromCode DioError: ${e.message}', context); 110 | } on IOException catch (e) { 111 | showLoading(false); 112 | Toast.show('getAccessTokenFromCode IOError: $e', context); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/ui/home/notification/notification.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_github/model/notification_model.dart'; 4 | import 'package:flutter_github/ui/base/base_page.dart'; 5 | import 'package:flutter_github/ui/base/base_state.dart'; 6 | import 'package:flutter_github/ui/home/notification/notification_change_model.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | import 'notification_vm.dart'; 10 | 11 | class NotificationTabPage extends BasePage<_NotificationPageState> { 12 | const NotificationTabPage(); 13 | 14 | @override 15 | _NotificationPageState createBaseState() => _NotificationPageState(); 16 | } 17 | 18 | class _NotificationPageState 19 | extends BaseState { 20 | @override 21 | NotificationVM createVM() => NotificationVM(context); 22 | 23 | @override 24 | Widget createContentWidget() { 25 | return RefreshIndicator( 26 | onRefresh: vm.handlerRefresh, 27 | child: Scrollbar( 28 | child: ListView.builder( 29 | itemCount: vm.notifications?.length ?? 0, 30 | itemBuilder: (BuildContext context, int index) { 31 | final NotificationModel item = vm.notifications[index]; 32 | return ChangeNotifierProvider( 33 | create: (context) => NotificationChangeModel(item.unread), 34 | child: Consumer( 35 | builder: (context, item, child) => GestureDetector( 36 | onTap: () { 37 | vm.contentTap(index, context); 38 | }, 39 | child: Container( 40 | color: item.unread 41 | ? Colors.white 42 | : Color.fromARGB(13, 0, 0, 0), 43 | padding: 44 | EdgeInsets.only(left: 15.0, top: 10.0, right: 15.0), 45 | child: child), 46 | ), 47 | child: Column( 48 | crossAxisAlignment: CrossAxisAlignment.start, 49 | children: [ 50 | Text( 51 | item.repository.fullName, 52 | style: TextStyle( 53 | fontWeight: FontWeight.bold, 54 | fontSize: 16.0, 55 | color: item.unread 56 | ? Colors.black87 57 | : Color.fromARGB(255, 102, 102, 102), 58 | ), 59 | ), 60 | Row( 61 | children: [ 62 | Padding( 63 | padding: EdgeInsets.only(top: 5.0), 64 | child: Image.asset( 65 | vm.getTypeFlagSrc(item.subject.type), 66 | width: 18.0, 67 | height: 18.0, 68 | ), 69 | ), 70 | Expanded( 71 | child: Padding( 72 | padding: EdgeInsets.only(top: 5.0, left: 10.0), 73 | child: Text( 74 | item.subject.title, 75 | overflow: TextOverflow.ellipsis, 76 | maxLines: 1, 77 | style: TextStyle( 78 | fontSize: 14.0, 79 | color: item.unread 80 | ? Color.fromARGB(255, 17, 17, 17) 81 | : Color.fromARGB(255, 102, 102, 102), 82 | ), 83 | ), 84 | ), 85 | ), 86 | ], 87 | ), 88 | Padding( 89 | padding: EdgeInsets.only(top: 10.0), 90 | child: Divider( 91 | height: 1.0, 92 | endIndent: 0.0, 93 | color: Color.fromARGB(255, 207, 216, 220), 94 | ), 95 | ), 96 | ], 97 | ), 98 | ), 99 | ); 100 | }, 101 | ), 102 | ), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/ui/login/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_github/ui/base/base_page.dart'; 4 | import 'package:flutter_github/ui/base/base_state.dart'; 5 | import 'package:flutter_github/ui/base/base_widget.dart'; 6 | import 'package:flutter_github/ui/login/login_vm.dart'; 7 | 8 | class LoginPage extends BasePage<_LoginState> { 9 | @override 10 | _LoginState createBaseState() => _LoginState(); 11 | } 12 | 13 | class _LoginState extends BaseState 14 | with WidgetsBindingObserver { 15 | final _passwordFocusNode = FocusNode(); 16 | final _usernameTextEditingController = TextEditingController(); 17 | final _passwordTextEditingController = TextEditingController(); 18 | 19 | AppLifecycleState _lastLifecycleState; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | WidgetsBinding.instance.addObserver(this); 25 | } 26 | 27 | @override 28 | void didChangeAppLifecycleState(AppLifecycleState state) { 29 | setState(() { 30 | _lastLifecycleState = state; 31 | }); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | WidgetsBinding.instance.removeObserver(this); 37 | super.dispose(); 38 | } 39 | 40 | @override 41 | LoginVM createVM() => LoginVM(context); 42 | 43 | @override 44 | Widget createContentWidget() { 45 | vm.callLoginCode(_lastLifecycleState); 46 | return Center( 47 | child: Column( 48 | crossAxisAlignment: CrossAxisAlignment.center, 49 | children: [ 50 | Padding( 51 | padding: EdgeInsets.only(top: 50.0), 52 | child: Image.asset( 53 | 'images/app_welcome.png', 54 | width: 100, 55 | height: 83, 56 | ), 57 | ), 58 | Padding( 59 | padding: EdgeInsets.only(top: 20.0), 60 | child: Text( 61 | 'Sign in to Github', 62 | style: TextStyle( 63 | fontSize: 25.0, 64 | fontWeight: FontWeight.bold, 65 | color: Color.fromARGB(255, 28, 49, 58)), 66 | ), 67 | ), 68 | Padding( 69 | padding: EdgeInsets.only(top: 50.0, left: 35.0, right: 35.0), 70 | child: TextField( 71 | controller: _usernameTextEditingController, 72 | textInputAction: TextInputAction.next, 73 | decoration: InputDecoration( 74 | labelText: 'Username or email address', 75 | ), 76 | onChanged: (content) { 77 | setState(() { 78 | vm.username = content; 79 | }); 80 | }, 81 | onEditingComplete: () => 82 | FocusScope.of(context).requestFocus(_passwordFocusNode), 83 | ), 84 | ), 85 | Padding( 86 | padding: EdgeInsets.only(top: 15.0, left: 35.0, right: 35.0), 87 | child: TextField( 88 | controller: _passwordTextEditingController, 89 | keyboardType: TextInputType.visiblePassword, 90 | decoration: InputDecoration( 91 | labelText: 'Password', 92 | ), 93 | onChanged: (content) { 94 | setState(() { 95 | vm.password = content; 96 | }); 97 | }, 98 | obscureText: true, 99 | focusNode: _passwordFocusNode, 100 | ), 101 | ), 102 | Padding( 103 | padding: EdgeInsets.only(top: 45.0), 104 | child: MaterialButton( 105 | shape: RoundedRectangleBorder( 106 | borderRadius: BorderRadius.circular(5.0)), 107 | color: Color.fromARGB(255, 139, 195, 74), 108 | focusColor: Color.fromARGB(255, 76, 145, 80), 109 | disabledColor: Color.fromARGB(77, 139, 195, 74), 110 | minWidth: 280.0, 111 | height: 45.0, 112 | child: Text( 113 | 'Sign in', 114 | style: TextStyle(fontSize: 16.0), 115 | ), 116 | disabledTextColor: Colors.white, 117 | textColor: Colors.white, 118 | onPressed: vm.signInOnPress(), 119 | )), 120 | Padding( 121 | padding: EdgeInsets.only(top: 25.0), 122 | child: MaterialButton( 123 | shape: RoundedRectangleBorder( 124 | borderRadius: BorderRadius.circular(5.0)), 125 | color: Color.fromARGB(255, 3, 169, 244), 126 | focusColor: Color.fromARGB(255, 0, 188, 211), 127 | minWidth: 280.0, 128 | height: 45.0, 129 | child: Text( 130 | 'Authorize', 131 | style: TextStyle(fontSize: 16.0), 132 | ), 133 | textColor: Colors.white, 134 | onPressed: vm.authorization(), 135 | )) 136 | ], 137 | ), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/ui/home/user/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_github/routes/app_routes.dart'; 4 | import 'package:flutter_github/ui/base/base_page.dart'; 5 | import 'package:flutter_github/ui/base/base_state.dart'; 6 | import 'package:flutter_github/ui/home/user/user_vm.dart'; 7 | 8 | class UserTabPage extends BasePage<_UserTabPageState> { 9 | const UserTabPage(); 10 | 11 | @override 12 | _UserTabPageState createBaseState() => _UserTabPageState(); 13 | } 14 | 15 | class _UserTabPageState extends BaseState { 16 | @override 17 | UserVM createVM() => UserVM(context); 18 | 19 | @override 20 | Widget createContentWidget() { 21 | return Column( 22 | children: [ 23 | Row( 24 | children: [ 25 | Padding( 26 | padding: EdgeInsets.only(top: 20.0, left: 15.0, right: 25.0), 27 | child: _buildUserImage(), 28 | ), 29 | Expanded( 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Padding( 34 | padding: EdgeInsets.only(top: 10.0, right: 15.0), 35 | child: Text( 36 | vm.userModel?.name ?? '', 37 | style: TextStyle( 38 | fontSize: 22.0, 39 | fontWeight: FontWeight.bold, 40 | color: Colors.grey[600]), 41 | ), 42 | ), 43 | Padding( 44 | padding: EdgeInsets.only(top: 5.0, right: 15.0), 45 | child: Text( 46 | vm.userModel?.bio ?? '', 47 | style: TextStyle( 48 | fontSize: 14.0, 49 | color: Colors.grey[600], 50 | ), 51 | overflow: TextOverflow.ellipsis, 52 | maxLines: 2, 53 | ), 54 | ), 55 | Padding( 56 | padding: EdgeInsets.only(top: 5.0, right: 15.0), 57 | child: Text( 58 | vm.userModel?.location ?? '', 59 | style: TextStyle(fontSize: 14.0, color: Colors.grey[600]), 60 | ), 61 | ) 62 | ], 63 | ), 64 | ), 65 | ], 66 | ), 67 | Padding( 68 | padding: EdgeInsets.only(top: 15.0, bottom: 10.0), 69 | child: Divider( 70 | height: 1.0, 71 | color: Color.fromARGB(255, 207, 216, 220), 72 | ), 73 | ), 74 | Row( 75 | mainAxisAlignment: MainAxisAlignment.spaceAround, 76 | children: [ 77 | _buildContactWidget(vm.userModel?.publicRepos ?? 0, 'Repos', () { 78 | Navigator.of(context).pushNamed(repositoryRoute.routeName); 79 | }), 80 | _buildContactWidget(vm.userModel?.followers ?? 0, 'Followers', () { 81 | Navigator.of(context).pushNamed(followersRoute.routeName); 82 | }), 83 | _buildContactWidget(vm.userModel?.following ?? 0, 'Following', () { 84 | Navigator.of(context).pushNamed(followingRoute.routeName); 85 | }) 86 | ], 87 | ), 88 | Expanded( 89 | child: Align( 90 | alignment: Alignment.bottomCenter, 91 | child: Padding( 92 | padding: EdgeInsets.only(bottom: 25.0), 93 | child: MaterialButton( 94 | shape: RoundedRectangleBorder( 95 | side: BorderSide(color: Colors.grey), 96 | borderRadius: BorderRadius.circular(5.0)), 97 | minWidth: 280.0, 98 | height: 45.0, 99 | child: Text( 100 | 'logout', 101 | style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w500), 102 | ), 103 | onPressed: vm.logout, 104 | ), 105 | ), 106 | ), 107 | ), 108 | ], 109 | ); 110 | } 111 | 112 | Widget _buildUserImage() { 113 | if (vm.userModel == null) { 114 | return Image.asset( 115 | 'images/app_welcome.png', 116 | width: 120.0, 117 | height: 120.0, 118 | ); 119 | } else { 120 | return FadeInImage.assetNetwork( 121 | image: vm.userModel?.avatarUrl ?? '', 122 | placeholder: 'images/app_welcome.png', 123 | width: 120.0, 124 | height: 120.0, 125 | ); 126 | } 127 | } 128 | 129 | Widget _buildContactWidget( 130 | int num, String category, GestureTapCallback tapCallback) { 131 | return GestureDetector( 132 | onTap: tapCallback, 133 | behavior: HitTestBehavior.opaque, 134 | child: Column( 135 | children: [ 136 | Text( 137 | '$num', 138 | style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), 139 | ), 140 | Padding( 141 | padding: EdgeInsets.only(top: 5.0, bottom: 10.0), 142 | child: Text( 143 | category, 144 | style: TextStyle( 145 | fontWeight: FontWeight.bold, color: Colors.grey[600]), 146 | ), 147 | ), 148 | ], 149 | ), 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/widget/repository_item_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_github/common/colors.dart'; 4 | import 'package:flutter_github/model/notification_model.dart'; 5 | import 'package:flutter_github/ui/webview/webview.dart'; 6 | import 'package:flutter_github/widget/text_with_side.dart'; 7 | import 'package:toast/toast.dart'; 8 | 9 | class RepositoryItemView extends StatelessWidget { 10 | final Repository _item; 11 | final GestureTapCallback tapCallback; 12 | 13 | RepositoryItemView(this._item, {this.tapCallback}); 14 | 15 | final bottomInfoTextStyle = 16 | TextStyle(fontSize: 12.0, color: color_999, fontStyle: FontStyle.italic); 17 | 18 | _goToRepositoryDetail(BuildContext context) { 19 | return () { 20 | Navigator.push(context, MaterialPageRoute(builder: (_) { 21 | return WebViewPage(); 22 | }, settings: RouteSettings(arguments: { WebViewPage.ARGS_TITLE: _item.name, WebViewPage.ARGS_URL: _item.htmlUrl }))); 23 | }; 24 | } 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final _licenseLength = _item.license?.name?.length ?? 0; 29 | final _license = _item.license?.name 30 | ?.substring(0, _licenseLength > 15 ? 15 : _licenseLength) ?? 31 | ''; 32 | final _nameLength = _item.name.length; 33 | final _name = _item.name.substring(0, _nameLength > 20 ? 20 : _nameLength); 34 | return GestureDetector( 35 | onTap: tapCallback ?? _goToRepositoryDetail(context), 36 | behavior: HitTestBehavior.opaque, 37 | child: Container( 38 | padding: EdgeInsets.only(left: 15.0, right: 15.0, top: 20.0), 39 | child: Column( 40 | crossAxisAlignment: CrossAxisAlignment.start, 41 | children: [ 42 | Row( 43 | children: [ 44 | Text( 45 | _name, 46 | style: TextStyle( 47 | fontSize: 20.0, 48 | fontWeight: FontWeight.bold, 49 | color: Color.fromARGB(255, 33, 117, 243), 50 | ), 51 | ), 52 | Visibility( 53 | visible: _item.private, 54 | child: Container( 55 | margin: EdgeInsets.only(left: 10.0), 56 | padding: 57 | EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0), 58 | child: Text( 59 | 'private', 60 | style: TextStyle( 61 | fontSize: 12.0, 62 | color: color_666, 63 | ), 64 | ), 65 | decoration: BoxDecoration( 66 | border: Border.all(width: 1.0, color: Colors.grey[400]), 67 | borderRadius: BorderRadius.all( 68 | Radius.circular(2.0), 69 | ), 70 | ), 71 | ), 72 | ), 73 | ], 74 | ), 75 | Padding( 76 | padding: EdgeInsets.only(top: 10.0), 77 | child: Text( 78 | _item.description ?? '', 79 | style: TextStyle( 80 | fontSize: 14.0, 81 | color: color_666, 82 | ), 83 | maxLines: 4, 84 | overflow: TextOverflow.ellipsis, 85 | ), 86 | ), 87 | Padding( 88 | padding: EdgeInsets.only(top: 10.0), 89 | child: Row( 90 | children: [ 91 | _buildBottomInfo(_item?.language ?? '', 'images/circle.png', 92 | _item.language?.isNotEmpty ?? false), 93 | _buildBottomInfo(_item.stargazersCount.toString(), 94 | 'images/start.png', _item.stargazersCount > 0), 95 | _buildBottomInfo(_item.forksCount.toString(), 96 | 'images/git_repo_forked.png', _item.forksCount > 0), 97 | _buildBottomInfo(_license, 'images/license.png', 98 | _item.license?.name?.isNotEmpty ?? false), 99 | Expanded( 100 | child: Text( 101 | _updateAtContent(_item.updatedAt), 102 | maxLines: 1, 103 | overflow: TextOverflow.ellipsis, 104 | style: bottomInfoTextStyle, 105 | ), 106 | ), 107 | ], 108 | ), 109 | ), 110 | Padding( 111 | padding: EdgeInsets.only(top: 15.0), 112 | child: Divider( 113 | thickness: 1.0, 114 | color: colorEAEAEA, 115 | height: 1.0, 116 | endIndent: 0.0, 117 | ), 118 | ) 119 | ], 120 | ), 121 | ), 122 | ); 123 | } 124 | 125 | Widget _buildBottomInfo(String content, String imagePath, bool visible) { 126 | return Visibility( 127 | visible: visible, 128 | child: Padding( 129 | padding: EdgeInsets.only(right: 10.0), 130 | child: TextWithSide( 131 | side: Image.asset( 132 | imagePath, 133 | width: 12.0, 134 | height: 12.0, 135 | ), 136 | text: Text( 137 | content, 138 | style: bottomInfoTextStyle, 139 | maxLines: 1, 140 | overflow: TextOverflow.ellipsis, 141 | ), 142 | sideDistance: 5.0, 143 | ), 144 | ), 145 | ); 146 | } 147 | 148 | _updateAtContent(String updateAt) { 149 | if (updateAt != null && updateAt.indexOf('T') >= 0) { 150 | return updateAt.substring(0, updateAt.indexOf('T')); 151 | } 152 | return ''; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/model/user_model.dart: -------------------------------------------------------------------------------- 1 | // To parse this JSON data, do 2 | // 3 | // final userModel = userModelFromJson(jsonString); 4 | 5 | import 'dart:convert'; 6 | 7 | UserModel userModelFromJson(String str) => UserModel.fromJson(json.decode(str)); 8 | 9 | String userModelToJson(UserModel data) => json.encode(data.toJson()); 10 | 11 | class UserModel { 12 | String login; 13 | int id; 14 | String nodeId; 15 | String avatarUrl; 16 | String gravatarId; 17 | String url; 18 | String htmlUrl; 19 | String followersUrl; 20 | String followingUrl; 21 | String gistsUrl; 22 | String starredUrl; 23 | String subscriptionsUrl; 24 | String organizationsUrl; 25 | String reposUrl; 26 | String eventsUrl; 27 | String receivedEventsUrl; 28 | String type; 29 | bool siteAdmin; 30 | String name; 31 | dynamic company; 32 | String blog; 33 | String location; 34 | String email; 35 | dynamic hireable; 36 | String bio; 37 | int publicRepos; 38 | int publicGists; 39 | int followers; 40 | int following; 41 | DateTime createdAt; 42 | DateTime updatedAt; 43 | int privateGists; 44 | int totalPrivateRepos; 45 | int ownedPrivateRepos; 46 | int diskUsage; 47 | int collaborators; 48 | bool twoFactorAuthentication; 49 | Plan plan; 50 | 51 | UserModel({ 52 | this.login, 53 | this.id, 54 | this.nodeId, 55 | this.avatarUrl, 56 | this.gravatarId, 57 | this.url, 58 | this.htmlUrl, 59 | this.followersUrl, 60 | this.followingUrl, 61 | this.gistsUrl, 62 | this.starredUrl, 63 | this.subscriptionsUrl, 64 | this.organizationsUrl, 65 | this.reposUrl, 66 | this.eventsUrl, 67 | this.receivedEventsUrl, 68 | this.type, 69 | this.siteAdmin, 70 | this.name, 71 | this.company, 72 | this.blog, 73 | this.location, 74 | this.email, 75 | this.hireable, 76 | this.bio, 77 | this.publicRepos, 78 | this.publicGists, 79 | this.followers, 80 | this.following, 81 | this.createdAt, 82 | this.updatedAt, 83 | this.privateGists, 84 | this.totalPrivateRepos, 85 | this.ownedPrivateRepos, 86 | this.diskUsage, 87 | this.collaborators, 88 | this.twoFactorAuthentication, 89 | this.plan, 90 | }); 91 | 92 | factory UserModel.fromJson(Map json) => UserModel( 93 | login: json["login"], 94 | id: json["id"], 95 | nodeId: json["node_id"], 96 | avatarUrl: json["avatar_url"], 97 | gravatarId: json["gravatar_id"], 98 | url: json["url"], 99 | htmlUrl: json["html_url"], 100 | followersUrl: json["followers_url"], 101 | followingUrl: json["following_url"], 102 | gistsUrl: json["gists_url"], 103 | starredUrl: json["starred_url"], 104 | subscriptionsUrl: json["subscriptions_url"], 105 | organizationsUrl: json["organizations_url"], 106 | reposUrl: json["repos_url"], 107 | eventsUrl: json["events_url"], 108 | receivedEventsUrl: json["received_events_url"], 109 | type: json["type"], 110 | siteAdmin: json["site_admin"], 111 | name: json["name"], 112 | company: json["company"], 113 | blog: json["blog"], 114 | location: json["location"], 115 | email: json["email"], 116 | hireable: json["hireable"], 117 | bio: json["bio"], 118 | publicRepos: json["public_repos"], 119 | publicGists: json["public_gists"], 120 | followers: json["followers"], 121 | following: json["following"], 122 | createdAt: DateTime.parse(json["created_at"]), 123 | updatedAt: DateTime.parse(json["updated_at"]), 124 | privateGists: json["private_gists"], 125 | totalPrivateRepos: json["total_private_repos"], 126 | ownedPrivateRepos: json["owned_private_repos"], 127 | diskUsage: json["disk_usage"], 128 | collaborators: json["collaborators"], 129 | twoFactorAuthentication: json["two_factor_authentication"], 130 | plan: Plan.fromJson(json["plan"]), 131 | ); 132 | 133 | Map toJson() => { 134 | "login": login, 135 | "id": id, 136 | "node_id": nodeId, 137 | "avatar_url": avatarUrl, 138 | "gravatar_id": gravatarId, 139 | "url": url, 140 | "html_url": htmlUrl, 141 | "followers_url": followersUrl, 142 | "following_url": followingUrl, 143 | "gists_url": gistsUrl, 144 | "starred_url": starredUrl, 145 | "subscriptions_url": subscriptionsUrl, 146 | "organizations_url": organizationsUrl, 147 | "repos_url": reposUrl, 148 | "events_url": eventsUrl, 149 | "received_events_url": receivedEventsUrl, 150 | "type": type, 151 | "site_admin": siteAdmin, 152 | "name": name, 153 | "company": company, 154 | "blog": blog, 155 | "location": location, 156 | "email": email, 157 | "hireable": hireable, 158 | "bio": bio, 159 | "public_repos": publicRepos, 160 | "public_gists": publicGists, 161 | "followers": followers, 162 | "following": following, 163 | "created_at": createdAt.toIso8601String(), 164 | "updated_at": updatedAt.toIso8601String(), 165 | "private_gists": privateGists, 166 | "total_private_repos": totalPrivateRepos, 167 | "owned_private_repos": ownedPrivateRepos, 168 | "disk_usage": diskUsage, 169 | "collaborators": collaborators, 170 | "two_factor_authentication": twoFactorAuthentication, 171 | "plan": plan.toJson(), 172 | }; 173 | } 174 | 175 | class Plan { 176 | String name; 177 | int space; 178 | int collaborators; 179 | int privateRepos; 180 | 181 | Plan({ 182 | this.name, 183 | this.space, 184 | this.collaborators, 185 | this.privateRepos, 186 | }); 187 | 188 | factory Plan.fromJson(Map json) => Plan( 189 | name: json["name"], 190 | space: json["space"], 191 | collaborators: json["collaborators"], 192 | privateRepos: json["private_repos"], 193 | ); 194 | 195 | Map toJson() => { 196 | "name": name, 197 | "space": space, 198 | "collaborators": collaborators, 199 | "private_repos": privateRepos, 200 | }; 201 | } 202 | -------------------------------------------------------------------------------- /lib/ui/home/search/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_github/common/colors.dart'; 3 | import 'package:flutter_github/model/notification_model.dart'; 4 | import 'package:flutter_github/model/search_model.dart'; 5 | import 'package:flutter_github/ui/base/base_page.dart'; 6 | import 'package:flutter_github/ui/base/base_state.dart'; 7 | import 'package:flutter_github/ui/home/search/search_vm.dart'; 8 | import 'package:flutter_github/widget/repository_item_view.dart'; 9 | 10 | class SearchTabPage extends BasePage { 11 | const SearchTabPage(); 12 | 13 | @override 14 | _SearchPageState createBaseState() => _SearchPageState(); 15 | } 16 | 17 | typedef SearchListWidget = Widget Function(SearchModel); 18 | 19 | class _SearchPageState extends BaseState { 20 | _SearchDelegate _delegate; 21 | 22 | SearchListWidget _getListWidget; 23 | 24 | @override 25 | void initState() { 26 | super.initState(); 27 | _getListWidget = (model) { 28 | return ListView.builder( 29 | padding: EdgeInsets.only(bottom: 10.0), 30 | itemCount: model?.items?.length ?? 0, 31 | itemBuilder: (BuildContext context, int index) { 32 | return RepositoryItemView(model.items[index]); 33 | }, 34 | ); 35 | }; 36 | _delegate = _SearchDelegate(vm, _getListWidget); 37 | } 38 | 39 | @override 40 | SearchVM createVM() => SearchVM(context); 41 | 42 | @override 43 | Widget createContentWidget() { 44 | return Column( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | Padding( 48 | padding: EdgeInsets.only(left: 5.0, top: 5.0), 49 | child: IconButton( 50 | onPressed: () { 51 | showSearch(context: context, delegate: _delegate); 52 | }, 53 | icon: Icon( 54 | Icons.search, 55 | color: Colors.grey[700], 56 | size: 25.0, 57 | ), 58 | ), 59 | ), 60 | Expanded( 61 | child: Scrollbar( 62 | child: _getListWidget(vm.searchModel), 63 | ), 64 | ), 65 | ], 66 | ); 67 | } 68 | } 69 | 70 | class _SearchDelegate extends SearchDelegate { 71 | SearchVM _searchVM; 72 | 73 | // String _query; 74 | ValueNotifier _searchModelNotifier; 75 | SearchListWidget _getSearchListWidget; 76 | final List _hotSuggestionsList = const [ 77 | 'android-api-analysis', 78 | 'AwesomeGithub', 79 | 'flutter_github' 80 | ]; 81 | List _suggestionsList; 82 | final int _maxSuggestionsSize = 6; 83 | 84 | _SearchDelegate(this._searchVM, this._getSearchListWidget) { 85 | _searchModelNotifier = ValueNotifier(_searchVM.searchModel); 86 | } 87 | 88 | final bottomInfoTextStyle = 89 | TextStyle(fontSize: 12.0, color: color_999, fontStyle: FontStyle.italic); 90 | 91 | @override 92 | List buildActions(BuildContext context) { 93 | return query.isEmpty 94 | ? null 95 | : [ 96 | IconButton( 97 | tooltip: 'Clear', 98 | icon: const Icon(Icons.clear), 99 | onPressed: () { 100 | query = ''; 101 | showSuggestions(context); 102 | }, 103 | ), 104 | ]; 105 | } 106 | 107 | @override 108 | Widget buildLeading(BuildContext context) { 109 | return IconButton( 110 | tooltip: 'Back', 111 | icon: AnimatedIcon( 112 | icon: AnimatedIcons.menu_arrow, 113 | progress: transitionAnimation, 114 | ), 115 | onPressed: () { 116 | close(context, null); 117 | }, 118 | ); 119 | } 120 | 121 | @override 122 | void showResults(BuildContext context) { 123 | if (!_hotSuggestionsList.contains(query)) { 124 | if (_suggestionsList.contains(query)) { 125 | _suggestionsList.remove(query); 126 | } 127 | if (_suggestionsList.length >= _maxSuggestionsSize) { 128 | _suggestionsList.removeLast(); 129 | } 130 | _suggestionsList.insert(_hotSuggestionsList.length, query); 131 | } 132 | _searchVM.search(query); 133 | close(context, null); 134 | } 135 | 136 | @override 137 | Widget buildResults(BuildContext context) { 138 | // if (_query != query || _searchVM.searchModel == null) { 139 | // _query = query; 140 | // _searchModelNotifier.value = null; 141 | // _searchVM.search(query).then((success) { 142 | // _searchModelNotifier.value = _searchVM.searchModel ?? SearchModel(); 143 | // }); 144 | // } 145 | return ValueListenableBuilder( 146 | valueListenable: _searchModelNotifier, 147 | builder: (BuildContext context, SearchModel model, Widget child) { 148 | return model == null 149 | ? Center( 150 | child: Image.asset('images/loading.gif'), 151 | ) 152 | : _getSearchListWidget(model); 153 | }, 154 | ); 155 | } 156 | 157 | @override 158 | Widget buildSuggestions(BuildContext context) { 159 | if (_suggestionsList == null) { 160 | _suggestionsList = [..._hotSuggestionsList]; 161 | } 162 | final Iterable suggestions = query.isEmpty 163 | ? _suggestionsList 164 | : _suggestionsList.where((String i) => i.startsWith(query)); 165 | return _SuggestionList( 166 | query: query, 167 | suggestions: suggestions.toList(), 168 | onSelected: (String suggestions) { 169 | query = suggestions; 170 | showResults(context); 171 | }, 172 | ); 173 | } 174 | } 175 | 176 | class _SuggestionList extends StatelessWidget { 177 | const _SuggestionList({this.suggestions, this.query, this.onSelected}); 178 | 179 | final List suggestions; 180 | final String query; 181 | final ValueChanged onSelected; 182 | 183 | @override 184 | Widget build(BuildContext context) { 185 | final ThemeData theme = Theme.of(context); 186 | return ListView.builder( 187 | itemCount: suggestions.length, 188 | itemBuilder: (BuildContext context, int i) { 189 | final String suggestion = suggestions[i]; 190 | return ListTile( 191 | leading: query.isEmpty ? const Icon(Icons.history) : const Icon(null), 192 | title: RichText( 193 | text: TextSpan( 194 | text: suggestion.substring(0, query.length), 195 | style: 196 | theme.textTheme.subhead.copyWith(fontWeight: FontWeight.bold), 197 | children: [ 198 | TextSpan( 199 | text: suggestion.substring(query.length), 200 | style: theme.textTheme.subhead, 201 | ), 202 | ], 203 | ), 204 | ), 205 | onTap: () { 206 | onSelected(suggestion); 207 | }, 208 | ); 209 | }, 210 | ); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.2" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.14.11" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.3" 60 | cupertino_icons: 61 | dependency: "direct main" 62 | description: 63 | name: cupertino_icons 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "0.1.3" 67 | dio: 68 | dependency: "direct main" 69 | description: 70 | name: dio 71 | url: "https://pub.flutter-io.cn" 72 | source: hosted 73 | version: "3.0.7" 74 | flutter: 75 | dependency: "direct main" 76 | description: flutter 77 | source: sdk 78 | version: "0.0.0" 79 | flutter_test: 80 | dependency: "direct dev" 81 | description: flutter 82 | source: sdk 83 | version: "0.0.0" 84 | flutter_web_plugins: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.0" 89 | http: 90 | dependency: "direct main" 91 | description: 92 | name: http 93 | url: "https://pub.flutter-io.cn" 94 | source: hosted 95 | version: "0.12.0+4" 96 | http_parser: 97 | dependency: transitive 98 | description: 99 | name: http_parser 100 | url: "https://pub.flutter-io.cn" 101 | source: hosted 102 | version: "3.1.3" 103 | image: 104 | dependency: transitive 105 | description: 106 | name: image 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "2.1.4" 110 | matcher: 111 | dependency: transitive 112 | description: 113 | name: matcher 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "0.12.6" 117 | meta: 118 | dependency: transitive 119 | description: 120 | name: meta 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "1.1.8" 124 | path: 125 | dependency: transitive 126 | description: 127 | name: path 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "1.6.4" 131 | pedantic: 132 | dependency: transitive 133 | description: 134 | name: pedantic 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.8.0+1" 138 | petitparser: 139 | dependency: transitive 140 | description: 141 | name: petitparser 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "2.4.0" 145 | plugin_platform_interface: 146 | dependency: transitive 147 | description: 148 | name: plugin_platform_interface 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "1.0.2" 152 | provider: 153 | dependency: "direct main" 154 | description: 155 | name: provider 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "3.2.0" 159 | quiver: 160 | dependency: transitive 161 | description: 162 | name: quiver 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "2.0.5" 166 | shared_preferences: 167 | dependency: "direct main" 168 | description: 169 | name: shared_preferences 170 | url: "https://pub.flutter-io.cn" 171 | source: hosted 172 | version: "0.5.6+1" 173 | shared_preferences_macos: 174 | dependency: transitive 175 | description: 176 | name: shared_preferences_macos 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "0.0.1+6" 180 | shared_preferences_platform_interface: 181 | dependency: transitive 182 | description: 183 | name: shared_preferences_platform_interface 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "1.0.3" 187 | shared_preferences_web: 188 | dependency: transitive 189 | description: 190 | name: shared_preferences_web 191 | url: "https://pub.flutter-io.cn" 192 | source: hosted 193 | version: "0.1.2+4" 194 | sky_engine: 195 | dependency: transitive 196 | description: flutter 197 | source: sdk 198 | version: "0.0.99" 199 | source_span: 200 | dependency: transitive 201 | description: 202 | name: source_span 203 | url: "https://pub.flutter-io.cn" 204 | source: hosted 205 | version: "1.5.5" 206 | stack_trace: 207 | dependency: transitive 208 | description: 209 | name: stack_trace 210 | url: "https://pub.flutter-io.cn" 211 | source: hosted 212 | version: "1.9.3" 213 | stream_channel: 214 | dependency: transitive 215 | description: 216 | name: stream_channel 217 | url: "https://pub.flutter-io.cn" 218 | source: hosted 219 | version: "2.0.0" 220 | string_scanner: 221 | dependency: transitive 222 | description: 223 | name: string_scanner 224 | url: "https://pub.flutter-io.cn" 225 | source: hosted 226 | version: "1.0.5" 227 | term_glyph: 228 | dependency: transitive 229 | description: 230 | name: term_glyph 231 | url: "https://pub.flutter-io.cn" 232 | source: hosted 233 | version: "1.1.0" 234 | test_api: 235 | dependency: transitive 236 | description: 237 | name: test_api 238 | url: "https://pub.flutter-io.cn" 239 | source: hosted 240 | version: "0.2.11" 241 | toast: 242 | dependency: "direct main" 243 | description: 244 | name: toast 245 | url: "https://pub.flutter-io.cn" 246 | source: hosted 247 | version: "0.1.5" 248 | typed_data: 249 | dependency: transitive 250 | description: 251 | name: typed_data 252 | url: "https://pub.flutter-io.cn" 253 | source: hosted 254 | version: "1.1.6" 255 | url_launcher: 256 | dependency: "direct main" 257 | description: 258 | name: url_launcher 259 | url: "https://pub.flutter-io.cn" 260 | source: hosted 261 | version: "5.4.1" 262 | url_launcher_macos: 263 | dependency: transitive 264 | description: 265 | name: url_launcher_macos 266 | url: "https://pub.flutter-io.cn" 267 | source: hosted 268 | version: "0.0.1+4" 269 | url_launcher_platform_interface: 270 | dependency: transitive 271 | description: 272 | name: url_launcher_platform_interface 273 | url: "https://pub.flutter-io.cn" 274 | source: hosted 275 | version: "1.0.6" 276 | url_launcher_web: 277 | dependency: transitive 278 | description: 279 | name: url_launcher_web 280 | url: "https://pub.flutter-io.cn" 281 | source: hosted 282 | version: "0.1.1+1" 283 | vector_math: 284 | dependency: transitive 285 | description: 286 | name: vector_math 287 | url: "https://pub.flutter-io.cn" 288 | source: hosted 289 | version: "2.0.8" 290 | webview_flutter: 291 | dependency: "direct main" 292 | description: 293 | name: webview_flutter 294 | url: "https://pub.flutter-io.cn" 295 | source: hosted 296 | version: "0.3.19+8" 297 | xml: 298 | dependency: transitive 299 | description: 300 | name: xml 301 | url: "https://pub.flutter-io.cn" 302 | source: hosted 303 | version: "3.5.0" 304 | sdks: 305 | dart: ">2.4.0 <3.0.0" 306 | flutter: ">=1.12.13+hotfix.5 <2.0.0" 307 | -------------------------------------------------------------------------------- /lib/model/notification_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | List notificationModelFromJson(String str) => 4 | List.from( 5 | json.decode(str).map((x) => NotificationModel.fromJson(x))); 6 | 7 | String notificationModelToJson(List data) => 8 | json.encode(List.from(data.map((x) => x.toJson()))); 9 | 10 | class NotificationModel { 11 | String id; 12 | Repository repository; 13 | Subject subject; 14 | String reason; 15 | bool unread; 16 | String url; 17 | 18 | NotificationModel({ 19 | this.id, 20 | this.repository, 21 | this.subject, 22 | this.reason, 23 | this.unread, 24 | this.url, 25 | }); 26 | 27 | factory NotificationModel.fromJson(Map json) => 28 | NotificationModel( 29 | id: json["id"], 30 | repository: Repository.fromJson(json["repository"]), 31 | subject: Subject.fromJson(json["subject"]), 32 | reason: json["reason"], 33 | unread: json["unread"], 34 | url: json["url"], 35 | ); 36 | 37 | Map toJson() => { 38 | "id": id, 39 | "repository": repository.toJson(), 40 | "subject": subject.toJson(), 41 | "reason": reason, 42 | "unread": unread, 43 | "url": url, 44 | }; 45 | } 46 | 47 | List repositoryListFromJson(dynamic list) => 48 | List.from(list.map((x) => Repository.fromJson(x))); 49 | 50 | class Repository { 51 | int id; 52 | String nodeId; 53 | String name; 54 | String fullName; 55 | Owner owner; 56 | bool private; 57 | String htmlUrl; 58 | String description; 59 | bool fork; 60 | String url; 61 | String archiveUrl; 62 | String assigneesUrl; 63 | String blobsUrl; 64 | String branchesUrl; 65 | String collaboratorsUrl; 66 | String commentsUrl; 67 | String commitsUrl; 68 | String compareUrl; 69 | String contentsUrl; 70 | String contributorsUrl; 71 | String deploymentsUrl; 72 | String downloadsUrl; 73 | String eventsUrl; 74 | String forksUrl; 75 | String gitCommitsUrl; 76 | String gitRefsUrl; 77 | String gitTagsUrl; 78 | String gitUrl; 79 | String issueCommentUrl; 80 | String issueEventsUrl; 81 | String issuesUrl; 82 | String keysUrl; 83 | String labelsUrl; 84 | String languagesUrl; 85 | String mergesUrl; 86 | String milestonesUrl; 87 | String notificationsUrl; 88 | String pullsUrl; 89 | String releasesUrl; 90 | String sshUrl; 91 | String stargazersUrl; 92 | String statusesUrl; 93 | String subscribersUrl; 94 | String subscriptionUrl; 95 | String tagsUrl; 96 | String teamsUrl; 97 | String treesUrl; 98 | String createAt; 99 | String updatedAt; 100 | String pushedAt; 101 | int stargazersCount; 102 | int watchersCount; 103 | String homePage; 104 | String language; 105 | int forksCount; 106 | int openIssuesCount; 107 | License license; 108 | 109 | Repository({ 110 | this.id, 111 | this.nodeId, 112 | this.name, 113 | this.fullName, 114 | this.owner, 115 | this.private, 116 | this.htmlUrl, 117 | this.description, 118 | this.fork, 119 | this.url, 120 | this.archiveUrl, 121 | this.assigneesUrl, 122 | this.blobsUrl, 123 | this.branchesUrl, 124 | this.collaboratorsUrl, 125 | this.commentsUrl, 126 | this.commitsUrl, 127 | this.compareUrl, 128 | this.contentsUrl, 129 | this.contributorsUrl, 130 | this.deploymentsUrl, 131 | this.downloadsUrl, 132 | this.eventsUrl, 133 | this.forksUrl, 134 | this.gitCommitsUrl, 135 | this.gitRefsUrl, 136 | this.gitTagsUrl, 137 | this.gitUrl, 138 | this.issueCommentUrl, 139 | this.issueEventsUrl, 140 | this.issuesUrl, 141 | this.keysUrl, 142 | this.labelsUrl, 143 | this.languagesUrl, 144 | this.mergesUrl, 145 | this.milestonesUrl, 146 | this.notificationsUrl, 147 | this.pullsUrl, 148 | this.releasesUrl, 149 | this.sshUrl, 150 | this.stargazersUrl, 151 | this.statusesUrl, 152 | this.subscribersUrl, 153 | this.subscriptionUrl, 154 | this.tagsUrl, 155 | this.teamsUrl, 156 | this.treesUrl, 157 | this.createAt, 158 | this.updatedAt, 159 | this.pushedAt, 160 | this.stargazersCount, 161 | this.watchersCount, 162 | this.homePage, 163 | this.language, 164 | this.forksCount, 165 | this.openIssuesCount, 166 | this.license, 167 | }); 168 | 169 | factory Repository.fromJson(Map json) => Repository( 170 | id: json["id"], 171 | nodeId: json["node_id"], 172 | name: json["name"], 173 | fullName: json["full_name"], 174 | owner: Owner.fromJson(json["owner"]), 175 | private: json["private"], 176 | htmlUrl: json["html_url"], 177 | description: json["description"], 178 | fork: json["fork"], 179 | url: json["url"], 180 | archiveUrl: json["archive_url"], 181 | assigneesUrl: json["assignees_url"], 182 | blobsUrl: json["blobs_url"], 183 | branchesUrl: json["branches_url"], 184 | collaboratorsUrl: json["collaborators_url"], 185 | commentsUrl: json["comments_url"], 186 | commitsUrl: json["commits_url"], 187 | compareUrl: json["compare_url"], 188 | contentsUrl: json["contents_url"], 189 | contributorsUrl: json["contributors_url"], 190 | deploymentsUrl: json["deployments_url"], 191 | downloadsUrl: json["downloads_url"], 192 | eventsUrl: json["events_url"], 193 | forksUrl: json["forks_url"], 194 | gitCommitsUrl: json["git_commits_url"], 195 | gitRefsUrl: json["git_refs_url"], 196 | gitTagsUrl: json["git_tags_url"], 197 | gitUrl: json["git_url"], 198 | issueCommentUrl: json["issue_comment_url"], 199 | issueEventsUrl: json["issue_events_url"], 200 | issuesUrl: json["issues_url"], 201 | keysUrl: json["keys_url"], 202 | labelsUrl: json["labels_url"], 203 | languagesUrl: json["languages_url"], 204 | mergesUrl: json["merges_url"], 205 | milestonesUrl: json["milestones_url"], 206 | notificationsUrl: json["notifications_url"], 207 | pullsUrl: json["pulls_url"], 208 | releasesUrl: json["releases_url"], 209 | sshUrl: json["ssh_url"], 210 | stargazersUrl: json["stargazers_url"], 211 | statusesUrl: json["statuses_url"], 212 | subscribersUrl: json["subscribers_url"], 213 | subscriptionUrl: json["subscription_url"], 214 | tagsUrl: json["tags_url"], 215 | teamsUrl: json["teams_url"], 216 | treesUrl: json["trees_url"], 217 | createAt: json['created_at'], 218 | updatedAt: json['updated_at'], 219 | pushedAt: json['pushed_at'], 220 | stargazersCount: json['stargazers_count'], 221 | watchersCount: json['watchers_count'], 222 | homePage: json['homePage'], 223 | language: json['language'], 224 | forksCount: json['forks_count'], 225 | openIssuesCount: json['open_issues_count'], 226 | license: License.fromJson(json['license']), 227 | ); 228 | 229 | Map toJson() => { 230 | "id": id, 231 | "node_id": nodeId, 232 | "name": name, 233 | "full_name": fullName, 234 | "owner": owner.toJson(), 235 | "private": private, 236 | "html_url": htmlUrl, 237 | "description": description, 238 | "fork": fork, 239 | "url": url, 240 | "archive_url": archiveUrl, 241 | "assignees_url": assigneesUrl, 242 | "blobs_url": blobsUrl, 243 | "branches_url": branchesUrl, 244 | "collaborators_url": collaboratorsUrl, 245 | "comments_url": commentsUrl, 246 | "commits_url": commitsUrl, 247 | "compare_url": compareUrl, 248 | "contents_url": contentsUrl, 249 | "contributors_url": contributorsUrl, 250 | "deployments_url": deploymentsUrl, 251 | "downloads_url": downloadsUrl, 252 | "events_url": eventsUrl, 253 | "forks_url": forksUrl, 254 | "git_commits_url": gitCommitsUrl, 255 | "git_refs_url": gitRefsUrl, 256 | "git_tags_url": gitTagsUrl, 257 | "git_url": gitUrl, 258 | "issue_comment_url": issueCommentUrl, 259 | "issue_events_url": issueEventsUrl, 260 | "issues_url": issuesUrl, 261 | "keys_url": keysUrl, 262 | "labels_url": labelsUrl, 263 | "languages_url": languagesUrl, 264 | "merges_url": mergesUrl, 265 | "milestones_url": milestonesUrl, 266 | "notifications_url": notificationsUrl, 267 | "pulls_url": pullsUrl, 268 | "releases_url": releasesUrl, 269 | "ssh_url": sshUrl, 270 | "stargazers_url": stargazersUrl, 271 | "statuses_url": statusesUrl, 272 | "subscribers_url": subscribersUrl, 273 | "subscription_url": subscriptionUrl, 274 | "tags_url": tagsUrl, 275 | "teams_url": teamsUrl, 276 | "trees_url": treesUrl, 277 | }; 278 | } 279 | 280 | class License { 281 | String name; 282 | 283 | License({this.name}); 284 | 285 | factory License.fromJson(Map json) => 286 | json == null ? null : License(name: json['name']); 287 | } 288 | 289 | class Owner { 290 | String login; 291 | int id; 292 | String nodeId; 293 | String avatarUrl; 294 | String gravatarId; 295 | String url; 296 | String htmlUrl; 297 | String followersUrl; 298 | String followingUrl; 299 | String gistsUrl; 300 | String starredUrl; 301 | String subscriptionsUrl; 302 | String organizationsUrl; 303 | String reposUrl; 304 | String eventsUrl; 305 | String receivedEventsUrl; 306 | String type; 307 | bool siteAdmin; 308 | 309 | Owner({ 310 | this.login, 311 | this.id, 312 | this.nodeId, 313 | this.avatarUrl, 314 | this.gravatarId, 315 | this.url, 316 | this.htmlUrl, 317 | this.followersUrl, 318 | this.followingUrl, 319 | this.gistsUrl, 320 | this.starredUrl, 321 | this.subscriptionsUrl, 322 | this.organizationsUrl, 323 | this.reposUrl, 324 | this.eventsUrl, 325 | this.receivedEventsUrl, 326 | this.type, 327 | this.siteAdmin, 328 | }); 329 | 330 | factory Owner.fromJson(Map json) => Owner( 331 | login: json["login"], 332 | id: json["id"], 333 | nodeId: json["node_id"], 334 | avatarUrl: json["avatar_url"], 335 | gravatarId: json["gravatar_id"], 336 | url: json["url"], 337 | htmlUrl: json["html_url"], 338 | followersUrl: json["followers_url"], 339 | followingUrl: json["following_url"], 340 | gistsUrl: json["gists_url"], 341 | starredUrl: json["starred_url"], 342 | subscriptionsUrl: json["subscriptions_url"], 343 | organizationsUrl: json["organizations_url"], 344 | reposUrl: json["repos_url"], 345 | eventsUrl: json["events_url"], 346 | receivedEventsUrl: json["received_events_url"], 347 | type: json["type"], 348 | siteAdmin: json["site_admin"], 349 | ); 350 | 351 | Map toJson() => { 352 | "login": login, 353 | "id": id, 354 | "node_id": nodeId, 355 | "avatar_url": avatarUrl, 356 | "gravatar_id": gravatarId, 357 | "url": url, 358 | "html_url": htmlUrl, 359 | "followers_url": followersUrl, 360 | "following_url": followingUrl, 361 | "gists_url": gistsUrl, 362 | "starred_url": starredUrl, 363 | "subscriptions_url": subscriptionsUrl, 364 | "organizations_url": organizationsUrl, 365 | "repos_url": reposUrl, 366 | "events_url": eventsUrl, 367 | "received_events_url": receivedEventsUrl, 368 | "type": type, 369 | "site_admin": siteAdmin, 370 | }; 371 | } 372 | 373 | class Subject { 374 | String title; 375 | String url; 376 | String latestCommentUrl; 377 | String type; 378 | 379 | Subject({ 380 | this.title, 381 | this.url, 382 | this.latestCommentUrl, 383 | this.type, 384 | }); 385 | 386 | factory Subject.fromJson(Map json) => Subject( 387 | title: json["title"], 388 | url: json["url"], 389 | latestCommentUrl: json["latest_comment_url"], 390 | type: json["type"], 391 | ); 392 | 393 | Map toJson() => { 394 | "title": title, 395 | "url": url, 396 | "latest_comment_url": latestCommentUrl, 397 | "type": type, 398 | }; 399 | } 400 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 4F43B68BB6D3F17C2A3867B4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B72588CF304D7A6538C3DB65 /* Pods_Runner.framework */; }; 15 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 16 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 17 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 18 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 19 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 20 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXCopyFilesBuildPhase section */ 24 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 25 | isa = PBXCopyFilesBuildPhase; 26 | buildActionMask = 2147483647; 27 | dstPath = ""; 28 | dstSubfolderSpec = 10; 29 | files = ( 30 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 31 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 32 | ); 33 | name = "Embed Frameworks"; 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXCopyFilesBuildPhase section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 40 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 41 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 42 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 43 | 70E9908990FB6E09EECAB598 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 44 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 45 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 46 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 47 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 48 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 49 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 50 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 51 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 54 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 55 | B72588CF304D7A6538C3DB65 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | D2E3A88B0239D20FD22EFFF6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 57 | DC57E1DD2ABC86E152A7F856 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 66 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 67 | 4F43B68BB6D3F17C2A3867B4 /* Pods_Runner.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 6E2964C0B0BABF890057AAED /* Pods */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 70E9908990FB6E09EECAB598 /* Pods-Runner.debug.xcconfig */, 78 | DC57E1DD2ABC86E152A7F856 /* Pods-Runner.release.xcconfig */, 79 | D2E3A88B0239D20FD22EFFF6 /* Pods-Runner.profile.xcconfig */, 80 | ); 81 | path = Pods; 82 | sourceTree = ""; 83 | }; 84 | 9740EEB11CF90186004384FC /* Flutter */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 3B80C3931E831B6300D905FE /* App.framework */, 88 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 89 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 90 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 91 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 92 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 93 | ); 94 | name = Flutter; 95 | sourceTree = ""; 96 | }; 97 | 97C146E51CF9000F007C117D = { 98 | isa = PBXGroup; 99 | children = ( 100 | 9740EEB11CF90186004384FC /* Flutter */, 101 | 97C146F01CF9000F007C117D /* Runner */, 102 | 97C146EF1CF9000F007C117D /* Products */, 103 | 6E2964C0B0BABF890057AAED /* Pods */, 104 | B6CBCA4742DBD4EEEC55023D /* Frameworks */, 105 | ); 106 | sourceTree = ""; 107 | }; 108 | 97C146EF1CF9000F007C117D /* Products */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 97C146EE1CF9000F007C117D /* Runner.app */, 112 | ); 113 | name = Products; 114 | sourceTree = ""; 115 | }; 116 | 97C146F01CF9000F007C117D /* Runner */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 120 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 121 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 122 | 97C147021CF9000F007C117D /* Info.plist */, 123 | 97C146F11CF9000F007C117D /* Supporting Files */, 124 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 125 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 126 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 127 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 128 | ); 129 | path = Runner; 130 | sourceTree = ""; 131 | }; 132 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | ); 136 | name = "Supporting Files"; 137 | sourceTree = ""; 138 | }; 139 | B6CBCA4742DBD4EEEC55023D /* Frameworks */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | B72588CF304D7A6538C3DB65 /* Pods_Runner.framework */, 143 | ); 144 | name = Frameworks; 145 | sourceTree = ""; 146 | }; 147 | /* End PBXGroup section */ 148 | 149 | /* Begin PBXNativeTarget section */ 150 | 97C146ED1CF9000F007C117D /* Runner */ = { 151 | isa = PBXNativeTarget; 152 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 153 | buildPhases = ( 154 | D404C74B9BBA6B433782A467 /* [CP] Check Pods Manifest.lock */, 155 | 9740EEB61CF901F6004384FC /* Run Script */, 156 | 97C146EA1CF9000F007C117D /* Sources */, 157 | 97C146EB1CF9000F007C117D /* Frameworks */, 158 | 97C146EC1CF9000F007C117D /* Resources */, 159 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 160 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 161 | A0B085DC0379789A8E0526D1 /* [CP] Embed Pods Frameworks */, 162 | ); 163 | buildRules = ( 164 | ); 165 | dependencies = ( 166 | ); 167 | name = Runner; 168 | productName = Runner; 169 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 170 | productType = "com.apple.product-type.application"; 171 | }; 172 | /* End PBXNativeTarget section */ 173 | 174 | /* Begin PBXProject section */ 175 | 97C146E61CF9000F007C117D /* Project object */ = { 176 | isa = PBXProject; 177 | attributes = { 178 | LastUpgradeCheck = 1020; 179 | ORGANIZATIONNAME = "The Chromium Authors"; 180 | TargetAttributes = { 181 | 97C146ED1CF9000F007C117D = { 182 | CreatedOnToolsVersion = 7.3.1; 183 | LastSwiftMigration = 1100; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 188 | compatibilityVersion = "Xcode 3.2"; 189 | developmentRegion = en; 190 | hasScannedForEncodings = 0; 191 | knownRegions = ( 192 | en, 193 | Base, 194 | ); 195 | mainGroup = 97C146E51CF9000F007C117D; 196 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 197 | projectDirPath = ""; 198 | projectRoot = ""; 199 | targets = ( 200 | 97C146ED1CF9000F007C117D /* Runner */, 201 | ); 202 | }; 203 | /* End PBXProject section */ 204 | 205 | /* Begin PBXResourcesBuildPhase section */ 206 | 97C146EC1CF9000F007C117D /* Resources */ = { 207 | isa = PBXResourcesBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 211 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 212 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 213 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 214 | ); 215 | runOnlyForDeploymentPostprocessing = 0; 216 | }; 217 | /* End PBXResourcesBuildPhase section */ 218 | 219 | /* Begin PBXShellScriptBuildPhase section */ 220 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 221 | isa = PBXShellScriptBuildPhase; 222 | buildActionMask = 2147483647; 223 | files = ( 224 | ); 225 | inputPaths = ( 226 | ); 227 | name = "Thin Binary"; 228 | outputPaths = ( 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | shellPath = /bin/sh; 232 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 233 | }; 234 | 9740EEB61CF901F6004384FC /* Run Script */ = { 235 | isa = PBXShellScriptBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | ); 239 | inputPaths = ( 240 | ); 241 | name = "Run Script"; 242 | outputPaths = ( 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | shellPath = /bin/sh; 246 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 247 | }; 248 | A0B085DC0379789A8E0526D1 /* [CP] Embed Pods Frameworks */ = { 249 | isa = PBXShellScriptBuildPhase; 250 | buildActionMask = 2147483647; 251 | files = ( 252 | ); 253 | inputPaths = ( 254 | ); 255 | name = "[CP] Embed Pods Frameworks"; 256 | outputPaths = ( 257 | ); 258 | runOnlyForDeploymentPostprocessing = 0; 259 | shellPath = /bin/sh; 260 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 261 | showEnvVarsInLog = 0; 262 | }; 263 | D404C74B9BBA6B433782A467 /* [CP] Check Pods Manifest.lock */ = { 264 | isa = PBXShellScriptBuildPhase; 265 | buildActionMask = 2147483647; 266 | files = ( 267 | ); 268 | inputFileListPaths = ( 269 | ); 270 | inputPaths = ( 271 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 272 | "${PODS_ROOT}/Manifest.lock", 273 | ); 274 | name = "[CP] Check Pods Manifest.lock"; 275 | outputFileListPaths = ( 276 | ); 277 | outputPaths = ( 278 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | shellPath = /bin/sh; 282 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 283 | showEnvVarsInLog = 0; 284 | }; 285 | /* End PBXShellScriptBuildPhase section */ 286 | 287 | /* Begin PBXSourcesBuildPhase section */ 288 | 97C146EA1CF9000F007C117D /* Sources */ = { 289 | isa = PBXSourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 293 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 294 | ); 295 | runOnlyForDeploymentPostprocessing = 0; 296 | }; 297 | /* End PBXSourcesBuildPhase section */ 298 | 299 | /* Begin PBXVariantGroup section */ 300 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 301 | isa = PBXVariantGroup; 302 | children = ( 303 | 97C146FB1CF9000F007C117D /* Base */, 304 | ); 305 | name = Main.storyboard; 306 | sourceTree = ""; 307 | }; 308 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 309 | isa = PBXVariantGroup; 310 | children = ( 311 | 97C147001CF9000F007C117D /* Base */, 312 | ); 313 | name = LaunchScreen.storyboard; 314 | sourceTree = ""; 315 | }; 316 | /* End PBXVariantGroup section */ 317 | 318 | /* Begin XCBuildConfiguration section */ 319 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 320 | isa = XCBuildConfiguration; 321 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 322 | buildSettings = { 323 | ALWAYS_SEARCH_USER_PATHS = NO; 324 | CLANG_ANALYZER_NONNULL = YES; 325 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 326 | CLANG_CXX_LIBRARY = "libc++"; 327 | CLANG_ENABLE_MODULES = YES; 328 | CLANG_ENABLE_OBJC_ARC = YES; 329 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 330 | CLANG_WARN_BOOL_CONVERSION = YES; 331 | CLANG_WARN_COMMA = YES; 332 | CLANG_WARN_CONSTANT_CONVERSION = YES; 333 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 334 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 344 | CLANG_WARN_STRICT_PROTOTYPES = YES; 345 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 346 | CLANG_WARN_UNREACHABLE_CODE = YES; 347 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 348 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 349 | COPY_PHASE_STRIP = NO; 350 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 351 | ENABLE_NS_ASSERTIONS = NO; 352 | ENABLE_STRICT_OBJC_MSGSEND = YES; 353 | GCC_C_LANGUAGE_STANDARD = gnu99; 354 | GCC_NO_COMMON_BLOCKS = YES; 355 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 356 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 357 | GCC_WARN_UNDECLARED_SELECTOR = YES; 358 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 359 | GCC_WARN_UNUSED_FUNCTION = YES; 360 | GCC_WARN_UNUSED_VARIABLE = YES; 361 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 362 | MTL_ENABLE_DEBUG_INFO = NO; 363 | SDKROOT = iphoneos; 364 | SUPPORTED_PLATFORMS = iphoneos; 365 | TARGETED_DEVICE_FAMILY = "1,2"; 366 | VALIDATE_PRODUCT = YES; 367 | }; 368 | name = Profile; 369 | }; 370 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 371 | isa = XCBuildConfiguration; 372 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 373 | buildSettings = { 374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 | CLANG_ENABLE_MODULES = YES; 376 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 377 | ENABLE_BITCODE = NO; 378 | FRAMEWORK_SEARCH_PATHS = ( 379 | "$(inherited)", 380 | "$(PROJECT_DIR)/Flutter", 381 | ); 382 | INFOPLIST_FILE = Runner/Info.plist; 383 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 384 | LIBRARY_SEARCH_PATHS = ( 385 | "$(inherited)", 386 | "$(PROJECT_DIR)/Flutter", 387 | ); 388 | PRODUCT_BUNDLE_IDENTIFIER = com.idisfkj.flutterGithub; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 391 | SWIFT_VERSION = 5.0; 392 | VERSIONING_SYSTEM = "apple-generic"; 393 | }; 394 | name = Profile; 395 | }; 396 | 97C147031CF9000F007C117D /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 399 | buildSettings = { 400 | ALWAYS_SEARCH_USER_PATHS = NO; 401 | CLANG_ANALYZER_NONNULL = YES; 402 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 403 | CLANG_CXX_LIBRARY = "libc++"; 404 | CLANG_ENABLE_MODULES = YES; 405 | CLANG_ENABLE_OBJC_ARC = YES; 406 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 407 | CLANG_WARN_BOOL_CONVERSION = YES; 408 | CLANG_WARN_COMMA = YES; 409 | CLANG_WARN_CONSTANT_CONVERSION = YES; 410 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 411 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 412 | CLANG_WARN_EMPTY_BODY = YES; 413 | CLANG_WARN_ENUM_CONVERSION = YES; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 418 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 419 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 420 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 421 | CLANG_WARN_STRICT_PROTOTYPES = YES; 422 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 423 | CLANG_WARN_UNREACHABLE_CODE = YES; 424 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 425 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 426 | COPY_PHASE_STRIP = NO; 427 | DEBUG_INFORMATION_FORMAT = dwarf; 428 | ENABLE_STRICT_OBJC_MSGSEND = YES; 429 | ENABLE_TESTABILITY = YES; 430 | GCC_C_LANGUAGE_STANDARD = gnu99; 431 | GCC_DYNAMIC_NO_PIC = NO; 432 | GCC_NO_COMMON_BLOCKS = YES; 433 | GCC_OPTIMIZATION_LEVEL = 0; 434 | GCC_PREPROCESSOR_DEFINITIONS = ( 435 | "DEBUG=1", 436 | "$(inherited)", 437 | ); 438 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 439 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 440 | GCC_WARN_UNDECLARED_SELECTOR = YES; 441 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 442 | GCC_WARN_UNUSED_FUNCTION = YES; 443 | GCC_WARN_UNUSED_VARIABLE = YES; 444 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 445 | MTL_ENABLE_DEBUG_INFO = YES; 446 | ONLY_ACTIVE_ARCH = YES; 447 | SDKROOT = iphoneos; 448 | TARGETED_DEVICE_FAMILY = "1,2"; 449 | }; 450 | name = Debug; 451 | }; 452 | 97C147041CF9000F007C117D /* Release */ = { 453 | isa = XCBuildConfiguration; 454 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 455 | buildSettings = { 456 | ALWAYS_SEARCH_USER_PATHS = NO; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 459 | CLANG_CXX_LIBRARY = "libc++"; 460 | CLANG_ENABLE_MODULES = YES; 461 | CLANG_ENABLE_OBJC_ARC = YES; 462 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 463 | CLANG_WARN_BOOL_CONVERSION = YES; 464 | CLANG_WARN_COMMA = YES; 465 | CLANG_WARN_CONSTANT_CONVERSION = YES; 466 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 467 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 468 | CLANG_WARN_EMPTY_BODY = YES; 469 | CLANG_WARN_ENUM_CONVERSION = YES; 470 | CLANG_WARN_INFINITE_RECURSION = YES; 471 | CLANG_WARN_INT_CONVERSION = YES; 472 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 474 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 476 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 477 | CLANG_WARN_STRICT_PROTOTYPES = YES; 478 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 479 | CLANG_WARN_UNREACHABLE_CODE = YES; 480 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 481 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 482 | COPY_PHASE_STRIP = NO; 483 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 484 | ENABLE_NS_ASSERTIONS = NO; 485 | ENABLE_STRICT_OBJC_MSGSEND = YES; 486 | GCC_C_LANGUAGE_STANDARD = gnu99; 487 | GCC_NO_COMMON_BLOCKS = YES; 488 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 489 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 490 | GCC_WARN_UNDECLARED_SELECTOR = YES; 491 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 492 | GCC_WARN_UNUSED_FUNCTION = YES; 493 | GCC_WARN_UNUSED_VARIABLE = YES; 494 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 495 | MTL_ENABLE_DEBUG_INFO = NO; 496 | SDKROOT = iphoneos; 497 | SUPPORTED_PLATFORMS = iphoneos; 498 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 499 | TARGETED_DEVICE_FAMILY = "1,2"; 500 | VALIDATE_PRODUCT = YES; 501 | }; 502 | name = Release; 503 | }; 504 | 97C147061CF9000F007C117D /* Debug */ = { 505 | isa = XCBuildConfiguration; 506 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 507 | buildSettings = { 508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 509 | CLANG_ENABLE_MODULES = YES; 510 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 511 | ENABLE_BITCODE = NO; 512 | FRAMEWORK_SEARCH_PATHS = ( 513 | "$(inherited)", 514 | "$(PROJECT_DIR)/Flutter", 515 | ); 516 | INFOPLIST_FILE = Runner/Info.plist; 517 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 518 | LIBRARY_SEARCH_PATHS = ( 519 | "$(inherited)", 520 | "$(PROJECT_DIR)/Flutter", 521 | ); 522 | PRODUCT_BUNDLE_IDENTIFIER = com.idisfkj.flutterGithub; 523 | PRODUCT_NAME = "$(TARGET_NAME)"; 524 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 525 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 526 | SWIFT_VERSION = 5.0; 527 | VERSIONING_SYSTEM = "apple-generic"; 528 | }; 529 | name = Debug; 530 | }; 531 | 97C147071CF9000F007C117D /* Release */ = { 532 | isa = XCBuildConfiguration; 533 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 534 | buildSettings = { 535 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 536 | CLANG_ENABLE_MODULES = YES; 537 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 538 | ENABLE_BITCODE = NO; 539 | FRAMEWORK_SEARCH_PATHS = ( 540 | "$(inherited)", 541 | "$(PROJECT_DIR)/Flutter", 542 | ); 543 | INFOPLIST_FILE = Runner/Info.plist; 544 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 545 | LIBRARY_SEARCH_PATHS = ( 546 | "$(inherited)", 547 | "$(PROJECT_DIR)/Flutter", 548 | ); 549 | PRODUCT_BUNDLE_IDENTIFIER = com.idisfkj.flutterGithub; 550 | PRODUCT_NAME = "$(TARGET_NAME)"; 551 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 552 | SWIFT_VERSION = 5.0; 553 | VERSIONING_SYSTEM = "apple-generic"; 554 | }; 555 | name = Release; 556 | }; 557 | /* End XCBuildConfiguration section */ 558 | 559 | /* Begin XCConfigurationList section */ 560 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 561 | isa = XCConfigurationList; 562 | buildConfigurations = ( 563 | 97C147031CF9000F007C117D /* Debug */, 564 | 97C147041CF9000F007C117D /* Release */, 565 | 249021D3217E4FDB00AE95B9 /* Profile */, 566 | ); 567 | defaultConfigurationIsVisible = 0; 568 | defaultConfigurationName = Release; 569 | }; 570 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 571 | isa = XCConfigurationList; 572 | buildConfigurations = ( 573 | 97C147061CF9000F007C117D /* Debug */, 574 | 97C147071CF9000F007C117D /* Release */, 575 | 249021D4217E4FDB00AE95B9 /* Profile */, 576 | ); 577 | defaultConfigurationIsVisible = 0; 578 | defaultConfigurationName = Release; 579 | }; 580 | /* End XCConfigurationList section */ 581 | }; 582 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 583 | } 584 | --------------------------------------------------------------------------------