├── android ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── logoko.png │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ ├── xml │ │ │ │ │ └── network_security_config.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ ├── launch_background.xml │ │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── java │ │ │ │ ├── com │ │ │ │ │ └── vjujiao │ │ │ │ │ │ └── app │ │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ │ └── MainApplication.java │ │ │ │ └── io │ │ │ │ │ └── flutter │ │ │ │ │ └── plugins │ │ │ │ │ └── GeneratedPluginRegistrant.java │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── local.properties ├── settings.gradle ├── build.gradle ├── gradlew.bat └── gradlew ├── flrs └── teddy.flr ├── assets └── timg.jpg ├── picture ├── WechatIMG1.jpeg ├── WechatIMG2.jpeg └── WechatIMG3.jpeg ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ ├── Generated.xcconfig │ ├── flutter_export_environment.sh │ └── AppFrameworkInfo.plist ├── Runner │ ├── AppDelegate.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── main.m │ ├── GeneratedPluginRegistrant.h │ ├── AppDelegate.m │ ├── GeneratedPluginRegistrant.m │ ├── Info.plist │ └── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme └── Podfile ├── lib ├── utils │ ├── tools.dart │ ├── constant.dart │ ├── toast.dart │ ├── eventbus.dart │ └── sp.dart ├── widget │ ├── basestate.dart │ ├── networkImageWrapper.dart │ ├── loadingState.dart │ ├── loadingButton.dart │ ├── refreshList.dart │ └── networkWrapper.dart ├── pages │ ├── openSource.dart │ ├── about.dart │ ├── splash.dart │ ├── feedback.dart │ ├── uploadError.dart │ ├── movewebview.dart │ ├── systemMsg.dart │ ├── applyRSS.dart │ ├── me.dart │ ├── mycollectlist.dart │ ├── mysubrsslist.dart │ ├── subscriptionIndex.dart │ ├── systemSetting.dart │ ├── webview.dart │ ├── register.dart │ ├── index.dart │ ├── home.dart │ ├── rsslist.dart │ ├── login.dart │ ├── search.dart │ ├── rssdetail.dart │ └── resetPassword.dart ├── net │ └── http.dart └── main.dart ├── .flutter-plugins ├── test └── widget_test.dart ├── README.md ├── pubspec.yaml └── .packages /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /flrs/teddy.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/flrs/teddy.flr -------------------------------------------------------------------------------- /assets/timg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/assets/timg.jpg -------------------------------------------------------------------------------- /picture/WechatIMG1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/picture/WechatIMG1.jpeg -------------------------------------------------------------------------------- /picture/WechatIMG2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/picture/WechatIMG2.jpeg -------------------------------------------------------------------------------- /picture/WechatIMG3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/picture/WechatIMG3.jpeg -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/logoko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-mdpi/logoko.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir=/Users/kimi/Library/Android/sdk 2 | flutter.sdk=/Users/kimi/downloads/flutter 3 | flutter.buildMode=debug 4 | flutter.versionName=1.0.0 5 | flutter.versionCode=1 -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /lib/utils/tools.dart: -------------------------------------------------------------------------------- 1 | /** 2 | * 校验邮箱 3 | */ 4 | isEmail(text){ 5 | RegExp reg = new RegExp(r"([a-zA-Z]|[0-9])(\w|\-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})"); 6 | return reg.hasMatch(text); 7 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/flutter-vjujiao/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/widget/basestate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BaseState extends State { 4 | @override 5 | void setState(fn) { 6 | if (mounted) { 7 | super.setState(fn); 8 | } 9 | } 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/utils/constant.dart: -------------------------------------------------------------------------------- 1 | 2 | class Constant{ 3 | static const String USER_INFO = "user_info"; 4 | static const String TOKEN = "token"; 5 | static const String THEME_COLOR = "theme_color"; 6 | static bool showTokenTipsDialog = false; 7 | 8 | static Map defaultUserinfo = {"name": "", "email": ""}; 9 | static Map defaultRssinfo = {"name": "", "logo": "","hasSub":0}; 10 | 11 | } -------------------------------------------------------------------------------- /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/GeneratedPluginRegistrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #ifndef GeneratedPluginRegistrant_h 6 | #define GeneratedPluginRegistrant_h 7 | 8 | #import 9 | 10 | @interface GeneratedPluginRegistrant : NSObject 11 | + (void)registerWithRegistry:(NSObject*)registry; 12 | @end 13 | 14 | #endif /* GeneratedPluginRegistrant_h */ 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /lib/utils/toast.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void showToast(context, text) { 4 | try { 5 | var snackBar = new SnackBar( 6 | content: new Text(text), 7 | duration: Duration(seconds: 1), 8 | backgroundColor: Theme.of(context).primaryColor); 9 | Scaffold.of(context).hideCurrentSnackBar(); 10 | Scaffold.of(context).showSnackBar(snackBar); 11 | } catch (e) {} 12 | } 13 | -------------------------------------------------------------------------------- /ios/Flutter/Generated.xcconfig: -------------------------------------------------------------------------------- 1 | // This is a generated file; do not edit or check into version control. 2 | FLUTTER_ROOT=/Users/kimi/downloads/flutter 3 | FLUTTER_APPLICATION_PATH=/Users/kimi/Desktop/project/vjujiao-app/app 4 | FLUTTER_TARGET=lib/main.dart 5 | FLUTTER_BUILD_DIR=build 6 | SYMROOT=${SOURCE_ROOT}/../build/ios 7 | FLUTTER_FRAMEWORK_DIR=/Users/kimi/downloads/flutter/bin/cache/artifacts/engine/ios 8 | FLUTTER_BUILD_NAME=1.0.0 9 | FLUTTER_BUILD_NUMBER=1 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/utils/eventbus.dart: -------------------------------------------------------------------------------- 1 | import 'package:event_bus/event_bus.dart'; 2 | 3 | EventBus eventBus = EventBus(); 4 | 5 | class EventAction{ 6 | 7 | static const SUB_RSS_SUCCESS_ACTION = "sub_rss_success_action"; 8 | static const UNSUB_RSS_SUCCESS_ACTION = "unsub_rss_success_action"; 9 | static const CHANGE_THEME_ACTION = "change_theme_action"; 10 | static const INVALIDATE_TOKEN_ACTION = "invalidate_token_action"; 11 | 12 | String action; 13 | var data; 14 | EventAction(this.action,this.data); 15 | } -------------------------------------------------------------------------------- /.flutter-plugins: -------------------------------------------------------------------------------- 1 | open_file=/Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/open_file-2.0.3/ 2 | package_info=/Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/package_info-0.4.0+4/ 3 | path_provider=/Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.1.2/ 4 | share=/Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/share-0.6.1+1/ 5 | shared_preferences=/Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.3+2/ 6 | webview_flutter=/Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-0.3.10/ 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/kimi/downloads/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/kimi/Desktop/project/vjujiao-app/app" 5 | export "FLUTTER_TARGET=lib/main.dart" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "SYMROOT=${SOURCE_ROOT}/../build/ios" 8 | export "FLUTTER_FRAMEWORK_DIR=/Users/kimi/downloads/flutter/bin/cache/artifacts/engine/ios" 9 | export "FLUTTER_BUILD_NAME=1.0.0" 10 | export "FLUTTER_BUILD_NUMBER=1" 11 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/utils/sp.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | Future putStr(key,value)async{ 4 | SharedPreferences sp = await SharedPreferences.getInstance(); 5 | await sp.setString(key, value); 6 | } 7 | 8 | Future getStr(key)async{ 9 | SharedPreferences sp = await SharedPreferences.getInstance(); 10 | return sp.getString(key); 11 | } 12 | 13 | Future putInt(key,value)async{ 14 | SharedPreferences sp = await SharedPreferences.getInstance(); 15 | await sp.setInt(key, value); 16 | } 17 | 18 | Future getInt(key)async{ 19 | SharedPreferences sp = await SharedPreferences.getInstance(); 20 | return sp.getInt(key); 21 | } 22 | 23 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | maven { url 'https://dl.bintray.com/umsdk/release' } 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.2.1' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | maven { url 'https://dl.bintray.com/umsdk/release' } 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/vjujiao/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vjujiao.app; 2 | 3 | import android.Manifest; 4 | import android.os.Bundle; 5 | import androidx.core.app.ActivityCompat; 6 | import io.flutter.app.FlutterActivity; 7 | import io.flutter.plugins.GeneratedPluginRegistrant; 8 | 9 | public class MainActivity extends FlutterActivity { 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | ActivityCompat.requestPermissions(this,new String[]{ 14 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 15 | Manifest.permission.READ_EXTERNAL_STORAGE, 16 | Manifest.permission.READ_PHONE_STATE 17 | },200); 18 | 19 | GeneratedPluginRegistrant.registerWith(this); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/pages/openSource.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../widget/basestate.dart'; 3 | 4 | class OpenSource extends StatefulWidget { 5 | @override 6 | State createState() { 7 | return OpenSourcetate(); 8 | } 9 | } 10 | 11 | class OpenSourcetate extends BaseState { 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text("开源组件"), 17 | ), 18 | body: Builder(builder: (BuildContext context) { 19 | return ListView( 20 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), 21 | children: [ 22 | Text(""), 23 | Text(""), 24 | ]); 25 | })); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/vjujiao/app/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.vjujiao.app; 2 | 3 | import com.umeng.analytics.MobclickAgent; 4 | import com.umeng.commonsdk.UMConfigure; 5 | import io.flutter.app.FlutterApplication; 6 | 7 | public class MainApplication extends FlutterApplication { 8 | 9 | @Override 10 | public void onCreate() { 11 | super.onCreate(); 12 | /** 13 | * 注意: 即使您已经在AndroidManifest.xml中配置过appkey和channel值,也需要在App代码中调 14 | * 用初始化接口(如需要使用AndroidManifest.xml中配置好的appkey和channel值, 15 | * UMConfigure.init调用中appkey和channel参数请置为null)。 16 | */ 17 | UMConfigure.init(this, "5d2699fc3fc19506ac001331", "common", UMConfigure.DEVICE_TYPE_PHONE, null); 18 | MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.AUTO); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/widget/networkImageWrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:extended_image/extended_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class NetworkImageWrapper extends StatelessWidget { 5 | String url; 6 | double width, height; 7 | BoxShape shape; 8 | NetworkImageWrapper(this.url, {this.width, this.height, this.shape}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return ExtendedImage.network(this.url, 13 | width: this.width, 14 | height: this.height, 15 | fit: BoxFit.cover, 16 | shape: this.shape, 17 | cache: true, loadStateChanged: ((ExtendedImageState state) { 18 | if (state.extendedImageLoadState == LoadState.failed) { 19 | return Container( 20 | decoration: BoxDecoration(borderRadius: BorderRadius.circular(5.0),color: Colors.grey), 21 | ); 22 | }else if(state.extendedImageLoadState == LoadState.loading){ 23 | return Container( 24 | width: width, 25 | height: height, 26 | color: Colors.transparent, 27 | ); 28 | } 29 | })); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:app/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /ios/Runner/GeneratedPluginRegistrant.m: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | #import "GeneratedPluginRegistrant.h" 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | 13 | @implementation GeneratedPluginRegistrant 14 | 15 | + (void)registerWithRegistry:(NSObject*)registry { 16 | [OpenFilePlugin registerWithRegistrar:[registry registrarForPlugin:@"OpenFilePlugin"]]; 17 | [FLTPackageInfoPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPackageInfoPlugin"]]; 18 | [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]]; 19 | [FLTSharePlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTSharePlugin"]]; 20 | [FLTSharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTSharedPreferencesPlugin"]]; 21 | [FLTWebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTWebViewFlutterPlugin"]]; 22 | } 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 俺的小网站,希望多多支持 [https://www.vjujiao.com](https://www.vjujiao.com) 2 | 3 | ### V聚焦 4 | 5 | #### 微信交流群,扫码加入(微信群不能加入的,加我vx:inskimi 我邀请你) 6 | ![https://raw.githubusercontent.com/ikimiler/react-native-video-project/master/picture/TIM%E6%88%AA%E5%9B%BE20191106090439.png](https://raw.githubusercontent.com/ikimiler/react-native-video-project/master/picture/TIM%E6%88%AA%E5%9B%BE20191106090439.png) 7 | 8 | 纯Flutter构建的一款跨平台app,主要功能包含: 9 | 10 | 1.用户登录&注册&找回密码,登陆界面采用了精美,大气,漂亮的2D动画 11 | 2.RSS栏目订阅&RSS列表&详情,支持收藏 12 | 3.多主题切换,颜色随心所欲 13 | 14 | [更多请自行下载体验,点我](https://www.lanzous.com/i56dj0d) 15 | 16 | [完整后台代码,点我](https://github.com/ikimiler/flutter-vjujiao-server) 17 | 18 | #### app截图: 19 | ![https://github.com/ikimiler/flutter-vjujiao/blob/master/picture/WechatIMG1.jpeg](https://github.com/ikimiler/flutter-vjujiao/blob/master/picture/WechatIMG1.jpeg) 20 | 21 | ![https://github.com/ikimiler/flutter-vjujiao/blob/master/picture/WechatIMG2.jpeg](https://github.com/ikimiler/flutter-vjujiao/blob/master/picture/WechatIMG2.jpeg) 22 | 23 | ![https://github.com/ikimiler/flutter-vjujiao/blob/master/picture/WechatIMG3.jpeg](https://github.com/ikimiler/flutter-vjujiao/blob/master/picture/WechatIMG3.jpeg) 24 | 25 | 如有疑问请提issues,开源不易,求star! 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/net/http.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import '../utils/eventbus.dart'; 3 | 4 | BaseOptions options2 = new BaseOptions( 5 | // baseUrl: "http://10.1.1.217:3000", 6 | // baseUrl: "http://192.168.0.204:3000", 7 | baseUrl: "http://47.93.205.239", 8 | connectTimeout: 1000 * 60, 9 | receiveTimeout: 1000 * 60, 10 | ); 11 | 12 | Dio http2 = new Dio(options2); 13 | 14 | authorization({token = ""}) { 15 | http2.interceptors.clear(); 16 | http2.interceptors 17 | .add(InterceptorsWrapper(onRequest: (RequestOptions options) { 18 | options.headers["authorization"] = 'Bearer $token'; 19 | // 在请求被发送之前做一些事情 20 | return options; //continue 21 | // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。 22 | // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data. 23 | // 24 | // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`, 25 | // 这样请求将被中止并触发异常,上层catchError会被调用。 26 | }, onResponse: (Response response) { 27 | // 在返回响应数据之前做一些预处理 28 | return response; // continue 29 | }, onError: (DioError e) { 30 | if (e != null && e.response != null && e.response.statusCode == 401) { 31 | eventBus.fire( 32 | EventAction(EventAction.INVALIDATE_TOKEN_ACTION, "")); 33 | } 34 | // 当请求失败时做一些预处理 35 | return e; //continue 36 | })); 37 | } 38 | -------------------------------------------------------------------------------- /lib/pages/about.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../widget/basestate.dart'; 3 | 4 | class AboutPage extends StatefulWidget { 5 | @override 6 | State createState() { 7 | return AboutPageState(); 8 | } 9 | } 10 | 11 | class AboutPageState extends BaseState { 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text("关于我们"), 17 | ), 18 | body: Builder(builder: (BuildContext context) { 19 | return ListView( 20 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 10), 21 | children: [ 22 | Text( 23 | "版权声明:\n", 24 | style: TextStyle(fontWeight: FontWeight.bold), 25 | ), 26 | Text( 27 | "该应用提供的信息资料、图片及视频等均来源于公开网络,我们不存储任何数据内容,仅提供类似搜索引擎的推荐服务,所有详细信息都跳转到原始网页地址访问,如果侵犯了您的权益,请与我们联系,我们会尽快处理.同时请注意原网站的观点不表示我们也认同,信息内容真实性请自己辨别。\n", 28 | style: TextStyle()), 29 | Text( 30 | "联系方式:\n", 31 | style: TextStyle(fontWeight: FontWeight.bold), 32 | ), 33 | Text("email: admin@vjujiao.com\n"), 34 | Text( 35 | "微信群:\n", 36 | style: TextStyle(fontWeight: FontWeight.bold), 37 | ), 38 | Text("加andmizi,邀请你进群"), 39 | ]); 40 | })); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/pages/splash.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/home.dart'; 2 | import 'package:app/pages/login.dart'; 3 | import 'package:app/utils/sp.dart'; 4 | import 'package:flutter/material.dart'; 5 | import '../utils/constant.dart'; 6 | import '../net/http.dart'; 7 | 8 | class Splash extends StatefulWidget { 9 | @override 10 | State createState() { 11 | return SplashState(); 12 | } 13 | } 14 | 15 | class SplashState extends State { 16 | @override 17 | void initState() { 18 | super.initState(); 19 | Future.delayed(Duration(seconds: 3), () { 20 | getStr(Constant.TOKEN).then((value) { 21 | if (value != null && value.length > 0) { 22 | print("netlog- token = $value"); 23 | authorization(token: value); 24 | Navigator.of(context).pushReplacement( 25 | MaterialPageRoute(builder: (BuildContext context) { 26 | return MyHomePage(); 27 | })); 28 | } else { 29 | Navigator.of(context).pushReplacement( 30 | MaterialPageRoute(builder: (BuildContext context) { 31 | return Login(); 32 | })); 33 | } 34 | }).catchError((error) { 35 | Navigator.of(context) 36 | .pushReplacement(MaterialPageRoute(builder: (BuildContext context) { 37 | return Login(); 38 | })); 39 | }); 40 | }); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return Scaffold(body: Center(child: Text("关注你所关心的",style: TextStyle(fontWeight: FontWeight.bold,fontSize: 16),))); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/widget/loadingState.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum LoadingStatus { Running, Success, Fail, Empty } 4 | 5 | class LoadingState extends State { 6 | LoadingStatus currentLoadingStatus = LoadingStatus.Running; 7 | 8 | @override 9 | void initState() { 10 | initData(); 11 | super.initState(); 12 | } 13 | 14 | initData() {} 15 | 16 | buildRunningWidget() { 17 | return Center( 18 | child: CircularProgressIndicator( 19 | valueColor: AlwaysStoppedAnimation(Colors.black), 20 | )); 21 | } 22 | 23 | buildEmptyWidget() { 24 | return Center(child: Text("空空如也~")); 25 | } 26 | 27 | buildFailWidget() { 28 | return Column( 29 | mainAxisAlignment: MainAxisAlignment.center, 30 | children: [ 31 | Text("加载出错,请重试"), 32 | RaisedButton( 33 | color: Colors.black, 34 | onPressed: initData, 35 | child: Text( 36 | "重试", 37 | style: TextStyle(color: Colors.white), 38 | ), 39 | ) 40 | ], 41 | ); 42 | } 43 | 44 | buildSuccessWidget() {} 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | if (currentLoadingStatus == LoadingStatus.Empty) { 49 | return buildEmptyWidget(); 50 | } else if (currentLoadingStatus == LoadingStatus.Fail) { 51 | return buildFailWidget(); 52 | } else if (currentLoadingStatus == LoadingStatus.Success) { 53 | return buildSuccessWidget(); 54 | } else if (currentLoadingStatus == LoadingStatus.Running) { 55 | return buildRunningWidget(); 56 | } else { 57 | return null; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/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 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import io.flutter.plugin.common.PluginRegistry; 4 | import com.crazecoder.openfile.OpenFilePlugin; 5 | import io.flutter.plugins.packageinfo.PackageInfoPlugin; 6 | import io.flutter.plugins.pathprovider.PathProviderPlugin; 7 | import io.flutter.plugins.share.SharePlugin; 8 | import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; 9 | import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; 10 | 11 | /** 12 | * Generated file. Do not edit. 13 | */ 14 | public final class GeneratedPluginRegistrant { 15 | public static void registerWith(PluginRegistry registry) { 16 | if (alreadyRegisteredWith(registry)) { 17 | return; 18 | } 19 | OpenFilePlugin.registerWith(registry.registrarFor("com.crazecoder.openfile.OpenFilePlugin")); 20 | PackageInfoPlugin.registerWith(registry.registrarFor("io.flutter.plugins.packageinfo.PackageInfoPlugin")); 21 | PathProviderPlugin.registerWith(registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); 22 | SharePlugin.registerWith(registry.registrarFor("io.flutter.plugins.share.SharePlugin")); 23 | SharedPreferencesPlugin.registerWith(registry.registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); 24 | WebViewFlutterPlugin.registerWith(registry.registrarFor("io.flutter.plugins.webviewflutter.WebViewFlutterPlugin")); 25 | } 26 | 27 | private static boolean alreadyRegisteredWith(PluginRegistry registry) { 28 | final String key = GeneratedPluginRegistrant.class.getCanonicalName(); 29 | if (registry.hasPlugin(key)) { 30 | return true; 31 | } 32 | registry.registrarFor(key); 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:app/utils/sp.dart'; 3 | import 'package:flutter/material.dart'; 4 | import './pages/splash.dart'; 5 | import './widget/basestate.dart'; 6 | import './utils/eventbus.dart'; 7 | import './utils/constant.dart'; 8 | 9 | 10 | void main() => runApp(MyApp()); 11 | 12 | class MyApp extends StatefulWidget { 13 | @override 14 | State createState() { 15 | return MyAppState(); 16 | } 17 | } 18 | 19 | class MyAppState extends BaseState { 20 | StreamSubscription subscription; 21 | Color themeColor = Colors.red; 22 | 23 | @override 24 | void initState() { 25 | super.initState(); 26 | getInt(Constant.THEME_COLOR).then((value) { 27 | if (value != null) { 28 | setState(() { 29 | themeColor = Color(value); 30 | }); 31 | } 32 | }); 33 | subscription = eventBus.on().listen((event) { 34 | if (event.action == EventAction.CHANGE_THEME_ACTION) { 35 | if (themeColor == event.data) return; 36 | putInt(Constant.THEME_COLOR, event.data.value); 37 | setState(() { 38 | themeColor = event.data; 39 | }); 40 | } 41 | }); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | super.dispose(); 47 | if (subscription != null) { 48 | subscription.cancel(); 49 | } 50 | } 51 | 52 | getTheme() { 53 | if (themeColor.value == Colors.black.value) { 54 | return ThemeData(brightness: Brightness.dark, buttonColor: Colors.black); 55 | } else { 56 | return ThemeData( 57 | scaffoldBackgroundColor: Colors.white, 58 | primaryColor: themeColor, 59 | buttonColor: themeColor, 60 | cursorColor: themeColor, 61 | ); 62 | } 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return MaterialApp( 68 | title: 'V聚焦', 69 | theme: getTheme(), 70 | home: Splash(), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/widget/loadingButton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum LoadingStatus { normal, loadding } 4 | 5 | class LoadingButton extends StatelessWidget { 6 | final String title; 7 | final LoadingStatus loadingStatus; 8 | final Color buttonBackgroundColor; 9 | final Color textColor; 10 | final VoidCallback onPressed; 11 | final double width; 12 | final double height; 13 | 14 | LoadingButton( 15 | this.title, { 16 | this.onPressed, 17 | this.loadingStatus = LoadingStatus.normal, 18 | this.buttonBackgroundColor = Colors.black, 19 | this.textColor = Colors.white, 20 | this.width = 300, 21 | this.height = 45, 22 | }); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return SizedBox( 27 | width: width, 28 | height: height, 29 | child: RaisedButton( 30 | onPressed: (){ 31 | if(loadingStatus != LoadingStatus.loadding) onPressed(); 32 | }, 33 | child: Row( 34 | crossAxisAlignment: CrossAxisAlignment.center, 35 | mainAxisAlignment: MainAxisAlignment.center, 36 | children: [ 37 | Offstage( 38 | offstage: loadingStatus != LoadingStatus.loadding, 39 | child: SizedBox( 40 | width: 20, 41 | height: 20, 42 | child: CircularProgressIndicator( 43 | strokeWidth: 2, 44 | valueColor: AlwaysStoppedAnimation(Colors.white) 45 | ), 46 | ), 47 | ), 48 | Container( 49 | width: loadingStatus != LoadingStatus.loadding ? 0 : 50, 50 | ), 51 | Text( 52 | title, 53 | style: TextStyle(color: textColor, fontSize: 15), 54 | ), 55 | ], 56 | ), 57 | // color: buttonBackgroundColor, 58 | // shape: StadiumBorder(), 59 | )); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | 29 | compileSdkVersion 28 30 | 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.vjujiao.app" 38 | minSdkVersion 16 39 | targetSdkVersion 28 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 43 | } 44 | 45 | buildTypes { 46 | release { 47 | // TODO: Add your own signing config for the release build. 48 | // Signing with the debug keys for now, so `flutter run --release` works. 49 | signingConfig signingConfigs.debug 50 | } 51 | } 52 | } 53 | 54 | flutter { 55 | source '../..' 56 | } 57 | 58 | dependencies { 59 | testImplementation 'junit:junit:4.12' 60 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 61 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 62 | implementation "androidx.core:core:1.0.0" 63 | implementation 'com.umeng.umsdk:analytics:8.0.0' 64 | implementation 'com.umeng.umsdk:common:2.0.0' 65 | } 66 | -------------------------------------------------------------------------------- /lib/widget/refreshList.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/widget/networkWrapper.dart' as prefix0; 2 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class RefreshList extends StatelessWidget { 6 | Widget child; 7 | OnRefresh onRefresh; 8 | LoadMore onLoadmore; 9 | bool firstRefresh; 10 | Widget emptyWidget; 11 | RefreshList({ 12 | this.child, 13 | this.onRefresh, 14 | this.onLoadmore, 15 | this.firstRefresh, 16 | this.emptyWidget, 17 | this.easyRefreshKey, 18 | this.headerKey, 19 | this.footerKey, 20 | }); 21 | 22 | GlobalKey easyRefreshKey; 23 | GlobalKey headerKey; 24 | GlobalKey footerKey; 25 | 26 | Widget buildEmptyWidget(context) { 27 | var screen = MediaQuery.of(context); 28 | var height = 29 | screen.size.height - screen.padding.top - 56 - screen.padding.bottom; 30 | return Container( 31 | height: height, 32 | child: emptyWidget != null ? emptyWidget : prefix0.EmptyWidget()); 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return EasyRefresh( 38 | firstRefresh: firstRefresh, 39 | emptyWidget: emptyWidget != null ? emptyWidget : null, 40 | key: easyRefreshKey, 41 | behavior: ScrollOverBehavior(), 42 | refreshHeader: ClassicsHeader( 43 | key: headerKey, 44 | refreshText: "下拉刷新", 45 | refreshReadyText: "松开刷新", 46 | refreshingText: "刷新...", 47 | refreshedText: "刷新完成", 48 | moreInfo: "上次刷新 %T", 49 | bgColor: Colors.transparent, 50 | textColor: Colors.black87, 51 | moreInfoColor: Colors.black54, 52 | showMore: true, 53 | ), 54 | refreshFooter: ClassicsFooter( 55 | key: footerKey, 56 | loadText: "加载更多", 57 | loadReadyText: "松开加载", 58 | loadingText: "加载中...", 59 | loadedText: "加载完成", 60 | noMoreText: "没有更多", 61 | moreInfo: "上次加载 %T", 62 | bgColor: Colors.transparent, 63 | textColor: Colors.black87, 64 | moreInfoColor: Colors.black54, 65 | showMore: true, 66 | ), 67 | child: child, 68 | onRefresh: onRefresh, 69 | loadMore: onLoadmore, 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 37 | # referring to absolute paths on developers' machines. 38 | system('rm -rf .symlinks') 39 | system('mkdir -p .symlinks/plugins') 40 | 41 | # Flutter Pods 42 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 43 | if generated_xcode_build_settings.empty? 44 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 45 | end 46 | generated_xcode_build_settings.map { |p| 47 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 48 | symlink = File.join('.symlinks', 'flutter') 49 | File.symlink(File.dirname(p[:path]), symlink) 50 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 51 | end 52 | } 53 | 54 | # Plugin Pods 55 | plugin_pods = parse_KV_file('../.flutter-plugins') 56 | plugin_pods.map { |p| 57 | symlink = File.join('.symlinks', 'plugins', p[:name]) 58 | File.symlink(p[:path], symlink) 59 | pod p[:name], :path => File.join(symlink, 'ios') 60 | } 61 | end 62 | 63 | post_install do |installer| 64 | installer.pods_project.targets.each do |target| 65 | target.build_configurations.each do |config| 66 | config.build_settings['ENABLE_BITCODE'] = 'NO' 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /ios/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 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 33 | 37 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/pages/feedback.dart: -------------------------------------------------------------------------------- 1 | import '../net/http.dart'; 2 | import '../widget/loadingButton.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../widget/basestate.dart'; 5 | import '../utils/toast.dart'; 6 | 7 | class FeedbackPage extends StatefulWidget { 8 | @override 9 | State createState() { 10 | return FeedbackPageState(); 11 | } 12 | } 13 | 14 | class FeedbackPageState extends BaseState { 15 | String value = ""; 16 | LoadingStatus loadingStatus = LoadingStatus.normal; 17 | var controller = TextEditingController(); 18 | 19 | @override 20 | void initState() { 21 | super.initState(); 22 | } 23 | 24 | sendFeedback(BuildContext context) async { 25 | if (value.length == 0) { 26 | showToast(context, "请先输入反馈内容~"); 27 | } else if (value.length < 10) { 28 | showToast(context, "反馈内容过短,请继续输入~"); 29 | } else { 30 | try { 31 | setState(() { 32 | loadingStatus = LoadingStatus.loadding; 33 | }); 34 | var response = 35 | await http2.post("/app/user/addFeedback", data: {"content": value}); 36 | if (response.data["code"] == -1) { 37 | showToast(context, response.data["message"]); 38 | } else { 39 | showToast(context, "反馈成功"); 40 | controller.clear(); 41 | value = ""; 42 | } 43 | } catch (e) { 44 | print(e); 45 | showToast(context, "反馈失败,请重试~"); 46 | } finally { 47 | setState(() { 48 | loadingStatus = LoadingStatus.normal; 49 | }); 50 | } 51 | } 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return Scaffold( 57 | appBar: AppBar( 58 | title: Text("意见反馈"), 59 | ), 60 | body: Builder(builder: (BuildContext context) { 61 | return ListView( 62 | padding: EdgeInsets.symmetric(horizontal: 10), 63 | children: [ 64 | TextField( 65 | controller: controller, 66 | onChanged: (value) { 67 | this.value = value; 68 | }, 69 | maxLines: 10, 70 | autofocus: true, 71 | decoration: InputDecoration( 72 | border: InputBorder.none, labelText: "请输入反馈内容"), 73 | ), 74 | Container( 75 | margin: EdgeInsets.only(bottom: 60,top: 30), 76 | child: Column( 77 | children: [ 78 | LoadingButton("发送", loadingStatus: loadingStatus, 79 | onPressed: () { 80 | sendFeedback(context); 81 | }) 82 | ], 83 | ), 84 | ) 85 | ], 86 | ); 87 | }), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/pages/uploadError.dart: -------------------------------------------------------------------------------- 1 | import '../net/http.dart'; 2 | import '../widget/loadingButton.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../widget/basestate.dart'; 5 | import '../utils/toast.dart'; 6 | 7 | class UploadError extends StatefulWidget { 8 | var rss_id; 9 | UploadError(this.rss_id); 10 | 11 | @override 12 | State createState() { 13 | return UploadErrorState(); 14 | } 15 | } 16 | 17 | class UploadErrorState extends BaseState { 18 | String value = ""; 19 | LoadingStatus loadingStatus = LoadingStatus.normal; 20 | var controller = TextEditingController(); 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | } 26 | 27 | uploadError(BuildContext context) async { 28 | if (value.length == 0) { 29 | showToast(context, "请先输入错误描述~"); 30 | } else if (value.length < 10) { 31 | showToast(context, "错误描述过短,请继续输入~"); 32 | } else { 33 | try { 34 | setState(() { 35 | loadingStatus = LoadingStatus.loadding; 36 | }); 37 | var response = await http2.post("/app/user/rss/error", 38 | data: {"target_backup_rss_id": widget.rss_id, "content": value}); 39 | if (response.data["code"] == 200) { 40 | showToast(context, "上报成功"); 41 | controller.clear(); 42 | value = ""; 43 | } else { 44 | showToast(context, "上报失败"); 45 | } 46 | } catch (e) { 47 | showToast(context, "上报失败"); 48 | } finally { 49 | setState(() { 50 | loadingStatus = LoadingStatus.normal; 51 | }); 52 | } 53 | } 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Scaffold( 59 | appBar: AppBar( 60 | title: Text("上报错误"), 61 | ), 62 | body: Builder(builder: (BuildContext context) { 63 | return ListView( 64 | padding: EdgeInsets.symmetric(horizontal: 10), 65 | children: [ 66 | TextField( 67 | controller: controller, 68 | onChanged: (value) { 69 | this.value = value; 70 | }, 71 | maxLines: 10, 72 | autofocus: true, 73 | decoration: InputDecoration( 74 | border: InputBorder.none, labelText: "请输入错误描述"), 75 | ), 76 | Container( 77 | margin: EdgeInsets.only(bottom: 60, top: 30), 78 | child: Column( 79 | children: [ 80 | LoadingButton("发送", loadingStatus: loadingStatus, 81 | onPressed: () { 82 | uploadError(context); 83 | }) 84 | ], 85 | ), 86 | ) 87 | ], 88 | ); 89 | }), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/pages/movewebview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:webview_flutter/webview_flutter.dart'; 3 | import '../widget/basestate.dart'; 4 | import '../widget/networkWrapper.dart'; 5 | 6 | class MoveWebview extends StatefulWidget { 7 | String url; 8 | String title; 9 | MoveWebview(this.url, this.title); 10 | 11 | @override 12 | State createState() { 13 | return MoveWebviewState(); 14 | } 15 | } 16 | 17 | class MoveWebviewState extends BaseState { 18 | bool finished = false; 19 | WebViewController webViewController; 20 | 21 | buildLoading() { 22 | if (finished) { 23 | return Container( 24 | width: 0, 25 | height: 0, 26 | ); 27 | } else { 28 | return LoadingWidget(); 29 | } 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Scaffold( 35 | appBar: AppBar( 36 | title: Text(widget.title), 37 | leading: new IconButton( 38 | icon: Icon(Icons.arrow_back), 39 | onPressed: () => { 40 | if (webViewController != null) 41 | { 42 | webViewController.canGoBack().then((value) { 43 | if (value) { 44 | webViewController.goBack(); 45 | } else { 46 | Navigator.pop(context); 47 | } 48 | }).catchError((error) { 49 | Navigator.pop(context); 50 | }) 51 | } 52 | else 53 | {Navigator.pop(context)} 54 | }, 55 | ), 56 | ), 57 | body: Stack(children: [ 58 | Positioned( 59 | child: WebView( 60 | initialUrl: widget.url, 61 | // debuggingEnabled:true, 62 | javascriptMode: JavascriptMode.unrestricted, 63 | onWebViewCreated: (WebViewController webViewController) { 64 | this.webViewController = webViewController; 65 | }, 66 | // javascriptChannels: [].toSet(), 67 | navigationDelegate: (NavigationRequest request) { 68 | if (request.url.startsWith("http://") || 69 | request.url.startsWith("https://")) { 70 | return NavigationDecision.navigate; 71 | } else { 72 | return NavigationDecision.prevent; 73 | } 74 | // return NavigationDecision.navigate; 75 | }, 76 | onPageFinished: (String url) { 77 | setState(() { 78 | finished = true; 79 | }); 80 | })), 81 | buildLoading() 82 | ])); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/widget/networkWrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../widget/basestate.dart'; 3 | 4 | enum NetworkStatus { 5 | Loading, 6 | Fail, 7 | Success, 8 | Empty, 9 | NetworkDisable, 10 | } 11 | 12 | class NetworkWrapper extends StatefulWidget { 13 | NetworkStatus networkStatus; 14 | Widget child; 15 | VoidCallback retryInit; 16 | bool emptyAndRefresh; 17 | NetworkWrapper({ 18 | this.child, 19 | this.networkStatus = NetworkStatus.Loading, 20 | this.retryInit, 21 | this.emptyAndRefresh = false, 22 | }); 23 | 24 | @override 25 | State createState() { 26 | return NetworkWrapperState(); 27 | } 28 | } 29 | 30 | class NetworkWrapperState extends BaseState { 31 | buildSuccess() { 32 | return widget.child; 33 | } 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | Widget content; 38 | switch (widget.networkStatus) { 39 | case NetworkStatus.Empty: 40 | content = widget.emptyAndRefresh ? buildSuccess() : EmptyWidget(); 41 | break; 42 | case NetworkStatus.Success: 43 | content = buildSuccess(); 44 | break; 45 | case NetworkStatus.Fail: 46 | content = ErrorWidget(onTap: () { 47 | widget.retryInit(); 48 | }); 49 | break; 50 | case NetworkStatus.Loading: 51 | content = LoadingWidget(); 52 | break; 53 | default: 54 | content = Container(); 55 | } 56 | 57 | return Container( 58 | child: content, 59 | ); 60 | } 61 | } 62 | 63 | class LoadingWidget extends StatelessWidget { 64 | @override 65 | Widget build(BuildContext context) { 66 | return Container( 67 | color: Theme.of(context).brightness == Brightness.dark ? Colors.transparent : Colors.white, 68 | child: Column( 69 | mainAxisAlignment: MainAxisAlignment.center, 70 | children: [ 71 | SizedBox( 72 | width: 25, 73 | height: 25, 74 | child: CircularProgressIndicator( 75 | valueColor: 76 | AlwaysStoppedAnimation(Theme.of(context).primaryColor), 77 | ), 78 | ), 79 | Container( 80 | height: 10, 81 | ), 82 | Text("拼命加载中...") 83 | ], 84 | ), 85 | ); 86 | } 87 | } 88 | 89 | class EmptyWidget extends StatelessWidget { 90 | @override 91 | Widget build(BuildContext context) { 92 | return Center( 93 | child: Text("暂时没有相关数据哦~"), 94 | ); 95 | } 96 | } 97 | 98 | class ErrorWidget extends StatelessWidget { 99 | var onTap; 100 | ErrorWidget({this.onTap}); 101 | 102 | @override 103 | Widget build(BuildContext context) { 104 | return Center( 105 | child: RaisedButton( 106 | onPressed: () { 107 | if (onTap != null) { 108 | onTap(); 109 | } 110 | }, 111 | child: Text("加载失败了~",style: TextStyle(color: Colors.white),), 112 | ), 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: ^0.1.2 26 | dio: ^2.1.0 27 | xml: ^3.4.1 28 | extended_image: 29 | git: 30 | url: https://github.com/fluttercandies/extended_image.git 31 | ref: dev 32 | webview_flutter: ^0.3.9+1 33 | share: ^0.6.1+1 34 | font_awesome_flutter: 8.4.0 35 | flutter_easyrefresh: ^1.2.7 36 | event_bus: ^1.1.0 37 | shared_preferences: ^0.5.3+1 38 | package_info: ^0.4.0+4 39 | path_provider: ^1.1.0 40 | open_file: ^2.0.3 41 | flare_flutter: ^1.5.4 42 | 43 | 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | 49 | # For information on the generic Dart part of this file, see the 50 | # following page: https://www.dartlang.org/tools/pub/pubspec 51 | 52 | # The following section is specific to Flutter. 53 | flutter: 54 | 55 | # The following line ensures that the Material Icons font is 56 | # included with your application, so that you can use the icons in 57 | # the material Icons class. 58 | uses-material-design: true 59 | 60 | # To add assets to your application, add an assets section, like this: 61 | assets: 62 | - assets/timg.jpg 63 | - flrs/ 64 | 65 | # An image asset can refer to one or more resolution-specific "variants", see 66 | # https://flutter.dev/assets-and-images/#resolution-aware. 67 | 68 | # For details regarding adding assets from package dependencies, see 69 | # https://flutter.dev/assets-and-images/#from-packages 70 | 71 | # To add custom fonts to your application, add a fonts section here, 72 | # in this "flutter" section. Each entry in this list should have a 73 | # "family" key with the font family name, and a "fonts" key with a 74 | # list giving the asset and other descriptors for the font. For 75 | # example: 76 | # fonts: 77 | # - family: Schyler 78 | # fonts: 79 | # - asset: fonts/Schyler-Regular.ttf 80 | # - asset: fonts/Schyler-Italic.ttf 81 | # style: italic 82 | # - family: Trajan Pro 83 | # fonts: 84 | # - asset: fonts/TrajanPro.ttf 85 | # - asset: fonts/TrajanPro_Bold.ttf 86 | # weight: 700 87 | # 88 | # For details regarding fonts from package dependencies, 89 | # see https://flutter.dev/custom-fonts/#from-packages 90 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /.packages: -------------------------------------------------------------------------------- 1 | # Generated by pub on 2019-10-20 10:06:47.315185. 2 | async:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/async-2.3.0/lib/ 3 | boolean_selector:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-1.0.5/lib/ 4 | charcode:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/charcode-1.1.2/lib/ 5 | collection:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/collection-1.14.11/lib/ 6 | convert:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/convert-2.1.1/lib/ 7 | cookie_jar:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/cookie_jar-1.0.1/lib/ 8 | crypto:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/crypto-2.0.6/lib/ 9 | cupertino_icons:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/cupertino_icons-0.1.2/lib/ 10 | dio:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/dio-2.1.13/lib/ 11 | event_bus:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/event_bus-1.1.0/lib/ 12 | extended_image:file:///Users/kimi/.pub-cache/git/extended_image-8aa790dda93141d98508598cec524d14136c479c/lib/ 13 | extended_image_library:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/extended_image_library-0.1.4/lib/ 14 | flare_dart:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/flare_dart-1.4.3/lib/ 15 | flare_flutter:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/flare_flutter-1.5.4/lib/ 16 | flutter:file:///Users/kimi/downloads/flutter/packages/flutter/lib/ 17 | flutter_easyrefresh:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/flutter_easyrefresh-1.2.7/lib/ 18 | flutter_test:file:///Users/kimi/downloads/flutter/packages/flutter_test/lib/ 19 | font_awesome_flutter:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/font_awesome_flutter-8.4.0/lib/ 20 | http:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/http-0.12.0+2/lib/ 21 | http_client_helper:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/http_client_helper-0.2.0/lib/ 22 | http_parser:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/http_parser-3.1.3/lib/ 23 | matcher:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.5/lib/ 24 | meta:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/meta-1.1.7/lib/ 25 | open_file:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/open_file-2.0.3/lib/ 26 | package_info:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/package_info-0.4.0+4/lib/ 27 | path:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/path-1.6.4/lib/ 28 | path_provider:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/path_provider-1.1.2/lib/ 29 | pedantic:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/pedantic-1.8.0+1/lib/ 30 | petitparser:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/petitparser-2.4.0/lib/ 31 | quiver:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/quiver-2.0.5/lib/ 32 | share:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/share-0.6.1+1/lib/ 33 | shared_preferences:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-0.5.3+2/lib/ 34 | sky_engine:file:///Users/kimi/downloads/flutter/bin/cache/pkg/sky_engine/lib/ 35 | source_span:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.5.5/lib/ 36 | stack_trace:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.9.3/lib/ 37 | stream_channel:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.0.0/lib/ 38 | string_scanner:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.0.5/lib/ 39 | term_glyph:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.1.0/lib/ 40 | test_api:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.2.5/lib/ 41 | typed_data:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.1.6/lib/ 42 | vector_math:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.0.8/lib/ 43 | webview_flutter:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-0.3.10/lib/ 44 | xml:file:///Users/kimi/.pub-cache/hosted/pub.flutter-io.cn/xml-3.5.0/lib/ 45 | app:lib/ 46 | -------------------------------------------------------------------------------- /lib/pages/systemMsg.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../net/http.dart'; 3 | import '../widget/basestate.dart'; 4 | import '../widget/refreshList.dart'; 5 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 6 | import 'dart:async'; 7 | import '../widget/networkWrapper.dart'; 8 | 9 | class SystemMsg extends StatefulWidget { 10 | @override 11 | State createState() { 12 | return SystemMsgState(); 13 | } 14 | } 15 | 16 | class SystemMsgState extends BaseState { 17 | List datas = []; 18 | int pageIndex = 0, pageSize = 20; 19 | GlobalKey _easyRefreshKey = 20 | new GlobalKey(); 21 | GlobalKey _headerKey = 22 | new GlobalKey(); 23 | GlobalKey _footerKey = 24 | new GlobalKey(); 25 | bool firstRefresh = false; 26 | NetworkStatus currentNetwokStatus = NetworkStatus.Loading; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | requestHttp(); 32 | } 33 | 34 | @override 35 | void dispose() { 36 | super.dispose(); 37 | } 38 | 39 | requestHttp() async { 40 | if (pageIndex == 0 && currentNetwokStatus == NetworkStatus.Fail) { 41 | setState(() { 42 | currentNetwokStatus = NetworkStatus.Loading; 43 | }); 44 | } 45 | try { 46 | var params = {"offset": pageIndex * pageSize, "limit": pageSize}; 47 | var response = 48 | await http2.get("/app/user/systemMsg", queryParameters: params); 49 | if (response.data["code"] == 200) { 50 | setState(() { 51 | if (pageIndex == 0) { 52 | datas = response.data["data"]; 53 | currentNetwokStatus = response.data["data"].length == 0 54 | ? NetworkStatus.Empty 55 | : NetworkStatus.Success; 56 | } else { 57 | datas.addAll(response.data["data"]); 58 | } 59 | }); 60 | } else { 61 | if (pageIndex == 0) { 62 | setState(() { 63 | currentNetwokStatus = NetworkStatus.Fail; 64 | }); 65 | } 66 | } 67 | } catch (e) { 68 | print(e); 69 | if (pageIndex == 0) { 70 | setState(() { 71 | currentNetwokStatus = NetworkStatus.Fail; 72 | }); 73 | } 74 | } finally { 75 | // setState(() { 76 | // firstRefresh = false; 77 | // }); 78 | } 79 | } 80 | 81 | Widget buildItem(context, index) { 82 | var item = datas[index]; 83 | var date = DateTime.fromMillisecondsSinceEpoch(int.parse(item["time"])); 84 | var year = date.year; 85 | var month = date.month; 86 | var day = date.day; 87 | return Card( 88 | elevation: 5, 89 | child: Container( 90 | padding: EdgeInsets.all(10), 91 | child: Column( 92 | crossAxisAlignment: CrossAxisAlignment.end, 93 | children: [ 94 | Text(item["text"]), 95 | Text("$year年$month月$day日"), 96 | ], 97 | ), 98 | ), 99 | ); 100 | } 101 | 102 | Future onRefresh() async { 103 | pageIndex = 0; 104 | await requestHttp(); 105 | } 106 | 107 | Future onLoadMore() async { 108 | pageIndex++; 109 | await requestHttp(); 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | return Scaffold( 115 | appBar: AppBar( 116 | title: Text("系统消息"), 117 | ), 118 | body: NetworkWrapper( 119 | networkStatus: currentNetwokStatus, 120 | retryInit: requestHttp, 121 | child: RefreshList( 122 | easyRefreshKey: _easyRefreshKey, 123 | headerKey: _headerKey, 124 | footerKey: _footerKey, 125 | firstRefresh: firstRefresh, 126 | onRefresh: onRefresh, 127 | onLoadmore: onLoadMore, 128 | child: ListView.builder( 129 | padding: EdgeInsets.all(10), 130 | itemCount: datas.length, 131 | itemBuilder: buildItem, 132 | ), 133 | ), 134 | )); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/pages/applyRSS.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../widget/basestate.dart'; 3 | import '../widget/loadingButton.dart'; 4 | import '../net/http.dart'; 5 | import '../utils/toast.dart'; 6 | 7 | class ApplyRSS extends StatefulWidget { 8 | @override 9 | State createState() { 10 | return ApplyRSSState(); 11 | } 12 | } 13 | 14 | class ApplyRSSState extends BaseState { 15 | var form_key = GlobalKey(); 16 | String name, url; 17 | var loadingStatus = LoadingStatus.normal; 18 | bool isDisableButton = false; 19 | 20 | startApply(context) async { 21 | try { 22 | if (form_key.currentState.validate()) { 23 | form_key.currentState.save(); 24 | if (isDisableButton) return; 25 | isDisableButton = true; 26 | setState(() { 27 | loadingStatus = LoadingStatus.loadding; 28 | }); 29 | var params = {"name": name, "url": url}; 30 | var response = await http2.post("/app/user/rss/apply", data: params); 31 | print(response); 32 | if (response.data["code"] == 200) { 33 | showToast(context, "申请成功,请耐心等待~"); 34 | form_key.currentState.reset(); 35 | } else { 36 | showToast(context, "申请失败,请重试~"); 37 | } 38 | } 39 | } catch (error) { 40 | print(error); 41 | showToast(context, "申请失败,请重试~"); 42 | } finally { 43 | setState(() { 44 | loadingStatus = LoadingStatus.normal; 45 | }); 46 | isDisableButton = false; 47 | } 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return Scaffold( 53 | appBar: AppBar( 54 | title: Text("申请栏目"), 55 | ), 56 | body: Builder(builder: (BuildContext context) { 57 | return Form( 58 | key: form_key, 59 | child: ListView( 60 | padding: EdgeInsets.symmetric(horizontal: 10), 61 | children: [ 62 | Column( 63 | children: [ 64 | Container( 65 | child: TextFormField( 66 | autofocus: true, 67 | decoration: InputDecoration( 68 | labelText: "请输入网站名称", 69 | ), 70 | validator: (value) { 71 | if (value.length == 0) { 72 | return "名称不能为空"; 73 | } 74 | }, 75 | onSaved: (value) { 76 | name = value; 77 | }, 78 | ), 79 | ), 80 | Container( 81 | margin: EdgeInsets.only(top: 10), 82 | child: TextFormField( 83 | decoration: InputDecoration( 84 | labelText: "请输入目标网址", 85 | ), 86 | validator: (value) { 87 | if (value.length == 0) { 88 | return "网址不能为空"; 89 | } 90 | }, 91 | onSaved: (value) { 92 | url = value; 93 | }, 94 | )), 95 | 96 | Container( 97 | margin: EdgeInsets.only(top: 20), 98 | child: Text("说明:\n提交申请你喜欢的RSS栏目(通常为网站/APP等),开发小哥哥收到你的申请后会尽快进行开发,完成后我们会第一时间邮件通知您~"), 99 | ), 100 | Container( 101 | margin: EdgeInsets.only(top: 60, bottom: 60), 102 | alignment: Alignment.center, 103 | child: LoadingButton( 104 | "申请", 105 | onPressed: () { 106 | startApply(context); 107 | }, 108 | loadingStatus: loadingStatus, 109 | ), 110 | ) 111 | ], 112 | ) 113 | ])); 114 | })); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/pages/me.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:app/pages/applyRSS.dart'; 3 | import 'package:app/pages/mycollectlist.dart'; 4 | import 'package:app/pages/systemMsg.dart'; 5 | import 'package:app/utils/sp.dart'; 6 | import 'package:flutter/material.dart'; 7 | import './mysubrsslist.dart'; 8 | import '../widget/basestate.dart'; 9 | import '../utils/constant.dart'; 10 | import './systemSetting.dart'; 11 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 12 | 13 | class Me extends StatefulWidget { 14 | @override 15 | State createState() { 16 | return MeState(); 17 | } 18 | } 19 | 20 | class MeState extends BaseState { 21 | Map userinfo = Constant.defaultUserinfo; 22 | 23 | initState() { 24 | super.initState(); 25 | getStr(Constant.USER_INFO).then((value) { 26 | if (value != null) { 27 | var jsonMap = json.decode(value); 28 | setState(() { 29 | userinfo = jsonMap; 30 | }); 31 | } 32 | }); 33 | } 34 | 35 | List headerSliverBuilder( 36 | BuildContext context, bool innerBoxIsScrolled) { 37 | var date = DateTime.now(); 38 | var year = date.year; 39 | var month = date.month; 40 | var day = date.day; 41 | var idxArray = ['一', '二', '三', '四', '五', '六', '日']; 42 | var weekday = idxArray[date.weekday - 1]; 43 | 44 | return [ 45 | SliverAppBar( 46 | actions: [ 47 | IconButton( 48 | onPressed: () { 49 | Navigator.of(context) 50 | .push(MaterialPageRoute(builder: (BuildContext context) { 51 | return SystemMsg(); 52 | })); 53 | }, 54 | icon: Icon(FontAwesomeIcons.comment), 55 | ) 56 | ], 57 | flexibleSpace: FlexibleSpaceBar( 58 | background: Container( 59 | color: Theme.of(context).primaryColor, 60 | child: Column( 61 | mainAxisAlignment: MainAxisAlignment.center, 62 | crossAxisAlignment: CrossAxisAlignment.center, 63 | children: [ 64 | Text('欢迎您 ${userinfo["name"]}', 65 | style: TextStyle(color: Colors.white)), 66 | Container(height: 10), 67 | Text('今天是 $year年 $month月 $day日 周$weekday', 68 | style: TextStyle(color: Colors.white)), 69 | Container(height: 10), 70 | Text(userinfo["email"], 71 | style: TextStyle(color: Colors.white)), 72 | ], 73 | )), 74 | ), 75 | expandedHeight: 200, 76 | ) 77 | ]; 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return NestedScrollView( 83 | headerSliverBuilder: headerSliverBuilder, 84 | body: ListView( 85 | children: [ 86 | ListTile( 87 | dense: false, 88 | onTap: () { 89 | Navigator.of(context) 90 | .push(MaterialPageRoute(builder: (BuildContext context) { 91 | return MySubRssList(); 92 | })); 93 | }, 94 | trailing: Icon(Icons.keyboard_arrow_right), 95 | title: Text("我的订阅"), 96 | ), 97 | Divider( 98 | height: 1, 99 | ), 100 | ListTile( 101 | dense: false, 102 | onTap: () { 103 | Navigator.of(context) 104 | .push(MaterialPageRoute(builder: (BuildContext context) { 105 | return MyCollectList(); 106 | })); 107 | }, 108 | trailing: Icon(Icons.keyboard_arrow_right), 109 | title: Text("我的收藏"), 110 | ), 111 | Divider( 112 | height: 1, 113 | ), 114 | ListTile( 115 | dense: false, 116 | onTap: () { 117 | Navigator.of(context) 118 | .push(MaterialPageRoute(builder: (BuildContext context) { 119 | return ApplyRSS(); 120 | })); 121 | }, 122 | trailing: Icon(Icons.keyboard_arrow_right), 123 | title: Text("申请栏目"), 124 | ), 125 | Divider( 126 | height: 1, 127 | ), 128 | ListTile( 129 | dense: false, 130 | onTap: () { 131 | Navigator.of(context) 132 | .push(MaterialPageRoute(builder: (BuildContext context) { 133 | return SystemSetting(); 134 | })); 135 | }, 136 | trailing: Icon(Icons.keyboard_arrow_right), 137 | title: Text("系统设置"), 138 | ), 139 | Divider( 140 | height: 1, 141 | ), 142 | ], 143 | ), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/pages/mycollectlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/webview.dart'; 2 | import 'package:app/widget/networkImageWrapper.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../net/http.dart'; 5 | import './rssdetail.dart'; 6 | import '../widget/basestate.dart'; 7 | import '../widget/refreshList.dart'; 8 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 9 | import '../utils/eventbus.dart'; 10 | import 'dart:async'; 11 | import '../widget/networkWrapper.dart'; 12 | 13 | class MyCollectList extends StatefulWidget { 14 | @override 15 | State createState() { 16 | return MyCollectListState(); 17 | } 18 | } 19 | 20 | class MyCollectListState extends BaseState { 21 | List datas = []; 22 | int pageIndex = 0, pageSize = 20; 23 | GlobalKey _easyRefreshKey = 24 | new GlobalKey(); 25 | GlobalKey _headerKey = 26 | new GlobalKey(); 27 | GlobalKey _footerKey = 28 | new GlobalKey(); 29 | bool firstRefresh = false; 30 | StreamSubscription subscription; 31 | NetworkStatus currentNetwokStatus = NetworkStatus.Loading; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | // subscription = eventBus.on().listen((eventAction) { 37 | // if (eventAction.action == EventAction.SUB_RSS_SUCCESS_ACTION) { 38 | // pageIndex = 0; 39 | // requestHttp(); 40 | // } else if (eventAction.action == EventAction.UNSUB_RSS_SUCCESS_ACTION) { 41 | // pageIndex = 0; 42 | // requestHttp(); 43 | // } 44 | // }); 45 | requestHttp(); 46 | } 47 | 48 | @override 49 | void dispose() { 50 | super.dispose(); 51 | if (subscription != null) { 52 | subscription.cancel(); 53 | } 54 | } 55 | 56 | requestHttp() async { 57 | if (pageIndex == 0 && currentNetwokStatus == NetworkStatus.Fail) { 58 | setState(() { 59 | currentNetwokStatus = NetworkStatus.Loading; 60 | }); 61 | } 62 | try { 63 | var params = {"offset": pageIndex * pageSize, "limit": pageSize}; 64 | var response = 65 | await http2.get("/app/user/rss/collect", queryParameters: params); 66 | if (response.data["code"] == 200) { 67 | setState(() { 68 | if (pageIndex == 0) { 69 | datas = response.data["data"]; 70 | currentNetwokStatus = response.data["data"].length == 0 71 | ? NetworkStatus.Empty 72 | : NetworkStatus.Success; 73 | } else { 74 | datas.addAll(response.data["data"]); 75 | } 76 | }); 77 | } else { 78 | if (pageIndex == 0) { 79 | setState(() { 80 | currentNetwokStatus = NetworkStatus.Fail; 81 | }); 82 | } 83 | } 84 | } catch (e) { 85 | print(e); 86 | if (pageIndex == 0) { 87 | setState(() { 88 | currentNetwokStatus = NetworkStatus.Fail; 89 | }); 90 | } 91 | } finally { 92 | // setState(() { 93 | // firstRefresh = false; 94 | // }); 95 | } 96 | } 97 | 98 | Widget buildItem(context, index) { 99 | var item = datas[index]; 100 | return Column( 101 | children: [ 102 | ListTile( 103 | dense: false, 104 | onTap: () { 105 | item["rss_id"] = item["target_rss_id"]; 106 | Navigator.of(context) 107 | .push(MaterialPageRoute(builder: (BuildContext context) { 108 | return WebViewPage(item); 109 | })); 110 | }, 111 | title: Text( 112 | item["title"], 113 | maxLines: 2, 114 | overflow: TextOverflow.ellipsis, 115 | ), 116 | ), 117 | Divider( 118 | height: 1, 119 | ) 120 | ], 121 | ); 122 | } 123 | 124 | Future onRefresh() async { 125 | pageIndex = 0; 126 | await requestHttp(); 127 | } 128 | 129 | Future onLoadMore() async { 130 | pageIndex++; 131 | await requestHttp(); 132 | } 133 | 134 | @override 135 | Widget build(BuildContext context) { 136 | return Scaffold( 137 | appBar: AppBar( 138 | title: Text("我的收藏"), 139 | ), 140 | body: NetworkWrapper( 141 | networkStatus: currentNetwokStatus, 142 | retryInit: requestHttp, 143 | child: RefreshList( 144 | easyRefreshKey: _easyRefreshKey, 145 | headerKey: _headerKey, 146 | footerKey: _footerKey, 147 | firstRefresh: firstRefresh, 148 | onRefresh: onRefresh, 149 | onLoadmore: onLoadMore, 150 | child: ListView.builder( 151 | itemCount: datas.length, 152 | itemBuilder: buildItem, 153 | ), 154 | ), 155 | )); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /lib/pages/mysubrsslist.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/movewebview.dart'; 2 | import 'package:app/widget/networkImageWrapper.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../net/http.dart'; 5 | import './rssdetail.dart'; 6 | import '../widget/basestate.dart'; 7 | import '../widget/refreshList.dart'; 8 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 9 | import '../utils/eventbus.dart'; 10 | import 'dart:async'; 11 | import '../widget/networkWrapper.dart'; 12 | 13 | class MySubRssList extends StatefulWidget { 14 | @override 15 | State createState() { 16 | return MySubRssListState(); 17 | } 18 | } 19 | 20 | class MySubRssListState extends BaseState { 21 | List datas = []; 22 | int pageIndex = 0, pageSize = 20; 23 | GlobalKey _easyRefreshKey = 24 | new GlobalKey(); 25 | GlobalKey _headerKey = 26 | new GlobalKey(); 27 | GlobalKey _footerKey = 28 | new GlobalKey(); 29 | bool firstRefresh = false; 30 | StreamSubscription subscription; 31 | NetworkStatus currentNetwokStatus = NetworkStatus.Loading; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | subscription = eventBus.on().listen((eventAction) { 37 | if (eventAction.action == EventAction.SUB_RSS_SUCCESS_ACTION) { 38 | pageIndex = 0; 39 | requestHttp(); 40 | } else if (eventAction.action == EventAction.UNSUB_RSS_SUCCESS_ACTION) { 41 | pageIndex = 0; 42 | requestHttp(); 43 | } 44 | }); 45 | requestHttp(); 46 | } 47 | 48 | @override 49 | void dispose() { 50 | super.dispose(); 51 | if (subscription != null) { 52 | subscription.cancel(); 53 | } 54 | } 55 | 56 | requestHttp() async { 57 | if (pageIndex == 0 && currentNetwokStatus == NetworkStatus.Fail) { 58 | setState(() { 59 | currentNetwokStatus = NetworkStatus.Loading; 60 | }); 61 | } 62 | try { 63 | var params = {"offset": pageIndex * pageSize, "limit": pageSize}; 64 | var response = 65 | await http2.get("/app/user/rss/mySubRss", queryParameters: params); 66 | if (response.data["code"] == 200) { 67 | setState(() { 68 | if (pageIndex == 0) { 69 | datas = response.data["data"]; 70 | currentNetwokStatus = response.data["data"].length == 0 71 | ? NetworkStatus.Empty 72 | : NetworkStatus.Success; 73 | } else { 74 | datas.addAll(response.data["data"]); 75 | } 76 | }); 77 | } else { 78 | if (pageIndex == 0) { 79 | setState(() { 80 | currentNetwokStatus = NetworkStatus.Fail; 81 | }); 82 | } 83 | } 84 | } catch (e) { 85 | print(e); 86 | if (pageIndex == 0) { 87 | setState(() { 88 | currentNetwokStatus = NetworkStatus.Fail; 89 | }); 90 | } 91 | } finally { 92 | // setState(() { 93 | // firstRefresh = false; 94 | // }); 95 | } 96 | } 97 | 98 | Widget buildItem(context, index) { 99 | var item = datas[index]; 100 | String tag = 'rss_image3_${index.toString()}'; 101 | return Column( 102 | children: [ 103 | ListTile( 104 | dense: false, 105 | onTap: () { 106 | item["hasSub"] = 1; 107 | if (item["type"] == 1) { 108 | Navigator.of(context) 109 | .push(MaterialPageRoute(builder: (BuildContext context) { 110 | return RssDetail( 111 | item["rss_id"], 112 | tag: tag, 113 | ); 114 | })); 115 | } else { 116 | Navigator.of(context) 117 | .push(MaterialPageRoute(builder: (BuildContext context) { 118 | return MoveWebview( 119 | item["rss_url"], 120 | item["name"], 121 | ); 122 | })); 123 | } 124 | }, 125 | leading: Hero( 126 | tag: tag, 127 | child: NetworkImageWrapper( 128 | item["logo"], 129 | width: 25, 130 | height: 25, 131 | ), 132 | ), 133 | title: Text(item["name"]), 134 | ), 135 | Divider( 136 | height: 1, 137 | ) 138 | ], 139 | ); 140 | } 141 | 142 | Future onRefresh() async { 143 | pageIndex = 0; 144 | await requestHttp(); 145 | } 146 | 147 | Future onLoadMore() async { 148 | pageIndex++; 149 | await requestHttp(); 150 | } 151 | 152 | @override 153 | Widget build(BuildContext context) { 154 | return Scaffold( 155 | appBar: AppBar( 156 | title: Text("我的订阅"), 157 | ), 158 | body: NetworkWrapper( 159 | networkStatus: currentNetwokStatus, 160 | retryInit: requestHttp, 161 | child: RefreshList( 162 | easyRefreshKey: _easyRefreshKey, 163 | headerKey: _headerKey, 164 | footerKey: _footerKey, 165 | firstRefresh: firstRefresh, 166 | onRefresh: onRefresh, 167 | onLoadmore: onLoadMore, 168 | child: ListView.builder( 169 | itemCount: datas.length, 170 | itemBuilder: buildItem, 171 | ), 172 | ), 173 | )); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/pages/subscriptionIndex.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/movewebview.dart'; 2 | import 'package:app/pages/search.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../net/http.dart'; 5 | import '../widget/networkWrapper.dart'; 6 | import '../widget/networkImageWrapper.dart'; 7 | import './rsslist.dart'; 8 | import './rssdetail.dart'; 9 | import '../widget/basestate.dart'; 10 | 11 | class SubscriptionIndex extends StatefulWidget { 12 | @override 13 | State createState() { 14 | return SubscriptionIndexState(); 15 | } 16 | } 17 | 18 | class SubscriptionIndexState extends BaseState { 19 | NetworkStatus currentNetworkStatus = NetworkStatus.Loading; 20 | List datas = []; 21 | @override 22 | void initState() { 23 | super.initState(); 24 | requestHttp(); 25 | } 26 | 27 | dispose() { 28 | super.dispose(); 29 | } 30 | 31 | requestHttp() async { 32 | if (currentNetworkStatus == NetworkStatus.Fail) { 33 | setState(() { 34 | currentNetworkStatus = NetworkStatus.Loading; 35 | }); 36 | } 37 | try { 38 | var response = await http2.get("/app/user/rss/recommend"); 39 | if (response.data["code"] == 200) { 40 | setState(() { 41 | currentNetworkStatus = NetworkStatus.Success; 42 | datas = response.data["data"]; 43 | }); 44 | } else { 45 | setState(() { 46 | currentNetworkStatus = NetworkStatus.Fail; 47 | }); 48 | } 49 | } catch (e) { 50 | print("请求失败了 $e"); 51 | setState(() { 52 | currentNetworkStatus = NetworkStatus.Fail; 53 | }); 54 | } 55 | } 56 | 57 | Widget buildItem(context, index) { 58 | var tag = datas[index]; 59 | String name = tag["name"]; 60 | int tag_id = tag["tag_id"]; 61 | List childs = tag["child"]; 62 | return Container( 63 | padding: EdgeInsets.all(10), 64 | height: 130, 65 | child: Column( 66 | crossAxisAlignment: CrossAxisAlignment.start, 67 | children: [ 68 | Row( 69 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 70 | children: [ 71 | Text(name, style: TextStyle(fontWeight: FontWeight.bold)), 72 | InkResponse( 73 | onTap: () { 74 | Navigator.of(context) 75 | .push(MaterialPageRoute(builder: (BuildContext context) { 76 | return RssList(name, tag_id); 77 | })); 78 | }, 79 | child: Text("更多"), 80 | ) 81 | ], 82 | ), 83 | Flexible( 84 | child: ListView( 85 | scrollDirection: Axis.horizontal, 86 | children: childs.map((item) { 87 | String tagName = '${tag["name"]}rss_image_${item["name"]}'; 88 | Widget result = GestureDetector( 89 | onTap: () { 90 | //跳转不同的界面 91 | if (item["type"] == 1) { 92 | Navigator.of(context).push( 93 | MaterialPageRoute(builder: (BuildContext context) { 94 | return RssDetail(item["rss_id"], tag: tagName); 95 | })); 96 | } else { 97 | Navigator.of(context).push( 98 | MaterialPageRoute(builder: (BuildContext context) { 99 | return MoveWebview( 100 | item["rss_url"], 101 | item["name"], 102 | ); 103 | })); 104 | } 105 | }, 106 | child: Container( 107 | width: 80, 108 | margin: EdgeInsets.only(right: 5), 109 | child: Column( 110 | mainAxisAlignment: MainAxisAlignment.center, 111 | children: [ 112 | Hero( 113 | tag: tagName, 114 | child: NetworkImageWrapper( 115 | item["logo"], 116 | width: 50, 117 | height: 50, 118 | ), 119 | ), 120 | Container( 121 | height: 5, 122 | ), 123 | Text( 124 | item["name"], 125 | overflow: TextOverflow.ellipsis, 126 | ) 127 | ], 128 | ), 129 | ), 130 | ); 131 | return result; 132 | }).toList()), 133 | ) 134 | ], 135 | ), 136 | ); 137 | } 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | return Scaffold( 142 | appBar: AppBar( 143 | title: Text("广场"), 144 | actions: [ 145 | IconButton( 146 | tooltip: "搜索", 147 | onPressed: () { 148 | Navigator.of(context) 149 | .push(MaterialPageRoute(builder: (BuildContext context) { 150 | return Search(); 151 | })); 152 | }, 153 | icon: Icon(Icons.search), 154 | ) 155 | ], 156 | ), 157 | body: NetworkWrapper( 158 | child: ListView.builder( 159 | itemCount: datas.length, 160 | itemBuilder: buildItem, 161 | ), 162 | retryInit: () { 163 | requestHttp(); 164 | }, 165 | networkStatus: currentNetworkStatus, 166 | ), 167 | ); 168 | } 169 | } 170 | 171 | class Item { 172 | String name; 173 | String desc; 174 | int rss_id; 175 | String group_name; 176 | int enable; 177 | String logo; 178 | String url; 179 | 180 | Item fromJson(Map map) { 181 | name = map["name"]; 182 | desc = map["desc"]; 183 | rss_id = map["rss_id"]; 184 | group_name = map["group_name"]; 185 | enable = map["enable"]; 186 | logo = map["logo"]; 187 | url = map["url"]; 188 | return this; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/pages/systemSetting.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:app/net/http.dart'; 3 | import 'package:app/pages/applyRSS.dart'; 4 | import 'package:app/pages/login.dart'; 5 | import 'package:app/pages/openSource.dart'; 6 | import 'package:app/pages/resetPassword.dart'; 7 | import 'package:app/utils/sp.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:package_info/package_info.dart'; 10 | import '../widget/basestate.dart'; 11 | import '../utils/eventbus.dart'; 12 | import './feedback.dart'; 13 | import '../utils/constant.dart'; 14 | import './about.dart'; 15 | 16 | class SystemSetting extends StatefulWidget { 17 | @override 18 | State createState() { 19 | return SystemSettingState(); 20 | } 21 | } 22 | 23 | class SystemSettingState extends BaseState { 24 | var colors = [ 25 | ColorModel("暗黑", Colors.black), 26 | ColorModel("红色", Colors.red), 27 | ColorModel("蓝色", Colors.blue), 28 | ColorModel("绿色", Colors.green), 29 | ColorModel("粉红", Colors.pink), 30 | ColorModel("紫色", Colors.purple), 31 | ]; 32 | Map userinfo = Constant.defaultUserinfo; 33 | String versionName = "1.0.0"; 34 | 35 | @override 36 | initState() { 37 | super.initState(); 38 | PackageInfo.fromPlatform().then((info){ 39 | setState((){ 40 | versionName = info.version; 41 | }); 42 | }); 43 | getStr(Constant.USER_INFO).then((value) { 44 | if (value != null) { 45 | var jsonMap = json.decode(value); 46 | setState(() { 47 | userinfo = jsonMap; 48 | }); 49 | } 50 | }); 51 | } 52 | 53 | showColorsModalBottomSheet(BuildContext context) { 54 | showModalBottomSheet( 55 | context: context, 56 | builder: (BuildContext context) { 57 | return Container( 58 | height: 300, 59 | child: ListView( 60 | children: colors.map((color) { 61 | Widget view = Column( 62 | children: [ 63 | ListTile( 64 | dense: false, 65 | onTap: () { 66 | eventBus.fire(EventAction( 67 | EventAction.CHANGE_THEME_ACTION, color.color)); 68 | }, 69 | title: Text( 70 | color.name, 71 | style: TextStyle(color: color.color), 72 | ), 73 | ), 74 | Divider( 75 | height: 1, 76 | ) 77 | ], 78 | ); 79 | return view; 80 | }).toList()), 81 | ); 82 | }); 83 | } 84 | 85 | @override 86 | Widget build(BuildContext context) { 87 | return Scaffold( 88 | appBar: AppBar( 89 | title: Text("系统设置"), 90 | ), 91 | body: Builder(builder: (BuildContext context) { 92 | return ListView( 93 | children: [ 94 | ListTile( 95 | dense: false, 96 | onTap: () { 97 | showColorsModalBottomSheet(context); 98 | }, 99 | trailing: Icon(Icons.keyboard_arrow_right), 100 | title: Text("更换主题"), 101 | ), 102 | Divider( 103 | height: 1, 104 | ), 105 | ListTile( 106 | dense: false, 107 | onTap: () { 108 | Navigator.of(context) 109 | .push(MaterialPageRoute(builder: (BuildContext context) { 110 | return FeedbackPage(); 111 | })); 112 | }, 113 | trailing: Icon(Icons.keyboard_arrow_right), 114 | title: Text("意见反馈"), 115 | ), 116 | Divider( 117 | height: 1, 118 | ), 119 | ListTile( 120 | dense: false, 121 | onTap: () { 122 | Navigator.of(context) 123 | .push(MaterialPageRoute(builder: (BuildContext context) { 124 | return ResetPassword( 125 | email: userinfo["email"], 126 | ); 127 | })); 128 | }, 129 | trailing: Icon(Icons.keyboard_arrow_right), 130 | title: Text("重设密码"), 131 | ), 132 | Divider( 133 | height: 1, 134 | ), 135 | ListTile( 136 | dense: false, 137 | onTap: () { 138 | Navigator.of(context) 139 | .push(MaterialPageRoute(builder: (BuildContext context) { 140 | return AboutPage(); 141 | })); 142 | }, 143 | trailing: Icon(Icons.keyboard_arrow_right), 144 | title: Text("关于我们"), 145 | ), 146 | Divider( 147 | height: 1, 148 | ), 149 | // ListTile( 150 | // dense: false, 151 | // onTap: () { 152 | // Navigator.of(context) 153 | // .push(MaterialPageRoute(builder: (BuildContext context) { 154 | // return OpenSource(); 155 | // })); 156 | // }, 157 | // trailing: Icon(Icons.keyboard_arrow_right), 158 | // title: Text("开源组件"), 159 | // ), 160 | // Divider( 161 | // height: 1, 162 | // ), 163 | Container( 164 | width: 300, 165 | height: 45, 166 | margin: EdgeInsets.symmetric(horizontal: 20, vertical: 60), 167 | child: RaisedButton( 168 | onPressed: () { 169 | logout(context); 170 | }, 171 | child: Text( 172 | "退出登录", 173 | style: TextStyle(color: Colors.white, fontSize: 15), 174 | ), 175 | ), 176 | ), 177 | Container( 178 | margin: EdgeInsets.only(top: 80), 179 | alignment: Alignment.bottomCenter, 180 | child: Text('version $versionName'), 181 | ) 182 | ], 183 | ); 184 | })); 185 | } 186 | 187 | logout(context) { 188 | putStr(Constant.TOKEN, "").then((value) { 189 | authorization(); 190 | Navigator.of(context).pushAndRemoveUntil( 191 | MaterialPageRoute(builder: (BuildContext context) { 192 | return Login(); 193 | }), (router) => false); 194 | }); 195 | } 196 | } 197 | 198 | class ColorModel { 199 | String name; 200 | Color color; 201 | ColorModel(this.name, this.color); 202 | } 203 | -------------------------------------------------------------------------------- /lib/pages/webview.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/net/http.dart'; 2 | import 'package:app/pages/rssdetail.dart'; 3 | import 'package:app/pages/uploadError.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:webview_flutter/webview_flutter.dart'; 7 | import 'package:share/share.dart'; 8 | import '../widget/basestate.dart'; 9 | import '../utils/toast.dart'; 10 | import '../widget/networkWrapper.dart'; 11 | 12 | class WebViewPage extends StatefulWidget { 13 | dynamic item; 14 | WebViewPage(this.item); 15 | 16 | @override 17 | State createState() { 18 | return WebViewPageState(); 19 | } 20 | } 21 | 22 | class WebViewPageState extends BaseState { 23 | bool finished = false; 24 | WebViewController webViewController; 25 | 26 | buildLoading() { 27 | if (finished) { 28 | return Container( 29 | width: 0, 30 | height: 0, 31 | ); 32 | } else { 33 | return LoadingWidget(); 34 | } 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Scaffold( 40 | appBar: CustomAppbar(widget.item, webViewController), 41 | body: Stack(children: [ 42 | Positioned( 43 | child: WebView( 44 | initialUrl: widget.item["link"], 45 | // debuggingEnabled:true, 46 | javascriptMode: JavascriptMode.unrestricted, 47 | onWebViewCreated: (WebViewController webViewController) { 48 | this.webViewController = webViewController; 49 | }, 50 | // javascriptChannels: [].toSet(), 51 | navigationDelegate: (NavigationRequest request) { 52 | if (request.url.startsWith("http://") || 53 | request.url.startsWith("https://")) { 54 | return NavigationDecision.navigate; 55 | } else { 56 | return NavigationDecision.prevent; 57 | } 58 | }, 59 | onPageFinished: (String url) { 60 | setState(() { 61 | finished = true; 62 | }); 63 | })), 64 | buildLoading() 65 | ])); 66 | } 67 | } 68 | 69 | class CustomAppbar extends StatefulWidget with PreferredSizeWidget { 70 | dynamic item; 71 | WebViewController webViewController; 72 | CustomAppbar(this.item, this.webViewController); 73 | 74 | @override 75 | State createState() { 76 | return CustomAppbarState(); 77 | } 78 | 79 | @override 80 | Size get preferredSize => Size.fromHeight(kToolbarHeight); 81 | } 82 | 83 | class CustomAppbarState extends BaseState { 84 | bool isCollectFlag = false; 85 | 86 | @override 87 | initState() { 88 | super.initState(); 89 | isCollect(); 90 | } 91 | 92 | isCollect() async { 93 | var response = await http2.post("/app/user/rss/collect/isCollect", 94 | data: {"backup_rss_id": widget.item["backup_rss_id"]}); 95 | if (response.data["code"] == 200) { 96 | setState(() { 97 | isCollectFlag = response.data["data"].length > 0 ? true : false; 98 | }); 99 | } 100 | } 101 | 102 | addCollect() async { 103 | try { 104 | var response = await http2.post("/app/user/rss/collect", 105 | data: {"backup_rss_id": widget.item["backup_rss_id"]}); 106 | if (response.data["code"] == 200) { 107 | showToast(context, "收藏成功"); 108 | setState(() { 109 | isCollectFlag = true; 110 | }); 111 | } else { 112 | showToast(context, "收藏失败"); 113 | } 114 | } catch (e) { 115 | showToast(context, "收藏失败"); 116 | } 117 | } 118 | 119 | cancleCollect() async { 120 | try { 121 | var response = await http2.delete("/app/user/rss/collect", 122 | queryParameters: {"backup_rss_id": widget.item["backup_rss_id"]}); 123 | if (response.data["code"] == 200) { 124 | showToast(context, "取消成功"); 125 | setState(() { 126 | isCollectFlag = false; 127 | }); 128 | } else { 129 | showToast(context, "取消失败"); 130 | } 131 | } catch (e) { 132 | showToast(context, "取消失败"); 133 | } 134 | } 135 | 136 | @override 137 | Widget build(BuildContext context) { 138 | return AppBar( 139 | leading: new IconButton( 140 | icon: Icon(Icons.arrow_back), 141 | onPressed: () => { 142 | if (widget.webViewController != null) 143 | { 144 | widget.webViewController.canGoBack().then((value) { 145 | if (value) { 146 | widget.webViewController.goBack(); 147 | } else { 148 | Navigator.pop(context); 149 | } 150 | }) 151 | } 152 | else 153 | {Navigator.pop(context)} 154 | }, 155 | ), 156 | title: Text(widget.item["title"]), 157 | actions: [buildPopumenuButton(context)], 158 | ); 159 | } 160 | 161 | buildPopumenuButton(cxt) { 162 | return PopupMenuButton( 163 | itemBuilder: (context) { 164 | return >[ 165 | PopupMenuItem( 166 | value: "goin", 167 | child: Text("进入栏目"), 168 | ), 169 | PopupMenuItem( 170 | value: "collect", 171 | child: Text(isCollectFlag ? "取消收藏" : "添加收藏"), 172 | ), 173 | PopupMenuItem( 174 | value: "share", 175 | child: Text("好友分享"), 176 | ), 177 | PopupMenuItem( 178 | value: "copy", 179 | child: Text("复制链接"), 180 | ), 181 | PopupMenuItem( 182 | value: "uploadError", 183 | child: Text("报告错误"), 184 | ), 185 | ]; 186 | }, 187 | onSelected: (value) { 188 | if (value == "copy") { 189 | Clipboard.setData(ClipboardData(text: widget.item["link"])); 190 | showToast(cxt, "复制成功~"); 191 | } else if (value == "share") { 192 | Share.share( 193 | '${widget.item["title"]} ${widget.item["link"]} 分享自[V聚焦]'); 194 | } else if (value == "collect") { 195 | isCollectFlag ? cancleCollect() : addCollect(); 196 | } else if (value == "uploadError") { 197 | Navigator.of(context) 198 | .push(MaterialPageRoute(builder: (BuildContext context) { 199 | return UploadError(widget.item["rss_id"]); 200 | })); 201 | } else if (value == "goin") { 202 | Navigator.of(context) 203 | .push(MaterialPageRoute(builder: (BuildContext context) { 204 | return RssDetail(widget.item["rss_id"]); 205 | })); 206 | } 207 | }, 208 | ); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /lib/pages/register.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | import '../widget/loadingButton.dart'; 3 | import '../utils/toast.dart'; 4 | import '../net/http.dart'; 5 | import '../widget/basestate.dart'; 6 | import '../utils/tools.dart'; 7 | 8 | class Register extends StatefulWidget { 9 | @override 10 | State createState() { 11 | return RegisterState(); 12 | } 13 | } 14 | 15 | class RegisterState extends BaseState { 16 | final form_key = GlobalKey(); 17 | 18 | String email, password, confirmPassword, intro, name; 19 | String sexStr = "男"; 20 | bool passwordHidden = true, confirmPasswordHidden = true; 21 | LoadingStatus loadingStatus = LoadingStatus.normal; 22 | var image; 23 | bool isDisableButton = false; 24 | 25 | var passwordController = TextEditingController(); 26 | var confirmPasswordController = TextEditingController(); 27 | 28 | register(context) async { 29 | try { 30 | if (form_key.currentState.validate()) { 31 | form_key.currentState.save(); 32 | if (isDisableButton) return; 33 | isDisableButton = true; 34 | setState(() { 35 | loadingStatus = LoadingStatus.loadding; 36 | }); 37 | //注册用户 38 | var params = { 39 | "email": email, 40 | "password": password, 41 | "sex": sexStr == "男" ? 1 : 0, 42 | "name": name, 43 | "avatar_url": "", 44 | "intro": "", 45 | }; 46 | var userResponse = await http2.post("/app/user/register", data: params); 47 | if (userResponse.data["code"] == 200) { 48 | showToast(context, "注册成功"); 49 | form_key.currentState.reset(); 50 | } else { 51 | showToast(context, userResponse.data["message"]); 52 | } 53 | } 54 | } catch (error) { 55 | print(error); 56 | showToast(context, "注册失败,请重试~"); 57 | } finally { 58 | setState(() { 59 | loadingStatus = LoadingStatus.normal; 60 | }); 61 | isDisableButton = false; 62 | } 63 | } 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return Scaffold( 68 | appBar: AppBar( 69 | title: Text("注册"), 70 | ), 71 | body: Builder(builder: (BuildContext context) { 72 | return Form( 73 | key: form_key, 74 | child: ListView( 75 | padding: EdgeInsets.symmetric(horizontal: 10), 76 | children: [ 77 | TextFormField( 78 | decoration: InputDecoration( 79 | labelText: "请输入昵称", 80 | suffixIcon: Icon(Icons.person, 81 | color: Theme.of(context).primaryColor), 82 | ), 83 | validator: (value) { 84 | if (value.length == 0) { 85 | return "昵称不能为空"; 86 | } 87 | }, 88 | onSaved: (value) { 89 | name = value; 90 | }, 91 | ), 92 | TextFormField( 93 | decoration: InputDecoration( 94 | labelText: "请输入邮箱", 95 | suffixIcon: Icon(Icons.email, 96 | color: Theme.of(context).primaryColor), 97 | ), 98 | validator: (value) { 99 | if (value.length == 0) { 100 | return "邮箱不能为空"; 101 | } else if (!isEmail(value)) { 102 | return "请输入合法的邮箱"; 103 | } 104 | }, 105 | onSaved: (value) { 106 | email = value; 107 | }, 108 | ), 109 | TextFormField( 110 | controller: passwordController, 111 | obscureText: passwordHidden, 112 | decoration: InputDecoration( 113 | labelText: "请输入密码", 114 | suffixIcon: IconButton( 115 | icon: Icon( 116 | Icons.remove_red_eye, 117 | color: passwordHidden 118 | ? Colors.grey 119 | : Theme.of(context).primaryColor, 120 | ), 121 | onPressed: () { 122 | setState(() { 123 | passwordHidden = !passwordHidden; 124 | }); 125 | }, 126 | ), 127 | ), 128 | validator: (value) { 129 | if (value.length == 0) { 130 | return "密码不能为空"; 131 | } else if (value != confirmPasswordController.text) { 132 | return "两次输入的密码不一致"; 133 | } 134 | }, 135 | onSaved: (value) { 136 | password = value; 137 | }, 138 | ), 139 | TextFormField( 140 | controller: confirmPasswordController, 141 | obscureText: confirmPasswordHidden, 142 | decoration: InputDecoration( 143 | labelText: "请再次输入密码", 144 | suffixIcon: IconButton( 145 | icon: Icon( 146 | Icons.remove_red_eye, 147 | color: confirmPasswordHidden 148 | ? Colors.grey 149 | : Theme.of(context).primaryColor, 150 | ), 151 | onPressed: () { 152 | setState(() { 153 | confirmPasswordHidden = !confirmPasswordHidden; 154 | }); 155 | }, 156 | ), 157 | ), 158 | validator: (value) { 159 | if (value.length == 0) { 160 | return "密码不能为空"; 161 | } else if (value != passwordController.text) { 162 | return "两次输入的密码不一致"; 163 | } 164 | }, 165 | onSaved: (value) { 166 | confirmPassword = value; 167 | }, 168 | ), 169 | Container( 170 | margin: EdgeInsets.only(top: 60, bottom: 60), 171 | child: Center( 172 | child: LoadingButton( 173 | "注册", 174 | onPressed: () { 175 | register(context); 176 | }, 177 | loadingStatus: loadingStatus, 178 | ), 179 | )), 180 | ], 181 | ), 182 | ); 183 | })); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /lib/pages/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/net/http.dart'; 2 | import 'package:app/pages/search.dart'; 3 | import 'package:app/utils/eventbus.dart'; 4 | import 'package:app/widget/networkImageWrapper.dart'; 5 | import 'package:app/widget/networkWrapper.dart'; 6 | import 'package:flutter/material.dart'; 7 | import './webview.dart'; 8 | import '../widget/basestate.dart'; 9 | import '../widget/refreshList.dart'; 10 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 11 | import 'dart:async'; 12 | 13 | class Index extends StatefulWidget { 14 | @override 15 | State createState() { 16 | return IndexState(); 17 | } 18 | } 19 | 20 | class IndexState extends BaseState { 21 | List datas = []; 22 | int pageIndex = 0, pageSize = 5; 23 | GlobalKey _easyRefreshKey = 24 | new GlobalKey(); 25 | GlobalKey _headerKey = 26 | new GlobalKey(); 27 | GlobalKey _footerKey = 28 | new GlobalKey(); 29 | bool firstRefresh = false; 30 | 31 | NetworkStatus currentNetwokStatus = NetworkStatus.Loading; 32 | 33 | StreamSubscription subscription; 34 | 35 | @override 36 | initState() { 37 | super.initState(); 38 | subscription = eventBus.on().listen((event) { 39 | if (event.action == EventAction.SUB_RSS_SUCCESS_ACTION) { 40 | requestHttp(); 41 | } 42 | }); 43 | requestHttp(); 44 | } 45 | 46 | requestHttp() async { 47 | if (pageIndex == 0 && currentNetwokStatus == NetworkStatus.Fail) { 48 | setState(() { 49 | currentNetwokStatus = NetworkStatus.Loading; 50 | }); 51 | } 52 | try { 53 | var params = {"offset": pageIndex * pageSize, "limit": pageSize}; 54 | var response = 55 | await http2.get("/app/user/index", queryParameters: params); 56 | if (response.data["code"] == 200) { 57 | setState(() { 58 | if (pageIndex == 0) { 59 | datas = response.data["data"]; 60 | currentNetwokStatus = response.data["data"].length == 0 61 | ? NetworkStatus.Empty 62 | : NetworkStatus.Success; 63 | } else { 64 | datas.addAll(response.data["data"]); 65 | } 66 | }); 67 | } else { 68 | if (pageIndex == 0) { 69 | setState(() { 70 | currentNetwokStatus = NetworkStatus.Fail; 71 | }); 72 | } 73 | } 74 | } catch (e) { 75 | print(e); 76 | if (pageIndex == 0) { 77 | setState(() { 78 | currentNetwokStatus = NetworkStatus.Fail; 79 | }); 80 | } 81 | } finally { 82 | // setState(() { 83 | // firstRefresh = false; 84 | // }); 85 | } 86 | } 87 | 88 | @override 89 | void dispose() { 90 | super.dispose(); 91 | if (subscription != null) { 92 | subscription.cancel(); 93 | } 94 | } 95 | 96 | Widget buildChildItem(item, index) { 97 | return GestureDetector( 98 | onTap: () { 99 | Navigator.of(context) 100 | .push(MaterialPageRoute(builder: (BuildContext context) { 101 | return WebViewPage(item); 102 | })); 103 | }, 104 | child: ListTile( 105 | dense: true, 106 | contentPadding: EdgeInsets.only(left: 0,right: 10), 107 | leading: Container( 108 | width: 48, 109 | child: Center( 110 | child: Container( 111 | width: 1, 112 | color: Colors.black26, 113 | ), 114 | ), 115 | ), 116 | title: Text( 117 | item["title"], 118 | maxLines: 2, 119 | overflow: TextOverflow.ellipsis, 120 | ), 121 | )); 122 | } 123 | 124 | Widget buildItem(context, index) { 125 | var item = datas[index]; 126 | if (index > 0 && datas[index - 1]["name"] == item["name"]) { 127 | return buildChildItem(item, index); 128 | } else { 129 | var name = item["name"]; 130 | var timespan = item["timespan"]; 131 | return Column( 132 | children: [ 133 | ListTile( 134 | contentPadding: EdgeInsets.only(left: 0,right: 10), 135 | leading: Container( 136 | width: 48, 137 | height: 48, 138 | child: Center( 139 | child: NetworkImageWrapper( 140 | item["logo"], 141 | width: 25, 142 | height: 25, 143 | ), 144 | ), 145 | ), 146 | title: Text( 147 | name, 148 | maxLines: 1, 149 | overflow: TextOverflow.ellipsis, 150 | ), 151 | trailing: Text(timespan)), 152 | buildChildItem(item, IndexedStack) 153 | ], 154 | ); 155 | } 156 | } 157 | 158 | Future onRefresh() async { 159 | pageIndex = 0; 160 | await requestHttp(); 161 | } 162 | 163 | Future onLoadMore() async { 164 | pageIndex++; 165 | await requestHttp(); 166 | } 167 | 168 | buildCustomEmptyWidget() { 169 | var screen = MediaQuery.of(context); 170 | return Container( 171 | width: screen.size.width, 172 | height: 173 | screen.size.height - screen.padding.top - screen.padding.bottom - 112, 174 | child: Column( 175 | mainAxisAlignment: MainAxisAlignment.center, 176 | crossAxisAlignment: CrossAxisAlignment.center, 177 | children: [ 178 | Text("你还没有订阅任何栏目"), 179 | Container( 180 | height: 5, 181 | ), 182 | Text("请去广场订阅") 183 | ], 184 | ), 185 | ); 186 | } 187 | 188 | @override 189 | Widget build(BuildContext context) { 190 | return Scaffold( 191 | appBar: AppBar( 192 | title: Text("V聚焦"), 193 | actions: [ 194 | IconButton( 195 | tooltip: "搜索", 196 | onPressed: () { 197 | Navigator.of(context) 198 | .push(MaterialPageRoute(builder: (BuildContext context) { 199 | return Search( 200 | type: 1, 201 | ); 202 | })); 203 | }, 204 | icon: Icon(Icons.search), 205 | ) 206 | ], 207 | ), 208 | body: NetworkWrapper( 209 | networkStatus: currentNetwokStatus, 210 | retryInit: requestHttp, 211 | emptyAndRefresh: true, 212 | child: RefreshList( 213 | emptyWidget: buildCustomEmptyWidget(), 214 | easyRefreshKey: _easyRefreshKey, 215 | headerKey: _headerKey, 216 | footerKey: _footerKey, 217 | firstRefresh: firstRefresh, 218 | onRefresh: onRefresh, 219 | onLoadmore: onLoadMore, 220 | child: ListView.builder( 221 | itemCount: datas.length, 222 | itemBuilder: buildItem, 223 | )))); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /lib/pages/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/utils/sp.dart'; 2 | import 'package:flutter/material.dart'; 3 | import './subscriptionIndex.dart'; 4 | import './me.dart'; 5 | import './index.dart'; 6 | import '../utils/eventbus.dart'; 7 | import '../utils/constant.dart'; 8 | import '../widget/basestate.dart'; 9 | import 'dart:async'; 10 | import './login.dart'; 11 | import 'package:dio/dio.dart'; 12 | import 'package:open_file/open_file.dart'; 13 | import '../net/http.dart'; 14 | import 'package:package_info/package_info.dart'; 15 | import 'package:path_provider/path_provider.dart'; 16 | import 'dart:io'; 17 | 18 | class MyHomePage extends StatefulWidget { 19 | MyHomePage({Key key, this.title}) : super(key: key); 20 | 21 | final String title; 22 | 23 | @override 24 | _MyHomePageState createState() => _MyHomePageState(); 25 | } 26 | 27 | class _MyHomePageState extends BaseState { 28 | final List items = [ 29 | BottomNavigationBarItem(title: Text("首页"), icon: Icon(Icons.home)), 30 | BottomNavigationBarItem(title: Text("广场"), icon: Icon(Icons.public)), 31 | BottomNavigationBarItem(title: Text("我的"), icon: Icon(Icons.portrait)), 32 | ]; 33 | int currentIndex = 0; 34 | List pages = [Index(), SubscriptionIndex(), Me()]; 35 | StreamSubscription subscription; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | subscription = eventBus.on().listen((event) { 41 | if (event.action == EventAction.INVALIDATE_TOKEN_ACTION) { 42 | showTokenInvalidateAlertDialog(); 43 | } 44 | }); 45 | 46 | Future.delayed(Duration(seconds: 3), () { 47 | checkApp(); 48 | }); 49 | } 50 | 51 | checkApp() async { 52 | var response = await http2.get("/config/config.json"); 53 | if (response != null && response.data != null) { 54 | if (Platform.isAndroid) { 55 | var androidNewVersion = response.data["android_new_version"]; 56 | var android_download_url = response.data["android_download_url"]; 57 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 58 | if (int.parse(androidNewVersion.toString()) > 59 | int.parse(packageInfo.buildNumber.toString())) { 60 | showDownloadAlertDialog(android_download_url); 61 | } 62 | } else if (Platform.isIOS) {} 63 | } 64 | } 65 | 66 | showDownloadAlertDialog(url) { 67 | showDialog( 68 | barrierDismissible: false, 69 | context: context, 70 | builder: (context2) { 71 | return AlertDialog( 72 | content: Text("发现最新版本,请立即更新"), 73 | actions: [ 74 | FlatButton( 75 | textColor: Theme.of(context).primaryColor, 76 | onPressed: () { 77 | Navigator.of(context).pop(); 78 | }, 79 | child: Text("取消"), 80 | ), 81 | FlatButton( 82 | textColor: Theme.of(context).primaryColor, 83 | onPressed: () { 84 | Navigator.of(context).pop(); 85 | startDownload(url); 86 | }, 87 | child: Text("确定"), 88 | ) 89 | ], 90 | ); 91 | }); 92 | } 93 | 94 | startDownload(url) async { 95 | try { 96 | Directory directory = await getExternalStorageDirectory(); 97 | Directory directory2 = Directory(directory.path + "/vjujiao"); 98 | var exists = await directory2.exists(); 99 | if (!exists) { 100 | await directory2.create(); 101 | } 102 | File file = File(directory2.path + "/vjujiao.apk"); 103 | var response = await Dio().get(url, 104 | onReceiveProgress: (int count, int total) { 105 | print("download progress &count $total"); 106 | }, 107 | options: Options( 108 | responseType: ResponseType.bytes, followRedirects: false)); 109 | file.createSync(); 110 | var raf = file.openSync(mode: FileMode.write); 111 | raf.writeFromSync(response.data); 112 | await raf.close(); 113 | print("download -success"); 114 | await OpenFile.open(file.path); 115 | } catch (e) { 116 | print("download error $e"); 117 | } 118 | } 119 | 120 | @override 121 | void dispose() { 122 | super.dispose(); 123 | if (subscription != null) { 124 | subscription.cancel(); 125 | } 126 | } 127 | 128 | showTokenInvalidateAlertDialog() { 129 | if (Constant.showTokenTipsDialog) return; 130 | Constant.showTokenTipsDialog = true; 131 | showDialog( 132 | barrierDismissible: false, 133 | context: context, 134 | builder: (context2) { 135 | return AlertDialog( 136 | content: Text("你的token已过期,请重新登录"), 137 | actions: [ 138 | FlatButton( 139 | textColor: Theme.of(context).primaryColor, 140 | onPressed: () async { 141 | await putStr(Constant.TOKEN, ""); 142 | await Navigator.of(context).pop(); 143 | await Navigator.of(context).pushAndRemoveUntil( 144 | MaterialPageRoute(builder: (BuildContext context) { 145 | return Login(); 146 | }), (router) => false); 147 | Constant.showTokenTipsDialog = false; 148 | }, 149 | child: Text("确定"), 150 | ) 151 | ], 152 | ); 153 | }); 154 | } 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | // This method is rerun every time setState is called, for instance as done 159 | // by the _incrementCounter method above. 160 | // 161 | // The Flutter framework has been optimized to make rerunning build methods 162 | // fast, so that you can just rebuild anything that needs updating rather 163 | // than having to individually change instances of widgets. 164 | return Scaffold( 165 | bottomNavigationBar: BottomNavigationBar( 166 | items: items, 167 | currentIndex: currentIndex, 168 | // selectedItemColor: Theme.of(context).primaryColor, 169 | onTap: (index) { 170 | setState(() { 171 | currentIndex = index; 172 | }); 173 | }, 174 | ), 175 | body: WillPopScope( 176 | onWillPop: _onWillPop, 177 | child: IndexedStack(index: currentIndex, children: pages)), 178 | ); 179 | } 180 | 181 | Future _onWillPop() { 182 | return showDialog( 183 | context: context, 184 | builder: (context) => new AlertDialog( 185 | title: new Text('提示:'), 186 | content: new Text('你确定要退出app吗'), 187 | actions: [ 188 | new FlatButton( 189 | onPressed: () => Navigator.of(context).pop(false), 190 | child: new Text('取消'), 191 | ), 192 | new FlatButton( 193 | onPressed: () => Navigator.of(context).pop(true), 194 | child: new Text('确认'), 195 | ), 196 | ], 197 | ), 198 | ) ?? 199 | false; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/pages/rsslist.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/movewebview.dart'; 2 | import 'package:app/widget/networkImageWrapper.dart'; 3 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 4 | import 'package:flutter/material.dart'; 5 | import '../net/http.dart'; 6 | import './rssdetail.dart'; 7 | import '../widget/basestate.dart'; 8 | import '../utils/toast.dart'; 9 | import '../widget/refreshList.dart'; 10 | import '../utils/eventbus.dart'; 11 | import 'dart:async'; 12 | import '../widget/networkWrapper.dart'; 13 | 14 | class RssList extends StatefulWidget { 15 | String name; 16 | int tag_id; 17 | RssList(this.name, this.tag_id); 18 | 19 | @override 20 | State createState() { 21 | return RssListState(); 22 | } 23 | } 24 | 25 | class RssListState extends BaseState { 26 | List datas = []; 27 | int pageIndex = 0, pageSize = 20; 28 | GlobalKey _easyRefreshKey = 29 | new GlobalKey(); 30 | GlobalKey _headerKey = 31 | new GlobalKey(); 32 | GlobalKey _footerKey = 33 | new GlobalKey(); 34 | bool firstRefresh = false; 35 | StreamSubscription subscription; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | subscription = eventBus.on().listen((eventAction) { 41 | var id = eventAction.data; 42 | datas.forEach((item) { 43 | if (item["rss_id"] == id) { 44 | if (eventAction.action == EventAction.SUB_RSS_SUCCESS_ACTION) { 45 | item["hasSub"] = 1; 46 | } else if (eventAction.action == 47 | EventAction.UNSUB_RSS_SUCCESS_ACTION) { 48 | item["hasSub"] = 0; 49 | } 50 | } 51 | }); 52 | setState(() {}); 53 | }); 54 | 55 | requestHttp(); 56 | } 57 | 58 | @override 59 | void dispose() { 60 | super.dispose(); 61 | if (subscription != null) { 62 | subscription.cancel(); 63 | } 64 | } 65 | 66 | subRss(context, id) async { 67 | try { 68 | var params = {"id": id}; 69 | var response = await http2.post("/app/user/rss/subRss", data: params); 70 | if (response.data["code"] == 200) { 71 | showToast(context, "订阅成功"); 72 | eventBus.fire(EventAction(EventAction.SUB_RSS_SUCCESS_ACTION, id)); 73 | } else { 74 | showToast(context, "订阅失败"); 75 | } 76 | } catch (e) { 77 | print(e); 78 | showToast(context, "订阅失败"); 79 | } 80 | } 81 | 82 | unsubRss(context, id) async { 83 | try { 84 | var params = {"id": id}; 85 | var response = await http2.post("/app/user/rss/unSubRss", data: params); 86 | if (response.data["code"] == 200) { 87 | showToast(context, "取消订阅成功"); 88 | eventBus.fire(EventAction(EventAction.UNSUB_RSS_SUCCESS_ACTION, id)); 89 | } else { 90 | showToast(context, "取消订阅失败"); 91 | } 92 | } catch (e) { 93 | print(e); 94 | showToast(context, "取消订阅失败"); 95 | } 96 | } 97 | 98 | NetworkStatus currentNetwokStatus = NetworkStatus.Loading; 99 | requestHttp() async { 100 | if (pageIndex == 0 && currentNetwokStatus == NetworkStatus.Fail) { 101 | setState(() { 102 | currentNetwokStatus = NetworkStatus.Loading; 103 | }); 104 | } 105 | try { 106 | var params = { 107 | "offset": pageIndex * pageSize, 108 | "limit": pageSize, 109 | "tag_id": widget.tag_id 110 | }; 111 | var response = 112 | await http2.get("/app/user/tag/rss", queryParameters: params); 113 | if (response.data["code"] == 200) { 114 | setState(() { 115 | if (pageIndex == 0) { 116 | datas = response.data["data"]; 117 | currentNetwokStatus = response.data["data"].length == 0 118 | ? NetworkStatus.Empty 119 | : NetworkStatus.Success; 120 | } else { 121 | datas.addAll(response.data["data"]); 122 | } 123 | }); 124 | } else { 125 | if (pageIndex == 0) { 126 | setState(() { 127 | currentNetwokStatus = NetworkStatus.Fail; 128 | }); 129 | } 130 | } 131 | } catch (e) { 132 | print(e); 133 | if (pageIndex == 0) { 134 | setState(() { 135 | currentNetwokStatus = NetworkStatus.Fail; 136 | }); 137 | } 138 | } finally { 139 | // setState(() { 140 | // firstRefresh = false; 141 | // }); 142 | } 143 | } 144 | 145 | Widget buildItem(context, index) { 146 | var item = datas[index]; 147 | String tag = 'rss_image2_${item["name"]}'; 148 | bool hasSub = item["hasSub"] == 1; 149 | 150 | return Column( 151 | children: [ 152 | ListTile( 153 | onTap: () { 154 | if (item["type"] == 1) { 155 | Navigator.of(context) 156 | .push(MaterialPageRoute(builder: (BuildContext context) { 157 | return RssDetail( 158 | item["rss_id"], 159 | tag: tag, 160 | ); 161 | })); 162 | } else { 163 | Navigator.of(context) 164 | .push(MaterialPageRoute(builder: (BuildContext context) { 165 | return MoveWebview( 166 | item["rss_url"], 167 | item["name"], 168 | ); 169 | })); 170 | } 171 | }, 172 | dense: false, 173 | leading: Hero( 174 | tag: tag, 175 | child: NetworkImageWrapper( 176 | item["logo"], 177 | width: 25, 178 | height: 25, 179 | ), 180 | ), 181 | trailing: InkResponse( 182 | onTap: () { 183 | hasSub 184 | ? unsubRss(context, item["rss_id"]) 185 | : subRss(context, item["rss_id"]); 186 | }, 187 | child: Icon(hasSub ? Icons.favorite : Icons.favorite_border, 188 | color: hasSub ? Theme.of(context).primaryColor : Colors.black), 189 | ), 190 | title: Text(item["name"]), 191 | ), 192 | Divider( 193 | height: 1, 194 | ) 195 | ], 196 | ); 197 | } 198 | 199 | Future onRefresh() async { 200 | pageIndex = 0; 201 | await requestHttp(); 202 | } 203 | 204 | Future onLoadMore() async { 205 | pageIndex++; 206 | await requestHttp(); 207 | } 208 | 209 | @override 210 | Widget build(BuildContext context) { 211 | return Scaffold( 212 | appBar: AppBar( 213 | title: Text(widget.name), 214 | ), 215 | body: NetworkWrapper( 216 | networkStatus: currentNetwokStatus, 217 | retryInit: requestHttp, 218 | child: RefreshList( 219 | easyRefreshKey: _easyRefreshKey, 220 | headerKey: _headerKey, 221 | footerKey: _footerKey, 222 | firstRefresh: firstRefresh, 223 | onRefresh: onRefresh, 224 | onLoadmore: onLoadMore, 225 | child: ListView.builder( 226 | itemCount: datas.length, 227 | itemBuilder: buildItem, 228 | )), 229 | ), 230 | ); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /lib/pages/login.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/home.dart'; 2 | import 'package:app/utils/sp.dart'; 3 | import 'package:app/utils/tools.dart'; 4 | import "package:flutter/material.dart"; 5 | import '../widget/loadingButton.dart'; 6 | import '../utils/constant.dart'; 7 | import '../net/http.dart'; 8 | import 'dart:convert'; 9 | import '../widget/basestate.dart'; 10 | import '../utils/toast.dart'; 11 | import './register.dart'; 12 | import './resetPassword.dart'; 13 | import 'package:flare_flutter/flare_actor.dart'; 14 | 15 | class Login extends StatefulWidget { 16 | @override 17 | State createState() { 18 | return LoginState(); 19 | } 20 | } 21 | 22 | class LoginState extends BaseState { 23 | var form_key = GlobalKey(); 24 | 25 | String email, password; 26 | var passwordHidden = true; 27 | var loadingStatus = LoadingStatus.normal; 28 | bool isDisableButton = false; 29 | final animationName = ["idle", "test", "success", "fail"]; 30 | String currentAnimationName = "idle"; 31 | 32 | login(context) async { 33 | try { 34 | if (form_key.currentState.validate()) { 35 | form_key.currentState.save(); 36 | if (isDisableButton) return; 37 | isDisableButton = true; 38 | setState(() { 39 | loadingStatus = LoadingStatus.loadding; 40 | currentAnimationName = "idle"; 41 | 42 | }); 43 | var params = {"email": email, "password": password}; 44 | var response = await http2.post("/app/user/login", data: params); 45 | if (response.data["code"] == 200) { 46 | await putStr(Constant.USER_INFO, jsonEncode(response.data["data"])); 47 | await putStr(Constant.TOKEN, response.data["data"]["token"]); 48 | authorization(token: response.data["data"]["token"]); 49 | 50 | setState(() { 51 | currentAnimationName = "success"; 52 | }); 53 | await Future.delayed(Duration(seconds: 3), () {}); 54 | Navigator.of(context).pushAndRemoveUntil( 55 | MaterialPageRoute(builder: (BuildContext context) { 56 | return MyHomePage(); 57 | }), (route) => route == null); 58 | } else { 59 | setState(() { 60 | currentAnimationName = "fail"; 61 | }); 62 | } 63 | } else { 64 | setState(() { 65 | currentAnimationName = "fail"; 66 | }); 67 | } 68 | } catch (error) { 69 | print(error); 70 | setState(() { 71 | currentAnimationName = "fail"; 72 | }); 73 | } finally { 74 | setState(() { 75 | loadingStatus = LoadingStatus.normal; 76 | }); 77 | isDisableButton = false; 78 | } 79 | } 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return Scaffold( 84 | backgroundColor: Color.fromRGBO(170, 207, 212, 1), 85 | body: Form( 86 | onChanged: () { 87 | setState(() { 88 | if (currentAnimationName == "fail") { 89 | currentAnimationName = "idle"; 90 | } else { 91 | currentAnimationName = "test"; 92 | } 93 | }); 94 | }, 95 | key: form_key, 96 | child: ListView( 97 | padding: EdgeInsets.only( 98 | left: 15, 99 | right: 15, 100 | ), 101 | children: [ 102 | Container( 103 | margin: EdgeInsets.only( 104 | top: MediaQuery.of(context).padding.top + 50), 105 | height: 200, 106 | child: FlareActor("flrs/teddy.flr", 107 | animation: currentAnimationName, 108 | fit: BoxFit.contain, 109 | callback: (animationName) {}), 110 | ), 111 | Card( 112 | elevation: 0, 113 | child: Container( 114 | padding: EdgeInsets.all(10), 115 | child: Column( 116 | children: [ 117 | TextFormField( 118 | decoration: InputDecoration( 119 | labelText: "请输入邮箱", 120 | ), 121 | validator: (value) { 122 | if (value.length == 0) { 123 | return "邮箱不能为空"; 124 | } else if (!isEmail(value)) { 125 | return "请输入合法的邮箱"; 126 | } 127 | }, 128 | onSaved: (value) { 129 | email = value; 130 | }, 131 | ), 132 | TextFormField( 133 | obscureText: passwordHidden, 134 | decoration: InputDecoration( 135 | labelText: "请输入密码", 136 | suffixIcon: IconButton( 137 | icon: Icon( 138 | Icons.remove_red_eye, 139 | color: passwordHidden 140 | ? Colors.grey 141 | : Theme.of(context).primaryColor, 142 | ), 143 | onPressed: () { 144 | setState(() { 145 | passwordHidden = !passwordHidden; 146 | }); 147 | }, 148 | ), 149 | ), 150 | validator: (value) { 151 | if (value.length == 0) { 152 | return "密码不能为空"; 153 | } 154 | }, 155 | onSaved: (value) { 156 | password = value; 157 | }, 158 | ), 159 | Container( 160 | height: 20, 161 | ), 162 | Row( 163 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 164 | children: [ 165 | InkResponse( 166 | onTap: () { 167 | Navigator.of(context).push(MaterialPageRoute( 168 | builder: (BuildContext context) { 169 | return Register(); 170 | })); 171 | }, 172 | child: Text("立即注册"), 173 | ), 174 | InkResponse( 175 | onTap: () { 176 | Navigator.of(context).push(MaterialPageRoute( 177 | builder: (BuildContext context) { 178 | return ResetPassword(); 179 | })); 180 | }, 181 | child: Text("找回密码"), 182 | ), 183 | ]), 184 | Container( 185 | height: 10, 186 | ), 187 | ], 188 | ), 189 | ), 190 | ), 191 | Container( 192 | margin: EdgeInsets.only(top: 30, bottom: 60), 193 | alignment: Alignment.center, 194 | child: LoadingButton( 195 | "登陆", 196 | onPressed: () { 197 | login(context); 198 | }, 199 | loadingStatus: loadingStatus, 200 | ), 201 | ) 202 | ], 203 | ), 204 | )); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/pages/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/pages/applyRSS.dart'; 2 | import 'package:app/pages/movewebview.dart'; 3 | import 'package:app/pages/rssdetail.dart'; 4 | import 'package:app/pages/webview.dart'; 5 | import 'package:app/widget/networkImageWrapper.dart'; 6 | import 'package:flutter/material.dart'; 7 | import '../widget/refreshList.dart'; 8 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 9 | import '../net/http.dart'; 10 | 11 | class Search extends StatefulWidget { 12 | int type; //1文章 2rss栏目 13 | Search({this.type = 2}); 14 | 15 | @override 16 | State createState() { 17 | return SearchState(); 18 | } 19 | } 20 | 21 | class SearchState extends State { 22 | List datas = []; 23 | int pageIndex = 0, pageSize = 20; 24 | GlobalKey _easyRefreshKey = 25 | new GlobalKey(); 26 | GlobalKey _headerKey = 27 | new GlobalKey(); 28 | GlobalKey _footerKey = 29 | new GlobalKey(); 30 | bool firstRefresh = false; 31 | TextEditingController textEditingController = TextEditingController(); 32 | String lastText = ""; 33 | 34 | startSearchRSS() async { 35 | var params = { 36 | "name": textEditingController.text, 37 | "offset": pageIndex * pageSize, 38 | "limit": pageSize 39 | }; 40 | var response = await http2.post("/app/user/rss/search", data: params); 41 | if (response.data["code"] == 200) { 42 | setState(() { 43 | if (pageIndex == 0) { 44 | datas = response.data["data"]; 45 | } else { 46 | datas.addAll(response.data["data"]); 47 | } 48 | }); 49 | } 50 | } 51 | 52 | startSearchArt() async { 53 | print(textEditingController.text); 54 | var params = { 55 | "name": textEditingController.text, 56 | "offset": pageIndex * pageSize, 57 | "limit": pageSize 58 | }; 59 | var response = 60 | await http2.post("/app/user/rss/backup/search", data: params); 61 | if (response.data["code"] == 200) { 62 | setState(() { 63 | if (pageIndex == 0) { 64 | datas = response.data["data"]; 65 | } else { 66 | datas.addAll(response.data["data"]); 67 | } 68 | }); 69 | } 70 | } 71 | 72 | Future onRefresh() async { 73 | pageIndex = 0; 74 | await widget.type == 1 ? startSearchArt() : startSearchRSS(); 75 | } 76 | 77 | Future onLoadMore() async { 78 | pageIndex++; 79 | await widget.type == 1 ? startSearchArt() : startSearchRSS(); 80 | } 81 | 82 | Widget buildItem(context, index) { 83 | var item = datas[index]; 84 | if (widget.type == 1) { 85 | return Column( 86 | children: [ 87 | ListTile( 88 | dense: false, 89 | onTap: () { 90 | item["rss_id"] = item["target_rss_id"]; 91 | Navigator.of(context) 92 | .push(MaterialPageRoute(builder: (BuildContext context) { 93 | return WebViewPage(item); 94 | })); 95 | }, 96 | title: Text(item["title"]), 97 | ), 98 | Divider( 99 | height: 1, 100 | ) 101 | ], 102 | ); 103 | } else { 104 | return Column( 105 | children: [ 106 | ListTile( 107 | onTap: () async { 108 | if (item["type"] == 1) { 109 | Navigator.of(context) 110 | .push(MaterialPageRoute(builder: (BuildContext context) { 111 | return RssDetail( 112 | item["rss_id"], 113 | tag: "tag", 114 | ); 115 | })); 116 | } else { 117 | Navigator.of(context) 118 | .push(MaterialPageRoute(builder: (BuildContext context) { 119 | return MoveWebview( 120 | item["rss_url"], 121 | item["name"], 122 | ); 123 | })); 124 | } 125 | }, 126 | leading: NetworkImageWrapper( 127 | item["logo"], 128 | width: 25, 129 | height: 25, 130 | ), 131 | title: Text(item["name"]), 132 | ), 133 | Divider( 134 | height: 1, 135 | ) 136 | ], 137 | ); 138 | } 139 | } 140 | 141 | @override 142 | Widget build(BuildContext context) { 143 | return Scaffold( 144 | backgroundColor: Colors.white, 145 | appBar: AppBar( 146 | leading: IconButton( 147 | onPressed: () { 148 | Navigator.of(context).pop(); 149 | }, 150 | icon: Icon( 151 | Icons.arrow_back, 152 | color: Colors.black, 153 | ), 154 | ), 155 | backgroundColor: Colors.white, 156 | title: TextField( 157 | autofocus: true, 158 | cursorColor: Colors.black, 159 | style: TextStyle(color: Colors.black), 160 | controller: textEditingController, 161 | textInputAction: TextInputAction.search, 162 | onSubmitted: (String value) { 163 | if (value.isEmpty || value == lastText) return; 164 | lastText = value; 165 | pageIndex = 0; 166 | widget.type == 1 ? startSearchArt() : startSearchRSS(); 167 | }, 168 | decoration: InputDecoration( 169 | border: InputBorder.none, 170 | hintText: "Search", 171 | hintStyle: TextStyle(color: Colors.grey), 172 | )), 173 | actions: [ 174 | IconButton( 175 | onPressed: () { 176 | textEditingController.clear(); 177 | }, 178 | icon: Icon( 179 | Icons.close, 180 | color: Colors.black, 181 | ), 182 | ) 183 | ], 184 | ), 185 | body: RefreshList( 186 | easyRefreshKey: _easyRefreshKey, 187 | headerKey: _headerKey, 188 | footerKey: _footerKey, 189 | firstRefresh: firstRefresh, 190 | emptyWidget: widget.type == 1 ? buildArtEmpty() : buildRssEmpty(), 191 | onRefresh: onRefresh, 192 | onLoadmore: onLoadMore, 193 | child: ListView.builder( 194 | itemCount: datas.length, 195 | itemBuilder: buildItem, 196 | )), 197 | ); 198 | } 199 | 200 | buildArtEmpty() { 201 | return Container( 202 | height: MediaQuery.of(context).size.height, 203 | padding: EdgeInsets.only(top: 200), 204 | child: Column( 205 | children: [ 206 | Text( 207 | "在这里你可以搜索你想看的文章哦~", 208 | style: TextStyle(color: Colors.black), 209 | ) 210 | ], 211 | )); 212 | } 213 | 214 | buildRssEmpty() { 215 | return Container( 216 | height: MediaQuery.of(context).size.height, 217 | padding: EdgeInsets.only(top: 200), 218 | child: Column( 219 | mainAxisAlignment: MainAxisAlignment.start, 220 | crossAxisAlignment: CrossAxisAlignment.center, 221 | children: [ 222 | Text( 223 | "没有栏目,可以点击申请哦~", 224 | style: TextStyle(color: Colors.black), 225 | ), 226 | Container( 227 | margin: EdgeInsets.only(top: 10), 228 | child: RaisedButton( 229 | color: Colors.black, 230 | onPressed: () { 231 | Navigator.of(context).push( 232 | MaterialPageRoute(builder: (BuildContext context) { 233 | return ApplyRSS(); 234 | })); 235 | }, 236 | child: Text( 237 | "申请栏目", 238 | style: TextStyle(fontSize: 15, color: Colors.white), 239 | ), 240 | )) 241 | ]), 242 | ); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /lib/pages/rssdetail.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/utils/constant.dart'; 2 | import 'package:app/widget/networkImageWrapper.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../net/http.dart'; 5 | import './webview.dart'; 6 | import '../widget/basestate.dart'; 7 | import 'package:flutter_easyrefresh/easy_refresh.dart'; 8 | import '../utils/eventbus.dart'; 9 | import '../utils/toast.dart'; 10 | import '../widget/networkWrapper.dart'; 11 | 12 | class RssDetail extends StatefulWidget { 13 | int rss_id; 14 | String tag; 15 | RssDetail(this.rss_id, {this.tag = "rss_image"}); 16 | 17 | @override 18 | State createState() { 19 | return RssDetailState(); 20 | } 21 | } 22 | 23 | class RssDetailState extends BaseState with HeaderListener { 24 | dynamic rssinfo = Constant.defaultRssinfo; 25 | 26 | List datas = []; 27 | int pageIndex = 0, pageSize = 20; 28 | GlobalKey _easyRefreshKey = 29 | new GlobalKey(); 30 | GlobalKey _headerKey = 31 | new GlobalKey(); 32 | GlobalKey _footerKey = 33 | new GlobalKey(); 34 | 35 | subRss(context) async { 36 | try { 37 | var params = {"id": widget.rss_id}; 38 | var response = await http2.post("/app/user/rss/subRss", data: params); 39 | if (response.data["code"] == 200) { 40 | setState(() { 41 | rssinfo["hasSub"] = 1; 42 | }); 43 | showToast(context, "订阅成功"); 44 | eventBus.fire( 45 | EventAction(EventAction.SUB_RSS_SUCCESS_ACTION, widget.rss_id)); 46 | } else { 47 | showToast(context, "订阅失败"); 48 | } 49 | } catch (e) { 50 | print(e); 51 | showToast(context, "订阅失败"); 52 | } 53 | } 54 | 55 | unsubRss(context) async { 56 | try { 57 | var params = {"id": widget.rss_id}; 58 | var response = await http2.post("/app/user/rss/unSubRss", data: params); 59 | if (response.data["code"] == 200) { 60 | setState(() { 61 | rssinfo["hasSub"] = 0; 62 | }); 63 | showToast(context, "取消订阅成功"); 64 | eventBus.fire( 65 | EventAction(EventAction.UNSUB_RSS_SUCCESS_ACTION, widget.rss_id)); 66 | } else { 67 | showToast(context, "取消订阅失败"); 68 | } 69 | } catch (e) { 70 | print(e); 71 | showToast(context, "取消订阅失败"); 72 | } 73 | } 74 | 75 | NetworkStatus currentNetwokStatus = NetworkStatus.Loading; 76 | requestHttp() async { 77 | if (pageIndex == 0 && currentNetwokStatus == NetworkStatus.Fail) { 78 | setState(() { 79 | currentNetwokStatus = NetworkStatus.Loading; 80 | }); 81 | } 82 | try { 83 | var params = { 84 | "rss_id": widget.rss_id, 85 | "offset": pageIndex * pageSize, 86 | "limit": pageSize 87 | }; 88 | var response = 89 | await http2.get("/app/user/rss/list", queryParameters: params); 90 | if (response.data["code"] == 200) { 91 | setState(() { 92 | if (pageIndex == 0) { 93 | datas = response.data["data"]; 94 | currentNetwokStatus = response.data["data"].length == 0 95 | ? NetworkStatus.Empty 96 | : NetworkStatus.Success; 97 | } else { 98 | datas.addAll(response.data["data"]); 99 | } 100 | }); 101 | } else { 102 | if (pageIndex == 0) { 103 | setState(() { 104 | currentNetwokStatus = NetworkStatus.Fail; 105 | }); 106 | } 107 | } 108 | } catch (e) { 109 | print(e); 110 | if (pageIndex == 0) { 111 | setState(() { 112 | currentNetwokStatus = NetworkStatus.Fail; 113 | }); 114 | } 115 | } 116 | } 117 | 118 | requestRssinfo() async { 119 | try { 120 | var params = {"rss_id": widget.rss_id}; 121 | var response = 122 | await http2.get("/app/user/rss/info", queryParameters: params); 123 | if (response.data["code"] == 200 && response.data["data"].length > 0) { 124 | setState(() { 125 | rssinfo = response.data["data"][0]; 126 | }); 127 | } 128 | } catch (e) { 129 | print(e); 130 | } 131 | } 132 | 133 | @override 134 | void initState() { 135 | super.initState(); 136 | requestRssinfo(); 137 | requestHttp(); 138 | } 139 | 140 | @override 141 | void dispose() { 142 | super.dispose(); 143 | } 144 | 145 | Widget buildSliverAppBar(context, innerBoxIsScrolled) { 146 | bool hasSub = rssinfo["hasSub"] == 1; 147 | return SliverAppBar( 148 | floating: false, 149 | pinned: true, 150 | snap: false, 151 | expandedHeight: 200, 152 | actions: [ 153 | Center( 154 | child: Container( 155 | margin: EdgeInsets.only(right: 10.0), 156 | width: 24.0, 157 | height: 24.0, 158 | child: CircularProgressIndicator( 159 | value: _indicatorValue, 160 | valueColor: AlwaysStoppedAnimation(Colors.white), 161 | strokeWidth: 2.4, 162 | ), 163 | )) 164 | ], 165 | flexibleSpace: FlexibleSpaceBar( 166 | centerTitle: true, 167 | collapseMode: CollapseMode.pin, 168 | title: Text( 169 | rssinfo["name"], 170 | maxLines: 1, 171 | overflow: TextOverflow.ellipsis, 172 | style: TextStyle(color: Colors.white), 173 | ), 174 | background: Stack( 175 | children: [ 176 | Positioned( 177 | top: 0, 178 | bottom: 0, 179 | left: 0, 180 | right: 0, 181 | child: Image.asset( 182 | "assets/timg.jpg", 183 | fit: BoxFit.cover, 184 | ), 185 | ), 186 | Positioned( 187 | left: 0, 188 | right: 0, 189 | top: 0, 190 | bottom: 0, 191 | child: Column( 192 | mainAxisAlignment: MainAxisAlignment.center, 193 | crossAxisAlignment: CrossAxisAlignment.center, 194 | children: [ 195 | Hero( 196 | tag: widget.tag, 197 | child: NetworkImageWrapper( 198 | rssinfo["logo"], 199 | width: 50, 200 | height: 50, 201 | ), 202 | ), 203 | Container(height: 10), 204 | RaisedButton( 205 | textColor: Colors.white, 206 | onPressed: () { 207 | hasSub ? unsubRss(context) : subRss(context); 208 | }, 209 | child: Text(hasSub ? "取消订阅" : "订阅"), 210 | ) 211 | ], 212 | ), 213 | ) 214 | ], 215 | ))); 216 | } 217 | 218 | Widget buildItem(context, index) { 219 | var item = datas[index]; 220 | return Column( 221 | children: [ 222 | ListTile( 223 | onTap: () { 224 | item["rss_id"] = widget.rss_id; 225 | Navigator.of(context) 226 | .push(MaterialPageRoute(builder: (BuildContext context) { 227 | return WebViewPage(item); 228 | })); 229 | }, 230 | dense: false, 231 | title: Text( 232 | item["title"], 233 | maxLines: 2, 234 | overflow: TextOverflow.ellipsis, 235 | ), 236 | leading: Container( 237 | width: 48, 238 | height: 48, 239 | child: Center( 240 | child: Text( 241 | (index + 1).toString(), 242 | style: TextStyle(fontWeight: FontWeight.bold), 243 | ), 244 | ), 245 | ), 246 | ), 247 | Divider( 248 | height: 1, 249 | ) 250 | ], 251 | ); 252 | } 253 | 254 | Future onRefresh() async { 255 | pageIndex = 0; 256 | await Future.delayed(Duration(seconds: 2), () {}); 257 | await requestHttp(); 258 | } 259 | 260 | Future onLoadMore() async { 261 | pageIndex++; 262 | await requestHttp(); 263 | } 264 | 265 | @override 266 | Widget build(BuildContext context) { 267 | return Scaffold(body: Builder(builder: (context) { 268 | return EasyRefresh( 269 | key: _easyRefreshKey, 270 | refreshHeader: ListenerHeader( 271 | key: _headerKey, 272 | refreshHeight: _refreshHeight, 273 | listener: this, 274 | ), 275 | refreshFooter: ClassicsFooter( 276 | key: _footerKey, 277 | loadText: "加载更多", 278 | loadReadyText: "松开加载", 279 | loadingText: "加载中...", 280 | loadedText: "加载完成", 281 | noMoreText: "没有更多", 282 | moreInfo: "上次加载 %T", 283 | bgColor: Colors.transparent, 284 | textColor: Colors.black87, 285 | moreInfoColor: Colors.black54, 286 | showMore: true, 287 | ), 288 | onRefresh: onRefresh, 289 | loadMore: onLoadMore, 290 | child: new CustomScrollView( 291 | // 手动维护semanticChildCount,用于判断是否没有更多数据 292 | semanticChildCount: datas.length, 293 | slivers: [ 294 | buildSliverAppBar(context, true), 295 | SliverPadding(padding: EdgeInsets.only(top: 10), sliver: buildBody()) 296 | ], 297 | )); 298 | })); 299 | } 300 | 301 | buildBody() { 302 | if (currentNetwokStatus == NetworkStatus.Success) { 303 | return SliverFixedExtentList( 304 | itemExtent: 57, 305 | delegate: SliverChildBuilderDelegate( 306 | buildItem, 307 | childCount: datas.length, 308 | )); 309 | } else { 310 | return SliverFillRemaining( 311 | child: Center( 312 | child: NetworkWrapper( 313 | networkStatus: currentNetwokStatus, 314 | retryInit: () {}, 315 | child: Text(""), 316 | ), 317 | ), 318 | ); 319 | } 320 | } 321 | 322 | double _refreshHeight = 100.0; 323 | double _indicatorValue = 0.0; 324 | bool _updateIndicatorValue = false; 325 | @override 326 | void onRefreshEnd() { 327 | setState(() { 328 | _indicatorValue = 0.0; 329 | }); 330 | } 331 | 332 | @override 333 | void onRefreshReady() {} 334 | 335 | @override 336 | void onRefreshRestore() {} 337 | 338 | @override 339 | void onRefreshStart() { 340 | _updateIndicatorValue = true; 341 | } 342 | 343 | @override 344 | void onRefreshed() { 345 | setState(() { 346 | _indicatorValue = 0.99; 347 | }); 348 | } 349 | 350 | @override 351 | void onRefreshing() { 352 | _updateIndicatorValue = false; 353 | setState(() { 354 | _indicatorValue = null; 355 | }); 356 | } 357 | 358 | var ratio = 1.0; 359 | 360 | @override 361 | void updateHeaderHeight(double newHeight) { 362 | if (_updateIndicatorValue) { 363 | double indicatorValue = newHeight / _refreshHeight * ratio; 364 | setState(() { 365 | _indicatorValue = indicatorValue < ratio ? indicatorValue : ratio; 366 | }); 367 | } 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /lib/pages/resetPassword.dart: -------------------------------------------------------------------------------- 1 | import 'package:app/utils/sp.dart'; 2 | import "package:flutter/material.dart"; 3 | import '../widget/loadingButton.dart'; 4 | import '../net/http.dart'; 5 | import '../utils/constant.dart'; 6 | import '../utils/toast.dart'; 7 | import '../utils/tools.dart'; 8 | import '../widget/basestate.dart'; 9 | import './login.dart'; 10 | 11 | class ResetPassword extends StatefulWidget { 12 | String email; 13 | ResetPassword({this.email = ""}); 14 | 15 | @override 16 | State createState() { 17 | return ResetPasswordState(); 18 | } 19 | } 20 | 21 | class ResetPasswordState extends BaseState { 22 | final form_key = GlobalKey(); 23 | 24 | String email, code, password, confirmPassword; 25 | var passwordHidden = true, confirmPasswordHidden = true; 26 | var loadingStatus = LoadingStatus.normal; 27 | var sendButtonLoadingStatus = LoadingStatus.normal; 28 | var passwordController = TextEditingController(); 29 | var confirmPasswordController = TextEditingController(); 30 | var emailController; 31 | bool isDisableEmailButton = false; 32 | bool isDisableResetButton = false; 33 | 34 | @override 35 | initState() { 36 | super.initState(); 37 | emailController = TextEditingController(text: widget.email); 38 | } 39 | 40 | resetPassword(context) async { 41 | try { 42 | if (form_key.currentState.validate()) { 43 | form_key.currentState.save(); 44 | if (isDisableResetButton) return; 45 | isDisableResetButton = true; 46 | setState(() { 47 | loadingStatus = LoadingStatus.loadding; 48 | }); 49 | var params = { 50 | "email": email, 51 | "code": code, 52 | "password": password, 53 | "confirmPassword": confirmPassword, 54 | }; 55 | var response = 56 | await http2.post("/app/user/resetPassword", data: params); 57 | if (response.data["code"] == 200) { 58 | showToast(context, "密码重置成功,请重新登录"); 59 | form_key.currentState.reset(); 60 | if (widget.email.length > 0) { 61 | authorization(); 62 | await putStr(Constant.TOKEN, ""); 63 | await Navigator.of(context).pushAndRemoveUntil( 64 | MaterialPageRoute(builder: (BuildContext context) { 65 | return Login(); 66 | }), (router) => false); 67 | } 68 | } else { 69 | showToast(context, response.data["message"]); 70 | } 71 | } 72 | } catch (error) { 73 | showToast(context, "密码重置失败,请重试~"); 74 | } finally { 75 | setState(() { 76 | loadingStatus = LoadingStatus.normal; 77 | }); 78 | isDisableResetButton = false; 79 | } 80 | } 81 | 82 | sendEmailCode(context) async { 83 | try { 84 | if (emailController.text.length > 0 && isEmail(emailController.text)) { 85 | if (isDisableEmailButton) return; 86 | isDisableEmailButton = true; 87 | setState(() { 88 | sendButtonLoadingStatus = LoadingStatus.loadding; 89 | }); 90 | var params = {"email": emailController.text}; 91 | var response = 92 | await http2.post("/app/user/sendEmailCode", data: params); 93 | if (response.data["code"] == 200) { 94 | showToast(context, "安全码发送成功,请到邮箱查看"); 95 | } else { 96 | showToast(context, response.data["message"]); 97 | } 98 | } else { 99 | showToast(context, "请输入合法的邮箱"); 100 | } 101 | } catch (error) { 102 | print(error); 103 | showToast(context, "出现错误,请重试~"); 104 | } finally { 105 | setState(() { 106 | sendButtonLoadingStatus = LoadingStatus.normal; 107 | }); 108 | isDisableEmailButton = false; 109 | } 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context2) { 114 | return Scaffold( 115 | appBar: AppBar( 116 | title: Text("重设密码"), 117 | ), 118 | body: Builder(builder: (BuildContext context) { 119 | return Form( 120 | key: form_key, 121 | child: ListView( 122 | padding: EdgeInsets.symmetric(horizontal: 10), 123 | children: [ 124 | Container( 125 | child: Column( 126 | children: [ 127 | Container( 128 | child: TextFormField( 129 | controller: emailController, 130 | decoration: InputDecoration( 131 | labelText: "请输入邮箱", 132 | suffixIcon: Icon(Icons.email, 133 | color: Theme.of(context).primaryColor), 134 | ), 135 | validator: (value) { 136 | if (value.length == 0) { 137 | return "邮箱不能为空"; 138 | } else if (!isEmail(value)) { 139 | return "请输入合法的邮箱"; 140 | } 141 | }, 142 | onSaved: (value) { 143 | email = value; 144 | }, 145 | ), 146 | ), 147 | Stack( 148 | alignment: Alignment.center, 149 | children: [ 150 | Container( 151 | margin: EdgeInsets.only(top: 10), 152 | child: TextFormField( 153 | obscureText: passwordHidden, 154 | decoration: InputDecoration( 155 | labelText: "请输入安全码", 156 | ), 157 | validator: (value) { 158 | if (value.length == 0) { 159 | return "安全码不能为空"; 160 | } 161 | }, 162 | onSaved: (value) { 163 | code = value; 164 | }, 165 | )), 166 | Positioned( 167 | right: 0, 168 | child: RaisedButton( 169 | shape: StadiumBorder(), 170 | onPressed: () { 171 | sendEmailCode(context); 172 | }, 173 | child: Row( 174 | mainAxisAlignment: 175 | MainAxisAlignment.spaceAround, 176 | children: [ 177 | Offstage( 178 | offstage: sendButtonLoadingStatus != 179 | LoadingStatus.loadding, 180 | child: SizedBox( 181 | width: 15, 182 | height: 15, 183 | child: CircularProgressIndicator( 184 | strokeWidth: 2, 185 | valueColor: AlwaysStoppedAnimation( 186 | Colors.white)), 187 | ), 188 | ), 189 | Container( 190 | width: 10, 191 | ), 192 | Text( 193 | "发送安全码", 194 | style: TextStyle(color: Colors.white), 195 | ) 196 | ], 197 | ), 198 | ), 199 | ) 200 | ], 201 | ), 202 | TextFormField( 203 | controller: passwordController, 204 | obscureText: passwordHidden, 205 | decoration: InputDecoration( 206 | labelText: "请输入密码", 207 | suffixIcon: IconButton( 208 | icon: Icon(Icons.remove_red_eye, 209 | color: passwordHidden 210 | ? Colors.grey 211 | : Theme.of(context).primaryColor), 212 | onPressed: () { 213 | setState(() { 214 | passwordHidden = !passwordHidden; 215 | }); 216 | }, 217 | ), 218 | ), 219 | validator: (value) { 220 | if (value.length == 0) { 221 | return "密码不能为空"; 222 | } else if (value != confirmPasswordController.text) { 223 | return "两次输入的密码不一致"; 224 | } 225 | }, 226 | onSaved: (value) { 227 | password = value; 228 | }, 229 | ), 230 | TextFormField( 231 | controller: confirmPasswordController, 232 | obscureText: confirmPasswordHidden, 233 | decoration: InputDecoration( 234 | labelText: "请再次输入密码", 235 | suffixIcon: IconButton( 236 | icon: Icon( 237 | Icons.remove_red_eye, 238 | color: confirmPasswordHidden 239 | ? Colors.grey 240 | : Theme.of(context).primaryColor, 241 | ), 242 | onPressed: () { 243 | setState(() { 244 | confirmPasswordHidden = !confirmPasswordHidden; 245 | }); 246 | }, 247 | ), 248 | ), 249 | validator: (value) { 250 | if (value.length == 0) { 251 | return "密码不能为空"; 252 | } else if (value != passwordController.text) { 253 | return "两次输入的密码不一致"; 254 | } 255 | }, 256 | onSaved: (value) { 257 | confirmPassword = value; 258 | }, 259 | ), 260 | Container( 261 | margin: EdgeInsets.only(top: 60, bottom: 60), 262 | alignment: Alignment.center, 263 | child: LoadingButton( 264 | "重设密码", 265 | onPressed: () { 266 | resetPassword(context); 267 | }, 268 | loadingStatus: loadingStatus, 269 | ), 270 | ) 271 | ], 272 | ), 273 | ) 274 | ], 275 | )); 276 | }), 277 | ); 278 | } 279 | } 280 | --------------------------------------------------------------------------------