├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── cfelixmac │ │ │ └── gankio │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── doc ├── gank_io_flutter_v1.0.apk ├── logo_c.png ├── screen1.jpg ├── screen2.jpg ├── screen3.jpg ├── screen4.jpg └── screen5.jpg ├── gankio.iml ├── gankio_android.iml ├── images ├── android_icon.png ├── app_icon.png ├── doc_icon.png ├── extend_icon.png ├── front_icon.png ├── gankio.png ├── ios_icon.png └── relax_icon.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── 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-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@1x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-76x76@3x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ ├── Icon-Small-50x50@1x.png │ │ ├── Icon-Small-50x50@2x.png │ │ └── ItunesArtwork@2x.png │ ├── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── iTunesArtwork@1x.png │ ├── iTunesArtwork@2x.png │ └── iTunesArtwork@3x.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── data │ ├── article.dart │ ├── history_result.dart │ ├── reading_data.dart │ └── search_result.dart ├── display │ ├── history.dart │ ├── history_details.dart │ ├── image_view.dart │ ├── reading.dart │ ├── reading_category.dart │ ├── search.dart │ ├── setting.dart │ ├── today.dart │ ├── today_details.dart │ └── web_view.dart ├── main.dart ├── presenter │ ├── history_presenter.dart │ └── reading_presenter.dart └── widget │ ├── photo_view.dart │ ├── photo_view_image_wrapper.dart │ ├── photo_view_scale_type.dart │ └── photo_view_utils.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | .idea 9 | 10 | .flutter-plugins 11 | -------------------------------------------------------------------------------- /.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: c7ea3ca377e909469c68f2ab878a5bc53d3cf66b 8 | channel: beta 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cfelixmac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/logo_c.png) Gank.IO 2 | **Another [Gank.io](https://gank.io/) Client by Flutter (for Android & iOS)** 3 | 4 | 又一个 Flutter 编写的 [Gank.io](https://gank.io/) App, 支持 Android、iOS平台 5 | 6 | 7 | 8 | ## Version 9 | 1.1 10 | ## Download Apk Link 11 | [LINK](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/gank_io_flutter_v1.0.apk) 12 | ## License 13 | MIT 14 | ## Screenshots 15 | ![](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/screen1.jpg)![](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/screen2.jpg)![](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/screen3.jpg)![](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/screen4.jpg)![](https://raw.githubusercontent.com/cfelixw/GankIOFlutter/master/doc/screen5.jpg) 16 | 17 | ## Known Issues 18 | * Large Image View can't load animated GIFs' animation. 19 | * Web View can't load web page correctly occasionally. 20 | 21 | 22 | 23 | ANY ISSUES & PRs are WELCOME! 24 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | key.properties 12 | -------------------------------------------------------------------------------- /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 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | def keystorePropertiesFile = rootProject.file("key.properties") 18 | def keystoreProperties = new Properties() 19 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 20 | 21 | android { 22 | compileSdkVersion 27 23 | 24 | lintOptions { 25 | disable 'InvalidPackage' 26 | } 27 | 28 | defaultConfig { 29 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 30 | applicationId "com.cfelixmac.gankio" 31 | minSdkVersion 16 32 | targetSdkVersion 27 33 | versionCode 1 34 | versionName "1.0" 35 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 36 | } 37 | 38 | signingConfigs { 39 | release { 40 | keyAlias keystoreProperties['keyAlias'] 41 | keyPassword keystoreProperties['keyPassword'] 42 | storeFile file(keystoreProperties['storeFile']) 43 | storePassword keystoreProperties['storePassword'] 44 | } 45 | } 46 | buildTypes { 47 | release { 48 | signingConfig signingConfigs.release 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/cfelixmac/gankio/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.cfelixmac.gankio; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.0.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /doc/gank_io_flutter_v1.0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/gank_io_flutter_v1.0.apk -------------------------------------------------------------------------------- /doc/logo_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/logo_c.png -------------------------------------------------------------------------------- /doc/screen1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/screen1.jpg -------------------------------------------------------------------------------- /doc/screen2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/screen2.jpg -------------------------------------------------------------------------------- /doc/screen3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/screen3.jpg -------------------------------------------------------------------------------- /doc/screen4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/screen4.jpg -------------------------------------------------------------------------------- /doc/screen5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/doc/screen5.jpg -------------------------------------------------------------------------------- /gankio.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /gankio_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /images/android_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/android_icon.png -------------------------------------------------------------------------------- /images/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/app_icon.png -------------------------------------------------------------------------------- /images/doc_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/doc_icon.png -------------------------------------------------------------------------------- /images/extend_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/extend_icon.png -------------------------------------------------------------------------------- /images/front_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/front_icon.png -------------------------------------------------------------------------------- /images/gankio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/gankio.png -------------------------------------------------------------------------------- /images/ios_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/ios_icon.png -------------------------------------------------------------------------------- /images/relax_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/images/relax_icon.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | .symlinks/ 46 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | def parse_KV_file(file, separator='=') 8 | file_abs_path = File.expand_path(file) 9 | if !File.exists? file_abs_path 10 | return []; 11 | end 12 | pods_ary = [] 13 | skip_line_start_symbols = ["#", "/"] 14 | File.foreach(file_abs_path) { |line| 15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 16 | plugin = line.split(pattern=separator) 17 | if plugin.length == 2 18 | podname = plugin[0].strip() 19 | path = plugin[1].strip() 20 | podpath = File.expand_path("#{path}", file_abs_path) 21 | pods_ary.push({:name => podname, :path => podpath}); 22 | else 23 | puts "Invalid plugin specification: #{line}" 24 | end 25 | } 26 | return pods_ary 27 | end 28 | 29 | target 'Runner' do 30 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 31 | # referring to absolute paths on developers' machines. 32 | system('rm -rf .symlinks') 33 | system('mkdir -p .symlinks/plugins') 34 | 35 | # Flutter Pods 36 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 37 | if generated_xcode_build_settings.empty? 38 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 39 | end 40 | generated_xcode_build_settings.map { |p| 41 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 42 | symlink = File.join('.symlinks', 'flutter') 43 | File.symlink(File.dirname(p[:path]), symlink) 44 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 45 | end 46 | } 47 | 48 | # Plugin Pods 49 | plugin_pods = parse_KV_file('../.flutter-plugins') 50 | plugin_pods.map { |p| 51 | symlink = File.join('.symlinks', 'plugins', p[:name]) 52 | File.symlink(p[:path], symlink) 53 | pod p[:name], :path => File.join(symlink, 'ios') 54 | } 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['ENABLE_BITCODE'] = 'NO' 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_webview_plugin (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `.symlinks/flutter/ios`) 8 | - flutter_webview_plugin (from `.symlinks/plugins/flutter_webview_plugin/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: ".symlinks/flutter/ios" 13 | flutter_webview_plugin: 14 | :path: ".symlinks/plugins/flutter_webview_plugin/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 18 | flutter_webview_plugin: 116575b48572029304775b768e9f15ebfc316274 19 | 20 | PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2 21 | 22 | COCOAPODS: 1.5.3 23 | -------------------------------------------------------------------------------- /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 | 16FB647A1ADB97CA5B3A34B5 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AD64129330EB2A7CA7A504D /* libPods-Runner.a */; }; 12 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 13 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 14 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 15 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 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 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 19 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 20 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 21 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 22 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXCopyFilesBuildPhase section */ 26 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 27 | isa = PBXCopyFilesBuildPhase; 28 | buildActionMask = 2147483647; 29 | dstPath = ""; 30 | dstSubfolderSpec = 10; 31 | files = ( 32 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 33 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 34 | ); 35 | name = "Embed Frameworks"; 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXCopyFilesBuildPhase section */ 39 | 40 | /* Begin PBXFileReference section */ 41 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 42 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 43 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 44 | 3AD64129330EB2A7CA7A504D /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 46 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 47 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 48 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 49 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 50 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 51 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 52 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 53 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 55 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 56 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 58 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 67 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 68 | 16FB647A1ADB97CA5B3A34B5 /* libPods-Runner.a in Frameworks */, 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXFrameworksBuildPhase section */ 73 | 74 | /* Begin PBXGroup section */ 75 | 325775A8BB7F8F47FF46C699 /* Pods */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | ); 79 | name = Pods; 80 | sourceTree = ""; 81 | }; 82 | 8E857B81BB18C1DC37F59F17 /* Frameworks */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 3AD64129330EB2A7CA7A504D /* libPods-Runner.a */, 86 | ); 87 | name = Frameworks; 88 | sourceTree = ""; 89 | }; 90 | 9740EEB11CF90186004384FC /* Flutter */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 94 | 3B80C3931E831B6300D905FE /* App.framework */, 95 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 96 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 97 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 98 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 99 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 100 | ); 101 | name = Flutter; 102 | sourceTree = ""; 103 | }; 104 | 97C146E51CF9000F007C117D = { 105 | isa = PBXGroup; 106 | children = ( 107 | 9740EEB11CF90186004384FC /* Flutter */, 108 | 97C146F01CF9000F007C117D /* Runner */, 109 | 97C146EF1CF9000F007C117D /* Products */, 110 | 325775A8BB7F8F47FF46C699 /* Pods */, 111 | 8E857B81BB18C1DC37F59F17 /* Frameworks */, 112 | ); 113 | sourceTree = ""; 114 | }; 115 | 97C146EF1CF9000F007C117D /* Products */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 97C146EE1CF9000F007C117D /* Runner.app */, 119 | ); 120 | name = Products; 121 | sourceTree = ""; 122 | }; 123 | 97C146F01CF9000F007C117D /* Runner */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 127 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 128 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 129 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 130 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 131 | 97C147021CF9000F007C117D /* Info.plist */, 132 | 97C146F11CF9000F007C117D /* Supporting Files */, 133 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 134 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 135 | ); 136 | path = Runner; 137 | sourceTree = ""; 138 | }; 139 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 97C146F21CF9000F007C117D /* main.m */, 143 | ); 144 | name = "Supporting Files"; 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 | 3F1C6BAB9069E325D3BDA3E8 /* [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 | 1D4E0814D767A969C61C98E0 /* [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 = 0940; 179 | ORGANIZATIONNAME = "The Chromium Authors"; 180 | TargetAttributes = { 181 | 97C146ED1CF9000F007C117D = { 182 | CreatedOnToolsVersion = 7.3.1; 183 | DevelopmentTeam = 46YU5RF96W; 184 | }; 185 | }; 186 | }; 187 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 188 | compatibilityVersion = "Xcode 3.2"; 189 | developmentRegion = English; 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 | 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 214 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | }; 218 | /* End PBXResourcesBuildPhase section */ 219 | 220 | /* Begin PBXShellScriptBuildPhase section */ 221 | 1D4E0814D767A969C61C98E0 /* [CP] Embed Pods Frameworks */ = { 222 | isa = PBXShellScriptBuildPhase; 223 | buildActionMask = 2147483647; 224 | files = ( 225 | ); 226 | inputPaths = ( 227 | "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", 228 | "${PODS_ROOT}/../.symlinks/flutter/ios/Flutter.framework", 229 | ); 230 | name = "[CP] Embed Pods Frameworks"; 231 | outputPaths = ( 232 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | shellPath = /bin/sh; 236 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 237 | showEnvVarsInLog = 0; 238 | }; 239 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 240 | isa = PBXShellScriptBuildPhase; 241 | buildActionMask = 2147483647; 242 | files = ( 243 | ); 244 | inputPaths = ( 245 | ); 246 | name = "Thin Binary"; 247 | outputPaths = ( 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | shellPath = /bin/sh; 251 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 252 | }; 253 | 3F1C6BAB9069E325D3BDA3E8 /* [CP] Check Pods Manifest.lock */ = { 254 | isa = PBXShellScriptBuildPhase; 255 | buildActionMask = 2147483647; 256 | files = ( 257 | ); 258 | inputPaths = ( 259 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 260 | "${PODS_ROOT}/Manifest.lock", 261 | ); 262 | name = "[CP] Check Pods Manifest.lock"; 263 | outputPaths = ( 264 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | shellPath = /bin/sh; 268 | 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"; 269 | showEnvVarsInLog = 0; 270 | }; 271 | 9740EEB61CF901F6004384FC /* Run Script */ = { 272 | isa = PBXShellScriptBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | ); 276 | inputPaths = ( 277 | ); 278 | name = "Run Script"; 279 | outputPaths = ( 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | shellPath = /bin/sh; 283 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 284 | }; 285 | /* End PBXShellScriptBuildPhase section */ 286 | 287 | /* Begin PBXSourcesBuildPhase section */ 288 | 97C146EA1CF9000F007C117D /* Sources */ = { 289 | isa = PBXSourcesBuildPhase; 290 | buildActionMask = 2147483647; 291 | files = ( 292 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 293 | 97C146F31CF9000F007C117D /* main.m in Sources */, 294 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | /* End PBXSourcesBuildPhase section */ 299 | 300 | /* Begin PBXVariantGroup section */ 301 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 302 | isa = PBXVariantGroup; 303 | children = ( 304 | 97C146FB1CF9000F007C117D /* Base */, 305 | ); 306 | name = Main.storyboard; 307 | sourceTree = ""; 308 | }; 309 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 310 | isa = PBXVariantGroup; 311 | children = ( 312 | 97C147001CF9000F007C117D /* Base */, 313 | ); 314 | name = LaunchScreen.storyboard; 315 | sourceTree = ""; 316 | }; 317 | /* End PBXVariantGroup section */ 318 | 319 | /* Begin XCBuildConfiguration section */ 320 | 97C147031CF9000F007C117D /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 323 | buildSettings = { 324 | ALWAYS_SEARCH_USER_PATHS = NO; 325 | CLANG_ANALYZER_NONNULL = YES; 326 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 327 | CLANG_CXX_LIBRARY = "libc++"; 328 | CLANG_ENABLE_MODULES = YES; 329 | CLANG_ENABLE_OBJC_ARC = YES; 330 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 331 | CLANG_WARN_BOOL_CONVERSION = YES; 332 | CLANG_WARN_COMMA = YES; 333 | CLANG_WARN_CONSTANT_CONVERSION = YES; 334 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 335 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 336 | CLANG_WARN_EMPTY_BODY = YES; 337 | CLANG_WARN_ENUM_CONVERSION = YES; 338 | CLANG_WARN_INFINITE_RECURSION = YES; 339 | CLANG_WARN_INT_CONVERSION = YES; 340 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 341 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 342 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 343 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 344 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 345 | CLANG_WARN_STRICT_PROTOTYPES = YES; 346 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 347 | CLANG_WARN_UNREACHABLE_CODE = YES; 348 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 349 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 350 | COPY_PHASE_STRIP = NO; 351 | DEBUG_INFORMATION_FORMAT = dwarf; 352 | ENABLE_STRICT_OBJC_MSGSEND = YES; 353 | ENABLE_TESTABILITY = YES; 354 | GCC_C_LANGUAGE_STANDARD = gnu99; 355 | GCC_DYNAMIC_NO_PIC = NO; 356 | GCC_NO_COMMON_BLOCKS = YES; 357 | GCC_OPTIMIZATION_LEVEL = 0; 358 | GCC_PREPROCESSOR_DEFINITIONS = ( 359 | "DEBUG=1", 360 | "$(inherited)", 361 | ); 362 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 363 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 364 | GCC_WARN_UNDECLARED_SELECTOR = YES; 365 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 366 | GCC_WARN_UNUSED_FUNCTION = YES; 367 | GCC_WARN_UNUSED_VARIABLE = YES; 368 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 369 | MTL_ENABLE_DEBUG_INFO = YES; 370 | ONLY_ACTIVE_ARCH = YES; 371 | SDKROOT = iphoneos; 372 | TARGETED_DEVICE_FAMILY = "1,2"; 373 | }; 374 | name = Debug; 375 | }; 376 | 97C147041CF9000F007C117D /* Release */ = { 377 | isa = XCBuildConfiguration; 378 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 379 | buildSettings = { 380 | ALWAYS_SEARCH_USER_PATHS = NO; 381 | CLANG_ANALYZER_NONNULL = YES; 382 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 383 | CLANG_CXX_LIBRARY = "libc++"; 384 | CLANG_ENABLE_MODULES = YES; 385 | CLANG_ENABLE_OBJC_ARC = YES; 386 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 387 | CLANG_WARN_BOOL_CONVERSION = YES; 388 | CLANG_WARN_COMMA = YES; 389 | CLANG_WARN_CONSTANT_CONVERSION = YES; 390 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 391 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 392 | CLANG_WARN_EMPTY_BODY = YES; 393 | CLANG_WARN_ENUM_CONVERSION = YES; 394 | CLANG_WARN_INFINITE_RECURSION = YES; 395 | CLANG_WARN_INT_CONVERSION = YES; 396 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 397 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 398 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 399 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 400 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 401 | CLANG_WARN_STRICT_PROTOTYPES = YES; 402 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 403 | CLANG_WARN_UNREACHABLE_CODE = YES; 404 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 405 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 406 | COPY_PHASE_STRIP = NO; 407 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 408 | ENABLE_NS_ASSERTIONS = NO; 409 | ENABLE_STRICT_OBJC_MSGSEND = YES; 410 | GCC_C_LANGUAGE_STANDARD = gnu99; 411 | GCC_NO_COMMON_BLOCKS = YES; 412 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 413 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 414 | GCC_WARN_UNDECLARED_SELECTOR = YES; 415 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 416 | GCC_WARN_UNUSED_FUNCTION = YES; 417 | GCC_WARN_UNUSED_VARIABLE = YES; 418 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 419 | MTL_ENABLE_DEBUG_INFO = NO; 420 | SDKROOT = iphoneos; 421 | TARGETED_DEVICE_FAMILY = "1,2"; 422 | VALIDATE_PRODUCT = YES; 423 | }; 424 | name = Release; 425 | }; 426 | 97C147061CF9000F007C117D /* Debug */ = { 427 | isa = XCBuildConfiguration; 428 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 429 | buildSettings = { 430 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 431 | CURRENT_PROJECT_VERSION = 1; 432 | DEVELOPMENT_TEAM = 46YU5RF96W; 433 | ENABLE_BITCODE = NO; 434 | FRAMEWORK_SEARCH_PATHS = ( 435 | "$(inherited)", 436 | "$(PROJECT_DIR)/Flutter", 437 | ); 438 | INFOPLIST_FILE = Runner/Info.plist; 439 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 440 | LIBRARY_SEARCH_PATHS = ( 441 | "$(inherited)", 442 | "$(PROJECT_DIR)/Flutter", 443 | ); 444 | PRODUCT_BUNDLE_IDENTIFIER = com.cfelixmac.gankio; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | VERSIONING_SYSTEM = "apple-generic"; 447 | }; 448 | name = Debug; 449 | }; 450 | 97C147071CF9000F007C117D /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 453 | buildSettings = { 454 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 455 | CURRENT_PROJECT_VERSION = 1; 456 | DEVELOPMENT_TEAM = 46YU5RF96W; 457 | ENABLE_BITCODE = NO; 458 | FRAMEWORK_SEARCH_PATHS = ( 459 | "$(inherited)", 460 | "$(PROJECT_DIR)/Flutter", 461 | ); 462 | INFOPLIST_FILE = Runner/Info.plist; 463 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 464 | LIBRARY_SEARCH_PATHS = ( 465 | "$(inherited)", 466 | "$(PROJECT_DIR)/Flutter", 467 | ); 468 | PRODUCT_BUNDLE_IDENTIFIER = com.cfelixmac.gankio; 469 | PRODUCT_NAME = "$(TARGET_NAME)"; 470 | VERSIONING_SYSTEM = "apple-generic"; 471 | }; 472 | name = Release; 473 | }; 474 | /* End XCBuildConfiguration section */ 475 | 476 | /* Begin XCConfigurationList section */ 477 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 478 | isa = XCConfigurationList; 479 | buildConfigurations = ( 480 | 97C147031CF9000F007C117D /* Debug */, 481 | 97C147041CF9000F007C117D /* Release */, 482 | ); 483 | defaultConfigurationIsVisible = 0; 484 | defaultConfigurationName = Release; 485 | }; 486 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 487 | isa = XCConfigurationList; 488 | buildConfigurations = ( 489 | 97C147061CF9000F007C117D /* Debug */, 490 | 97C147071CF9000F007C117D /* Release */, 491 | ); 492 | defaultConfigurationIsVisible = 0; 493 | defaultConfigurationName = Release; 494 | }; 495 | /* End XCConfigurationList section */ 496 | }; 497 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 498 | } 499 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 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/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images":[ 3 | { 4 | "idiom":"iphone", 5 | "size":"20x20", 6 | "scale":"2x", 7 | "filename":"Icon-App-20x20@2x.png" 8 | }, 9 | { 10 | "idiom":"iphone", 11 | "size":"20x20", 12 | "scale":"3x", 13 | "filename":"Icon-App-20x20@3x.png" 14 | }, 15 | { 16 | "idiom":"iphone", 17 | "size":"29x29", 18 | "scale":"1x", 19 | "filename":"Icon-App-29x29@1x.png" 20 | }, 21 | { 22 | "idiom":"iphone", 23 | "size":"29x29", 24 | "scale":"2x", 25 | "filename":"Icon-App-29x29@2x.png" 26 | }, 27 | { 28 | "idiom":"iphone", 29 | "size":"29x29", 30 | "scale":"3x", 31 | "filename":"Icon-App-29x29@3x.png" 32 | }, 33 | { 34 | "idiom":"iphone", 35 | "size":"40x40", 36 | "scale":"1x", 37 | "filename":"Icon-App-40x40@1x.png" 38 | }, 39 | { 40 | "idiom":"iphone", 41 | "size":"40x40", 42 | "scale":"2x", 43 | "filename":"Icon-App-40x40@2x.png" 44 | }, 45 | { 46 | "idiom":"iphone", 47 | "size":"40x40", 48 | "scale":"3x", 49 | "filename":"Icon-App-40x40@3x.png" 50 | }, 51 | { 52 | "idiom":"iphone", 53 | "size":"57x57", 54 | "scale":"1x", 55 | "filename":"Icon-App-57x57@1x.png" 56 | }, 57 | { 58 | "idiom":"iphone", 59 | "size":"57x57", 60 | "scale":"2x", 61 | "filename":"Icon-App-57x57@2x.png" 62 | }, 63 | { 64 | "idiom":"iphone", 65 | "size":"60x60", 66 | "scale":"1x", 67 | "filename":"Icon-App-60x60@1x.png" 68 | }, 69 | { 70 | "idiom":"iphone", 71 | "size":"60x60", 72 | "scale":"2x", 73 | "filename":"Icon-App-60x60@2x.png" 74 | }, 75 | { 76 | "idiom":"iphone", 77 | "size":"60x60", 78 | "scale":"3x", 79 | "filename":"Icon-App-60x60@3x.png" 80 | }, 81 | { 82 | "idiom":"iphone", 83 | "size":"76x76", 84 | "scale":"1x", 85 | "filename":"Icon-App-76x76@1x.png" 86 | }, 87 | { 88 | "idiom":"ipad", 89 | "size":"20x20", 90 | "scale":"1x", 91 | "filename":"Icon-App-20x20@1x.png" 92 | }, 93 | { 94 | "idiom":"ipad", 95 | "size":"20x20", 96 | "scale":"2x", 97 | "filename":"Icon-App-20x20@2x.png" 98 | }, 99 | { 100 | "idiom":"ipad", 101 | "size":"29x29", 102 | "scale":"1x", 103 | "filename":"Icon-App-29x29@1x.png" 104 | }, 105 | { 106 | "idiom":"ipad", 107 | "size":"29x29", 108 | "scale":"2x", 109 | "filename":"Icon-App-29x29@2x.png" 110 | }, 111 | { 112 | "idiom":"ipad", 113 | "size":"40x40", 114 | "scale":"1x", 115 | "filename":"Icon-App-40x40@1x.png" 116 | }, 117 | { 118 | "idiom":"ipad", 119 | "size":"40x40", 120 | "scale":"2x", 121 | "filename":"Icon-App-40x40@2x.png" 122 | }, 123 | { 124 | "size" : "50x50", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-Small-50x50@1x.png", 127 | "scale" : "1x" 128 | }, 129 | { 130 | "size" : "50x50", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-Small-50x50@2x.png", 133 | "scale" : "2x" 134 | }, 135 | { 136 | "idiom":"ipad", 137 | "size":"72x72", 138 | "scale":"1x", 139 | "filename":"Icon-App-72x72@1x.png" 140 | }, 141 | { 142 | "idiom":"ipad", 143 | "size":"72x72", 144 | "scale":"2x", 145 | "filename":"Icon-App-72x72@2x.png" 146 | }, 147 | { 148 | "idiom":"ipad", 149 | "size":"76x76", 150 | "scale":"1x", 151 | "filename":"Icon-App-76x76@1x.png" 152 | }, 153 | { 154 | "idiom":"ipad", 155 | "size":"76x76", 156 | "scale":"2x", 157 | "filename":"Icon-App-76x76@2x.png" 158 | }, 159 | { 160 | "idiom":"ipad", 161 | "size":"76x76", 162 | "scale":"3x", 163 | "filename":"Icon-App-76x76@3x.png" 164 | }, 165 | { 166 | "idiom":"ipad", 167 | "size":"83.5x83.5", 168 | "scale":"2x", 169 | "filename":"Icon-App-83.5x83.5@2x.png" 170 | }, 171 | { 172 | "size" : "1024x1024", 173 | "idiom" : "ios-marketing", 174 | "filename" : "ItunesArtwork@2x.png", 175 | "scale" : "1x" 176 | } 177 | ], 178 | "info":{ 179 | "version":1, 180 | "author":"makeappicon" 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/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/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/AppIcon.appiconset/ItunesArtwork@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/iTunesArtwork@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/iTunesArtwork@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/iTunesArtwork@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/iTunesArtwork@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/iTunesArtwork@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfelixmac-code/GankIOFlutter/0ba24f2ab177376f9e7c0c48b0d23ca2bce9ca6c/ios/Runner/Assets.xcassets/iTunesArtwork@3x.png -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Gank.IO 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | gankio 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/data/article.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class Article { 4 | Article({this.category, this.error, this.result}); 5 | 6 | List category; 7 | bool error; 8 | Map> result; 9 | 10 | factory Article.fromJson(String bodyString) { 11 | Map body = json.decode(bodyString); 12 | Map> results = Map(); 13 | Map rawResults = body['results']; 14 | 15 | results['App'] = parseSectors("App", rawResults); 16 | results['iOS'] = parseSectors("iOS", rawResults); 17 | results['Android'] = parseSectors("Android", rawResults); 18 | results['休息视频'] = parseSectors("休息视频", rawResults); 19 | results['前端'] = parseSectors("前端", rawResults); 20 | results['拓展资源'] = parseSectors("拓展资源", rawResults); 21 | results['瞎推荐'] = parseSectors("瞎推荐", rawResults); 22 | results['福利'] = parseSectors("福利", rawResults); 23 | 24 | return Article( 25 | category: new List.from(body['category']), 26 | error: body['error'], 27 | result: results, 28 | ); 29 | } 30 | } 31 | 32 | List parseSectors(String key, Map rawResults) { 33 | List raw = rawResults[key]; 34 | List result = List(); 35 | if (raw != null && raw.length != 0) { 36 | for (var article in raw) { 37 | result.add(ArticleSector.fromJson(article)); 38 | } 39 | } 40 | return result; 41 | } 42 | 43 | class ArticleSector { 44 | ArticleSector({this.createdAt, this.desc, this.images, this.publishedAt, this.url, this.who}); 45 | 46 | String createdAt; 47 | String desc; 48 | List images; 49 | String publishedAt; 50 | String url; 51 | String who; 52 | 53 | factory ArticleSector.fromJson(dynamic jsonString) { 54 | Map raw = jsonString; 55 | return new ArticleSector( 56 | createdAt: raw['createdAt'], 57 | desc: raw['desc'], 58 | images: (raw.containsKey('images') && raw['images'] != null) ? new List.from(raw['images']) : null, 59 | publishedAt: raw['publishedAt'], 60 | url: raw['url'], 61 | who: raw['who'], 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/data/history_result.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:html/dom.dart'; 3 | import 'package:html/parser.dart' show parse; 4 | 5 | class HistoryResult { 6 | HistoryResult({this.error, this.results}); 7 | 8 | bool error; 9 | 10 | List results; 11 | 12 | factory HistoryResult.fromJson(String raw) { 13 | var decoded = json.decode(raw); 14 | var results = List(); 15 | for (var rawItem in decoded['results']) { 16 | results.add(HistoryResultItem.fromJson(rawItem)); 17 | } 18 | return HistoryResult(error: decoded['error'], results: results); 19 | } 20 | } 21 | 22 | class HistoryResultItem { 23 | HistoryResultItem({this.id, this.sections, this.publishedAt, this.title}); 24 | 25 | String id; 26 | String publishedAt; 27 | String title; 28 | 29 | List sections; 30 | 31 | factory HistoryResultItem.fromJson(dynamic raw) { 32 | String id = raw['_id']; 33 | var resolvedSections = List(); 34 | 35 | Document document = parse(raw['content']); 36 | 37 | HistoryArticleSection section; 38 | bool isSectionValid = false; 39 | for (Node node in document.body.nodes) { 40 | try { 41 | if (node.toString() == '') { 42 | section = new HistoryArticleSection(); 43 | isSectionValid = true; 44 | section.name = node.text; 45 | } 46 | 47 | if (section != null && node.toString() == '') { 48 | section.articles = List(); 49 | for (var liItem in node.nodes) { 50 | if (liItem.toString() == '') { 51 | String value = ""; 52 | HistoryArticle article = HistoryArticle(); 53 | 54 | for (var liItemChild in liItem.nodes) { 55 | if (liItemChild.text != null && liItemChild.text.trim().length != 0) { 56 | if (article.url == null && 57 | liItemChild.attributes['href'] != null && 58 | liItemChild.attributes['href'].length != 0) { 59 | article.url = liItemChild.attributes['href'].trim(); 60 | } 61 | String currentText = liItemChild.text.trim(); 62 | value += currentText; 63 | } 64 | } 65 | article.title = value; 66 | 67 | section.articles.add(article); 68 | } 69 | } 70 | } 71 | if (section != null && isSectionValid) { 72 | resolvedSections.add(section); 73 | isSectionValid = false; 74 | } 75 | } catch (e) {} 76 | } 77 | return HistoryResultItem(id: id, sections: resolvedSections, publishedAt: raw['publishedAt'], title: raw['title']); 78 | } 79 | } 80 | 81 | class HistoryArticleSection { 82 | HistoryArticleSection({this.name, this.articles}); 83 | 84 | String name; 85 | List articles; 86 | } 87 | 88 | class HistoryArticle { 89 | HistoryArticle({this.title, this.url}); 90 | 91 | String title; 92 | String url; 93 | } 94 | -------------------------------------------------------------------------------- /lib/data/reading_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class ReadingCategoryResult { 4 | ReadingCategoryResult({this.error, this.results}); 5 | 6 | bool error; 7 | 8 | List results; 9 | 10 | factory ReadingCategoryResult.fromJson(String raw) { 11 | var decoded = json.decode(raw); 12 | var results = List(); 13 | for (var rawCategory in decoded['results']) { 14 | results.add(ReadingCategory.fromJson(rawCategory)); 15 | } 16 | return ReadingCategoryResult(error: decoded['error'], results: results); 17 | } 18 | } 19 | 20 | class ReadingCategory { 21 | ReadingCategory({this.id, this.name, this.enName}); 22 | 23 | String id; 24 | String name; 25 | String enName; 26 | 27 | bool selected = false; 28 | 29 | factory ReadingCategory.fromJson(dynamic raw) { 30 | Map rawCategory = raw; 31 | return ReadingCategory(id: rawCategory['_id'], name: rawCategory['name'], enName: rawCategory['en_name']); 32 | } 33 | } 34 | 35 | class ReadingSiteResult { 36 | ReadingSiteResult({this.error, this.results}); 37 | 38 | bool error; 39 | List results; 40 | 41 | factory ReadingSiteResult.fromJson(String raw) { 42 | var decoded = json.decode(raw); 43 | var results = List(); 44 | for (var rawSite in decoded['results']) { 45 | results.add(ReadingSite.fromJson(rawSite)); 46 | } 47 | return ReadingSiteResult(error: decoded['error'], results: results); 48 | } 49 | } 50 | 51 | class ReadingSite { 52 | ReadingSite({this.id, this.title, this.icon}); 53 | 54 | String id; 55 | String icon; 56 | String title; 57 | 58 | factory ReadingSite.fromJson(dynamic raw) { 59 | Map rawSite = raw; 60 | return ReadingSite(id: rawSite['id'], icon: rawSite['icon'], title: rawSite['title']); 61 | } 62 | } 63 | 64 | class ReadingArticleResult { 65 | ReadingArticleResult({this.error, this.results}); 66 | 67 | bool error; 68 | List results; 69 | 70 | factory ReadingArticleResult.fromJson(String raw) { 71 | var decoded = json.decode(raw); 72 | var results = List(); 73 | for (var rawArticle in decoded['results']) { 74 | results.add(ReadingArticle.fromJson(rawArticle)); 75 | } 76 | return ReadingArticleResult(error: decoded['error'], results: results); 77 | } 78 | } 79 | 80 | class ReadingArticle { 81 | ReadingArticle({this.url, this.title, this.published, this.author}); 82 | 83 | String url; 84 | 85 | String title; 86 | 87 | String published; 88 | 89 | String author; 90 | 91 | factory ReadingArticle.fromJson(dynamic raw) { 92 | String publishTime = raw['published_at']; 93 | publishTime = (publishTime != null && publishTime.length > 10) ? publishTime.substring(0, 10) : publishTime; 94 | 95 | return ReadingArticle(url: raw['url'], title: raw['title'], published: publishTime); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/data/search_result.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | class SearchResult { 4 | SearchResult({this.count, this.error, this.results}); 5 | 6 | int count; 7 | bool error; 8 | List results; 9 | 10 | factory SearchResult.fromJson(String raw) { 11 | var decoded = json.decode(raw); 12 | List resolvedResults = List(); 13 | for (var rawItem in decoded['results']) { 14 | resolvedResults.add(SearchResultItem.fromJson(rawItem)); 15 | } 16 | return SearchResult(count: decoded['count'], error: decoded['error'], results: resolvedResults); 17 | } 18 | } 19 | 20 | class SearchResultItem { 21 | SearchResultItem({this.desc, this.id, this.publishedAt, this.type, this.url, this.who}); 22 | 23 | String desc; 24 | String id; 25 | String publishedAt; 26 | String type; 27 | String url; 28 | String who; 29 | 30 | factory SearchResultItem.fromJson(dynamic raw) { 31 | Map rawItem = raw; 32 | return SearchResultItem( 33 | desc: rawItem['desc'], 34 | id: rawItem['ganhuo_id'], 35 | publishedAt: rawItem['publishedAt'], 36 | type: rawItem['type'], 37 | url: rawItem['url'], 38 | who: rawItem['who']); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/display/history.dart: -------------------------------------------------------------------------------- 1 | import 'package:event_bus/event_bus.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gankio/data/history_result.dart'; 4 | import 'package:gankio/display/history_details.dart'; 5 | import 'package:gankio/presenter/history_presenter.dart'; 6 | 7 | var _bus = EventBus(); 8 | 9 | class HistoryPage extends StatefulWidget { 10 | @override 11 | _HistoryPageState createState() => new _HistoryPageState(); 12 | } 13 | 14 | class _HistoryPageState extends State implements HistoryView { 15 | HistoryPresenter _presenter; 16 | 17 | var _data = List(); 18 | var _pageIndex = 1; 19 | var _requesting = false; 20 | var _end = false; 21 | 22 | _HistoryPageState() { 23 | _presenter = HistoryPresenter(this); 24 | } 25 | 26 | _updateResults(HistoryResult result) { 27 | if (this.mounted) { 28 | setState(() { 29 | _data.addAll(result.results); 30 | }); 31 | } 32 | } 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _presenter.fetchHistory(page: _pageIndex); 38 | _bus.on().listen((event) { 39 | _requesting = false; 40 | _updateResults(event.result); 41 | }); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | super.dispose(); 47 | } 48 | 49 | var _scrollController = ScrollController(); 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | _scrollController.addListener(() { 54 | if (!_requesting && _scrollController.position.pixels == _scrollController.position.maxScrollExtent) { 55 | _pageIndex++; 56 | _requesting = true; 57 | _presenter.fetchHistory(page: _pageIndex); 58 | } 59 | }); 60 | 61 | if (_data.length != 0) { 62 | return Center( 63 | child: ListView.builder( 64 | controller: _scrollController, 65 | padding: EdgeInsets.all(3.0), 66 | itemCount: _end ? _data.length : _data.length + 1, 67 | itemBuilder: (context, index) { 68 | if (index < _data.length) { 69 | return HistoryListItem(_data[index]); 70 | } else { 71 | return Container( 72 | height: 60.0, 73 | child: Center( 74 | child: Container( 75 | width: 24.0, 76 | height: 24.0, 77 | child: CircularProgressIndicator( 78 | strokeWidth: 2.0, 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | }, 85 | ), 86 | ); 87 | } else { 88 | return Center( 89 | child: Container( 90 | width: MediaQuery.of(context).size.width * 0.08, 91 | height: MediaQuery.of(context).size.width * 0.08, 92 | child: CircularProgressIndicator(), 93 | ), 94 | ); 95 | } 96 | } 97 | 98 | @override 99 | fetchReceived(HistoryResult result) { 100 | if (result.results != null && result.results.length != 0) { 101 | _bus.fire(HistoryResultEvent(result: result)); 102 | } else { 103 | _end = true; 104 | } 105 | } 106 | } 107 | 108 | class HistoryListItem extends StatelessWidget { 109 | HistoryListItem(this.item); 110 | 111 | final HistoryResultItem item; 112 | 113 | @override 114 | Widget build(BuildContext context) { 115 | var widgets = List(); 116 | widgets.add(Text( 117 | '${item.title}', 118 | maxLines: 2, 119 | overflow: TextOverflow.ellipsis, 120 | style: TextStyle(fontSize: 17.0, color: Colors.black87, fontWeight: FontWeight.w600), 121 | )); 122 | widgets.add(Text( 123 | ' ${item.publishedAt.length > 10 ? item.publishedAt.substring(0, 10) : item.publishedAt}', 124 | style: TextStyle(fontSize: 10.0, color: Colors.blueAccent, fontWeight: FontWeight.w500), 125 | )); 126 | String content = ''; 127 | for (HistoryArticleSection section in item.sections) { 128 | if (section.articles != null) { 129 | for (HistoryArticle article in section.articles) { 130 | content += article.title.substring(0, article.title.lastIndexOf('(')).trim(); 131 | content += ' '; 132 | } 133 | } 134 | } 135 | widgets.add(Container( 136 | child: Text( 137 | content, 138 | maxLines: 8, 139 | overflow: TextOverflow.ellipsis, 140 | style: TextStyle( 141 | fontSize: 13.3, 142 | color: Colors.black54, 143 | ), 144 | ), 145 | )); 146 | widgets.add(Container( 147 | margin: EdgeInsets.only(top: 8.0), 148 | child: MaterialButton( 149 | onPressed: () { 150 | showDialog( 151 | context: context, 152 | builder: (context) { 153 | return HistoryDetailDialog(item); 154 | }); 155 | }, 156 | color: Colors.blueAccent, 157 | child: Text( 158 | '详情', 159 | style: TextStyle(fontSize: 13.0, color: Colors.white, letterSpacing: 1.0), 160 | ), 161 | ), 162 | alignment: Alignment.topRight, 163 | )); 164 | 165 | return Card( 166 | child: Container( 167 | padding: EdgeInsets.all(10.0), 168 | child: Column( 169 | crossAxisAlignment: CrossAxisAlignment.start, 170 | children: widgets, 171 | ), 172 | ), 173 | ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /lib/display/history_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gankio/data/history_result.dart'; 3 | import 'package:gankio/display/web_view.dart'; 4 | import 'package:gankio/main.dart'; 5 | 6 | class HistoryDetailDialog extends StatefulWidget { 7 | HistoryDetailDialog(this.article); 8 | 9 | final HistoryResultItem article; 10 | 11 | @override 12 | _HistoryDetailDialogState createState() => _HistoryDetailDialogState(article); 13 | } 14 | 15 | class _HistoryDetailDialogState extends State { 16 | _HistoryDetailDialogState(this._item); 17 | 18 | HistoryResultItem _item; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | var contentWidgets = List(); 23 | for (HistoryArticleSection section in _item.sections) { 24 | if (section.articles != null && section.articles.length != 0) { 25 | for (HistoryArticle article in section.articles) { 26 | contentWidgets.add(_buildItem(section.name, article)); 27 | } 28 | } 29 | } 30 | 31 | return SimpleDialog( 32 | titlePadding: EdgeInsets.only(top: 15.0, left: 12.0, right: 12.0), 33 | contentPadding: EdgeInsets.only(bottom: 12.0), 34 | title: Column( 35 | crossAxisAlignment: CrossAxisAlignment.start, 36 | children: [ 37 | Text( 38 | '${(_item.publishedAt != null && _item.publishedAt.length > 10) ? _item.publishedAt.substring(0, 10) : _item 39 | .publishedAt}', 40 | maxLines: 1, 41 | overflow: TextOverflow.ellipsis, 42 | style: TextStyle( 43 | fontSize: 13.0, 44 | color: Colors.orangeAccent, 45 | fontWeight: FontWeight.w600, 46 | ), 47 | ), 48 | Text( 49 | '${_item.title}', 50 | maxLines: 2, 51 | overflow: TextOverflow.ellipsis, 52 | style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.w600), 53 | ), 54 | ], 55 | ), 56 | children: contentWidgets, 57 | ); 58 | } 59 | 60 | Widget _buildItem(String sectionName, HistoryArticle article) { 61 | return InkWell( 62 | onTap: () { 63 | Navigator.push( 64 | context, 65 | GankNavigator( 66 | builder: (context) => WebViewer( 67 | url: article.url, 68 | title: article.title, 69 | ))); 70 | }, 71 | child: Container( 72 | padding: EdgeInsets.only(left: 12.0, right: 12.0), 73 | height: 52.0, 74 | child: Column( 75 | crossAxisAlignment: CrossAxisAlignment.start, 76 | children: [ 77 | _buildItemTextWidget(sectionName, article), 78 | ], 79 | ), 80 | ), 81 | ); 82 | } 83 | 84 | Expanded _buildItemTextWidget(String sectionName, HistoryArticle article) { 85 | return Expanded( 86 | child: Row( 87 | children: [ 88 | Expanded( 89 | child: RichText( 90 | maxLines: 2, 91 | overflow: TextOverflow.ellipsis, 92 | text: TextSpan( 93 | children: [ 94 | TextSpan( 95 | text: '[$sectionName] ', 96 | style: TextStyle( 97 | fontSize: 11.0, 98 | color: Colors.blueAccent, 99 | fontWeight: FontWeight.w500, 100 | ), 101 | ), 102 | TextSpan( 103 | text: '${article.title}', 104 | style: TextStyle( 105 | fontSize: 13.0, 106 | color: Color(0xff464646), 107 | ), 108 | ), 109 | ], 110 | ), 111 | ), 112 | ), 113 | ], 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/display/image_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gankio/widget/photo_view.dart'; 3 | 4 | class ImageViewPage extends StatelessWidget { 5 | ImageViewPage(this.imageUrl); 6 | 7 | final String imageUrl; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return new Scaffold( 12 | appBar: new AppBar( 13 | title: Row( 14 | children: [ 15 | Expanded( 16 | child: new Text('查看大图'), 17 | ), 18 | ], 19 | ), 20 | ), 21 | body: new Container( 22 | child: new PhotoView( 23 | imageProvider: new NetworkImage(imageUrl), 24 | ), 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/display/reading.dart: -------------------------------------------------------------------------------- 1 | import 'package:event_bus/event_bus.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gankio/data/reading_data.dart'; 4 | import 'package:gankio/display/reading_category.dart'; 5 | import 'package:gankio/display/web_view.dart'; 6 | import 'package:gankio/main.dart'; 7 | import 'package:gankio/presenter/reading_presenter.dart'; 8 | 9 | var _bus = EventBus(); 10 | 11 | class ReadingPage extends StatefulWidget { 12 | @override 13 | _ReadingPageState createState() => _ReadingPageState(); 14 | } 15 | 16 | class _ReadingPageState extends State implements ReadingView { 17 | var _currentSiteName = '小众软件'; 18 | var _currentSiteId = 'appinn'; 19 | 20 | var _currentArticles = List(); 21 | var _isListEnd = false; 22 | 23 | ReadingPresenter _presenter; 24 | 25 | var _scrollController = ScrollController(); 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _presenter = ReadingPresenter(this); 31 | _presenter.fetchReadingArticles(_currentSiteId, true); 32 | _bus.on().listen((event) { 33 | if (this.mounted) { 34 | setState(() { 35 | if (event.clear) { 36 | _currentArticles.clear(); 37 | } 38 | _currentArticles.addAll(event.results); 39 | }); 40 | } 41 | }); 42 | _bus.on().listen((event) { 43 | if (this.mounted) { 44 | setState(() { 45 | _isListEnd = true; 46 | }); 47 | } 48 | }); 49 | 50 | _scrollController.addListener(() { 51 | if (!_isListEnd && _scrollController.position.pixels == _scrollController.position.maxScrollExtent) { 52 | _presenter.fetchReadingArticles(_currentSiteId, false); 53 | } 54 | }); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | var _articleWidgets = List(); 60 | for (var article in _currentArticles) { 61 | _articleWidgets.add(_buildArticleItem(article)); 62 | } 63 | if (!_isListEnd) { 64 | _articleWidgets.add(_buildArticleLoading()); 65 | } 66 | 67 | return Container( 68 | child: Column( 69 | children: [ 70 | Container( 71 | color: Color(0xcc538195), 72 | height: 60.0, 73 | padding: EdgeInsets.only(left: 15.0, right: 15.0), 74 | child: Row( 75 | children: [ 76 | Expanded( 77 | child: Text( 78 | _currentSiteName, 79 | style: TextStyle(color: Colors.white), 80 | ), 81 | ), 82 | Container( 83 | child: IconButton( 84 | onPressed: () async { 85 | Map results = await Navigator.push(context, MaterialPageRoute(builder: (context) => ReadingCategoryPage())); 86 | if (results != null) { 87 | setState(() { 88 | if (this.mounted) { 89 | _isListEnd = false; 90 | _currentArticles.clear(); 91 | _currentSiteName = results['siteName']; 92 | _currentSiteId = results['siteId']; 93 | _scrollController.jumpTo(0.0); 94 | _presenter.handleSiteChange(); 95 | _presenter.fetchReadingArticles(_currentSiteId, true); 96 | } 97 | }); 98 | } 99 | }, 100 | tooltip: '修改分类', 101 | icon: Icon(Icons.edit), 102 | color: Colors.white, 103 | ), 104 | ) 105 | ], 106 | ), 107 | ), 108 | Expanded( 109 | child: ListView( 110 | controller: _scrollController, 111 | children: _articleWidgets, 112 | ), 113 | ), 114 | ], 115 | ), 116 | ); 117 | } 118 | 119 | Widget _buildArticleLoading() { 120 | return Container( 121 | height: 60.0, 122 | child: Center( 123 | child: Container( 124 | width: 24.0, 125 | height: 24.0, 126 | child: CircularProgressIndicator( 127 | strokeWidth: 2.0, 128 | ), 129 | ), 130 | ), 131 | ); 132 | } 133 | 134 | Widget _buildArticleItem(ReadingArticle article) { 135 | return Container( 136 | height: 60.0, 137 | padding: EdgeInsets.only(left: 12.0, right: 12.0), 138 | child: Column( 139 | crossAxisAlignment: CrossAxisAlignment.start, 140 | children: [ 141 | Expanded( 142 | child: Material( 143 | child: InkWell( 144 | onTap: () { 145 | Navigator.push( 146 | context, 147 | GankNavigator( 148 | builder: (context) => WebViewer( 149 | url: article.url, 150 | title: article.title, 151 | ))); 152 | }, 153 | child: Row( 154 | children: [ 155 | Expanded( 156 | child: RichText( 157 | maxLines: 2, 158 | overflow: TextOverflow.ellipsis, 159 | text: TextSpan( 160 | children: [ 161 | TextSpan(text: '${article.published}\n', style: TextStyle(fontSize: 10.0, color: Colors.blueAccent)), 162 | TextSpan(text: article.title, style: TextStyle(fontSize: 13.0, color: Colors.black87)), 163 | ], 164 | ), 165 | ), 166 | ), 167 | ], 168 | ), 169 | ), 170 | color: Colors.transparent, 171 | ), 172 | ), 173 | Container( 174 | height: 1.0, 175 | color: Colors.black12, 176 | ) 177 | ], 178 | ), 179 | ); 180 | } 181 | 182 | @override 183 | fetchReadingArticlesReceived(ReadingArticleResult result, bool clear) { 184 | _bus.fire(ReadingArticlesEvent(results: result.results, clear: clear)); 185 | } 186 | 187 | @override 188 | fetchReadingArticlesEnd() { 189 | _bus.fire(ReadingArticlesEndEvent()); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /lib/display/reading_category.dart: -------------------------------------------------------------------------------- 1 | import 'package:event_bus/event_bus.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gankio/data/reading_data.dart'; 4 | import 'package:gankio/presenter/reading_presenter.dart'; 5 | 6 | var _bus = EventBus(); 7 | 8 | class ReadingCategoryPage extends StatefulWidget { 9 | @override 10 | _ReadingCategoryState createState() => _ReadingCategoryState(); 11 | } 12 | 13 | class _ReadingCategoryState extends State implements ReadingCategoryView { 14 | var _categories = List(); 15 | var _sites = List(); 16 | 17 | ReadingCategoryPresenter _presenter; 18 | 19 | _ReadingCategoryState() { 20 | _presenter = ReadingCategoryPresenter(this); 21 | } 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | _presenter.fetchCategory(); 27 | _bus.on().listen((event) { 28 | if (this.mounted) { 29 | setState(() { 30 | _categories.clear(); 31 | _categories.addAll(event.categories); 32 | changeCurrentSelectedCategory(event.categories[0]); 33 | _presenter.fetchSites(event.categories[0].enName); 34 | }); 35 | } 36 | }); 37 | _bus.on().listen((event) { 38 | if (this.mounted) { 39 | setState(() { 40 | _sites.clear(); 41 | _sites.addAll(event.sites); 42 | }); 43 | } 44 | }); 45 | _bus.on().listen((event) { 46 | if (this.mounted) { 47 | setState(() { 48 | if (changeCurrentSelectedCategory(event.category)) { 49 | _presenter.fetchSites(event.category.enName); 50 | } 51 | }); 52 | } 53 | }); 54 | } 55 | 56 | bool changeCurrentSelectedCategory(ReadingCategory category) { 57 | if (_categories != null) { 58 | bool result = false; 59 | for (var c in _categories) { 60 | if (c.enName == category.enName) { 61 | result = true; 62 | c.selected = true; 63 | } else { 64 | c.selected = false; 65 | } 66 | } 67 | return result; 68 | } 69 | return false; 70 | } 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | var _categoryWidgets = List(); 75 | for (var category in _categories) { 76 | _categoryWidgets.add(_buildCategoryItem(category)); 77 | } 78 | 79 | var _siteWidgets = List(); 80 | for (var site in _sites) { 81 | _siteWidgets.add(_buildSiteItem(site)); 82 | } 83 | 84 | return Scaffold( 85 | appBar: new AppBar( 86 | title: Row( 87 | children: [ 88 | Expanded( 89 | child: new Text('选择闲读分类'), 90 | ), 91 | ], 92 | ), 93 | ), 94 | body: Row( 95 | children: [ 96 | Container( 97 | color: Color(0x99D9DAE4), 98 | padding: EdgeInsets.only(top: 10.0), 99 | width: MediaQuery.of(context).size.width * 0.3, 100 | child: ListView( 101 | children: _categoryWidgets, 102 | ), 103 | ), 104 | Container( 105 | color: Colors.white, 106 | padding: EdgeInsets.only(top: 10.0), 107 | width: MediaQuery.of(context).size.width * 0.7, 108 | child: ListView(children: _siteWidgets), 109 | ), 110 | ], 111 | ), 112 | ); 113 | } 114 | 115 | Widget _buildSiteItem(ReadingSite site) { 116 | return Material( 117 | child: InkWell( 118 | onTap: () { 119 | Navigator.pop(context, {'siteId': site.id, 'siteName': site.title}); 120 | }, 121 | child: Container( 122 | padding: EdgeInsets.only(left: 10.0, right: 10.0, top: 10.0), 123 | margin: EdgeInsets.only(bottom: 18.0), 124 | child: Column( 125 | children: [ 126 | Row( 127 | children: [ 128 | Container( 129 | height: 40.0, 130 | width: 40.0, 131 | margin: EdgeInsets.only(right: 10.0), 132 | child: Image.network(site.icon), 133 | ), 134 | Expanded( 135 | child: Text( 136 | site.title, 137 | maxLines: 2, 138 | style: TextStyle( 139 | fontSize: 13.0, 140 | color: Colors.black87, 141 | ), 142 | overflow: TextOverflow.ellipsis, 143 | ), 144 | ), 145 | ], 146 | ), 147 | Container( 148 | height: 1.2, 149 | margin: EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0), 150 | color: Colors.black12, 151 | ) 152 | ], 153 | ), 154 | ), 155 | ), 156 | color: Colors.transparent, 157 | ); 158 | } 159 | 160 | Widget _buildCategoryItem(ReadingCategory category) { 161 | return GestureDetector( 162 | onTap: () { 163 | _bus.fire(ReadingCategorySwitchEvent(category)); 164 | }, 165 | child: Container( 166 | height: 46.0, 167 | padding: EdgeInsets.only(top: 8.0, bottom: 8.0, left: 8.0, right: 8.0), 168 | child: Container( 169 | padding: EdgeInsets.only(left: 10.0, right: 10.0), 170 | decoration: BoxDecoration( 171 | color: Colors.blueAccent, 172 | borderRadius: BorderRadius.all( 173 | Radius.circular(15.0), 174 | ), 175 | ), 176 | child: Center( 177 | child: Text( 178 | '${category.name}', 179 | maxLines: 1, 180 | overflow: TextOverflow.ellipsis, 181 | style: TextStyle( 182 | fontSize: 13.5, 183 | fontWeight: FontWeight.w300, 184 | color: category.selected ? Colors.deepOrangeAccent : Colors.white, 185 | ), 186 | ), 187 | ), 188 | ), 189 | ), 190 | ); 191 | } 192 | 193 | @override 194 | fetchCategoryReceived(ReadingCategoryResult result) { 195 | if (result.results != null && result.results.length != 0) { 196 | _bus.fire(ReadingCategoryEvent(categories: result.results)); 197 | } 198 | } 199 | 200 | @override 201 | fetchSitesReceived(ReadingSiteResult result) { 202 | if (result.results != null && result.results.length != 0) { 203 | _bus.fire(ReadingSiteEvent(sites: result.results)); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/display/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:event_bus/event_bus.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:gankio/data/search_result.dart'; 5 | import 'package:gankio/display/web_view.dart'; 6 | import 'package:gankio/main.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | var _bus = EventBus(); 10 | 11 | class SearchPage extends StatefulWidget { 12 | @override 13 | _SearchPageState createState() => new _SearchPageState(); 14 | } 15 | 16 | class _SearchPageState extends State { 17 | List _searchResults = List(); 18 | 19 | var _currentKeyword = ''; 20 | var _page = 1; 21 | 22 | var _pageTitle = '搜索干货'; 23 | 24 | _updateResults(List items, bool clear) { 25 | if (this.mounted) { 26 | setState(() { 27 | if (clear) _searchResults.clear(); 28 | _searchResults.addAll(items); 29 | }); 30 | } 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | var _keywordInputController = TextEditingController(text: _currentKeyword); 36 | var _requesting = false; 37 | var _endList = false; 38 | 39 | var _scrollController = ScrollController(); 40 | 41 | _bus.on<_SearchResultEvent>().listen((event) { 42 | _requesting = false; 43 | if (event.result.results != null && event.result.results.length != 0) { 44 | _updateResults(event.result.results, event.clear); 45 | } else { 46 | _endList = true; 47 | } 48 | }); 49 | 50 | _startSearch(bool clear) { 51 | if (clear) { 52 | _endList = false; 53 | _scrollController.jumpTo(0.0); 54 | } 55 | if (!_endList) { 56 | _currentKeyword = _keywordInputController.text; 57 | if (_currentKeyword != null && _currentKeyword.length != 0) { 58 | _pageTitle = '搜索干货:$_currentKeyword'; 59 | _requesting = true; 60 | _fetchSearchResult(key: _currentKeyword, page: _page, clear: clear); 61 | SystemChannels.textInput.invokeMethod('TextInput.hide'); 62 | } 63 | } 64 | } 65 | 66 | _scrollController.addListener(() { 67 | if (!_requesting && _scrollController.position.pixels == _scrollController.position.maxScrollExtent) { 68 | _page++; 69 | _startSearch(false); 70 | } 71 | }); 72 | 73 | ListView _buildListView() { 74 | return ListView.builder( 75 | controller: _scrollController, 76 | itemCount: _searchResults.length, 77 | itemBuilder: (context, index) { 78 | return _buildItemView(_searchResults[index], index); 79 | }); 80 | } 81 | 82 | return new Scaffold( 83 | appBar: new AppBar( 84 | title: new Text(_pageTitle), 85 | ), 86 | body: new Container( 87 | padding: EdgeInsets.only(left: 10.0, right: 10.0, top: 8.0, bottom: 8.0), 88 | child: new Column( 89 | children: [ 90 | new Container( 91 | padding: EdgeInsets.all(5.0), 92 | child: Row( 93 | children: [ 94 | Expanded( 95 | child: TextFormField( 96 | autofocus: true, 97 | style: TextStyle(fontSize: 14.0, color: Colors.black87), 98 | keyboardType: TextInputType.text, 99 | controller: _keywordInputController, 100 | onFieldSubmitted: (val) { 101 | _startSearch(true); 102 | }, 103 | decoration: InputDecoration(hintText: '在此输入关键词~~'), 104 | ), 105 | ), 106 | IconButton( 107 | onPressed: () { 108 | _startSearch(true); 109 | }, 110 | icon: Icon( 111 | Icons.search, 112 | color: Colors.blueGrey, 113 | ), 114 | ), 115 | ], 116 | ), 117 | ), 118 | new Expanded( 119 | child: _buildListView(), 120 | ) 121 | ], 122 | )), 123 | ); 124 | } 125 | 126 | Widget _buildItemView(SearchResultItem resultItem, int index) { 127 | return Container( 128 | margin: EdgeInsets.only(top: 3.5, bottom: 3.5), 129 | padding: EdgeInsets.only(top: 5.0, bottom: 5.0, left: 5.0, right: 2.0), 130 | child: GestureDetector( 131 | onTap: () { 132 | Navigator.push( 133 | context, 134 | MaterialPageRoute( 135 | builder: (context) => WebViewer( 136 | url: resultItem.url, 137 | title: resultItem.desc, 138 | ))); 139 | }, 140 | child: Material( 141 | child: InkWell( 142 | onTap: () { 143 | Navigator.push( 144 | context, 145 | GankNavigator( 146 | builder: (context) => WebViewer( 147 | url: resultItem.url, 148 | title: resultItem.desc, 149 | ))); 150 | }, 151 | child: Column( 152 | crossAxisAlignment: CrossAxisAlignment.start, 153 | children: [ 154 | Text( 155 | '${index + 1}. ${resultItem.desc}'.trim(), 156 | style: TextStyle(fontSize: 15.2, color: Colors.black87), 157 | maxLines: 2, 158 | overflow: TextOverflow.ellipsis, 159 | ), 160 | Container( 161 | margin: EdgeInsets.only(top: 3.0), 162 | child: Text( 163 | '[${resultItem.type}] By ${resultItem.who} at ${resultItem.publishedAt}', 164 | style: TextStyle(fontSize: 9.0, color: Colors.grey[500]), 165 | ), 166 | ), 167 | Container( 168 | margin: EdgeInsets.only(top: 5.5), 169 | color: Colors.black12, 170 | height: 1.5, 171 | ) 172 | ], 173 | ), 174 | ), 175 | color: Colors.transparent, 176 | ), 177 | ), 178 | ); 179 | } 180 | } 181 | 182 | _fetchSearchResult({String key = 'Gank', String category = 'all', int page = 1, bool clear = false}) async { 183 | final response = await http.get("https://gank.io/api/search/query/$key/category/$category/count/30/page/$page"); 184 | if (response.statusCode == 200) { 185 | _bus.fire(_SearchResultEvent(result: SearchResult.fromJson(response.body), clear: clear)); 186 | } else { 187 | throw Exception('Search-Error ${response.statusCode}'); 188 | } 189 | } 190 | 191 | class _SearchResultEvent { 192 | _SearchResultEvent({this.result, this.clear}); 193 | 194 | SearchResult result; 195 | 196 | bool clear = false; 197 | } 198 | -------------------------------------------------------------------------------- /lib/display/setting.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class SettingPage extends StatefulWidget { 5 | @override 6 | _SettingPageState createState() => _SettingPageState(); 7 | } 8 | 9 | class _SettingPageState extends State { 10 | @override 11 | Widget build(BuildContext context) { 12 | return Container( 13 | padding: EdgeInsets.only(left: 15.0, right: 15.0, top: 12.0), 14 | child: Column( 15 | children: [ 16 | Container( 17 | margin: EdgeInsets.only(top: 50.0, bottom: 20.0), 18 | height: 75.0, 19 | width: 75.0, 20 | child: Image.asset('images/gankio.png'), 21 | ), 22 | RichText( 23 | text: TextSpan( 24 | children: [ 25 | TextSpan( 26 | text: 'Gank.IO Flutter', 27 | style: TextStyle(color: Colors.black, fontSize: 17.0, fontWeight: FontWeight.w600)), 28 | TextSpan(text: '\nby cfelixmac_v1.0_201808', style: TextStyle(color: Colors.black54, fontSize: 12.0)), 29 | ], 30 | ), 31 | ), 32 | Container( 33 | margin: EdgeInsets.only(top: 80.0), 34 | child: Column( 35 | children: [ 36 | Text( 37 | '3rd Libraries:', 38 | style: TextStyle(fontWeight: FontWeight.w600, color: Colors.black45, fontSize: 13.0), 39 | ), 40 | Text('flutter_webview_plugin', 41 | style: TextStyle(fontWeight: FontWeight.w400, color: Colors.black45, fontSize: 12.0)), 42 | Text('event_bus', 43 | style: TextStyle(fontWeight: FontWeight.w400, color: Colors.black45, fontSize: 12.0)), 44 | Text('fluttertoast', 45 | style: TextStyle(fontWeight: FontWeight.w400, color: Colors.black45, fontSize: 12.0)), 46 | ], 47 | ), 48 | ) 49 | ], 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/display/today.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:gankio/data/article.dart'; 5 | import 'package:gankio/display/today_details.dart'; 6 | import 'package:html/dom.dart' as dom; 7 | import 'package:html/parser.dart' show parse; 8 | import 'package:http/http.dart' as http; 9 | 10 | class TodayPage extends StatefulWidget { 11 | @override 12 | _TodayPageState createState() => _TodayPageState(); 13 | } 14 | 15 | class _TodayPageState extends State { 16 | @override 17 | Widget build(BuildContext context) { 18 | return Container( 19 | color: const Color(0x99D9DAE4), 20 | child: FutureBuilder
( 21 | future: _fetchHomePage(), 22 | builder: (context, snapshot) { 23 | if (snapshot.hasData) { 24 | return _generateContent(snapshot.data, context); 25 | } else if (snapshot.hasError) { 26 | return new Center( 27 | child: Text("${snapshot.error}"), 28 | ); 29 | } 30 | return new Container( 31 | width: MediaQuery.of(context).size.width * 0.15, 32 | height: MediaQuery.of(context).size.width * 0.15, 33 | child: Center( 34 | child: CircularProgressIndicator(), // By default, show a loading spinner 35 | ), 36 | ); 37 | }, 38 | ), 39 | ); 40 | } 41 | } 42 | 43 | Widget _generateContent(Article article, BuildContext context) { 44 | List items = new List(); 45 | // ==============> Title 46 | items.add(FutureBuilder( 47 | future: _fetchHtml(), 48 | builder: (context, snapshot) { 49 | if (snapshot.hasData) { 50 | for (var node in snapshot.data.head.nodes) { 51 | if (node != null && node.toString() == '') { 52 | return _generateTitle(node.text); 53 | } 54 | } 55 | } 56 | return _generateTitle("Gank IO~"); 57 | }, 58 | )); 59 | // ==============> RequestTime 60 | items.add( 61 | new Container( 62 | padding: EdgeInsets.only(top: 8.0, bottom: 8.0), 63 | child: Text( 64 | "请求时间:${ DateTime.now().toString()}", 65 | textAlign: TextAlign.left, 66 | style: TextStyle(fontSize: 10.0, color: const Color(0xFF747474)), 67 | maxLines: 1, 68 | overflow: TextOverflow.ellipsis, 69 | )), 70 | ); 71 | 72 | // =================> App Content 73 | var appArticles = article.result['App']; 74 | if (appArticles != null && appArticles.length != 0) { 75 | items.add(_buildArticlesBlock(title: 'App', icon: 'images/app_icon.png', articles: appArticles, context: context)); 76 | items.add(_buildSectionSpace()); 77 | } 78 | // =================> Android Content 79 | var androidArticles = article.result['Android']; 80 | if (androidArticles != null && androidArticles.length != 0) { 81 | items.add(_buildArticlesBlock( 82 | title: 'Android', icon: 'images/android_icon.png', articles: androidArticles, context: context)); 83 | items.add(_buildSectionSpace()); 84 | } 85 | // =================> iOS Content 86 | var iOSArticles = article.result['iOS']; 87 | if (iOSArticles != null && iOSArticles.length != 0) { 88 | items.add(_buildArticlesBlock(title: 'iOS', icon: 'images/ios_icon.png', articles: iOSArticles, context: context)); 89 | items.add(_buildSectionSpace()); 90 | } 91 | // =================> QianDuan Content 92 | var frontArticles = article.result['前端']; 93 | if (frontArticles != null && frontArticles.length != 0) { 94 | items.add( 95 | _buildArticlesBlock(title: '前端', icon: 'images/front_icon.png', articles: frontArticles, context: context)); 96 | items.add(_buildSectionSpace()); 97 | } 98 | // =================> TuoZhan Content 99 | var extendArticles = article.result['拓展资源']; 100 | if (extendArticles != null && extendArticles.length != 0) { 101 | items.add( 102 | _buildArticlesBlock(title: '拓展资源', icon: 'images/extend_icon.png', articles: extendArticles, context: context)); 103 | items.add(_buildSectionSpace()); 104 | } 105 | // =================> XiuXi Content 106 | var relaxArticles = article.result['休息视频']; 107 | if (relaxArticles != null && relaxArticles.length != 0) { 108 | items.add( 109 | _buildArticlesBlock(title: '休息视频', icon: 'images/relax_icon.png', articles: relaxArticles, context: context)); 110 | items.add(_buildSectionSpace()); 111 | } 112 | // ==============> Girl Image 113 | String fuLiUrl = 114 | (article.result['福利'] != null && article.result['福利'].length > 0) ? article.result['福利'][0].url : null; 115 | if (fuLiUrl != null) { 116 | items.add(new Card( 117 | child: Container( 118 | height: 42.0, 119 | child: GestureDetector( 120 | onTap: () { 121 | showDialog( 122 | context: context, 123 | builder: (context) { 124 | return ProfitImageDialog(fuLiUrl); 125 | }); 126 | }, 127 | child: Row( 128 | children: [ 129 | Expanded( 130 | child: Container( 131 | color: Colors.white, 132 | child: Column( 133 | children: [ 134 | Expanded( 135 | child: Center( 136 | child: Text( 137 | '~TODAY妹子~', 138 | maxLines: 1, 139 | style: TextStyle(fontSize: 12.0, color: Colors.black38, letterSpacing: 3.0), 140 | ), 141 | ), 142 | ), 143 | ], 144 | )), 145 | ), 146 | ], 147 | ), 148 | ), 149 | ), 150 | )); 151 | items.add(Container( 152 | height: 12.0, 153 | )); 154 | } 155 | 156 | return ListView( 157 | padding: new EdgeInsets.only(left: 15.0, right: 15.0), 158 | children: items, 159 | ); 160 | } 161 | 162 | Widget _buildSectionSpace() { 163 | return Container( 164 | height: 10.0, 165 | ); 166 | } 167 | 168 | Widget _generateTitle(String text) { 169 | return new Container( 170 | padding: EdgeInsets.only(top: 15.0, bottom: 2.0), 171 | child: new Text( 172 | text, 173 | style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w600, color: const Color(0xFF747474)), 174 | ), 175 | ); 176 | } 177 | 178 | Widget _buildArticlesBlock({String title, String icon, List articles, BuildContext context}) { 179 | var articleItems = List(); 180 | articleItems.add(Container( 181 | height: 3.0, 182 | )); 183 | articleItems.add(_buildArticlesBlockTitle(title: title, icon: icon)); 184 | articleItems.add(Container( 185 | height: 5.0, 186 | )); 187 | for (var sector in articles) { 188 | articleItems.add(_buildArticleItem(sector, context)); 189 | } 190 | Color color = Colors.black26; 191 | switch (title) { 192 | case 'App': 193 | color = Colors.blueAccent; 194 | break; 195 | case 'iOS': 196 | color = Colors.deepPurpleAccent; 197 | break; 198 | case 'Android': 199 | color = Colors.lightGreen; 200 | break; 201 | case '前端': 202 | color = Colors.brown; 203 | break; 204 | case '拓展资源': 205 | color = Colors.blueGrey; 206 | break; 207 | case '休息视频': 208 | color = Colors.teal; 209 | break; 210 | } 211 | return Card( 212 | child: Container( 213 | color: color, 214 | child: Row( 215 | crossAxisAlignment: CrossAxisAlignment.center, 216 | children: [ 217 | Container( 218 | width: 5.0, 219 | ), 220 | Expanded( 221 | child: Container( 222 | color: Colors.white, 223 | padding: EdgeInsets.only(top: 10.0), 224 | child: Column( 225 | crossAxisAlignment: CrossAxisAlignment.start, 226 | children: articleItems, 227 | ), 228 | ), 229 | ) 230 | ], 231 | ), 232 | ), 233 | ); 234 | } 235 | 236 | Widget _buildArticlesBlockTitle({String title, String icon}) { 237 | return Row( 238 | children: [ 239 | Container( 240 | margin: EdgeInsets.only(left: 6.0), 241 | padding: EdgeInsets.all(0.0), 242 | width: 24.0, 243 | height: 24.0, 244 | child: Image.asset(icon), 245 | ), 246 | Text( 247 | ' $title', 248 | style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.w600, color: const Color(0xFF515151)), 249 | ), 250 | ], 251 | ); 252 | } 253 | 254 | Widget _buildArticleItem(ArticleSector article, BuildContext context) { 255 | return Material( 256 | child: InkWell( 257 | onTap: () { 258 | showDialog( 259 | context: context, 260 | builder: (context) { 261 | return TodayDetailDialog(article); 262 | }); 263 | }, 264 | child: Container( 265 | padding: EdgeInsets.only(left: 18.0, top: 10.0, right: 3.0, bottom: 10.0), 266 | child: Row( 267 | crossAxisAlignment: CrossAxisAlignment.start, 268 | children: [ 269 | Center( 270 | child: Container( 271 | margin: EdgeInsets.only(top: 3.5, right: 6.0), 272 | child: Image.asset('images/doc_icon.png'), 273 | width: 15.0, 274 | height: 15.0, 275 | ), 276 | ), 277 | Expanded( 278 | child: Column( 279 | crossAxisAlignment: CrossAxisAlignment.start, 280 | children: [ 281 | Container( 282 | margin: EdgeInsets.only(right: 5.0), 283 | child: Text( 284 | '${article.desc.trim()}', 285 | maxLines: 2, 286 | overflow: TextOverflow.ellipsis, 287 | style: TextStyle(fontSize: 13.0, color: Color(0xFF555555)), 288 | )), 289 | Container( 290 | child: Text( 291 | '${article.who} 发布于 ${article.publishedAt.substring(0, 10)}', 292 | maxLines: 2, 293 | overflow: TextOverflow.ellipsis, 294 | textAlign: TextAlign.right, 295 | style: TextStyle(fontSize: 9.0, color: Colors.black45), 296 | ), 297 | ), 298 | ], 299 | ), 300 | ), 301 | ], 302 | ), 303 | ), 304 | ), 305 | color: Colors.transparent, 306 | ); 307 | } 308 | 309 | Future
_fetchHomePage() async { 310 | final response = await http.get('https://gank.io/api/today'); 311 | if (response.statusCode == 200) { 312 | return Article.fromJson(response.body); 313 | } else { 314 | throw Exception('Failed to HomePage'); 315 | } 316 | } 317 | 318 | Future _fetchHtml() async { 319 | final response = await http.get('https://gank.io'); 320 | if (response.statusCode == 200) { 321 | return parse(response.body); 322 | } else { 323 | throw Exception('Failed to HomeHtml'); 324 | } 325 | } 326 | 327 | // /////////// IMAGE REQUEST NOTE /////////////////// 328 | // 329 | // items.add(_generateSectionTitle('福利')); 330 | // items.add(_generateHorizontalLine()); 331 | // String fuLiUrl = (article.result['福利'] != null && article.result['福利'].length > 0) ? article.result['福利'][0].url : null; 332 | // if (fuLiUrl != null) { 333 | // Image image = new Image.network(article.result['福利'][0].url); 334 | // Completer completer = new Completer(); 335 | // image.image.resolve(new ImageConfiguration()).addListener((ImageInfo info, bool _) => completer.complete(info.image)); 336 | // items.add(new FutureBuilder( 337 | // future: completer.future, 338 | // builder: (BuildContext context, AsyncSnapshot snapshot) { 339 | // if (snapshot.hasData) { 340 | // double imageWidthDefine = MediaQuery.of(context).size.width * 0.5; 341 | // return new Container( 342 | // margin: EdgeInsets.only(top: 3.0, bottom: 10.0), 343 | // width: imageWidthDefine, 344 | // height: imageWidthDefine / snapshot.data.width * snapshot.data.height, 345 | // child: image, 346 | // ); 347 | // } else { 348 | // return new Text('Loading...'); 349 | // } 350 | // }, 351 | // )); 352 | // } else { 353 | // items.add(new Container( 354 | // margin: EdgeInsets.only(top: 15.0, bottom: 15.0), 355 | // child: Text( 356 | // '今天没有福利~QAQ~', 357 | // style: TextStyle(fontSize: 15.0), 358 | // ), 359 | // )); 360 | // } 361 | -------------------------------------------------------------------------------- /lib/display/today_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gankio/data/article.dart'; 3 | import 'package:gankio/display/image_view.dart'; 4 | import 'package:gankio/display/web_view.dart'; 5 | import 'package:gankio/main.dart'; 6 | 7 | class TodayDetailDialog extends StatefulWidget { 8 | TodayDetailDialog(this.article); 9 | 10 | final ArticleSector article; 11 | 12 | @override 13 | _TodayDetailDialogState createState() => _TodayDetailDialogState(article); 14 | } 15 | 16 | class _TodayDetailDialogState extends State { 17 | _TodayDetailDialogState(this._article); 18 | 19 | ArticleSector _article; 20 | 21 | int _imagePagePosition = 1; 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | var imageWidgets = List(); 26 | if (_article.images != null && _article.images.length != 0) { 27 | for (var imgUrl in _article.images) { 28 | imageWidgets.add( 29 | Container( 30 | child: Stack( 31 | children: [ 32 | Center( 33 | child: Container( 34 | width: 20.0, 35 | height: 20.0, 36 | child: CircularProgressIndicator( 37 | strokeWidth: 2.0, 38 | ), 39 | ), 40 | ), 41 | Center( 42 | child: GestureDetector( 43 | onTap: () { 44 | Navigator.push(context, GankNavigator(builder: (context) => ImageViewPage(imgUrl))); 45 | }, 46 | child: Image.network(imgUrl), 47 | ), 48 | ), 49 | ], 50 | ), 51 | ), 52 | ); 53 | } 54 | } 55 | 56 | var contentWidgets = List(); 57 | if (imageWidgets.length != 0) { 58 | contentWidgets.add(Container( 59 | margin: EdgeInsets.only(top: 10.0, bottom: 8.0), 60 | width: MediaQuery.of(context).size.width, 61 | height: MediaQuery.of(context).size.height * 0.35, 62 | child: PageView( 63 | onPageChanged: (pos) { 64 | setState(() { 65 | _imagePagePosition = pos + 1; 66 | }); 67 | }, 68 | children: imageWidgets, 69 | ))); 70 | contentWidgets.add( 71 | new Center( 72 | child: Text( 73 | '$_imagePagePosition/${imageWidgets.length}', 74 | style: TextStyle( 75 | color: Colors.black38, 76 | fontSize: 12.0, 77 | ), 78 | ), 79 | ), 80 | ); 81 | } 82 | contentWidgets.add(_buildActionButtons(_article)); 83 | 84 | return SimpleDialog( 85 | titlePadding: EdgeInsets.only(top: 10.0, left: 10.0, right: 10.0), 86 | contentPadding: EdgeInsets.all(0.0), 87 | title: Column( 88 | crossAxisAlignment: CrossAxisAlignment.start, 89 | children: [ 90 | Text( 91 | '${_article.desc}', 92 | maxLines: 3, 93 | overflow: TextOverflow.ellipsis, 94 | style: TextStyle(fontSize: 15.0, color: Colors.black87, fontWeight: FontWeight.w500), 95 | ), 96 | Text( 97 | '${_article.url}', 98 | maxLines: 1, 99 | overflow: TextOverflow.ellipsis, 100 | style: TextStyle( 101 | fontSize: 10.5, 102 | color: Colors.blueAccent, 103 | ), 104 | ), 105 | ], 106 | ), 107 | children: contentWidgets, 108 | ); 109 | } 110 | 111 | Widget _buildActionButtons(ArticleSector article) { 112 | return GestureDetector( 113 | onTap: () { 114 | Navigator.push( 115 | context, 116 | GankNavigator( 117 | builder: (context) => WebViewer( 118 | url: article.url, 119 | title: article.desc, 120 | ))); 121 | }, 122 | child: Container( 123 | color: Color(0xFF6791EF), 124 | margin: EdgeInsets.only(top: 12.0), 125 | child: Column( 126 | crossAxisAlignment: CrossAxisAlignment.center, 127 | children: [ 128 | Row( 129 | children: [ 130 | Expanded( 131 | child: Container( 132 | height: 50.0, 133 | padding: EdgeInsets.all(0.0), 134 | child: Center( 135 | child: Text( 136 | '点击查看详情', 137 | style: TextStyle(color: Colors.white, fontSize: 16.0, letterSpacing: 1.0), 138 | ), 139 | ), 140 | ), 141 | ), 142 | ], 143 | ), 144 | ], 145 | ), 146 | ), 147 | ); 148 | } 149 | } 150 | 151 | class ProfitImageDialog extends StatelessWidget { 152 | ProfitImageDialog(this.url); 153 | 154 | final String url; 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | return SimpleDialog( 159 | contentPadding: EdgeInsets.only(top: 15.0), 160 | children: [ 161 | Column( 162 | children: [ 163 | Container( 164 | width: MediaQuery.of(context).size.width, 165 | height: MediaQuery.of(context).size.width, 166 | child: Image.network(url), 167 | ), 168 | GestureDetector( 169 | onTap: () { 170 | Navigator.pop(context, true); 171 | }, 172 | child: Container( 173 | margin: EdgeInsets.only(top: 12.0), 174 | height: 42.0, 175 | color: Color(0xfff0ac73), 176 | child: Row( 177 | children: [ 178 | Expanded( 179 | child: Center( 180 | child: Text( 181 | '关闭', 182 | style: TextStyle(color: Colors.white, fontSize: 13.0, letterSpacing: 5.0), 183 | ), 184 | ), 185 | ), 186 | ], 187 | ), 188 | ), 189 | ), 190 | ], 191 | ) 192 | ], 193 | ); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/display/web_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 3 | 4 | class WebViewer extends StatelessWidget { 5 | WebViewer({this.url, this.title = "Gank.IO"}); 6 | 7 | final String url; 8 | final String title; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return new WebviewScaffold( 13 | appBar: new AppBar( 14 | title: new Text(title), 15 | ), 16 | withJavascript: true, 17 | withLocalStorage: true, 18 | withZoom: false, 19 | enableAppScheme: true, 20 | url: url); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gankio/display/history.dart'; 3 | import 'package:gankio/display/reading.dart'; 4 | import 'package:gankio/display/search.dart'; 5 | import 'package:gankio/display/setting.dart'; 6 | import 'package:gankio/display/today.dart'; 7 | 8 | void main() => runApp(new MyApp()); 9 | 10 | class MyApp extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return new MaterialApp( 14 | title: 'Gank IO Flutter', 15 | theme: new ThemeData( 16 | primarySwatch: Colors.blueGrey, 17 | ), 18 | home: new HomePage(title: 'Gank IO'), 19 | ); 20 | } 21 | } 22 | 23 | class HomePage extends StatefulWidget { 24 | HomePage({Key key, this.title}) : super(key: key); 25 | 26 | final String title; 27 | 28 | @override 29 | _HomePageState createState() => new _HomePageState(); 30 | } 31 | 32 | class _HomePageState extends State { 33 | int _pageIndex = 0; 34 | var _pageController = PageController(keepPage: true); 35 | 36 | void _updatePageIndex(int index) { 37 | setState(() { 38 | if (this.mounted) { 39 | this._pageIndex = index; 40 | } 41 | }); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | try { 47 | // ignore: deprecated_member_use 48 | MaterialPageRoute.debugEnableFadingRoutes = true; 49 | } catch (e) { 50 | // nothing.. 51 | } 52 | return new Scaffold( 53 | appBar: new AppBar( 54 | title: Row( 55 | children: [ 56 | Expanded( 57 | child: new Text(widget.title), 58 | ), 59 | IconButton( 60 | icon: Icon(Icons.search), 61 | tooltip: 'Search', 62 | onPressed: () { 63 | Navigator.push(context, MaterialPageRoute(builder: (context) => SearchPage())); 64 | }, 65 | ) 66 | ], 67 | ), 68 | ), 69 | body: new PageView( 70 | onPageChanged: (int position) { 71 | _updatePageIndex(position); 72 | }, 73 | children: [ 74 | Container( 75 | key: PageStorageKey("today"), 76 | child: TodayPage(), 77 | ), 78 | Container( 79 | key: PageStorageKey("history"), 80 | child: HistoryPage(), 81 | ), 82 | Container( 83 | key: PageStorageKey("reading"), 84 | child: ReadingPage(), 85 | ), 86 | Container( 87 | key: PageStorageKey("setting"), 88 | child: SettingPage(), 89 | ), 90 | ], 91 | controller: _pageController, 92 | ), 93 | bottomNavigationBar: new BottomNavigationBar( 94 | type: BottomNavigationBarType.fixed, 95 | items: [ 96 | new BottomNavigationBarItem(icon: Icon(Icons.home), title: Text("今日")), 97 | new BottomNavigationBarItem(icon: Icon(Icons.history), title: Text("历史")), 98 | new BottomNavigationBarItem(icon: Icon(Icons.book), title: Text("闲读")), 99 | new BottomNavigationBarItem(icon: Icon(Icons.settings), title: Text("设置")), 100 | ], 101 | currentIndex: _pageIndex, 102 | onTap: (int pos) { 103 | _pageController.jumpToPage(pos); 104 | // _pageController.jumpTo(pos, duration: new Duration(milliseconds: 400), curve: Curves.easeInOut); 105 | _updatePageIndex(pos); 106 | }, 107 | ), 108 | ); 109 | } 110 | } 111 | 112 | class GankNavigator extends MaterialPageRoute { 113 | GankNavigator({@required WidgetBuilder builder}) : super(builder: builder); 114 | 115 | @override 116 | Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { 117 | return new FadeTransition( 118 | opacity: animation, 119 | child: builder(context), 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/presenter/history_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:gankio/data/history_result.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | abstract class HistoryView { 5 | fetchReceived(HistoryResult result); 6 | } 7 | 8 | class HistoryPresenter { 9 | HistoryPresenter(this._view); 10 | 11 | final HistoryView _view; 12 | 13 | fetchHistory({int page = 1}) async { 14 | final response = await http.get("https://gank.io/api/history/content/10/$page"); 15 | if (response.statusCode == 200) { 16 | _view.fetchReceived(HistoryResult.fromJson(response.body)); 17 | } else { 18 | throw Exception('Error ${response.statusCode}'); 19 | } 20 | } 21 | } 22 | 23 | class HistoryResultEvent { 24 | HistoryResultEvent({this.result}); 25 | 26 | HistoryResult result; 27 | } 28 | -------------------------------------------------------------------------------- /lib/presenter/reading_presenter.dart: -------------------------------------------------------------------------------- 1 | import 'package:gankio/data/reading_data.dart'; 2 | import 'package:http/http.dart' as http; 3 | 4 | abstract class ReadingCategoryView { 5 | fetchCategoryReceived(ReadingCategoryResult result); 6 | 7 | fetchSitesReceived(ReadingSiteResult result); 8 | } 9 | 10 | abstract class ReadingView { 11 | fetchReadingArticlesReceived(ReadingArticleResult result, bool clear); 12 | 13 | fetchReadingArticlesEnd(); 14 | } 15 | 16 | class ReadingCategoryPresenter { 17 | ReadingCategoryPresenter(this._categoryView); 18 | 19 | final ReadingCategoryView _categoryView; 20 | 21 | fetchCategory() async { 22 | final response = await http.get("https://gank.io/api/xiandu/categories"); 23 | if (response.statusCode == 200 && _categoryView != null) { 24 | _categoryView.fetchCategoryReceived(ReadingCategoryResult.fromJson(response.body)); 25 | } else { 26 | throw Exception('fetchCategory-Error ${response.statusCode}'); 27 | } 28 | } 29 | 30 | fetchSites(String category) async { 31 | final response = await http.get("https://gank.io/api/xiandu/category/$category"); 32 | if (response.statusCode == 200 && _categoryView != null) { 33 | _categoryView.fetchSitesReceived(ReadingSiteResult.fromJson(response.body)); 34 | } else { 35 | throw Exception('fetchSites-$category-Error ${response.statusCode}'); 36 | } 37 | } 38 | } 39 | 40 | class ReadingPresenter { 41 | ReadingPresenter(this._readingView); 42 | 43 | final ReadingView _readingView; 44 | 45 | var _page = 1; 46 | var _requesting = false; 47 | 48 | fetchReadingArticles(String site, bool clear) async { 49 | if (!_requesting) { 50 | if (!clear) _page++; 51 | _requesting = true; 52 | final response = await http.get("https://gank.io/api/xiandu/data/id/$site/count/25/page/$_page"); 53 | if (response.statusCode == 200 && _readingView != null) { 54 | _requesting = false; 55 | ReadingArticleResult result = ReadingArticleResult.fromJson(response.body); 56 | if (result != null && result.results != null && result.results.length != 0) { 57 | _readingView.fetchReadingArticlesReceived(result, clear); 58 | } else { 59 | _readingView.fetchReadingArticlesEnd(); 60 | } 61 | } else { 62 | throw Exception('fetchReadingArticles-$site-Error ${response.statusCode}'); 63 | } 64 | } 65 | } 66 | 67 | handleSiteChange() { 68 | _page = 1; 69 | _requesting = false; 70 | } 71 | } 72 | 73 | class ReadingCategoryEvent { 74 | ReadingCategoryEvent({this.categories}); 75 | 76 | List categories; 77 | } 78 | 79 | class ReadingSiteEvent { 80 | ReadingSiteEvent({this.sites}); 81 | 82 | List sites; 83 | } 84 | 85 | class ReadingCategorySwitchEvent { 86 | ReadingCategorySwitchEvent(this.category); 87 | 88 | ReadingCategory category; 89 | } 90 | 91 | class ReadingArticlesEvent { 92 | ReadingArticlesEvent({this.results, this.clear = false}); 93 | 94 | bool clear; 95 | List results; 96 | } 97 | 98 | class ReadingArticlesEndEvent {} 99 | -------------------------------------------------------------------------------- /lib/widget/photo_view.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Renan C. Araújo 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | * documentation files (the "Software"), * to deal in the Software without restriction, including without limitation 5 | * the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, 6 | * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: 7 | * 8 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 11 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 12 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 13 | * USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | library photo_view; 16 | 17 | import 'dart:async'; 18 | import 'package:flutter/material.dart'; 19 | import 'package:gankio/widget/photo_view_image_wrapper.dart'; 20 | import 'package:gankio/widget/photo_view_scale_type.dart'; 21 | import 'package:gankio/widget/photo_view_utils.dart'; 22 | 23 | class PhotoView extends StatefulWidget { 24 | final ImageProvider imageProvider; 25 | final Widget loadingChild; 26 | final Color backgroundColor; 27 | final double minScale; 28 | final double maxScale; 29 | 30 | PhotoView({ 31 | Key key, 32 | @required this.imageProvider, 33 | this.loadingChild, 34 | this.backgroundColor = const Color.fromRGBO(0, 0, 0, 1.0), 35 | this.minScale = 0.0, 36 | this.maxScale, 37 | }) : super(key: key); 38 | 39 | @override 40 | State createState() { 41 | return new _PhotoViewState(); 42 | } 43 | } 44 | 45 | class _PhotoViewState extends State { 46 | PhotoViewScaleType _scaleType; 47 | 48 | Future _getImage() { 49 | Completer completer = new Completer(); 50 | ImageStream stream = widget.imageProvider.resolve(new ImageConfiguration()); 51 | stream.addListener((ImageInfo info, bool completed) { 52 | if (!completer.isCompleted) { 53 | completer.complete(info); 54 | } 55 | }); 56 | return completer.future; 57 | } 58 | 59 | void onDoubleTap() { 60 | setState(() { 61 | _scaleType = nextScaleType(_scaleType); 62 | }); 63 | } 64 | 65 | void onStartPanning() { 66 | setState(() { 67 | _scaleType = PhotoViewScaleType.zooming; 68 | }); 69 | } 70 | 71 | @override 72 | void initState() { 73 | super.initState(); 74 | _scaleType = PhotoViewScaleType.contained; 75 | } 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return new FutureBuilder( 80 | future: _getImage(), 81 | builder: (BuildContext context, AsyncSnapshot info) { 82 | if (info.hasData) { 83 | return new PhotoViewImageWrapper( 84 | onDoubleTap: onDoubleTap, 85 | onStartPanning: onStartPanning, 86 | imageInfo: info.data, 87 | scaleType: _scaleType, 88 | backgroundColor: widget.backgroundColor, 89 | minScale: widget.minScale, 90 | maxScale: widget.maxScale, 91 | ); 92 | } else { 93 | return buildLoading(); 94 | } 95 | }); 96 | } 97 | 98 | Widget buildLoading() { 99 | return widget.loadingChild != null 100 | ? widget.loadingChild 101 | : new Center( 102 | child: new Text( 103 | "Loading", 104 | style: new TextStyle(color: Colors.white), 105 | )); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/widget/photo_view_image_wrapper.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Renan C. Araújo 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | * documentation files (the "Software"), * to deal in the Software without restriction, including without limitation 5 | * the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, 6 | * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: 7 | * 8 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 11 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 12 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 13 | * USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | import 'package:flutter/material.dart'; 16 | import 'package:gankio/widget/photo_view_scale_type.dart'; 17 | import 'package:gankio/widget/photo_view_utils.dart'; 18 | 19 | class PhotoViewImageWrapper extends StatefulWidget { 20 | const PhotoViewImageWrapper( 21 | {Key key, 22 | @required this.onDoubleTap, 23 | @required this.onStartPanning, 24 | @required this.imageInfo, 25 | @required this.scaleType, 26 | this.backgroundColor, 27 | this.minScale, 28 | this.maxScale}) 29 | : super(key: key); 30 | 31 | final Function onDoubleTap; 32 | final Function onStartPanning; 33 | final ImageInfo imageInfo; 34 | final PhotoViewScaleType scaleType; 35 | final Color backgroundColor; 36 | final double minScale; 37 | final double maxScale; 38 | 39 | @override 40 | State createState() { 41 | return new _PhotoViewImageWrapperState(); 42 | } 43 | } 44 | 45 | class _PhotoViewImageWrapperState extends State with TickerProviderStateMixin { 46 | Offset _position; 47 | Offset _normalizedPosition; 48 | double _scale; 49 | double _scaleBefore; 50 | 51 | AnimationController _scaleAnimationController; 52 | Animation _scaleAnimation; 53 | 54 | AnimationController _positionAnimationController; 55 | Animation _positionAnimation; 56 | 57 | void handleScaleAnimation() { 58 | setState(() { 59 | _scale = _scaleAnimation.value; 60 | }); 61 | } 62 | 63 | void handlePositionAnimate() { 64 | setState(() { 65 | _position = _positionAnimation.value; 66 | }); 67 | } 68 | 69 | void onScaleStart(ScaleStartDetails details) { 70 | _scaleBefore = scaleTypeAwareScale(); 71 | _normalizedPosition = (details.focalPoint - _position); 72 | _scaleAnimationController.stop(); 73 | _positionAnimationController.stop(); 74 | } 75 | 76 | void onScaleUpdate(ScaleUpdateDetails details) { 77 | final double newScale = (_scaleBefore * details.scale); 78 | final Offset delta = (details.focalPoint - _normalizedPosition); 79 | if (details.scale != 1.0) { 80 | widget.onStartPanning(); 81 | } 82 | setState(() { 83 | _scale = newScale; 84 | _position = clampPosition(delta * details.scale); 85 | }); 86 | } 87 | 88 | void onScaleEnd(ScaleEndDetails details) { 89 | //animate back to maxScale if gesture exceeded the maxscale specified 90 | if ((widget.maxScale != null) && (this._scale > widget.maxScale)) { 91 | double scaleComebackRatio = widget.maxScale / this._scale; 92 | print(scaleComebackRatio); 93 | 94 | animateScale(_scale, widget.maxScale); 95 | animatePosition(_position, clampPosition(_position * scaleComebackRatio)); 96 | return; 97 | } 98 | 99 | //animate back to minScale if gesture fell smaller than the minScale specified 100 | if (widget.minScale != null && this._scale < widget.minScale) { 101 | double scaleComebackRatio = widget.minScale / this._scale; 102 | animateScale(_scale, widget.minScale); 103 | animatePosition(_position, clampPosition(_position * scaleComebackRatio)); 104 | } 105 | } 106 | 107 | Offset clampPosition(Offset offset) { 108 | final x = offset.dx; 109 | final y = offset.dy; 110 | final computedWidth = widget.imageInfo.image.width * scaleTypeAwareScale(); 111 | final computedHeight = widget.imageInfo.image.height * scaleTypeAwareScale(); 112 | final screenWidth = MediaQuery.of(context).size.width; 113 | final screenHeight = MediaQuery.of(context).size.height; 114 | final screenHalfX = screenWidth / 2; 115 | final screenHalfY = screenHeight / 2; 116 | 117 | final double computedX = screenWidth < computedWidth 118 | ? x.clamp(0 - (computedWidth / 2) + screenHalfX, computedWidth / 2 - screenHalfX) 119 | : 0.0; 120 | 121 | final double computedY = screenHeight < computedHeight 122 | ? y.clamp(0 - (computedHeight / 2) + screenHalfY, computedHeight / 2 - screenHalfY) 123 | : 0.0; 124 | 125 | return new Offset(computedX, computedY); 126 | } 127 | 128 | double scaleTypeAwareScale() { 129 | return _scale != null || widget.scaleType == PhotoViewScaleType.zooming 130 | ? _scale 131 | : getScaleForScaleType( 132 | imageInfo: widget.imageInfo, scaleType: widget.scaleType, size: MediaQuery.of(context).size); 133 | } 134 | 135 | void animateScale(double from, double to) { 136 | _scaleAnimation = new Tween( 137 | begin: from, 138 | end: to, 139 | ).animate(_scaleAnimationController); 140 | _scaleAnimationController 141 | ..value = 0.0 142 | ..fling(velocity: 0.4); 143 | } 144 | 145 | void animatePosition(Offset from, Offset to) { 146 | _positionAnimation = new Tween(begin: from, end: to).animate(_positionAnimationController); 147 | _positionAnimationController 148 | ..value = 0.0 149 | ..fling(velocity: 0.4); 150 | } 151 | 152 | @override 153 | void initState() { 154 | super.initState(); 155 | _position = Offset.zero; 156 | _scale = null; 157 | _scaleAnimationController = new AnimationController(vsync: this)..addListener(handleScaleAnimation); 158 | 159 | _positionAnimationController = new AnimationController(vsync: this)..addListener(handlePositionAnimate); 160 | } 161 | 162 | @override 163 | void dispose() { 164 | _positionAnimationController.dispose(); 165 | _scaleAnimationController.dispose(); 166 | super.dispose(); 167 | } 168 | 169 | void didUpdateWidget(PhotoViewImageWrapper oldWidget) { 170 | super.didUpdateWidget(oldWidget); 171 | if (oldWidget.scaleType != widget.scaleType && widget.scaleType != PhotoViewScaleType.zooming) { 172 | animateScale( 173 | _scale == null 174 | ? getScaleForScaleType( 175 | imageInfo: widget.imageInfo, 176 | scaleType: PhotoViewScaleType.contained, 177 | size: MediaQuery.of(context).size) 178 | : _scale, 179 | getScaleForScaleType( 180 | imageInfo: widget.imageInfo, scaleType: widget.scaleType, size: MediaQuery.of(context).size)); 181 | animatePosition(_position, Offset.zero); 182 | } 183 | } 184 | 185 | @override 186 | Widget build(BuildContext context) { 187 | var matrix = new Matrix4.identity() 188 | ..translate(_position.dx, _position.dy) 189 | ..scale(scaleTypeAwareScale()); 190 | 191 | return new GestureDetector( 192 | child: new Container( 193 | child: new Center( 194 | child: new Transform( 195 | child: new CustomSingleChildLayout( 196 | delegate: new ImagePositionDelegate(widget.imageInfo.image.width / 1, widget.imageInfo.image.height / 1), 197 | child: new RawImage( 198 | image: widget.imageInfo.image, 199 | scale: widget.imageInfo.scale, 200 | ), 201 | ), 202 | transform: matrix, 203 | alignment: Alignment.center, 204 | )), 205 | decoration: new BoxDecoration(color: widget.backgroundColor), 206 | ), 207 | onDoubleTap: widget.onDoubleTap, 208 | onScaleStart: onScaleStart, 209 | onScaleUpdate: onScaleUpdate, 210 | onScaleEnd: onScaleEnd, 211 | ); 212 | } 213 | } 214 | 215 | class ImagePositionDelegate extends SingleChildLayoutDelegate { 216 | final double imageWidth; 217 | final double imageHeight; 218 | 219 | const ImagePositionDelegate(this.imageWidth, this.imageHeight); 220 | 221 | @override 222 | Offset getPositionForChild(Size size, Size childSize) { 223 | double offsetX = ((size.width - imageWidth) / 2); 224 | double offsetY = ((size.height - imageHeight) / 2); 225 | return new Offset(offsetX, offsetY); 226 | } 227 | 228 | @override 229 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 230 | return new BoxConstraints( 231 | maxWidth: imageWidth, 232 | maxHeight: imageHeight, 233 | minHeight: imageHeight, 234 | minWidth: imageWidth, 235 | ); 236 | } 237 | 238 | @override 239 | bool shouldRelayout(SingleChildLayoutDelegate oldDelegate) { 240 | return true; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /lib/widget/photo_view_scale_type.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Renan C. Araújo 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | * documentation files (the "Software"), * to deal in the Software without restriction, including without limitation 5 | * the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, 6 | * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: 7 | * 8 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 11 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 12 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 13 | * USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | enum PhotoViewScaleType { contained, covering, originalSize, zooming } 16 | -------------------------------------------------------------------------------- /lib/widget/photo_view_utils.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Renan C. Araújo 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | * documentation files (the "Software"), * to deal in the Software without restriction, including without limitation 5 | * the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, 6 | * and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: 7 | * 8 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 11 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 12 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 13 | * USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | import 'dart:math' as Math; 16 | 17 | import 'package:flutter/material.dart'; 18 | import 'package:gankio/widget/photo_view_scale_type.dart'; 19 | 20 | PhotoViewScaleType nextScaleType(PhotoViewScaleType actual) { 21 | switch (actual) { 22 | case PhotoViewScaleType.contained: 23 | return PhotoViewScaleType.covering; 24 | case PhotoViewScaleType.covering: 25 | return PhotoViewScaleType.originalSize; 26 | case PhotoViewScaleType.originalSize: 27 | return PhotoViewScaleType.contained; 28 | case PhotoViewScaleType.zooming: 29 | return PhotoViewScaleType.contained; 30 | default: 31 | return PhotoViewScaleType.contained; 32 | } 33 | } 34 | 35 | double getScaleForScaleType( 36 | {@required Size size, @required PhotoViewScaleType scaleType, @required ImageInfo imageInfo}) { 37 | switch (scaleType) { 38 | case PhotoViewScaleType.contained: 39 | return scaleForContained(size: size, imageInfo: imageInfo); 40 | case PhotoViewScaleType.covering: 41 | return scaleForCovering(size: size, imageInfo: imageInfo); 42 | case PhotoViewScaleType.originalSize: 43 | return 1.0; 44 | default: 45 | return null; 46 | } 47 | } 48 | 49 | double scaleForContained({@required Size size, @required ImageInfo imageInfo}) { 50 | final int imageWidth = imageInfo.image.width; 51 | final int imageHeight = imageInfo.image.height; 52 | 53 | final double screenWidth = size.width; 54 | final double screenHeight = size.height; 55 | 56 | return Math.min(screenWidth / imageWidth, screenHeight / imageHeight); 57 | } 58 | 59 | double scaleForCovering({@required Size size, @required ImageInfo imageInfo}) { 60 | final int imageWidth = imageInfo.image.width; 61 | final int imageHeight = imageInfo.image.height; 62 | 63 | final double screenWidth = size.width; 64 | final double screenHeight = size.height; 65 | 66 | return Math.max(screenWidth / imageWidth, screenHeight / imageHeight); 67 | } 68 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://www.dartlang.org/tools/pub/glossary#lockfile 3 | packages: 4 | analyzer: 5 | dependency: transitive 6 | description: 7 | name: analyzer 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "0.31.2-alpha.2" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.4.3" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.0.7" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.0.3" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.1" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.14.6" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "2.0.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.0.3" 60 | csslib: 61 | dependency: transitive 62 | description: 63 | name: csslib 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "0.14.4" 67 | cupertino_icons: 68 | dependency: "direct main" 69 | description: 70 | name: cupertino_icons 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.1.2" 74 | event_bus: 75 | dependency: "direct main" 76 | description: 77 | name: event_bus 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "1.0.1" 81 | flutter: 82 | dependency: "direct main" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | flutter_test: 87 | dependency: "direct dev" 88 | description: flutter 89 | source: sdk 90 | version: "0.0.0" 91 | flutter_webview_plugin: 92 | dependency: "direct main" 93 | description: 94 | name: flutter_webview_plugin 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "0.1.6" 98 | fluttertoast: 99 | dependency: "direct main" 100 | description: 101 | name: fluttertoast 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "2.0.7" 105 | front_end: 106 | dependency: transitive 107 | description: 108 | name: front_end 109 | url: "https://pub.dartlang.org" 110 | source: hosted 111 | version: "0.1.0-alpha.12" 112 | glob: 113 | dependency: transitive 114 | description: 115 | name: glob 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "1.1.5" 119 | html: 120 | dependency: "direct main" 121 | description: 122 | name: html 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "0.13.3" 126 | http: 127 | dependency: "direct main" 128 | description: 129 | name: http 130 | url: "https://pub.dartlang.org" 131 | source: hosted 132 | version: "0.11.3+16" 133 | http_multi_server: 134 | dependency: transitive 135 | description: 136 | name: http_multi_server 137 | url: "https://pub.dartlang.org" 138 | source: hosted 139 | version: "2.0.4" 140 | http_parser: 141 | dependency: transitive 142 | description: 143 | name: http_parser 144 | url: "https://pub.dartlang.org" 145 | source: hosted 146 | version: "3.1.2" 147 | io: 148 | dependency: transitive 149 | description: 150 | name: io 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "0.3.2+1" 154 | js: 155 | dependency: transitive 156 | description: 157 | name: js 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "0.6.1" 161 | kernel: 162 | dependency: transitive 163 | description: 164 | name: kernel 165 | url: "https://pub.dartlang.org" 166 | source: hosted 167 | version: "0.3.0-alpha.12" 168 | logging: 169 | dependency: transitive 170 | description: 171 | name: logging 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "0.11.3+1" 175 | matcher: 176 | dependency: transitive 177 | description: 178 | name: matcher 179 | url: "https://pub.dartlang.org" 180 | source: hosted 181 | version: "0.12.2+1" 182 | meta: 183 | dependency: transitive 184 | description: 185 | name: meta 186 | url: "https://pub.dartlang.org" 187 | source: hosted 188 | version: "1.1.5" 189 | mime: 190 | dependency: transitive 191 | description: 192 | name: mime 193 | url: "https://pub.dartlang.org" 194 | source: hosted 195 | version: "0.9.6" 196 | multi_server_socket: 197 | dependency: transitive 198 | description: 199 | name: multi_server_socket 200 | url: "https://pub.dartlang.org" 201 | source: hosted 202 | version: "1.0.1" 203 | node_preamble: 204 | dependency: transitive 205 | description: 206 | name: node_preamble 207 | url: "https://pub.dartlang.org" 208 | source: hosted 209 | version: "1.4.1" 210 | package_config: 211 | dependency: transitive 212 | description: 213 | name: package_config 214 | url: "https://pub.dartlang.org" 215 | source: hosted 216 | version: "1.0.3" 217 | package_resolver: 218 | dependency: transitive 219 | description: 220 | name: package_resolver 221 | url: "https://pub.dartlang.org" 222 | source: hosted 223 | version: "1.0.2" 224 | path: 225 | dependency: transitive 226 | description: 227 | name: path 228 | url: "https://pub.dartlang.org" 229 | source: hosted 230 | version: "1.5.1" 231 | plugin: 232 | dependency: transitive 233 | description: 234 | name: plugin 235 | url: "https://pub.dartlang.org" 236 | source: hosted 237 | version: "0.2.0+2" 238 | pool: 239 | dependency: transitive 240 | description: 241 | name: pool 242 | url: "https://pub.dartlang.org" 243 | source: hosted 244 | version: "1.3.4" 245 | pub_semver: 246 | dependency: transitive 247 | description: 248 | name: pub_semver 249 | url: "https://pub.dartlang.org" 250 | source: hosted 251 | version: "1.4.1" 252 | quiver: 253 | dependency: transitive 254 | description: 255 | name: quiver 256 | url: "https://pub.dartlang.org" 257 | source: hosted 258 | version: "0.29.0+1" 259 | shelf: 260 | dependency: transitive 261 | description: 262 | name: shelf 263 | url: "https://pub.dartlang.org" 264 | source: hosted 265 | version: "0.7.3" 266 | shelf_packages_handler: 267 | dependency: transitive 268 | description: 269 | name: shelf_packages_handler 270 | url: "https://pub.dartlang.org" 271 | source: hosted 272 | version: "1.0.3" 273 | shelf_static: 274 | dependency: transitive 275 | description: 276 | name: shelf_static 277 | url: "https://pub.dartlang.org" 278 | source: hosted 279 | version: "0.2.7" 280 | shelf_web_socket: 281 | dependency: transitive 282 | description: 283 | name: shelf_web_socket 284 | url: "https://pub.dartlang.org" 285 | source: hosted 286 | version: "0.2.2" 287 | sky_engine: 288 | dependency: transitive 289 | description: flutter 290 | source: sdk 291 | version: "0.0.99" 292 | source_map_stack_trace: 293 | dependency: transitive 294 | description: 295 | name: source_map_stack_trace 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "1.1.4" 299 | source_maps: 300 | dependency: transitive 301 | description: 302 | name: source_maps 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "0.10.5" 306 | source_span: 307 | dependency: transitive 308 | description: 309 | name: source_span 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "1.4.0" 313 | stack_trace: 314 | dependency: transitive 315 | description: 316 | name: stack_trace 317 | url: "https://pub.dartlang.org" 318 | source: hosted 319 | version: "1.9.2" 320 | stream_channel: 321 | dependency: transitive 322 | description: 323 | name: stream_channel 324 | url: "https://pub.dartlang.org" 325 | source: hosted 326 | version: "1.6.6" 327 | string_scanner: 328 | dependency: transitive 329 | description: 330 | name: string_scanner 331 | url: "https://pub.dartlang.org" 332 | source: hosted 333 | version: "1.0.2" 334 | term_glyph: 335 | dependency: transitive 336 | description: 337 | name: term_glyph 338 | url: "https://pub.dartlang.org" 339 | source: hosted 340 | version: "1.0.0" 341 | test: 342 | dependency: transitive 343 | description: 344 | name: test 345 | url: "https://pub.dartlang.org" 346 | source: hosted 347 | version: "0.12.37" 348 | typed_data: 349 | dependency: transitive 350 | description: 351 | name: typed_data 352 | url: "https://pub.dartlang.org" 353 | source: hosted 354 | version: "1.1.5" 355 | utf: 356 | dependency: transitive 357 | description: 358 | name: utf 359 | url: "https://pub.dartlang.org" 360 | source: hosted 361 | version: "0.9.0+4" 362 | vector_math: 363 | dependency: transitive 364 | description: 365 | name: vector_math 366 | url: "https://pub.dartlang.org" 367 | source: hosted 368 | version: "2.0.6" 369 | watcher: 370 | dependency: transitive 371 | description: 372 | name: watcher 373 | url: "https://pub.dartlang.org" 374 | source: hosted 375 | version: "0.9.7+7" 376 | web_socket_channel: 377 | dependency: transitive 378 | description: 379 | name: web_socket_channel 380 | url: "https://pub.dartlang.org" 381 | source: hosted 382 | version: "1.0.7" 383 | yaml: 384 | dependency: transitive 385 | description: 386 | name: yaml 387 | url: "https://pub.dartlang.org" 388 | source: hosted 389 | version: "2.1.13" 390 | sdks: 391 | dart: ">=2.0.0-dev.58.0 <=2.0.0-dev.58.0.flutter-f981f09760" 392 | flutter: ">=0.1.4 <2.0.0" 393 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: gankio 2 | description: A Gank.io Client by Flutter 3 | 4 | dependencies: 5 | flutter: 6 | sdk: flutter 7 | 8 | # The following adds the Cupertino Icons font to your application. 9 | # Use with the CupertinoIcons class for iOS style icons. 10 | cupertino_icons: ^0.1.2 11 | 12 | http: ^0.11.3+16 13 | html: ^0.13.3 14 | flutter_webview_plugin: ^0.1.6 15 | event_bus: ^1.0.1 16 | fluttertoast: ^2.0.7 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | # For information on the generic Dart part of this file, see the 23 | # following page: https://www.dartlang.org/tools/pub/pubspec 24 | 25 | # The following section is specific to Flutter. 26 | flutter: 27 | # The following line ensures that the Material Icons font is 28 | # included with your application, so that you can use the icons in 29 | # the material Icons class. 30 | uses-material-design: true 31 | 32 | assets: 33 | - images/doc_icon.png 34 | - images/android_icon.png 35 | - images/app_icon.png 36 | - images/extend_icon.png 37 | - images/front_icon.png 38 | - images/ios_icon.png 39 | - images/relax_icon.png 40 | - images/gankio.png 41 | 42 | # An image asset can refer to one or more resolution-specific "variants", see 43 | # https://flutter.io/assets-and-images/#resolution-aware. 44 | 45 | # For details regarding adding assets from package dependencies, see 46 | # https://flutter.io/assets-and-images/#from-packages 47 | 48 | # To add custom fonts to your application, add a fonts section here, 49 | # in this "flutter" section. Each entry in this list should have a 50 | # "family" key with the font family name, and a "fonts" key with a 51 | # list giving the asset and other descriptors for the font. For 52 | # example: 53 | # fonts: 54 | # - family: Schyler 55 | # fonts: 56 | # - asset: fonts/Schyler-Regular.ttf 57 | # - asset: fonts/Schyler-Italic.ttf 58 | # style: italic 59 | # - family: Trajan Pro 60 | # fonts: 61 | # - asset: fonts/TrajanPro.ttf 62 | # - asset: fonts/TrajanPro_Bold.ttf 63 | # weight: 700 64 | # 65 | # For details regarding fonts from package dependencies, 66 | # see https://flutter.io/custom-fonts/#from-packages 67 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | import 'package:gankio/main.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(new MyApp()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('0'), findsOneWidget); 19 | expect(find.text('1'), findsNothing); 20 | 21 | // Tap the '+' icon and trigger a frame. 22 | await tester.tap(find.byIcon(Icons.add)); 23 | await tester.pump(); 24 | 25 | // Verify that our counter has incremented. 26 | expect(find.text('0'), findsNothing); 27 | expect(find.text('1'), findsOneWidget); 28 | }); 29 | } 30 | --------------------------------------------------------------------------------