├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift └── .gitignore ├── android ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── qianyue │ │ │ │ │ └── wan_android_flutter │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── .gitignore ├── build.gradle ├── settings.gradle ├── gradlew.bat └── gradlew ├── assets └── images │ ├── ic_finish.png │ ├── icon_collect.png │ ├── icon_uncollect.png │ └── ic_default_avatar.png ├── lib ├── constants │ └── constants.dart ├── utils │ ├── error_handle.dart │ ├── log_util.dart │ └── persistent_util.dart ├── generated │ └── json │ │ ├── base │ │ ├── json_field.dart │ │ └── json_convert_content.dart │ │ ├── hot_keyword_entity.g.dart │ │ ├── user_tool_entity.g.dart │ │ ├── banner_entity.g.dart │ │ ├── user_info_entity.g.dart │ │ ├── project_category_entity.g.dart │ │ ├── my_todo_data_entity.g.dart │ │ └── my_shared_data_entity.g.dart ├── network │ ├── bean │ │ ├── hot_keyword_entity.dart │ │ ├── banner_entity.dart │ │ ├── user_tool_entity.dart │ │ ├── user_info_entity.dart │ │ ├── AppResponse.dart │ │ ├── project_category_entity.dart │ │ ├── my_todo_data_entity.dart │ │ ├── my_shared_data_entity.dart │ │ ├── article_data_entity.dart │ │ └── project_list_data_entity.dart │ ├── api.dart │ └── request_util.dart ├── user.dart ├── base │ └── base_page.dart ├── pages │ ├── detail_page.dart │ ├── setting_page.dart │ ├── search_result_page.dart │ ├── tabpage │ │ ├── plaza_page.dart │ │ ├── project_page.dart │ │ ├── home_page.dart │ │ └── mine_page.dart │ ├── login_register_page.dart │ ├── article_item_layout.dart │ ├── my_todo_page.dart │ ├── my_colllect_page.dart │ ├── my_shared_page.dart │ └── search_page.dart └── main.dart ├── .gitignore ├── test └── widget_test.dart ├── .metadata ├── analysis_options.yaml ├── README.md └── pubspec.yaml /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/images/ic_finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/assets/images/ic_finish.png -------------------------------------------------------------------------------- /assets/images/icon_collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/assets/images/icon_collect.png -------------------------------------------------------------------------------- /assets/images/icon_uncollect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/assets/images/icon_uncollect.png -------------------------------------------------------------------------------- /assets/images/ic_default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/assets/images/ic_default_avatar.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qianyue0317/wan_android_flutter/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/qianyue0317/wan_android_flutter/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/qianyue/wan_android_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.qianyue.wan_android_flutter 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /lib/constants/constants.dart: -------------------------------------------------------------------------------- 1 | 2 | class Constant { 3 | static bool debug = true; 4 | 5 | static const String baseUrl = "https://www.wanandroid.com/"; 6 | 7 | static const int successCode = 0; 8 | 9 | static const int invalidateToken = -1001; 10 | 11 | static const int otherError = -9999; 12 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/error_handle.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/cupertino.dart'; 6 | 7 | void handleError(void Function() body) { 8 | FlutterError.onError = (FlutterErrorDetails details) { 9 | FlutterError.dumpErrorToConsole(details); 10 | }; 11 | 12 | runZonedGuarded(body, (error, stack) async { 13 | await reportError(error, stack); 14 | }); 15 | } 16 | 17 | Future reportError(Object error, StackTrace stack) async { 18 | // 上传报错信息 19 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/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/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 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 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/generated/json/base/json_field.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names 2 | // ignore_for_file: camel_case_types 3 | // ignore_for_file: prefer_single_quotes 4 | 5 | // This file is automatically generated. DO NOT EDIT, all your changes would be lost. 6 | 7 | class JsonSerializable{ 8 | const JsonSerializable(); 9 | } 10 | 11 | class JSONField { 12 | //Specify the parse field name 13 | final String? name; 14 | 15 | //Whether to participate in toJson 16 | final bool? serialize; 17 | 18 | //Whether to participate in fromMap 19 | final bool? deserialize; 20 | 21 | //Enumeration or not 22 | final bool? isEnum; 23 | 24 | const JSONField({this.name, this.serialize, this.deserialize, this.isEnum}); 25 | } 26 | -------------------------------------------------------------------------------- /lib/network/bean/hot_keyword_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/hot_keyword_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/hot_keyword_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class HotKeywordEntity { 8 | late int id; 9 | late String link; 10 | late String name; 11 | late int order; 12 | late int visible; 13 | 14 | HotKeywordEntity(); 15 | 16 | factory HotKeywordEntity.fromJson(Map json) => $HotKeywordEntityFromJson(json); 17 | 18 | Map toJson() => $HotKeywordEntityToJson(this); 19 | 20 | @override 21 | String toString() { 22 | return jsonEncode(this); 23 | } 24 | } -------------------------------------------------------------------------------- /lib/utils/log_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | import 'package:wan_android_flutter/constants/constants.dart'; 3 | 4 | class WanLog { 5 | static const String tag = "WanLog"; 6 | 7 | static Logger logger = Logger(); 8 | 9 | static void i(String msg, {String tag = tag}) { 10 | if (Constant.debug) { 11 | logger.i(msg); 12 | } 13 | } 14 | 15 | static void d(String msg, {String tag = tag}) { 16 | if (Constant.debug) { 17 | logger.d(msg); 18 | } 19 | } 20 | 21 | static void w(String msg, {String tag = tag}) { 22 | if (Constant.debug) { 23 | logger.w(msg); 24 | } 25 | } 26 | 27 | static void e(String msg, {String tag = tag}) { 28 | if (Constant.debug) { 29 | logger.e(msg); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | plugins { 14 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 15 | } 16 | } 17 | 18 | include ":app" 19 | 20 | apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" 21 | -------------------------------------------------------------------------------- /lib/network/bean/banner_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/banner_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/banner_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class BannerEntity { 8 | late String desc; 9 | late int id; 10 | late String imagePath; 11 | late int isVisible; 12 | late int order; 13 | late String title; 14 | late int type; 15 | late String url; 16 | 17 | BannerEntity(); 18 | 19 | factory BannerEntity.fromJson(Map json) => $BannerEntityFromJson(json); 20 | 21 | Map toJson() => $BannerEntityToJson(this); 22 | 23 | @override 24 | String toString() { 25 | return jsonEncode(this); 26 | } 27 | } -------------------------------------------------------------------------------- /lib/utils/persistent_util.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:mmkv/mmkv.dart'; 3 | 4 | class Persistent { 5 | var mmkv = MMKV.defaultMMKV(); 6 | 7 | void encodeBool(String key, bool value) { 8 | mmkv.encodeBool(key, value); 9 | } 10 | 11 | void encodeString(String key, String value) { 12 | mmkv.encodeString(key, value); 13 | } 14 | 15 | void encodeInt(String key, int value) { 16 | mmkv.encodeInt(key, value); 17 | } 18 | 19 | bool decodeBool(String key, {bool defaultValue = false}) { 20 | return mmkv.decodeBool(key, defaultValue: defaultValue); 21 | } 22 | 23 | String? decodeString(String key) { 24 | return mmkv.decodeString(key); 25 | } 26 | 27 | int decodeInt(String key, {int defaultValue = 0}) { 28 | return mmkv.decodeInt(key, defaultValue: defaultValue); 29 | } 30 | } -------------------------------------------------------------------------------- /lib/network/bean/user_tool_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/user_tool_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/user_tool_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class UserToolEntity { 8 | late String desc; 9 | late String icon; 10 | late int id; 11 | late String link; 12 | late String name; 13 | late int order; 14 | late int userId; 15 | late int visible; 16 | 17 | UserToolEntity(); 18 | 19 | factory UserToolEntity.fromJson(Map json) => $UserToolEntityFromJson(json); 20 | 21 | Map toJson() => $UserToolEntityToJson(this); 22 | 23 | @override 24 | String toString() { 25 | return jsonEncode(this); 26 | } 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /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 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/network/bean/user_info_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/user_info_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/user_info_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class UserInfoEntity { 8 | late bool admin; 9 | late List chapterTops; 10 | late int coinCount; 11 | late List collectIds; 12 | late String email; 13 | late String icon; 14 | late int id; 15 | late String nickname; 16 | late String password; 17 | late String publicName; 18 | late String token; 19 | late int type; 20 | late String username; 21 | 22 | UserInfoEntity(); 23 | 24 | factory UserInfoEntity.fromJson(Map json) => $UserInfoEntityFromJson(json); 25 | 26 | Map toJson() => $UserInfoEntityToJson(this); 27 | 28 | @override 29 | String toString() { 30 | return jsonEncode(this); 31 | } 32 | } -------------------------------------------------------------------------------- /lib/network/bean/AppResponse.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/constants/constants.dart'; 2 | 3 | import '../../generated/json/base/json_convert_content.dart'; 4 | 5 | class AppResponse { 6 | int errorCode = -1; 7 | String? errorMsg; 8 | T? data; 9 | 10 | AppResponse(this.errorCode, this.errorMsg, this.data); 11 | 12 | AppResponse.fromJson(Map map) { 13 | errorCode = (map['errorCode'] as int?) ?? -1; 14 | errorMsg = map['errorMsg'] as String?; 15 | if (map.containsKey('data')) { 16 | data = _generateOBJ(map['data']); 17 | } 18 | } 19 | 20 | T? _generateOBJ(Object? json) { 21 | if (json == null) { 22 | return null; 23 | } 24 | if (T.toString() == 'String') { 25 | return json.toString() as T; 26 | } else if (T.toString() == 'Map') { 27 | return json as T; 28 | } else { 29 | /// List类型数据由fromJsonAsT判断处理 30 | return JsonConvert.fromJsonAsT(json); 31 | } 32 | } 33 | 34 | bool get isSuccessful => errorCode == Constant.successCode; 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/network/bean/project_category_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/project_category_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/project_category_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class ProjectCategoryEntity { 8 | late List articleList; 9 | late String author; 10 | late List children; 11 | late int courseId; 12 | late String cover; 13 | late String desc; 14 | late int id; 15 | late String lisense; 16 | late String lisenseLink; 17 | late String name; 18 | late int order; 19 | late int parentChapterId; 20 | late int type; 21 | late bool userControlSetTop; 22 | late int visible; 23 | 24 | ProjectCategoryEntity(); 25 | 26 | factory ProjectCategoryEntity.fromJson(Map json) => $ProjectCategoryEntityFromJson(json); 27 | 28 | Map toJson() => $ProjectCategoryEntityToJson(this); 29 | 30 | @override 31 | String toString() { 32 | return jsonEncode(this); 33 | } 34 | } -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /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 in the flutter_test package. 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:wan_android_flutter/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(const 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 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 17 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 18 | - platform: android 19 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 20 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 21 | - platform: ios 22 | create_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 23 | base_revision: ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /lib/network/bean/my_todo_data_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/my_todo_data_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/my_todo_data_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class MyTodoDataEntity { 8 | late int curPage; 9 | late List datas; 10 | late int offset; 11 | late bool over; 12 | late int pageCount; 13 | late int size; 14 | late int total; 15 | 16 | MyTodoDataEntity(); 17 | 18 | factory MyTodoDataEntity.fromJson(Map json) => $MyTodoDataEntityFromJson(json); 19 | 20 | Map toJson() => $MyTodoDataEntityToJson(this); 21 | 22 | @override 23 | String toString() { 24 | return jsonEncode(this); 25 | } 26 | } 27 | 28 | @JsonSerializable() 29 | class MyTodoDataItem { 30 | late int completeDate; 31 | late String completeDateStr; 32 | late String content; 33 | late int date; 34 | late String dateStr; 35 | late int id; 36 | late int priority; 37 | late int status; 38 | late String title; 39 | late int type; 40 | late int userId; 41 | 42 | MyTodoDataItem(); 43 | 44 | factory MyTodoDataItem.fromJson(Map json) => $MyTodoDataDatasFromJson(json); 45 | 46 | Map toJson() => $MyTodoDataDatasToJson(this); 47 | 48 | @override 49 | String toString() { 50 | return jsonEncode(this); 51 | } 52 | } -------------------------------------------------------------------------------- /lib/network/api.dart: -------------------------------------------------------------------------------- 1 | 2 | class Api { 3 | /// 首页文章 4 | static const String homePageArticle = "article/list/"; 5 | 6 | /// 置顶文章 7 | static const String topArticle = "article/top/json"; 8 | 9 | /// 获取banner 10 | static const String banner = "banner/json"; 11 | 12 | /// 登录 13 | static const String login = "user/login"; 14 | 15 | /// 注册 16 | static const String register = "user/register"; 17 | 18 | /// 退出登录 19 | static const String logout = "user/logout/json"; 20 | 21 | /// 项目分类 22 | static const String projectCategory = "project/tree/json"; 23 | 24 | /// 项目列表 25 | static const String projectList = "project/list/"; 26 | 27 | /// 搜索 28 | static const String searchForKeyword = "article/query/"; 29 | 30 | /// 广场页列表 31 | static const String plazaArticleList = "user_article/list/"; 32 | 33 | /// 点击收藏 34 | static const String collectArticle = "lg/collect/"; 35 | 36 | /// 取消收藏 37 | static const String uncollectArticel = "lg/uncollect_originId/"; 38 | 39 | /// 获取搜索热词 40 | static const String hotKeywords = "hotkey/json"; 41 | 42 | /// 获取收藏文章列表 43 | static const String collectList = "lg/collect/list/"; 44 | 45 | /// 收藏网站列表 46 | static const String collectWebaddressList = "lg/collect/usertools/json"; 47 | 48 | /// 我的分享 49 | static const String sharedList = "user/lg/private_articles/"; 50 | 51 | /// 分享文章 post 52 | static const String shareArticle = "lg/user_article/add/json"; 53 | 54 | /// todoList 55 | static const String todoList = "lg/todo/v2/list/"; 56 | } 57 | 58 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wan_android_flutter 2 | 3 | ## 玩Android的flutter客户端项目 4 | 5 | ## 注: 此项目中的数据实体类是通过JsonToDart插件生成的,因为dart没有像java中那样的反射,解析json都是要硬编码的,所以用插件生成这些硬编码的代码比较合适。插件地址https://plugins.jetbrains.com/plugin/12562-jsontodart-json-to-dart- 6 | 7 | | ![Screenshot_20231025_140005](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/b8f0f662-2f6b-4680-9d53-0c0689d18a90) | ![Screenshot_20231025_140031](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/dfb2b921-32ee-4479-bca1-ac383cbfba2c) | ![Screenshot_20231025_140039](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/dbd27cec-234c-43b8-ad74-3a2970528f32) | 8 | | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | 9 | | ![Screenshot_20231025_140049](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/26af17cb-8da8-4f51-8908-49a0ac1b0293) | ![Screenshot_20231025_140107](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/fc6142dd-1b94-4cae-9f0b-49c93e53e079) | ![Screenshot_20231025_140128](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/5ed71397-c24f-4c4d-9409-4f92a8fac1ed) | 10 | | ![Screenshot_20231025_140135](https://github.com/qianyue0317/wan_android_flutter/assets/17274658/bf3bdc96-bb7f-492a-9588-0ba9f5bc52ac) | | | 11 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Wan Android Flutter 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | wan_android_flutter 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/generated/json/hot_keyword_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/hot_keyword_entity.dart'; 3 | 4 | HotKeywordEntity $HotKeywordEntityFromJson(Map json) { 5 | final HotKeywordEntity hotKeywordEntity = HotKeywordEntity(); 6 | final int? id = jsonConvert.convert(json['id']); 7 | if (id != null) { 8 | hotKeywordEntity.id = id; 9 | } 10 | final String? link = jsonConvert.convert(json['link']); 11 | if (link != null) { 12 | hotKeywordEntity.link = link; 13 | } 14 | final String? name = jsonConvert.convert(json['name']); 15 | if (name != null) { 16 | hotKeywordEntity.name = name; 17 | } 18 | final int? order = jsonConvert.convert(json['order']); 19 | if (order != null) { 20 | hotKeywordEntity.order = order; 21 | } 22 | final int? visible = jsonConvert.convert(json['visible']); 23 | if (visible != null) { 24 | hotKeywordEntity.visible = visible; 25 | } 26 | return hotKeywordEntity; 27 | } 28 | 29 | Map $HotKeywordEntityToJson(HotKeywordEntity entity) { 30 | final Map data = {}; 31 | data['id'] = entity.id; 32 | data['link'] = entity.link; 33 | data['name'] = entity.name; 34 | data['order'] = entity.order; 35 | data['visible'] = entity.visible; 36 | return data; 37 | } 38 | 39 | extension HotKeywordEntityExtension on HotKeywordEntity { 40 | HotKeywordEntity copyWith({ 41 | int? id, 42 | String? link, 43 | String? name, 44 | int? order, 45 | int? visible, 46 | }) { 47 | return HotKeywordEntity() 48 | ..id = id ?? this.id 49 | ..link = link ?? this.link 50 | ..name = name ?? this.name 51 | ..order = order ?? this.order 52 | ..visible = visible ?? this.visible; 53 | } 54 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/network/bean/my_shared_data_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/my_shared_data_entity.g.dart'; 3 | import 'dart:convert'; 4 | 5 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 6 | export 'package:wan_android_flutter/generated/json/my_shared_data_entity.g.dart'; 7 | 8 | @JsonSerializable() 9 | class MySharedDataEntity { 10 | late MySharedDataCoinInfo coinInfo; 11 | late MySharedDataShareArticles shareArticles; 12 | 13 | MySharedDataEntity(); 14 | 15 | factory MySharedDataEntity.fromJson(Map json) => $MySharedDataEntityFromJson(json); 16 | 17 | Map toJson() => $MySharedDataEntityToJson(this); 18 | 19 | @override 20 | String toString() { 21 | return jsonEncode(this); 22 | } 23 | } 24 | 25 | @JsonSerializable() 26 | class MySharedDataCoinInfo { 27 | late int coinCount; 28 | late int level; 29 | late String nickname; 30 | late String rank; 31 | late int userId; 32 | late String username; 33 | 34 | MySharedDataCoinInfo(); 35 | 36 | factory MySharedDataCoinInfo.fromJson(Map json) => $MySharedDataCoinInfoFromJson(json); 37 | 38 | Map toJson() => $MySharedDataCoinInfoToJson(this); 39 | 40 | @override 41 | String toString() { 42 | return jsonEncode(this); 43 | } 44 | } 45 | 46 | @JsonSerializable() 47 | class MySharedDataShareArticles { 48 | late int curPage; 49 | late List datas; 50 | late int offset; 51 | late bool over; 52 | late int pageCount; 53 | late int size; 54 | late int total; 55 | 56 | MySharedDataShareArticles(); 57 | 58 | factory MySharedDataShareArticles.fromJson(Map json) => $MySharedDataShareArticlesFromJson(json); 59 | 60 | Map toJson() => $MySharedDataShareArticlesToJson(this); 61 | 62 | @override 63 | String toString() { 64 | return jsonEncode(this); 65 | } 66 | } -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.qianyue.wan_android_flutter" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.qianyue.wan_android_flutter" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion 26 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies { 68 | implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) 69 | } 70 | -------------------------------------------------------------------------------- /lib/network/bean/article_data_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:wan_android_flutter/generated/json/article_data_entity.g.dart'; 3 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 4 | import 'dart:convert'; 5 | 6 | export 'package:wan_android_flutter/generated/json/article_data_entity.g.dart'; 7 | 8 | @JsonSerializable() 9 | class ArticleDataEntity { 10 | late int curPage; 11 | late List datas; 12 | late int offset; 13 | late bool over; 14 | late int pageCount; 15 | late int size; 16 | late int total; 17 | 18 | ArticleDataEntity(); 19 | 20 | factory ArticleDataEntity.fromJson(Map json) => $ArticleDataEntityFromJson(json); 21 | 22 | Map toJson() => $ArticleDataEntityToJson(this); 23 | 24 | @override 25 | String toString() { 26 | return jsonEncode(this); 27 | } 28 | } 29 | 30 | @JsonSerializable() 31 | class ArticleItemEntity with ChangeNotifier { 32 | late bool adminAdd; 33 | late String apkLink; 34 | late int audit; 35 | late String? author; 36 | late bool canEdit; 37 | late int chapterId; 38 | late String? chapterName; 39 | bool? _collect; 40 | set collect(bool value) { 41 | _collect = value; 42 | notifyListeners(); 43 | } 44 | bool get collect => _collect ?? false; 45 | late int courseId; 46 | late String desc; 47 | late String descMd; 48 | late String envelopePic; 49 | late bool fresh; 50 | late String host; 51 | late int id; 52 | late bool isAdminAdd; 53 | late String link; 54 | late String niceDate; 55 | late String niceShareDate; 56 | late String origin; 57 | late String prefix; 58 | late String projectLink; 59 | late int publishTime; 60 | late int realSuperChapterId; 61 | late int selfVisible; 62 | late int shareDate; 63 | String? shareUser; 64 | late int superChapterId; 65 | String? superChapterName; 66 | late List tags; 67 | late String title; 68 | int? type; 69 | late int userId; 70 | late int visible; 71 | late int zan; 72 | 73 | ArticleItemEntity(); 74 | 75 | factory ArticleItemEntity.fromJson(Map json) => $ArticleItemEntityFromJson(json); 76 | 77 | Map toJson() => $ArticleItemEntityToJson(this); 78 | 79 | @override 80 | String toString() { 81 | return jsonEncode(this); 82 | } 83 | } -------------------------------------------------------------------------------- /lib/user.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'dart:convert'; 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:mmkv/mmkv.dart'; 7 | import 'package:wan_android_flutter/network/bean/user_info_entity.dart'; 8 | import 'package:wan_android_flutter/network/request_util.dart'; 9 | import 'package:wan_android_flutter/utils/log_util.dart'; 10 | 11 | typedef LoginStatusChangeCallback = void Function(); 12 | 13 | class User extends ChangeNotifier { 14 | static const String _userInfoKey = "userInfo"; 15 | 16 | User._internal(); 17 | 18 | static final User _singleton = User._internal(); 19 | 20 | factory User() => _singleton; 21 | 22 | UserInfoEntity? _userInfoEntity; 23 | 24 | bool isLoggedIn() => _userInfoEntity != null; 25 | 26 | String get userName => _userInfoEntity!.username; 27 | 28 | int get userCoinCount => _userInfoEntity!.coinCount; 29 | 30 | final List _list = []; 31 | 32 | on(LoginStatusChangeCallback loginStatusChange) { 33 | _list.add(loginStatusChange); 34 | } 35 | 36 | off(LoginStatusChangeCallback loginStatusChange) { 37 | _list.remove(loginStatusChange); 38 | } 39 | 40 | loadFromLocal() { 41 | try { 42 | MMKV mmkv = MMKV.defaultMMKV(); 43 | String? infoContent = mmkv.decodeString(_userInfoKey); 44 | if (infoContent == null || infoContent.isEmpty) { 45 | return; 46 | } 47 | _userInfoEntity = UserInfoEntity.fromJson(json.decoder.convert(infoContent)); 48 | } catch(e) { 49 | WanLog.e("load user info from local error- $e"); 50 | } 51 | } 52 | 53 | loginSuccess(UserInfoEntity userInfoEntity) { 54 | _userInfoEntity = userInfoEntity; 55 | try { 56 | MMKV mmkv = MMKV.defaultMMKV(); 57 | String infoContent = _userInfoEntity.toString(); 58 | mmkv.encodeString(_userInfoKey, infoContent); 59 | } catch(e) { 60 | WanLog.e("save user info to local error- $e"); 61 | } 62 | notifyListeners(); 63 | for (var callback in _list) { 64 | callback(); 65 | } 66 | } 67 | 68 | logout() { 69 | _userInfoEntity = null; 70 | HttpGo.instance.cookieJar?.deleteAll(); 71 | try { 72 | MMKV mmkv = MMKV.defaultMMKV(); 73 | mmkv.encodeString(_userInfoKey, ""); 74 | } catch(e) { 75 | WanLog.e("logout user info error- $e"); 76 | } 77 | 78 | notifyListeners(); 79 | for (var callback in _list) { 80 | callback(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/generated/json/user_tool_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/user_tool_entity.dart'; 3 | 4 | UserToolEntity $UserToolEntityFromJson(Map json) { 5 | final UserToolEntity userToolEntity = UserToolEntity(); 6 | final String? desc = jsonConvert.convert(json['desc']); 7 | if (desc != null) { 8 | userToolEntity.desc = desc; 9 | } 10 | final String? icon = jsonConvert.convert(json['icon']); 11 | if (icon != null) { 12 | userToolEntity.icon = icon; 13 | } 14 | final int? id = jsonConvert.convert(json['id']); 15 | if (id != null) { 16 | userToolEntity.id = id; 17 | } 18 | final String? link = jsonConvert.convert(json['link']); 19 | if (link != null) { 20 | userToolEntity.link = link; 21 | } 22 | final String? name = jsonConvert.convert(json['name']); 23 | if (name != null) { 24 | userToolEntity.name = name; 25 | } 26 | final int? order = jsonConvert.convert(json['order']); 27 | if (order != null) { 28 | userToolEntity.order = order; 29 | } 30 | final int? userId = jsonConvert.convert(json['userId']); 31 | if (userId != null) { 32 | userToolEntity.userId = userId; 33 | } 34 | final int? visible = jsonConvert.convert(json['visible']); 35 | if (visible != null) { 36 | userToolEntity.visible = visible; 37 | } 38 | return userToolEntity; 39 | } 40 | 41 | Map $UserToolEntityToJson(UserToolEntity entity) { 42 | final Map data = {}; 43 | data['desc'] = entity.desc; 44 | data['icon'] = entity.icon; 45 | data['id'] = entity.id; 46 | data['link'] = entity.link; 47 | data['name'] = entity.name; 48 | data['order'] = entity.order; 49 | data['userId'] = entity.userId; 50 | data['visible'] = entity.visible; 51 | return data; 52 | } 53 | 54 | extension UserToolEntityExtension on UserToolEntity { 55 | UserToolEntity copyWith({ 56 | String? desc, 57 | String? icon, 58 | int? id, 59 | String? link, 60 | String? name, 61 | int? order, 62 | int? userId, 63 | int? visible, 64 | }) { 65 | return UserToolEntity() 66 | ..desc = desc ?? this.desc 67 | ..icon = icon ?? this.icon 68 | ..id = id ?? this.id 69 | ..link = link ?? this.link 70 | ..name = name ?? this.name 71 | ..order = order ?? this.order 72 | ..userId = userId ?? this.userId 73 | ..visible = visible ?? this.visible; 74 | } 75 | } -------------------------------------------------------------------------------- /lib/generated/json/banner_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/banner_entity.dart'; 3 | 4 | BannerEntity $BannerEntityFromJson(Map json) { 5 | final BannerEntity bannerEntity = BannerEntity(); 6 | final String? desc = jsonConvert.convert(json['desc']); 7 | if (desc != null) { 8 | bannerEntity.desc = desc; 9 | } 10 | final int? id = jsonConvert.convert(json['id']); 11 | if (id != null) { 12 | bannerEntity.id = id; 13 | } 14 | final String? imagePath = jsonConvert.convert(json['imagePath']); 15 | if (imagePath != null) { 16 | bannerEntity.imagePath = imagePath; 17 | } 18 | final int? isVisible = jsonConvert.convert(json['isVisible']); 19 | if (isVisible != null) { 20 | bannerEntity.isVisible = isVisible; 21 | } 22 | final int? order = jsonConvert.convert(json['order']); 23 | if (order != null) { 24 | bannerEntity.order = order; 25 | } 26 | final String? title = jsonConvert.convert(json['title']); 27 | if (title != null) { 28 | bannerEntity.title = title; 29 | } 30 | final int? type = jsonConvert.convert(json['type']); 31 | if (type != null) { 32 | bannerEntity.type = type; 33 | } 34 | final String? url = jsonConvert.convert(json['url']); 35 | if (url != null) { 36 | bannerEntity.url = url; 37 | } 38 | return bannerEntity; 39 | } 40 | 41 | Map $BannerEntityToJson(BannerEntity entity) { 42 | final Map data = {}; 43 | data['desc'] = entity.desc; 44 | data['id'] = entity.id; 45 | data['imagePath'] = entity.imagePath; 46 | data['isVisible'] = entity.isVisible; 47 | data['order'] = entity.order; 48 | data['title'] = entity.title; 49 | data['type'] = entity.type; 50 | data['url'] = entity.url; 51 | return data; 52 | } 53 | 54 | extension BannerEntityExtension on BannerEntity { 55 | BannerEntity copyWith({ 56 | String? desc, 57 | int? id, 58 | String? imagePath, 59 | int? isVisible, 60 | int? order, 61 | String? title, 62 | int? type, 63 | String? url, 64 | }) { 65 | return BannerEntity() 66 | ..desc = desc ?? this.desc 67 | ..id = id ?? this.id 68 | ..imagePath = imagePath ?? this.imagePath 69 | ..isVisible = isVisible ?? this.isVisible 70 | ..order = order ?? this.order 71 | ..title = title ?? this.title 72 | ..type = type ?? this.type 73 | ..url = url ?? this.url; 74 | } 75 | } -------------------------------------------------------------------------------- /lib/base/base_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | 5 | mixin BasePage on State { 6 | /// 是否正在显示loading弹窗 7 | bool showingLoading = false; 8 | 9 | showLoadingDialog() async { 10 | if (showingLoading) { 11 | return; 12 | } 13 | showingLoading = true; 14 | await showDialog( 15 | context: context, 16 | barrierDismissible: true, 17 | builder: (context) { 18 | return const AlertDialog( 19 | content: Column( 20 | mainAxisSize: MainAxisSize.min, 21 | children: [ 22 | CircularProgressIndicator(), 23 | Padding( 24 | padding: EdgeInsets.only(top: 24), 25 | child: Text("请稍候..."), 26 | ) 27 | ], 28 | ), 29 | ); 30 | }); 31 | showingLoading = false; 32 | } 33 | 34 | dismissLoading() { 35 | if (showingLoading) { 36 | Navigator.of(context).pop(); 37 | } 38 | } 39 | } 40 | 41 | class RetryWidget extends StatelessWidget { 42 | const RetryWidget({super.key, required this.onTapRetry}); 43 | 44 | final void Function() onTapRetry; 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return GestureDetector( 49 | behavior: HitTestBehavior.opaque, 50 | onTap: onTapRetry, 51 | child: const SizedBox( 52 | width: double.infinity, 53 | height: double.infinity, 54 | child: Column( 55 | mainAxisAlignment: MainAxisAlignment.center, 56 | crossAxisAlignment: CrossAxisAlignment.center, 57 | children: [ 58 | Padding( 59 | padding: EdgeInsets.only(bottom: 16), 60 | child: Icon(Icons.refresh)), 61 | Text("加载失败,点击重试") 62 | ], 63 | ), 64 | )); 65 | } 66 | } 67 | 68 | class EmptyWidget extends StatelessWidget { 69 | const EmptyWidget({super.key}); 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | return const SizedBox( 74 | width: double.infinity, 75 | height: double.infinity, 76 | child: Column( 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | crossAxisAlignment: CrossAxisAlignment.center, 79 | children: [ 80 | Padding( 81 | padding: EdgeInsets.only(bottom: 16), child: Icon(Icons.book)), 82 | Text("无数据") 83 | ], 84 | ), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/network/bean/project_list_data_entity.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_field.dart'; 2 | import 'package:wan_android_flutter/generated/json/project_list_data_entity.g.dart'; 3 | import 'dart:convert'; 4 | export 'package:wan_android_flutter/generated/json/project_list_data_entity.g.dart'; 5 | 6 | @JsonSerializable() 7 | class ProjectListDataEntity { 8 | late int curPage; 9 | late List datas; 10 | late int offset; 11 | late bool over; 12 | late int pageCount; 13 | late int size; 14 | late int total; 15 | 16 | ProjectListDataEntity(); 17 | 18 | factory ProjectListDataEntity.fromJson(Map json) => $ProjectListDataEntityFromJson(json); 19 | 20 | Map toJson() => $ProjectListDataEntityToJson(this); 21 | 22 | @override 23 | String toString() { 24 | return jsonEncode(this); 25 | } 26 | } 27 | 28 | @JsonSerializable() 29 | class ProjectListDataItemEntity { 30 | late bool adminAdd; 31 | late String apkLink; 32 | late int audit; 33 | late String author; 34 | late bool canEdit; 35 | late int chapterId; 36 | late String chapterName; 37 | late bool collect; 38 | late int courseId; 39 | late String desc; 40 | late String descMd; 41 | late String envelopePic; 42 | late bool fresh; 43 | late String host; 44 | late int id; 45 | late bool isAdminAdd; 46 | late String link; 47 | late String niceDate; 48 | late String niceShareDate; 49 | late String origin; 50 | late String prefix; 51 | late String projectLink; 52 | late int publishTime; 53 | late int realSuperChapterId; 54 | late int selfVisible; 55 | late int shareDate; 56 | late String shareUser; 57 | late int superChapterId; 58 | late String superChapterName; 59 | late List tags; 60 | late String title; 61 | late int type; 62 | late int userId; 63 | late int visible; 64 | late int zan; 65 | 66 | ProjectListDataItemEntity(); 67 | 68 | factory ProjectListDataItemEntity.fromJson(Map json) => $ProjectListDataDatasFromJson(json); 69 | 70 | Map toJson() => $ProjectListDataDatasToJson(this); 71 | 72 | @override 73 | String toString() { 74 | return jsonEncode(this); 75 | } 76 | } 77 | 78 | @JsonSerializable() 79 | class ProjectListDataDatasTags { 80 | late String name; 81 | late String url; 82 | 83 | ProjectListDataDatasTags(); 84 | 85 | factory ProjectListDataDatasTags.fromJson(Map json) => $ProjectListDataDatasTagsFromJson(json); 86 | 87 | Map toJson() => $ProjectListDataDatasTagsToJson(this); 88 | 89 | @override 90 | String toString() { 91 | return jsonEncode(this); 92 | } 93 | } -------------------------------------------------------------------------------- /lib/pages/detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import 'package:webview_flutter/webview_flutter.dart'; 4 | 5 | class DetailPage extends StatefulWidget { 6 | const DetailPage(this.url, this.title, {Key? key}) : super(key: key); 7 | 8 | final String url; 9 | 10 | final String title; 11 | 12 | @override 13 | State createState() => _DetailPageState(); 14 | } 15 | 16 | class _DetailPageState extends State { 17 | _DetailPageState(); 18 | 19 | Key progressKey = GlobalKey(); 20 | 21 | Key contentKey = GlobalKey(); 22 | 23 | final WebViewController _controller = WebViewController(); 24 | 25 | bool finish = false; 26 | 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _controller 31 | ..setJavaScriptMode(JavaScriptMode.unrestricted) 32 | ..setNavigationDelegate(NavigationDelegate( 33 | onPageStarted: (url) {}, 34 | onProgress: (progress) {}, 35 | onPageFinished: (content) { 36 | setState(() { 37 | finish = true; 38 | }); 39 | })) 40 | ..loadRequest(Uri.parse(widget.url)); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return Scaffold( 46 | appBar: AppBar( 47 | titleSpacing: 0, 48 | title: Html( 49 | data: widget.title, 50 | style: { 51 | "html": Style( 52 | color: Colors.white, 53 | margin: Margins.zero, 54 | maxLines: 1, 55 | textOverflow: TextOverflow.ellipsis, 56 | fontSize: FontSize(18), 57 | padding: HtmlPaddings.zero, 58 | alignment: Alignment.topLeft), 59 | "body": Style( 60 | color: Colors.white, 61 | margin: Margins.zero, 62 | maxLines: 1, 63 | textOverflow: TextOverflow.ellipsis, 64 | fontSize: FontSize(18), 65 | padding: HtmlPaddings.zero, 66 | alignment: Alignment.topLeft) 67 | }, 68 | ), 69 | backgroundColor: Theme.of(context).primaryColor, 70 | iconTheme: const IconThemeData(color: Colors.white), 71 | ), 72 | body: !finish 73 | ? Container( 74 | key: progressKey, 75 | width: double.infinity, 76 | height: double.infinity, 77 | alignment: Alignment.center, 78 | child: const CircularProgressIndicator()) 79 | : WebViewWidget(key: contentKey, controller: _controller), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/pages/setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wan_android_flutter/base/base_page.dart'; 5 | import 'package:wan_android_flutter/network/api.dart'; 6 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 7 | import 'package:wan_android_flutter/network/request_util.dart'; 8 | 9 | import '../user.dart'; 10 | 11 | class SettingPage extends StatefulWidget { 12 | const SettingPage({Key? key}) : super(key: key); 13 | 14 | @override 15 | State createState() => _SettingPageState(); 16 | } 17 | 18 | class _SettingPageState extends State with BasePage { 19 | _showLogoutDialog() async { 20 | if (!User().isLoggedIn()) { 21 | Fluttertoast.showToast(msg: "当前未登录"); 22 | return; 23 | } 24 | bool result = await showDialog( 25 | context: context, 26 | builder: (context) { 27 | return AlertDialog( 28 | title: const Text("提示"), 29 | content: const Text("确定要退出吗?"), 30 | actions: [ 31 | TextButton( 32 | onPressed: () { 33 | Get.back(result: false); 34 | }, 35 | child: const Text("取消")), 36 | TextButton( 37 | onPressed: () { 38 | Get.back(result: true); 39 | }, 40 | child: const Text("确定")) 41 | ], 42 | ); 43 | }) ?? 44 | false; 45 | if (result) { 46 | showLoadingDialog(); 47 | AppResponse res = await HttpGo.instance.get(Api.logout); 48 | dismissLoading(); 49 | if (res.isSuccessful) { 50 | User().logout(); 51 | Fluttertoast.showToast(msg: "已退出登录!"); 52 | } else { 53 | Fluttertoast.showToast(msg:"退出登录失败-${res.errorMsg}"); 54 | } 55 | } 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Scaffold( 61 | appBar: AppBar( 62 | title: const Text( 63 | "系统设置", 64 | style: TextStyle(color: Colors.white), 65 | ), 66 | iconTheme: const IconThemeData(color: Colors.white), 67 | backgroundColor: Theme.of(context).primaryColor, 68 | ), 69 | body: Column( 70 | children: [ 71 | Padding( 72 | padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), 73 | child: GestureDetector( 74 | onTap: _showLogoutDialog, 75 | behavior: HitTestBehavior.opaque, 76 | child: SizedBox( 77 | height: 48, 78 | width: double.infinity, 79 | child: Card( 80 | child: Container( 81 | alignment: Alignment.centerLeft, 82 | padding: const EdgeInsets.fromLTRB(16, 0, 0, 0), 83 | child: const Text("退出登录")))))) 84 | ], 85 | )); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/pages/search_result_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:wan_android_flutter/network/api.dart'; 6 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 7 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 8 | import 'package:wan_android_flutter/network/request_util.dart'; 9 | import 'package:wan_android_flutter/pages/article_item_layout.dart'; 10 | import 'package:wan_android_flutter/pages/detail_page.dart'; 11 | 12 | class SearchResultPage extends StatefulWidget { 13 | const SearchResultPage({Key? key, required this.keyword}) : super(key: key); 14 | 15 | final String keyword; 16 | 17 | @override 18 | State createState() => _SearchResultPageState(); 19 | } 20 | 21 | class _SearchResultPageState extends State { 22 | int _currentIndex = 0; 23 | 24 | List data = []; 25 | 26 | final EasyRefreshController _refreshController = EasyRefreshController( 27 | controlFinishLoad: true, controlFinishRefresh: true); 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | _searchRequest(); 33 | } 34 | 35 | _searchRequest() async { 36 | AppResponse res = await HttpGo.instance.post( 37 | "${Api.searchForKeyword}$_currentIndex/json", 38 | data: {"k": widget.keyword}); 39 | if (_currentIndex == 0) { 40 | data.clear(); 41 | } 42 | if (res.isSuccessful) { 43 | setState(() { 44 | data.addAll(res.data!.datas); 45 | }); 46 | } 47 | _refreshController.finishRefresh(); 48 | _refreshController.finishLoad(); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return Scaffold( 54 | appBar: AppBar( 55 | title: Text( 56 | widget.keyword, 57 | style: const TextStyle(color: Colors.white), 58 | ), 59 | iconTheme: const IconThemeData(color: Colors.white), 60 | backgroundColor: Theme.of(context).primaryColor, 61 | ), 62 | body: EasyRefresh.builder( 63 | controller: _refreshController, 64 | childBuilder: (context, physics) { 65 | return ListView.builder( 66 | itemBuilder: (context, index) { 67 | return GestureDetector( 68 | behavior: HitTestBehavior.opaque, 69 | onTap: () => Get.to( 70 | () => DetailPage(data[index].link, data[index].title)), 71 | child: ArticleItemLayout( 72 | itemEntity: data[index], 73 | onCollectTap: () { 74 | _onCollectClick(data[index]); 75 | })); 76 | }, 77 | physics: physics, 78 | itemCount: data.length, 79 | ); 80 | }, 81 | onRefresh: () { 82 | _currentIndex = 0; 83 | _searchRequest(); 84 | }, 85 | onLoad: () { 86 | _currentIndex++; 87 | _searchRequest(); 88 | }, 89 | ), 90 | ); 91 | } 92 | 93 | _onCollectClick(ArticleItemEntity itemEntity) async { 94 | bool collected = itemEntity.collect; 95 | AppResponse res = await (collected 96 | ? HttpGo.instance.post("${Api.uncollectArticel}${itemEntity.id}/json") 97 | : HttpGo.instance.post("${Api.collectArticle}${itemEntity.id}/json")); 98 | 99 | if (res.isSuccessful) { 100 | Fluttertoast.showToast(msg: collected ? "取消收藏!" : "收藏成功!"); 101 | itemEntity.collect = !itemEntity.collect; 102 | } else { 103 | Fluttertoast.showToast( 104 | msg: (collected ? "取消失败 -- " : "收藏失败 -- ") + 105 | (res.errorMsg ?? res.errorCode.toString())); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /lib/generated/json/user_info_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/user_info_entity.dart'; 3 | 4 | UserInfoEntity $UserInfoEntityFromJson(Map json) { 5 | final UserInfoEntity userInfoEntity = UserInfoEntity(); 6 | final bool? admin = jsonConvert.convert(json['admin']); 7 | if (admin != null) { 8 | userInfoEntity.admin = admin; 9 | } 10 | final List? chapterTops = (json['chapterTops'] as List?) 11 | ?.map( 12 | (e) => e) 13 | .toList(); 14 | if (chapterTops != null) { 15 | userInfoEntity.chapterTops = chapterTops; 16 | } 17 | final int? coinCount = jsonConvert.convert(json['coinCount']); 18 | if (coinCount != null) { 19 | userInfoEntity.coinCount = coinCount; 20 | } 21 | final List? collectIds = (json['collectIds'] as List?)?.map( 22 | (e) => jsonConvert.convert(e) as int).toList(); 23 | if (collectIds != null) { 24 | userInfoEntity.collectIds = collectIds; 25 | } 26 | final String? email = jsonConvert.convert(json['email']); 27 | if (email != null) { 28 | userInfoEntity.email = email; 29 | } 30 | final String? icon = jsonConvert.convert(json['icon']); 31 | if (icon != null) { 32 | userInfoEntity.icon = icon; 33 | } 34 | final int? id = jsonConvert.convert(json['id']); 35 | if (id != null) { 36 | userInfoEntity.id = id; 37 | } 38 | final String? nickname = jsonConvert.convert(json['nickname']); 39 | if (nickname != null) { 40 | userInfoEntity.nickname = nickname; 41 | } 42 | final String? password = jsonConvert.convert(json['password']); 43 | if (password != null) { 44 | userInfoEntity.password = password; 45 | } 46 | final String? publicName = jsonConvert.convert(json['publicName']); 47 | if (publicName != null) { 48 | userInfoEntity.publicName = publicName; 49 | } 50 | final String? token = jsonConvert.convert(json['token']); 51 | if (token != null) { 52 | userInfoEntity.token = token; 53 | } 54 | final int? type = jsonConvert.convert(json['type']); 55 | if (type != null) { 56 | userInfoEntity.type = type; 57 | } 58 | final String? username = jsonConvert.convert(json['username']); 59 | if (username != null) { 60 | userInfoEntity.username = username; 61 | } 62 | return userInfoEntity; 63 | } 64 | 65 | Map $UserInfoEntityToJson(UserInfoEntity entity) { 66 | final Map data = {}; 67 | data['admin'] = entity.admin; 68 | data['chapterTops'] = entity.chapterTops; 69 | data['coinCount'] = entity.coinCount; 70 | data['collectIds'] = entity.collectIds; 71 | data['email'] = entity.email; 72 | data['icon'] = entity.icon; 73 | data['id'] = entity.id; 74 | data['nickname'] = entity.nickname; 75 | data['password'] = entity.password; 76 | data['publicName'] = entity.publicName; 77 | data['token'] = entity.token; 78 | data['type'] = entity.type; 79 | data['username'] = entity.username; 80 | return data; 81 | } 82 | 83 | extension UserInfoEntityExtension on UserInfoEntity { 84 | UserInfoEntity copyWith({ 85 | bool? admin, 86 | List? chapterTops, 87 | int? coinCount, 88 | List? collectIds, 89 | String? email, 90 | String? icon, 91 | int? id, 92 | String? nickname, 93 | String? password, 94 | String? publicName, 95 | String? token, 96 | int? type, 97 | String? username, 98 | }) { 99 | return UserInfoEntity() 100 | ..admin = admin ?? this.admin 101 | ..chapterTops = chapterTops ?? this.chapterTops 102 | ..coinCount = coinCount ?? this.coinCount 103 | ..collectIds = collectIds ?? this.collectIds 104 | ..email = email ?? this.email 105 | ..icon = icon ?? this.icon 106 | ..id = id ?? this.id 107 | ..nickname = nickname ?? this.nickname 108 | ..password = password ?? this.password 109 | ..publicName = publicName ?? this.publicName 110 | ..token = token ?? this.token 111 | ..type = type ?? this.type 112 | ..username = username ?? this.username; 113 | } 114 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:mmkv/mmkv.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:url_strategy/url_strategy.dart'; 7 | import 'package:wan_android_flutter/constants/constants.dart'; 8 | import 'package:wan_android_flutter/network/request_util.dart'; 9 | import 'package:wan_android_flutter/pages/search_page.dart'; 10 | import 'package:wan_android_flutter/pages/tabpage/home_page.dart'; 11 | import 'package:wan_android_flutter/pages/tabpage/mine_page.dart'; 12 | import 'package:wan_android_flutter/pages/tabpage/plaza_page.dart'; 13 | import 'package:wan_android_flutter/pages/tabpage/project_page.dart'; 14 | import 'package:wan_android_flutter/user.dart'; 15 | import 'package:wan_android_flutter/utils/error_handle.dart'; 16 | import 'package:wan_android_flutter/utils/log_util.dart'; 17 | 18 | Future main() async { 19 | handleError(() async { 20 | WidgetsFlutterBinding.ensureInitialized(); 21 | 22 | // 初始化mmkv 23 | final rootDir = await MMKV.initialize(); 24 | WanLog.i("mmkv rootDir: ${rootDir}"); 25 | 26 | // 加载本地用户信息 27 | User().loadFromLocal(); 28 | 29 | // 初始化dio 30 | configDio(baseUrl: Constant.baseUrl); 31 | 32 | setPathUrlStrategy(); 33 | runApp(ChangeNotifierProvider( 34 | create: (context) => User(), child: const MyApp())); 35 | }); 36 | } 37 | 38 | class MyApp extends StatelessWidget { 39 | const MyApp({super.key}); 40 | 41 | // This widget is the root of your application. 42 | @override 43 | Widget build(BuildContext context) { 44 | return GetMaterialApp( 45 | builder: FToastBuilder(), 46 | debugShowCheckedModeBanner: false, 47 | title: 'Flutter Demo', 48 | theme: ThemeData( 49 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 50 | useMaterial3: true, 51 | ), 52 | home: const MainPage(title: 'WanAndroidFlutter'), 53 | ); 54 | } 55 | } 56 | 57 | class MainPage extends StatefulWidget { 58 | const MainPage({super.key, required this.title}); 59 | 60 | final String title; 61 | 62 | @override 63 | State createState() => _MainPageState(); 64 | } 65 | 66 | class _MainPageState extends State { 67 | int _selectedItemIndex = 0; 68 | 69 | String _currentTitle = "首页"; 70 | 71 | final PageController _pageController = PageController(initialPage: 0); 72 | 73 | final List _titles = ["首页", "项目", "广场", "我的"]; 74 | final List _navIcons = [ 75 | const Icon(Icons.home), 76 | const Icon(Icons.ac_unit), 77 | const Icon(Icons.animation), 78 | const Icon(Icons.verified_user_rounded) 79 | ]; 80 | 81 | final List _pages = [ 82 | const HomePage(), 83 | const ProjectPage(), 84 | const PlazaPage(), 85 | const MinePage() 86 | ]; 87 | 88 | @override 89 | Widget build(BuildContext context) { 90 | return Scaffold( 91 | appBar: AppBar( 92 | backgroundColor: Theme.of(context).primaryColor, 93 | title: Text(_currentTitle, style: const TextStyle(color: Colors.white)), 94 | actions: [ 95 | IconButton( 96 | icon: const Icon(Icons.search, color: Colors.white), 97 | tooltip: '搜索', 98 | onPressed: () { 99 | Get.to(() => const SearchPage()); 100 | }, 101 | ), 102 | ], 103 | ), 104 | bottomNavigationBar: BottomNavigationBar( 105 | type: BottomNavigationBarType.fixed, 106 | selectedFontSize: 14, 107 | unselectedFontSize: 14, 108 | iconSize: 24, 109 | selectedItemColor: Theme.of(context).primaryColor, 110 | unselectedItemColor: Colors.grey, 111 | items: _generateBottomNavList(), 112 | currentIndex: _selectedItemIndex, 113 | onTap: _onNavItemTapped, 114 | ), 115 | body: PageView.builder( 116 | physics: const NeverScrollableScrollPhysics(), 117 | itemBuilder: (context, index) { 118 | return _pages[index]; 119 | }, 120 | onPageChanged: _onPageChanged, 121 | controller: _pageController, 122 | ), 123 | ); 124 | } 125 | 126 | List _generateBottomNavList() { 127 | return List.generate(_titles.length, (index) { 128 | return BottomNavigationBarItem( 129 | icon: _navIcons[index], label: _titles[index]); 130 | }); 131 | } 132 | 133 | void _onPageChanged(int index) { 134 | setState(() { 135 | _selectedItemIndex = index; 136 | _currentTitle = _titles[index]; 137 | }); 138 | } 139 | 140 | void _onNavItemTapped(int index) { 141 | // _pageController.animateToPage(index, duration: const Duration(milliseconds: 200), curve: Curves.ease); 142 | _pageController.jumpToPage(index); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: wan_android_flutter 2 | description: 玩Android的flutter客户端项目 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 1.0.0+1 20 | 21 | environment: 22 | sdk: '>=3.1.0 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | 35 | get: ^4.6.6 36 | logger: ^2.0.2 37 | mmkv: ^1.3.1 38 | dio: ^5.3.3 39 | easy_refresh: ^3.3.2+1 40 | # banner 41 | banner_carousel: ^1.2.1 42 | carousel_slider: ^4.2.1 43 | # cookie 44 | dio_cookie_manager: ^3.1.1 45 | # toast 46 | fluttertoast: ^8.2.2 47 | # webview 48 | webview_flutter: ^4.4.1 49 | # provider 50 | provider: ^6.0.5 51 | # 常用工具类 https://github.com/Sky24n/common_utils 52 | common_utils: ^2.1.0 53 | url_strategy: ^0.2.0 54 | # html解析 55 | flutter_widget_from_html: ^0.10.6 56 | flutter_html: ^3.0.0-beta.2 57 | # html: ^0.15.4 58 | url_launcher: ^6.1.14 59 | dio_cache_interceptor: ^3.4.4 60 | permission_handler: ^11.0.1 61 | 62 | # The following adds the Cupertino Icons font to your application. 63 | # Use with the CupertinoIcons class for iOS style icons. 64 | cupertino_icons: ^1.0.2 65 | 66 | dev_dependencies: 67 | flutter_test: 68 | sdk: flutter 69 | 70 | # The "flutter_lints" package below contains a set of recommended lints to 71 | # encourage good coding practices. The lint set provided by the package is 72 | # activated in the `analysis_options.yaml` file located at the root of your 73 | # package. See that file for information about deactivating specific lint 74 | # rules and activating additional ones. 75 | flutter_lints: ^2.0.0 76 | 77 | # For information on the generic Dart part of this file, see the 78 | # following page: https://dart.dev/tools/pub/pubspec 79 | 80 | # The following section is specific to Flutter packages. 81 | flutter: 82 | 83 | # The following line ensures that the Material Icons font is 84 | # included with your application, so that you can use the icons in 85 | # the material Icons class. 86 | uses-material-design: true 87 | 88 | # To add assets to your application, add an assets section, like this: 89 | assets: 90 | - assets/images/icon_collect.png 91 | - assets/images/icon_uncollect.png 92 | - assets/images/ic_default_avatar.png 93 | - assets/images/ic_finish.png 94 | 95 | # An image asset can refer to one or more resolution-specific "variants", see 96 | # https://flutter.dev/assets-and-images/#resolution-aware 97 | 98 | # For details regarding adding assets from package dependencies, see 99 | # https://flutter.dev/assets-and-images/#from-packages 100 | 101 | # To add custom fonts to your application, add a fonts section here, 102 | # in this "flutter" section. Each entry in this list should have a 103 | # "family" key with the font family name, and a "fonts" key with a 104 | # list giving the asset and other descriptors for the font. For 105 | # example: 106 | # fonts: 107 | # - family: Schyler 108 | # fonts: 109 | # - asset: fonts/Schyler-Regular.ttf 110 | # - asset: fonts/Schyler-Italic.ttf 111 | # style: italic 112 | # - family: Trajan Pro 113 | # fonts: 114 | # - asset: fonts/TrajanPro.ttf 115 | # - asset: fonts/TrajanPro_Bold.ttf 116 | # weight: 700 117 | # 118 | # For details regarding fonts from package dependencies, 119 | # see https://flutter.dev/custom-fonts/#from-packages 120 | -------------------------------------------------------------------------------- /lib/generated/json/project_category_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/project_category_entity.dart'; 3 | 4 | ProjectCategoryEntity $ProjectCategoryEntityFromJson( 5 | Map json) { 6 | final ProjectCategoryEntity projectCategoryEntity = ProjectCategoryEntity(); 7 | final List? articleList = (json['articleList'] as List?) 8 | ?.map( 9 | (e) => e) 10 | .toList(); 11 | if (articleList != null) { 12 | projectCategoryEntity.articleList = articleList; 13 | } 14 | final String? author = jsonConvert.convert(json['author']); 15 | if (author != null) { 16 | projectCategoryEntity.author = author; 17 | } 18 | final List? children = (json['children'] as List?)?.map( 19 | (e) => e).toList(); 20 | if (children != null) { 21 | projectCategoryEntity.children = children; 22 | } 23 | final int? courseId = jsonConvert.convert(json['courseId']); 24 | if (courseId != null) { 25 | projectCategoryEntity.courseId = courseId; 26 | } 27 | final String? cover = jsonConvert.convert(json['cover']); 28 | if (cover != null) { 29 | projectCategoryEntity.cover = cover; 30 | } 31 | final String? desc = jsonConvert.convert(json['desc']); 32 | if (desc != null) { 33 | projectCategoryEntity.desc = desc; 34 | } 35 | final int? id = jsonConvert.convert(json['id']); 36 | if (id != null) { 37 | projectCategoryEntity.id = id; 38 | } 39 | final String? lisense = jsonConvert.convert(json['lisense']); 40 | if (lisense != null) { 41 | projectCategoryEntity.lisense = lisense; 42 | } 43 | final String? lisenseLink = jsonConvert.convert(json['lisenseLink']); 44 | if (lisenseLink != null) { 45 | projectCategoryEntity.lisenseLink = lisenseLink; 46 | } 47 | final String? name = jsonConvert.convert(json['name']); 48 | if (name != null) { 49 | projectCategoryEntity.name = name; 50 | } 51 | final int? order = jsonConvert.convert(json['order']); 52 | if (order != null) { 53 | projectCategoryEntity.order = order; 54 | } 55 | final int? parentChapterId = jsonConvert.convert( 56 | json['parentChapterId']); 57 | if (parentChapterId != null) { 58 | projectCategoryEntity.parentChapterId = parentChapterId; 59 | } 60 | final int? type = jsonConvert.convert(json['type']); 61 | if (type != null) { 62 | projectCategoryEntity.type = type; 63 | } 64 | final bool? userControlSetTop = jsonConvert.convert( 65 | json['userControlSetTop']); 66 | if (userControlSetTop != null) { 67 | projectCategoryEntity.userControlSetTop = userControlSetTop; 68 | } 69 | final int? visible = jsonConvert.convert(json['visible']); 70 | if (visible != null) { 71 | projectCategoryEntity.visible = visible; 72 | } 73 | return projectCategoryEntity; 74 | } 75 | 76 | Map $ProjectCategoryEntityToJson( 77 | ProjectCategoryEntity entity) { 78 | final Map data = {}; 79 | data['articleList'] = entity.articleList; 80 | data['author'] = entity.author; 81 | data['children'] = entity.children; 82 | data['courseId'] = entity.courseId; 83 | data['cover'] = entity.cover; 84 | data['desc'] = entity.desc; 85 | data['id'] = entity.id; 86 | data['lisense'] = entity.lisense; 87 | data['lisenseLink'] = entity.lisenseLink; 88 | data['name'] = entity.name; 89 | data['order'] = entity.order; 90 | data['parentChapterId'] = entity.parentChapterId; 91 | data['type'] = entity.type; 92 | data['userControlSetTop'] = entity.userControlSetTop; 93 | data['visible'] = entity.visible; 94 | return data; 95 | } 96 | 97 | extension ProjectCategoryEntityExtension on ProjectCategoryEntity { 98 | ProjectCategoryEntity copyWith({ 99 | List? articleList, 100 | String? author, 101 | List? children, 102 | int? courseId, 103 | String? cover, 104 | String? desc, 105 | int? id, 106 | String? lisense, 107 | String? lisenseLink, 108 | String? name, 109 | int? order, 110 | int? parentChapterId, 111 | int? type, 112 | bool? userControlSetTop, 113 | int? visible, 114 | }) { 115 | return ProjectCategoryEntity() 116 | ..articleList = articleList ?? this.articleList 117 | ..author = author ?? this.author 118 | ..children = children ?? this.children 119 | ..courseId = courseId ?? this.courseId 120 | ..cover = cover ?? this.cover 121 | ..desc = desc ?? this.desc 122 | ..id = id ?? this.id 123 | ..lisense = lisense ?? this.lisense 124 | ..lisenseLink = lisenseLink ?? this.lisenseLink 125 | ..name = name ?? this.name 126 | ..order = order ?? this.order 127 | ..parentChapterId = parentChapterId ?? this.parentChapterId 128 | ..type = type ?? this.type 129 | ..userControlSetTop = userControlSetTop ?? this.userControlSetTop 130 | ..visible = visible ?? this.visible; 131 | } 132 | } -------------------------------------------------------------------------------- /lib/pages/tabpage/plaza_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:provider/provider.dart'; 6 | import 'package:wan_android_flutter/base/base_page.dart'; 7 | import 'package:wan_android_flutter/network/api.dart'; 8 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 9 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 10 | import 'package:wan_android_flutter/network/request_util.dart'; 11 | import 'package:wan_android_flutter/pages/article_item_layout.dart'; 12 | import 'package:wan_android_flutter/pages/detail_page.dart'; 13 | import 'package:wan_android_flutter/user.dart'; 14 | import 'package:wan_android_flutter/utils/log_util.dart'; 15 | 16 | class PlazaPage extends StatefulWidget { 17 | const PlazaPage({super.key}); 18 | 19 | @override 20 | State createState() => _PlazaState(); 21 | } 22 | 23 | class _PlazaState extends State 24 | with BasePage, AutomaticKeepAliveClientMixin { 25 | int _currentPageIndex = 0; 26 | 27 | List data = []; 28 | 29 | late RxList dataObs = data.obs; 30 | 31 | final EasyRefreshController _refreshController = EasyRefreshController( 32 | controlFinishRefresh: true, controlFinishLoad: true); 33 | 34 | Future _requestData() async { 35 | AppResponse res = await HttpGo.instance 36 | .get("${Api.plazaArticleList}$_currentPageIndex/json"); 37 | 38 | bool isRefresh = _currentPageIndex == 0; 39 | if (isRefresh) { 40 | data.clear(); 41 | } 42 | if (res.isSuccessful) { 43 | data.addAll(res.data!.datas); 44 | return true; 45 | } 46 | return false; 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | super.build(context); 52 | 53 | // 监听user的状态,当登录状态改变时,重新build 54 | return Consumer(builder: (context, user, child) { 55 | return FutureBuilder( 56 | future: _requestData(), 57 | builder: (context, snapshot) { 58 | WanLog.d("plaza connect state: ${snapshot.connectionState}"); 59 | if (snapshot.connectionState == ConnectionState.done) { 60 | if (snapshot.data == false) { 61 | return RetryWidget(onTapRetry: () { 62 | setState(() {}); 63 | }); 64 | } 65 | return _buildContent(); 66 | } else { 67 | return const Center( 68 | widthFactor: 1, 69 | heightFactor: 1, 70 | child: CircularProgressIndicator(), 71 | ); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | Widget _buildContent() { 78 | if (data.isEmpty) { 79 | return const EmptyWidget(); 80 | } 81 | return EasyRefresh.builder( 82 | controller: _refreshController, 83 | onRefresh: _onRefresh, 84 | onLoad: _onLoad, 85 | childBuilder: (context, physics) { 86 | return Obx(() { 87 | // ignore: invalid_use_of_protected_member 88 | dataObs.value; 89 | return ListView.builder( 90 | physics: physics, 91 | itemBuilder: (context, index) { 92 | ArticleItemEntity itemEntity = data[index]; 93 | return GestureDetector( 94 | onTap: () { 95 | Get.to(() => DetailPage(itemEntity.link, itemEntity.title)); 96 | }, 97 | child: ArticleItemLayout( 98 | itemEntity: itemEntity, 99 | onCollectTap: () { 100 | _onCollectClick(itemEntity); 101 | }), 102 | ); 103 | }, 104 | itemCount: data.length); 105 | }); 106 | }, 107 | ); 108 | } 109 | 110 | _onRefresh() async { 111 | _currentPageIndex = 0; 112 | await _requestData(); 113 | _refreshController.finishRefresh(); 114 | dataObs.refresh(); 115 | } 116 | 117 | _onLoad() async { 118 | _currentPageIndex++; 119 | await _requestData(); 120 | _refreshController.finishLoad(); 121 | dataObs.refresh(); 122 | } 123 | 124 | _onCollectClick(ArticleItemEntity itemEntity) async { 125 | bool collected = itemEntity.collect; 126 | AppResponse res = await (collected 127 | ? HttpGo.instance.post("${Api.uncollectArticel}${itemEntity.id}/json") 128 | : HttpGo.instance.post("${Api.collectArticle}${itemEntity.id}/json")); 129 | 130 | if (res.isSuccessful) { 131 | Fluttertoast.showToast(msg: collected ? "取消收藏!" : "收藏成功!"); 132 | itemEntity.collect = !itemEntity.collect; 133 | } else { 134 | Fluttertoast.showToast( 135 | msg: (collected ? "取消失败 -- " : "收藏失败 -- ") + 136 | (res.errorMsg ?? res.errorCode.toString())); 137 | } 138 | } 139 | 140 | @override 141 | bool get wantKeepAlive => true; 142 | } 143 | -------------------------------------------------------------------------------- /lib/pages/login_register_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fluttertoast/fluttertoast.dart'; 3 | import 'package:wan_android_flutter/base/base_page.dart'; 4 | import 'package:wan_android_flutter/network/api.dart'; 5 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 6 | import 'package:wan_android_flutter/network/bean/user_info_entity.dart'; 7 | import 'package:wan_android_flutter/network/request_util.dart'; 8 | import 'package:wan_android_flutter/user.dart'; 9 | import 'package:get/get.dart'; 10 | 11 | class LoginRegisterPage extends StatefulWidget { 12 | // ignore: use_key_in_widget_constructors 13 | const LoginRegisterPage(); 14 | 15 | @override 16 | State createState() => _LoginRegisterPageState(); 17 | } 18 | 19 | class _LoginRegisterPageState extends State 20 | with BasePage { 21 | final TextEditingController nameTextController = TextEditingController(); 22 | 23 | final TextEditingController passwordTextController = TextEditingController(); 24 | 25 | final Key loginBtnKey = GlobalKey(); 26 | 27 | final Key modeBtnKey = GlobalKey(); 28 | 29 | final TextEditingController repasswordTextController = 30 | TextEditingController(); 31 | 32 | bool isLogin = true; 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: const Text( 39 | "登录/注册", 40 | style: TextStyle(color: Colors.white), 41 | ), 42 | backgroundColor: Theme.of(context).primaryColor, 43 | iconTheme: const IconThemeData(color: Colors.white), 44 | ), 45 | body: Container( 46 | child: Column( 47 | children: [ 48 | Container( 49 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 50 | child: TextField( 51 | controller: nameTextController, 52 | decoration: const InputDecoration( 53 | contentPadding: 54 | EdgeInsets.symmetric(vertical: 4, horizontal: 8), 55 | hintText: "用户名", 56 | border: OutlineInputBorder( 57 | borderRadius: BorderRadius.all(Radius.circular(4)))), 58 | ), 59 | ), 60 | Container( 61 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 62 | child: TextField( 63 | obscureText: true, 64 | controller: passwordTextController, 65 | decoration: const InputDecoration( 66 | contentPadding: 67 | EdgeInsets.symmetric(vertical: 4, horizontal: 8), 68 | hintText: "密码", 69 | border: OutlineInputBorder( 70 | borderRadius: BorderRadius.all(Radius.circular(4)))), 71 | ), 72 | ), 73 | if (!isLogin) 74 | Container( 75 | padding: 76 | const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 77 | child: TextField( 78 | obscureText: true, 79 | controller: repasswordTextController, 80 | decoration: const InputDecoration( 81 | contentPadding: 82 | EdgeInsets.symmetric(vertical: 4, horizontal: 8), 83 | hintText: "确认密码", 84 | border: OutlineInputBorder( 85 | borderRadius: BorderRadius.all(Radius.circular(4)))), 86 | ), 87 | ), 88 | Container( 89 | key: loginBtnKey, 90 | alignment: Alignment.center, 91 | child: TextButton( 92 | onPressed: _onLoginOrRegister, 93 | child: Text(isLogin ? "登录" : "注册")), 94 | ), 95 | Container( 96 | key: modeBtnKey, 97 | alignment: Alignment.center, 98 | child: TextButton( 99 | onPressed: _onChangeMode, 100 | child: Text(isLogin ? "没有账号?去注册" : "已有账号?去登录")), 101 | ), 102 | ], 103 | ), 104 | ), 105 | ); 106 | } 107 | 108 | void _onChangeMode() { 109 | setState(() { 110 | isLogin = !isLogin; 111 | }); 112 | } 113 | 114 | _onLoginOrRegister() async { 115 | FocusScope.of(context).unfocus(); 116 | showLoadingDialog(); 117 | var data = isLogin 118 | ? { 119 | "username": nameTextController.text.trim(), 120 | "password": passwordTextController.text.trim() 121 | } 122 | : { 123 | "username": nameTextController.text.trim(), 124 | "password": passwordTextController.text.trim(), 125 | "repassword": repasswordTextController.text.trim() 126 | }; 127 | 128 | AppResponse res = await HttpGo.instance 129 | .post(isLogin ? Api.login : Api.register, data: data); 130 | dismissLoading(); 131 | 132 | if (res.isSuccessful) { 133 | User().loginSuccess(res.data!); 134 | Fluttertoast.showToast(msg: isLogin ? "登录成功" : "注册成功"); 135 | Get.back(); 136 | } else { 137 | Fluttertoast.showToast( 138 | msg: isLogin ? "登录失败:${res.errorMsg}" : "注册失败:${res.errorMsg}"); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /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/generated/json/my_todo_data_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/my_todo_data_entity.dart'; 3 | 4 | MyTodoDataEntity $MyTodoDataEntityFromJson(Map json) { 5 | final MyTodoDataEntity myTodoDataEntity = MyTodoDataEntity(); 6 | final int? curPage = jsonConvert.convert(json['curPage']); 7 | if (curPage != null) { 8 | myTodoDataEntity.curPage = curPage; 9 | } 10 | final List? datas = (json['datas'] as List?) 11 | ?.map( 12 | (e) => jsonConvert.convert(e) as MyTodoDataItem) 13 | .toList(); 14 | if (datas != null) { 15 | myTodoDataEntity.datas = datas; 16 | } 17 | final int? offset = jsonConvert.convert(json['offset']); 18 | if (offset != null) { 19 | myTodoDataEntity.offset = offset; 20 | } 21 | final bool? over = jsonConvert.convert(json['over']); 22 | if (over != null) { 23 | myTodoDataEntity.over = over; 24 | } 25 | final int? pageCount = jsonConvert.convert(json['pageCount']); 26 | if (pageCount != null) { 27 | myTodoDataEntity.pageCount = pageCount; 28 | } 29 | final int? size = jsonConvert.convert(json['size']); 30 | if (size != null) { 31 | myTodoDataEntity.size = size; 32 | } 33 | final int? total = jsonConvert.convert(json['total']); 34 | if (total != null) { 35 | myTodoDataEntity.total = total; 36 | } 37 | return myTodoDataEntity; 38 | } 39 | 40 | Map $MyTodoDataEntityToJson(MyTodoDataEntity entity) { 41 | final Map data = {}; 42 | data['curPage'] = entity.curPage; 43 | data['datas'] = entity.datas.map((v) => v.toJson()).toList(); 44 | data['offset'] = entity.offset; 45 | data['over'] = entity.over; 46 | data['pageCount'] = entity.pageCount; 47 | data['size'] = entity.size; 48 | data['total'] = entity.total; 49 | return data; 50 | } 51 | 52 | extension MyTodoDataEntityExtension on MyTodoDataEntity { 53 | MyTodoDataEntity copyWith({ 54 | int? curPage, 55 | List? datas, 56 | int? offset, 57 | bool? over, 58 | int? pageCount, 59 | int? size, 60 | int? total, 61 | }) { 62 | return MyTodoDataEntity() 63 | ..curPage = curPage ?? this.curPage 64 | ..datas = datas ?? this.datas 65 | ..offset = offset ?? this.offset 66 | ..over = over ?? this.over 67 | ..pageCount = pageCount ?? this.pageCount 68 | ..size = size ?? this.size 69 | ..total = total ?? this.total; 70 | } 71 | } 72 | 73 | MyTodoDataItem $MyTodoDataDatasFromJson(Map json) { 74 | final MyTodoDataItem myTodoDataDatas = MyTodoDataItem(); 75 | final int? completeDate = jsonConvert.convert(json['completeDate']); 76 | if (completeDate != null) { 77 | myTodoDataDatas.completeDate = completeDate; 78 | } 79 | final String? completeDateStr = jsonConvert.convert( 80 | json['completeDateStr']); 81 | if (completeDateStr != null) { 82 | myTodoDataDatas.completeDateStr = completeDateStr; 83 | } 84 | final String? content = jsonConvert.convert(json['content']); 85 | if (content != null) { 86 | myTodoDataDatas.content = content; 87 | } 88 | final int? date = jsonConvert.convert(json['date']); 89 | if (date != null) { 90 | myTodoDataDatas.date = date; 91 | } 92 | final String? dateStr = jsonConvert.convert(json['dateStr']); 93 | if (dateStr != null) { 94 | myTodoDataDatas.dateStr = dateStr; 95 | } 96 | final int? id = jsonConvert.convert(json['id']); 97 | if (id != null) { 98 | myTodoDataDatas.id = id; 99 | } 100 | final int? priority = jsonConvert.convert(json['priority']); 101 | if (priority != null) { 102 | myTodoDataDatas.priority = priority; 103 | } 104 | final int? status = jsonConvert.convert(json['status']); 105 | if (status != null) { 106 | myTodoDataDatas.status = status; 107 | } 108 | final String? title = jsonConvert.convert(json['title']); 109 | if (title != null) { 110 | myTodoDataDatas.title = title; 111 | } 112 | final int? type = jsonConvert.convert(json['type']); 113 | if (type != null) { 114 | myTodoDataDatas.type = type; 115 | } 116 | final int? userId = jsonConvert.convert(json['userId']); 117 | if (userId != null) { 118 | myTodoDataDatas.userId = userId; 119 | } 120 | return myTodoDataDatas; 121 | } 122 | 123 | Map $MyTodoDataDatasToJson(MyTodoDataItem entity) { 124 | final Map data = {}; 125 | data['completeDate'] = entity.completeDate; 126 | data['completeDateStr'] = entity.completeDateStr; 127 | data['content'] = entity.content; 128 | data['date'] = entity.date; 129 | data['dateStr'] = entity.dateStr; 130 | data['id'] = entity.id; 131 | data['priority'] = entity.priority; 132 | data['status'] = entity.status; 133 | data['title'] = entity.title; 134 | data['type'] = entity.type; 135 | data['userId'] = entity.userId; 136 | return data; 137 | } 138 | 139 | extension MyTodoDataDatasExtension on MyTodoDataItem { 140 | MyTodoDataItem copyWith({ 141 | int? completeDate, 142 | String? completeDateStr, 143 | String? content, 144 | int? date, 145 | String? dateStr, 146 | int? id, 147 | int? priority, 148 | int? status, 149 | String? title, 150 | int? type, 151 | int? userId, 152 | }) { 153 | return MyTodoDataItem() 154 | ..completeDate = completeDate ?? this.completeDate 155 | ..completeDateStr = completeDateStr ?? this.completeDateStr 156 | ..content = content ?? this.content 157 | ..date = date ?? this.date 158 | ..dateStr = dateStr ?? this.dateStr 159 | ..id = id ?? this.id 160 | ..priority = priority ?? this.priority 161 | ..status = status ?? this.status 162 | ..title = title ?? this.title 163 | ..type = type ?? this.type 164 | ..userId = userId ?? this.userId; 165 | } 166 | } -------------------------------------------------------------------------------- /lib/pages/article_item_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_html/flutter_html.dart'; 3 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 4 | 5 | class ArticleItemLayout extends StatefulWidget { 6 | const ArticleItemLayout( 7 | {Key? key, 8 | required this.itemEntity, 9 | required this.onCollectTap, 10 | this.showCollectBtn}) 11 | : super(key: key); 12 | 13 | final ArticleItemEntity itemEntity; 14 | 15 | final void Function() onCollectTap; 16 | 17 | final bool? showCollectBtn; 18 | 19 | @override 20 | State createState() => _ArticleItemState(); 21 | } 22 | 23 | class _ArticleItemState extends State { 24 | @override 25 | void initState() { 26 | super.initState(); 27 | widget.itemEntity.addListener(_onCollectChange); 28 | } 29 | 30 | @override 31 | void didUpdateWidget(ArticleItemLayout oldWidget) { 32 | super.didUpdateWidget(oldWidget); 33 | if (oldWidget.itemEntity != widget.itemEntity) { 34 | oldWidget.itemEntity.removeListener(_onCollectChange); 35 | widget.itemEntity.addListener(_onCollectChange); 36 | } 37 | } 38 | 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | widget.itemEntity.removeListener(_onCollectChange); 43 | } 44 | 45 | _onCollectChange() { 46 | setState(() {}); 47 | } 48 | 49 | @override 50 | Widget build(BuildContext context) { 51 | String publishTime = 52 | DateTime.fromMillisecondsSinceEpoch(widget.itemEntity.publishTime) 53 | .toString(); 54 | publishTime = publishTime.substring(0, publishTime.length - 4); 55 | StringBuffer sb = StringBuffer(widget.itemEntity.superChapterName ?? ""); 56 | if (sb.isNotEmpty && 57 | widget.itemEntity.chapterName != null && 58 | widget.itemEntity.chapterName!.isNotEmpty) { 59 | sb.write("·"); 60 | } 61 | sb.write(widget.itemEntity.chapterName ?? ""); 62 | return Container( 63 | padding: const EdgeInsets.symmetric(horizontal: 8), 64 | child: Card( 65 | surfaceTintColor: Colors.white, 66 | color: Colors.white, 67 | elevation: 8, 68 | child: Padding( 69 | padding: const EdgeInsets.all(8), 70 | child: Column( 71 | children: [ 72 | Row( 73 | children: [ 74 | if (widget.itemEntity.type == 1) 75 | const Padding( 76 | padding: EdgeInsets.only(left: 8), 77 | child: Text( 78 | "置顶", 79 | style: TextStyle(color: Colors.red), 80 | )), 81 | Container( 82 | padding: widget.itemEntity.type == 1 83 | ? const EdgeInsets.fromLTRB(8, 0, 0, 0) 84 | : const EdgeInsets.fromLTRB(12, 0, 0, 0), 85 | child: Text(widget.itemEntity.author?.isNotEmpty == true 86 | ? widget.itemEntity.author! 87 | : widget.itemEntity.shareUser ?? ""), 88 | ), 89 | Expanded( 90 | child: Container( 91 | padding: const EdgeInsets.only(right: 8), 92 | alignment: Alignment.centerRight, 93 | child: Text(publishTime), 94 | ), 95 | ) 96 | ], 97 | ), 98 | Container( 99 | padding: const EdgeInsets.fromLTRB(10, 8, 8, 8), 100 | child: Row( 101 | children: [ 102 | Expanded( 103 | child: Html( 104 | data: widget.itemEntity.title, 105 | style: { 106 | "html": Style( 107 | margin: Margins.zero, 108 | maxLines: 2, 109 | textOverflow: TextOverflow.ellipsis, 110 | fontSize: FontSize(14), 111 | padding: HtmlPaddings.zero, 112 | alignment: Alignment.topLeft), 113 | "body": Style( 114 | margin: Margins.zero, 115 | maxLines: 2, 116 | textOverflow: TextOverflow.ellipsis, 117 | fontSize: FontSize(14), 118 | padding: HtmlPaddings.zero, 119 | alignment: Alignment.topLeft) 120 | }, 121 | )) 122 | ], 123 | ), 124 | ), 125 | Row( 126 | children: [ 127 | Padding( 128 | padding: const EdgeInsets.only(left: 10), 129 | child: Text(sb.toString())), 130 | Expanded( 131 | child: Container( 132 | width: 24, 133 | height: 24, 134 | alignment: Alignment.topRight, 135 | padding: const EdgeInsets.only(right: 8), 136 | child: Builder(builder: (context) { 137 | if (widget.showCollectBtn == false) { 138 | return Container(); 139 | } 140 | return GestureDetector( 141 | onTap: widget.onCollectTap, 142 | child: Image.asset(widget.itemEntity.collect 143 | ? "assets/images/icon_collect.png" 144 | : "assets/images/icon_uncollect.png"), 145 | ); 146 | }))) 147 | ], 148 | ) 149 | ], 150 | ), 151 | ), 152 | )); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/pages/my_todo_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wan_android_flutter/network/api.dart'; 5 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 6 | import 'package:wan_android_flutter/network/bean/my_todo_data_entity.dart'; 7 | import 'package:wan_android_flutter/network/request_util.dart'; 8 | 9 | import '../base/base_page.dart'; 10 | 11 | const List typeList = ["工作", "生活", "编程", "面试", "娱乐", "剪辑"]; 12 | const List levelList = ["低", "中", "高"]; 13 | 14 | class MyTodoListPage extends StatefulWidget { 15 | const MyTodoListPage({Key? key}) : super(key: key); 16 | 17 | @override 18 | State createState() => _MyTodoListPageState(); 19 | } 20 | 21 | class _MyTodoListPageState extends State { 22 | final EasyRefreshController _refreshController = EasyRefreshController( 23 | controlFinishRefresh: true, controlFinishLoad: true); 24 | 25 | final List _data = []; 26 | 27 | int _currentIndex = 1; 28 | 29 | late final _dataObs = _data.obs; 30 | 31 | Future _requestData() async { 32 | AppResponse res = 33 | await HttpGo.instance.get("${Api.todoList}$_currentIndex/json"); 34 | bool isRefresh = _currentIndex == 1; 35 | if (isRefresh) { 36 | _data.clear(); 37 | } 38 | if (res.isSuccessful) { 39 | _data.addAll(res.data!.datas); 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return Scaffold( 48 | appBar: AppBar( 49 | title: const Text("我的待办", style: TextStyle(color: Colors.white)), 50 | backgroundColor: Theme.of(context).primaryColor, 51 | iconTheme: const IconThemeData(color: Colors.white), 52 | ), 53 | body: SizedBox( 54 | width: double.infinity, 55 | height: double.infinity, 56 | child: _getBody())); 57 | } 58 | 59 | Widget _getBody() { 60 | return FutureBuilder( 61 | future: _requestData(), 62 | builder: (context, snapshot) { 63 | if (snapshot.connectionState == ConnectionState.done) { 64 | if (snapshot.data == false) { 65 | return RetryWidget(onTapRetry: () { 66 | setState(() {}); 67 | }); 68 | } 69 | return _buildContent(); 70 | } else { 71 | return const Center( 72 | widthFactor: 1, 73 | heightFactor: 1, 74 | child: CircularProgressIndicator(), 75 | ); 76 | } 77 | }); 78 | } 79 | 80 | Widget _buildContent() { 81 | if (_data.isEmpty) { 82 | return const EmptyWidget(); 83 | } 84 | return EasyRefresh.builder( 85 | controller: _refreshController, 86 | onRefresh: _onRefresh, 87 | onLoad: _onLoad, 88 | childBuilder: (context, physics) { 89 | return Padding( 90 | padding: const EdgeInsets.only(left: 16, right: 16), 91 | child: Obx(() { 92 | // ignore: invalid_use_of_protected_member 93 | _dataObs.value; 94 | return ListView.builder( 95 | physics: physics, 96 | itemBuilder: (context, index) { 97 | MyTodoDataItem item = _data[index]; 98 | return _generateItemLayout(item); 99 | }, 100 | itemCount: _data.length, 101 | ); 102 | })); 103 | }, 104 | ); 105 | } 106 | 107 | _onRefresh() async { 108 | _currentIndex = 1; 109 | await _requestData(); 110 | _refreshController.finishRefresh(); 111 | _dataObs.refresh(); 112 | } 113 | 114 | _onLoad() async { 115 | _currentIndex++; 116 | await _requestData(); 117 | _refreshController.finishLoad(); 118 | _dataObs.refresh(); 119 | } 120 | 121 | Widget _generateItemLayout(MyTodoDataItem item) { 122 | bool isFinished = item.status == 1; 123 | return Card( 124 | surfaceTintColor: Colors.white, 125 | color: Colors.white, 126 | elevation: 4, 127 | child: Stack( 128 | alignment: Alignment.center, 129 | children: [ 130 | Positioned( 131 | child: Container( 132 | height: 100, 133 | width: double.infinity, 134 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 135 | child: Row( 136 | mainAxisSize: MainAxisSize.max, 137 | mainAxisAlignment: MainAxisAlignment.start, 138 | children: [ 139 | Expanded( 140 | child: Column( 141 | crossAxisAlignment: CrossAxisAlignment.start, 142 | children: [ 143 | Text( 144 | item.title, 145 | style: const TextStyle(fontSize: 16), 146 | ), 147 | Row( 148 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 149 | children: [ 150 | Text(item.dateStr), 151 | Text(typeList[ 152 | ((item.type ?? 1) - 1) % typeList.length]), 153 | Text(levelList[ 154 | ((item.priority ?? 1) - 1) % levelList.length]) 155 | ], 156 | ) 157 | ], 158 | ), 159 | ), 160 | Column( 161 | children: [ 162 | TextButton(onPressed: () {}, child: const Text("标记未完成")), 163 | // TextButton(onPressed: () {}, child: Text("删除")) 164 | ], 165 | ) 166 | ], 167 | ), 168 | )), 169 | if (isFinished) 170 | Positioned( 171 | child: Image.asset( 172 | "assets/images/ic_finish.png", 173 | width: 80, 174 | height: 80, 175 | )) 176 | ], 177 | )); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/generated/json/my_shared_data_entity.g.dart: -------------------------------------------------------------------------------- 1 | import 'package:wan_android_flutter/generated/json/base/json_convert_content.dart'; 2 | import 'package:wan_android_flutter/network/bean/my_shared_data_entity.dart'; 3 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 4 | 5 | 6 | MySharedDataEntity $MySharedDataEntityFromJson(Map json) { 7 | final MySharedDataEntity mySharedDataEntity = MySharedDataEntity(); 8 | final MySharedDataCoinInfo? coinInfo = jsonConvert.convert< 9 | MySharedDataCoinInfo>(json['coinInfo']); 10 | if (coinInfo != null) { 11 | mySharedDataEntity.coinInfo = coinInfo; 12 | } 13 | final MySharedDataShareArticles? shareArticles = jsonConvert.convert< 14 | MySharedDataShareArticles>(json['shareArticles']); 15 | if (shareArticles != null) { 16 | mySharedDataEntity.shareArticles = shareArticles; 17 | } 18 | return mySharedDataEntity; 19 | } 20 | 21 | Map $MySharedDataEntityToJson(MySharedDataEntity entity) { 22 | final Map data = {}; 23 | data['coinInfo'] = entity.coinInfo.toJson(); 24 | data['shareArticles'] = entity.shareArticles.toJson(); 25 | return data; 26 | } 27 | 28 | extension MySharedDataEntityExtension on MySharedDataEntity { 29 | MySharedDataEntity copyWith({ 30 | MySharedDataCoinInfo? coinInfo, 31 | MySharedDataShareArticles? shareArticles, 32 | }) { 33 | return MySharedDataEntity() 34 | ..coinInfo = coinInfo ?? this.coinInfo 35 | ..shareArticles = shareArticles ?? this.shareArticles; 36 | } 37 | } 38 | 39 | MySharedDataCoinInfo $MySharedDataCoinInfoFromJson(Map json) { 40 | final MySharedDataCoinInfo mySharedDataCoinInfo = MySharedDataCoinInfo(); 41 | final int? coinCount = jsonConvert.convert(json['coinCount']); 42 | if (coinCount != null) { 43 | mySharedDataCoinInfo.coinCount = coinCount; 44 | } 45 | final int? level = jsonConvert.convert(json['level']); 46 | if (level != null) { 47 | mySharedDataCoinInfo.level = level; 48 | } 49 | final String? nickname = jsonConvert.convert(json['nickname']); 50 | if (nickname != null) { 51 | mySharedDataCoinInfo.nickname = nickname; 52 | } 53 | final String? rank = jsonConvert.convert(json['rank']); 54 | if (rank != null) { 55 | mySharedDataCoinInfo.rank = rank; 56 | } 57 | final int? userId = jsonConvert.convert(json['userId']); 58 | if (userId != null) { 59 | mySharedDataCoinInfo.userId = userId; 60 | } 61 | final String? username = jsonConvert.convert(json['username']); 62 | if (username != null) { 63 | mySharedDataCoinInfo.username = username; 64 | } 65 | return mySharedDataCoinInfo; 66 | } 67 | 68 | Map $MySharedDataCoinInfoToJson(MySharedDataCoinInfo entity) { 69 | final Map data = {}; 70 | data['coinCount'] = entity.coinCount; 71 | data['level'] = entity.level; 72 | data['nickname'] = entity.nickname; 73 | data['rank'] = entity.rank; 74 | data['userId'] = entity.userId; 75 | data['username'] = entity.username; 76 | return data; 77 | } 78 | 79 | extension MySharedDataCoinInfoExtension on MySharedDataCoinInfo { 80 | MySharedDataCoinInfo copyWith({ 81 | int? coinCount, 82 | int? level, 83 | String? nickname, 84 | String? rank, 85 | int? userId, 86 | String? username, 87 | }) { 88 | return MySharedDataCoinInfo() 89 | ..coinCount = coinCount ?? this.coinCount 90 | ..level = level ?? this.level 91 | ..nickname = nickname ?? this.nickname 92 | ..rank = rank ?? this.rank 93 | ..userId = userId ?? this.userId 94 | ..username = username ?? this.username; 95 | } 96 | } 97 | 98 | MySharedDataShareArticles $MySharedDataShareArticlesFromJson( 99 | Map json) { 100 | final MySharedDataShareArticles mySharedDataShareArticles = MySharedDataShareArticles(); 101 | final int? curPage = jsonConvert.convert(json['curPage']); 102 | if (curPage != null) { 103 | mySharedDataShareArticles.curPage = curPage; 104 | } 105 | final List? datas = (json['datas'] as List?) 106 | ?.map( 107 | (e) => jsonConvert.convert(e) as ArticleItemEntity) 108 | .toList(); 109 | if (datas != null) { 110 | mySharedDataShareArticles.datas = datas; 111 | } 112 | final int? offset = jsonConvert.convert(json['offset']); 113 | if (offset != null) { 114 | mySharedDataShareArticles.offset = offset; 115 | } 116 | final bool? over = jsonConvert.convert(json['over']); 117 | if (over != null) { 118 | mySharedDataShareArticles.over = over; 119 | } 120 | final int? pageCount = jsonConvert.convert(json['pageCount']); 121 | if (pageCount != null) { 122 | mySharedDataShareArticles.pageCount = pageCount; 123 | } 124 | final int? size = jsonConvert.convert(json['size']); 125 | if (size != null) { 126 | mySharedDataShareArticles.size = size; 127 | } 128 | final int? total = jsonConvert.convert(json['total']); 129 | if (total != null) { 130 | mySharedDataShareArticles.total = total; 131 | } 132 | return mySharedDataShareArticles; 133 | } 134 | 135 | Map $MySharedDataShareArticlesToJson( 136 | MySharedDataShareArticles entity) { 137 | final Map data = {}; 138 | data['curPage'] = entity.curPage; 139 | data['datas'] = entity.datas.map((v) => v.toJson()).toList(); 140 | data['offset'] = entity.offset; 141 | data['over'] = entity.over; 142 | data['pageCount'] = entity.pageCount; 143 | data['size'] = entity.size; 144 | data['total'] = entity.total; 145 | return data; 146 | } 147 | 148 | extension MySharedDataShareArticlesExtension on MySharedDataShareArticles { 149 | MySharedDataShareArticles copyWith({ 150 | int? curPage, 151 | List? datas, 152 | int? offset, 153 | bool? over, 154 | int? pageCount, 155 | int? size, 156 | int? total, 157 | }) { 158 | return MySharedDataShareArticles() 159 | ..curPage = curPage ?? this.curPage 160 | ..datas = datas ?? this.datas 161 | ..offset = offset ?? this.offset 162 | ..over = over ?? this.over 163 | ..pageCount = pageCount ?? this.pageCount 164 | ..size = size ?? this.size 165 | ..total = total ?? this.total; 166 | } 167 | } -------------------------------------------------------------------------------- /lib/network/request_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:dio/dio.dart'; 5 | import 'package:dio_cookie_manager/dio_cookie_manager.dart'; 6 | import 'package:cookie_jar/cookie_jar.dart'; 7 | import "package:path_provider/path_provider.dart"; 8 | import 'package:wan_android_flutter/user.dart'; 9 | import 'package:wan_android_flutter/utils/log_util.dart'; 10 | import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; 11 | 12 | import '../constants/constants.dart'; 13 | import 'bean/AppResponse.dart'; 14 | 15 | // 是否用compute异步 16 | bool useCompute = false; 17 | bool configured = false; 18 | 19 | Duration _connectTimeout = const Duration(seconds: 10); 20 | Duration _receiveTimeout = const Duration(seconds: 10); 21 | Duration _sendTimeout = const Duration(seconds: 10); 22 | String _baseUrl = ""; 23 | List _interceptors = []; 24 | 25 | void configDio({ 26 | Duration? connectTimeout, 27 | Duration? receiveTimeout, 28 | Duration? sendTimeout, 29 | String? baseUrl, 30 | List? interceptors, 31 | }) { 32 | configured = true; 33 | _connectTimeout = connectTimeout ?? _connectTimeout; 34 | _receiveTimeout = receiveTimeout ?? _receiveTimeout; 35 | _sendTimeout = sendTimeout ?? _sendTimeout; 36 | _baseUrl = baseUrl ?? _baseUrl; 37 | _interceptors = interceptors ?? _interceptors; 38 | } 39 | 40 | class HttpGo { 41 | late Dio _dio; 42 | 43 | static final HttpGo _singleton = HttpGo._internal(); 44 | 45 | static HttpGo get instance => _singleton; 46 | 47 | CookieJar? cookieJar; 48 | 49 | HttpGo._internal() { 50 | if (!configured) { 51 | WanLog.w("you have not config the dio!"); 52 | } 53 | 54 | // Global options 55 | final options = CacheOptions( 56 | // A default store is required for interceptor. 57 | store: MemCacheStore(), 58 | 59 | // All subsequent fields are optional. 60 | 61 | // Default. 62 | policy: CachePolicy.request, 63 | // Returns a cached response on error but for statuses 401 & 403. 64 | // Also allows to return a cached response on network errors (e.g. offline usage). 65 | // Defaults to [null]. 66 | hitCacheOnErrorExcept: [401, 403], 67 | // Overrides any HTTP directive to delete entry past this duration. 68 | // Useful only when origin server has no cache config or custom behaviour is desired. 69 | // Defaults to [null]. 70 | maxStale: const Duration(days: 7), 71 | // Default. Allows 3 cache sets and ease cleanup. 72 | priority: CachePriority.normal, 73 | // Default. Body and headers encryption with your own algorithm. 74 | cipher: null, 75 | // Default. Key builder to retrieve requests. 76 | keyBuilder: CacheOptions.defaultCacheKeyBuilder, 77 | // Default. Allows to cache POST requests. 78 | // Overriding [keyBuilder] is strongly recommended when [true]. 79 | allowPostMethod: false, 80 | ); 81 | 82 | _dio = Dio(BaseOptions( 83 | //请求的Content-Type,默认值是"application/json; charset=utf-8",Headers.formUrlEncodedContentType会自动编码请求体. 84 | contentType: Headers.formUrlEncodedContentType, 85 | responseType: ResponseType.plain, 86 | baseUrl: _baseUrl, 87 | connectTimeout: _connectTimeout, 88 | receiveTimeout: _receiveTimeout, 89 | sendTimeout: _sendTimeout)); 90 | Future dirResult = getApplicationDocumentsDirectory(); 91 | dirResult.then((value) { 92 | CookieManager cookieManager = CookieManager( 93 | PersistCookieJar(storage: FileStorage("${value.path}/.cookies/"))); 94 | cookieJar = cookieManager.cookieJar; 95 | _dio.interceptors.add(cookieManager); 96 | }); 97 | // _dio.interceptors.add(DioCacheInterceptor(options: options)); 98 | _dio.interceptors.addAll(_interceptors); 99 | } 100 | 101 | Future> request(String url, String method, 102 | {Object? data, 103 | Map? queryParams, 104 | CancelToken? cancelToken, 105 | Options? options, 106 | ProgressCallback? progressCallback, 107 | ProgressCallback? receiveCallback}) async { 108 | AppResponse result; 109 | try { 110 | Response response = await _dio.request(url, 111 | data: data, 112 | queryParameters: queryParams, 113 | cancelToken: cancelToken, 114 | options: (options ?? Options())..method = method, 115 | onSendProgress: progressCallback, 116 | onReceiveProgress: receiveCallback); 117 | Map map = json.decode(response.data.toString()); 118 | result = AppResponse.fromJson(map); 119 | if (result.errorCode == Constant.invalidateToken) { 120 | User().logout(); 121 | } 122 | } on DioException catch (error) { 123 | WanLog.e("request error-- $error"); 124 | result = AppResponse(Constant.otherError, error.message, null); 125 | } 126 | return result; 127 | } 128 | 129 | Future> get(String url, 130 | {Object? data, 131 | Map? queryParams, 132 | CancelToken? cancelToken, 133 | Options? options, 134 | ProgressCallback? progressCallback, 135 | ProgressCallback? receiveCallback}) async { 136 | return request(url, "GET", 137 | data: data, 138 | queryParams: queryParams, 139 | cancelToken: cancelToken, 140 | options: options, 141 | progressCallback: progressCallback, 142 | receiveCallback: receiveCallback); 143 | } 144 | 145 | Future> post(String url, 146 | {Object? data, 147 | Map? queryParams, 148 | CancelToken? cancelToken, 149 | Options? options, 150 | ProgressCallback? progressCallback, 151 | ProgressCallback? receiveCallback}) async { 152 | return request(url, "POST", 153 | data: data, 154 | queryParams: queryParams, 155 | cancelToken: cancelToken, 156 | options: options, 157 | progressCallback: progressCallback, 158 | receiveCallback: receiveCallback); 159 | } 160 | 161 | Future> delete(String url, 162 | {Object? data, 163 | Map? queryParams, 164 | CancelToken? cancelToken, 165 | Options? options, 166 | ProgressCallback? progressCallback, 167 | ProgressCallback? receiveCallback}) async { 168 | return request(url, "DELETE", 169 | data: data, 170 | queryParams: queryParams, 171 | cancelToken: cancelToken, 172 | options: options, 173 | progressCallback: progressCallback, 174 | receiveCallback: receiveCallback); 175 | } 176 | 177 | Future> put(String url, 178 | {Object? data, 179 | Map? queryParams, 180 | CancelToken? cancelToken, 181 | Options? options, 182 | ProgressCallback? progressCallback, 183 | ProgressCallback? receiveCallback}) async { 184 | return request(url, "PUT", 185 | data: data, 186 | queryParams: queryParams, 187 | cancelToken: cancelToken, 188 | options: options, 189 | progressCallback: progressCallback, 190 | receiveCallback: receiveCallback); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /lib/pages/tabpage/project_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_html/flutter_html.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:wan_android_flutter/base/base_page.dart'; 6 | import 'package:wan_android_flutter/network/api.dart'; 7 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 8 | import 'package:wan_android_flutter/network/bean/project_category_entity.dart'; 9 | import 'package:wan_android_flutter/network/bean/project_list_data_entity.dart'; 10 | import 'package:wan_android_flutter/network/request_util.dart'; 11 | import 'package:wan_android_flutter/pages/detail_page.dart'; 12 | 13 | class ProjectPage extends StatefulWidget { 14 | const ProjectPage({super.key}); 15 | 16 | @override 17 | State createState() => _ProjectPageState(); 18 | } 19 | 20 | class _ProjectPageState extends State 21 | with 22 | BasePage, 23 | AutomaticKeepAliveClientMixin, 24 | SingleTickerProviderStateMixin { 25 | List _tabs = []; 26 | 27 | TabController? tabController; 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | HttpGo.instance 33 | .get>(Api.projectCategory) 34 | .then((res) { 35 | if (res.isSuccessful) { 36 | _tabs = res.data!; 37 | } 38 | setState(() {}); 39 | }); 40 | } 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | super.build(context); 45 | 46 | if (_tabs.isEmpty) { 47 | return const Center( 48 | widthFactor: 1, heightFactor: 1, child: CircularProgressIndicator()); 49 | } 50 | 51 | tabController ??= TabController(length: _tabs.length, vsync: this); 52 | 53 | return Scaffold( 54 | appBar: AppBar( 55 | toolbarHeight: 0, 56 | bottom: TabBar( 57 | isScrollable: true, 58 | tabs: _tabs.map((e) { 59 | return Tab(text: e.name); 60 | }).toList(), 61 | controller: tabController, 62 | ), 63 | ), 64 | body: TabBarView( 65 | controller: tabController, 66 | children: _tabs.map((e) { 67 | return ProjectListPage(e.id); 68 | }).toList()), 69 | ); 70 | } 71 | 72 | @override 73 | bool get wantKeepAlive => true; 74 | } 75 | 76 | class ProjectListPage extends StatefulWidget { 77 | const ProjectListPage(this.cid, {super.key}); 78 | 79 | final int cid; 80 | 81 | @override 82 | // ignore: no_logic_in_create_state 83 | State createState() => _ProjectListPageState(cid); 84 | } 85 | 86 | class _ProjectListPageState extends State 87 | with BasePage { 88 | _ProjectListPageState(this.cid); 89 | 90 | final int cid; 91 | 92 | int _currentPageIndex = 1; 93 | 94 | List data = []; 95 | 96 | final EasyRefreshController _refreshController = EasyRefreshController( 97 | controlFinishRefresh: true, controlFinishLoad: true); 98 | 99 | @override 100 | void initState() { 101 | super.initState(); 102 | _getProjectListData(); 103 | } 104 | 105 | _getProjectListData() async { 106 | AppResponse res = await HttpGo.instance.get( 107 | "${Api.projectList}$_currentPageIndex/json", 108 | queryParams: {"cid": cid}); 109 | 110 | if (_currentPageIndex == 1) { 111 | data.clear(); 112 | _refreshController.finishRefresh(); 113 | } else { 114 | _refreshController.finishLoad(); 115 | } 116 | if (res.isSuccessful) { 117 | setState(() { 118 | data.addAll(res.data!.datas); 119 | }); 120 | } 121 | } 122 | 123 | @override 124 | Widget build(BuildContext context) { 125 | return EasyRefresh.builder( 126 | controller: _refreshController, 127 | onRefresh: () { 128 | _currentPageIndex = 1; 129 | _getProjectListData(); 130 | }, 131 | onLoad: () { 132 | _currentPageIndex++; 133 | _getProjectListData(); 134 | }, 135 | childBuilder: (context, physics) { 136 | return Padding( 137 | padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), 138 | child: GridView.builder( 139 | clipBehavior: Clip.none, 140 | itemBuilder: (context, index) { 141 | return GestureDetector( 142 | onTap: () { 143 | Get.to(() => 144 | DetailPage(data[index].link, data[index].title)); 145 | }, 146 | child: _generateItem(context, index)); 147 | }, 148 | physics: physics, 149 | itemCount: data.length, 150 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 151 | crossAxisSpacing: 8, 152 | mainAxisSpacing: 8, 153 | crossAxisCount: 2, 154 | childAspectRatio: 0.45, 155 | ), 156 | )); 157 | }, 158 | ); 159 | } 160 | 161 | Widget _generateItem(BuildContext context, int index) { 162 | var entity = data[index]; 163 | return SizedBox( 164 | width: double.infinity, 165 | child: Card( 166 | surfaceTintColor: Colors.white, 167 | color: Colors.white, 168 | elevation: 4, 169 | clipBehavior: Clip.hardEdge, 170 | child: Padding( 171 | padding: const EdgeInsets.all(8), 172 | child: Column( 173 | children: [ 174 | Expanded( 175 | child: ClipRRect( 176 | borderRadius: 177 | const BorderRadius.all(Radius.circular(8)), 178 | child: SizedBox.expand( 179 | child: Image.network( 180 | entity.envelopePic, 181 | fit: BoxFit.cover, 182 | ), 183 | ))), 184 | Container( 185 | padding: const EdgeInsets.only(top: 8), 186 | height: 46, 187 | child: 188 | Html(data: entity.title, shrinkWrap: false, style: { 189 | "html": Style( 190 | margin: Margins.zero, 191 | maxLines: 2, 192 | textOverflow: TextOverflow.ellipsis, 193 | fontSize: FontSize(12), 194 | padding: HtmlPaddings.zero, 195 | alignment: Alignment.topLeft), 196 | "body": Style( 197 | margin: Margins.zero, 198 | maxLines: 2, 199 | textOverflow: TextOverflow.ellipsis, 200 | fontSize: FontSize(12), 201 | padding: HtmlPaddings.zero, 202 | alignment: Alignment.topLeft) 203 | })) 204 | ], 205 | ), 206 | ))); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/pages/my_colllect_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | import 'package:wan_android_flutter/base/base_page.dart'; 5 | import 'package:wan_android_flutter/network/api.dart'; 6 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 7 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 8 | import 'package:wan_android_flutter/network/bean/user_tool_entity.dart'; 9 | import 'package:wan_android_flutter/network/request_util.dart'; 10 | import 'package:wan_android_flutter/pages/article_item_layout.dart'; 11 | import 'package:wan_android_flutter/pages/detail_page.dart'; 12 | 13 | class MyColllectPage extends StatefulWidget { 14 | const MyColllectPage({Key? key}) : super(key: key); 15 | 16 | @override 17 | State createState() => _MyColllectPageState(); 18 | } 19 | 20 | class _MyColllectPageState extends State 21 | with SingleTickerProviderStateMixin { 22 | late final TabController _tabController = 23 | TabController(length: 2, vsync: this); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Scaffold( 28 | appBar: AppBar( 29 | title: const Text( 30 | "我的收藏", 31 | style: TextStyle(color: Colors.white), 32 | ), 33 | backgroundColor: Theme.of(context).primaryColor, 34 | iconTheme: const IconThemeData(color: Colors.white), 35 | ), 36 | body: Column(children: [ 37 | TabBar( 38 | controller: _tabController, 39 | isScrollable: false, 40 | tabs: const [ 41 | Tab( 42 | text: "文章", 43 | ), 44 | Tab( 45 | text: "网站", 46 | ) 47 | ], 48 | ), 49 | Expanded( 50 | child: TabBarView( 51 | controller: _tabController, 52 | children: const [_CollectListPage(0), _CollectListPage(1)], 53 | )), 54 | ])); 55 | } 56 | } 57 | 58 | class _CollectListPage extends StatefulWidget { 59 | const _CollectListPage(this._tag, {super.key}); 60 | 61 | final int _tag; 62 | 63 | @override 64 | State createState() => _CollectListPageState(); 65 | } 66 | 67 | class _CollectListPageState extends State<_CollectListPage> 68 | with AutomaticKeepAliveClientMixin { 69 | int _pageIndex = 0; 70 | 71 | final List data = []; 72 | 73 | final List data2 = []; 74 | 75 | late var dataObs = data.obs; 76 | 77 | late var data2Obs = data2.obs; 78 | 79 | bool _over = false; 80 | 81 | final EasyRefreshController _refreshController = EasyRefreshController( 82 | controlFinishRefresh: true, controlFinishLoad: true); 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | super.build(context); 87 | if (widget._tag == 1) { 88 | _over = true; 89 | } 90 | return FutureBuilder(future: (() async { 91 | _pageIndex = 0; 92 | return await _requestData(); 93 | })(), builder: (context, snapshot) { 94 | if (snapshot.connectionState == ConnectionState.done) { 95 | if (snapshot.data == false) { 96 | return RetryWidget(onTapRetry: () => setState(() {})); 97 | } 98 | return widget._tag == 0 ? _buildContent() : _buildContent2(); 99 | } else { 100 | return const Center( 101 | widthFactor: 1, 102 | heightFactor: 1, 103 | child: CircularProgressIndicator(), 104 | ); 105 | } 106 | }); 107 | } 108 | 109 | Future _requestData() async { 110 | if (widget._tag == 0) { 111 | AppResponse res = 112 | await HttpGo.instance.get("${Api.collectList}/$_pageIndex/json"); 113 | if (res.isSuccessful) { 114 | if (_pageIndex == 0) { 115 | data.clear(); 116 | } 117 | _over = res.data!.over; 118 | data.addAll(res.data!.datas); 119 | return true; 120 | } 121 | return false; 122 | } else { 123 | AppResponse> res = 124 | await HttpGo.instance.get(Api.collectWebaddressList); 125 | if (res.isSuccessful) { 126 | if (_pageIndex == 0) { 127 | data2.clear(); 128 | } 129 | data2.addAll(res.data!); 130 | return true; 131 | } 132 | return false; 133 | } 134 | } 135 | 136 | _onRefresh() async { 137 | _pageIndex = 0; 138 | await _requestData(); 139 | _refreshController.finishRefresh(); 140 | if (mounted) { 141 | if (widget._tag == 0) { 142 | dataObs.refresh(); 143 | } else { 144 | data2Obs.refresh(); 145 | } 146 | } 147 | } 148 | 149 | _onLoad() async { 150 | _pageIndex++; 151 | await _requestData(); 152 | _refreshController 153 | .finishLoad(_over ? IndicatorResult.noMore : IndicatorResult.success); 154 | if (mounted) { 155 | if (widget._tag == 0) { 156 | dataObs.refresh(); 157 | } else { 158 | data2Obs.refresh(); 159 | } 160 | } 161 | } 162 | 163 | Widget _buildContent() { 164 | if (data.isEmpty) { 165 | return const EmptyWidget(); 166 | } 167 | return Obx(() { 168 | // ignore: invalid_use_of_protected_member 169 | dataObs.value; 170 | return EasyRefresh.builder( 171 | onRefresh: _onRefresh, 172 | onLoad: _onLoad, 173 | controller: _refreshController, 174 | childBuilder: (context, physics) { 175 | return ListView.builder( 176 | physics: physics, 177 | itemCount: data.length, 178 | itemBuilder: (context, index) { 179 | ArticleItemEntity itemEntity = data[index]; 180 | return GestureDetector( 181 | behavior: HitTestBehavior.opaque, 182 | onTap: () { 183 | Get.to( 184 | () => DetailPage(itemEntity.link, itemEntity.title)); 185 | }, 186 | child: ArticleItemLayout( 187 | itemEntity: itemEntity, 188 | onCollectTap: () {}, 189 | showCollectBtn: false), 190 | ); 191 | }); 192 | }); 193 | }); 194 | } 195 | 196 | Widget _buildContent2() { 197 | if (data2.isEmpty) { 198 | return const EmptyWidget(); 199 | } 200 | 201 | return ListView.builder( 202 | itemCount: data2.length, 203 | itemBuilder: (context, index) { 204 | UserToolEntity userToolEntity = data2[index]; 205 | return GestureDetector( 206 | behavior: HitTestBehavior.opaque, 207 | child: Container( 208 | padding: const EdgeInsets.only(left: 8, right: 8, top: 8), 209 | child: Card( 210 | surfaceTintColor: Colors.white, 211 | color: Colors.white, 212 | elevation: 8, 213 | child: Padding( 214 | padding: const EdgeInsets.all(8), 215 | child: Row( 216 | children: [ 217 | Expanded( 218 | child: Column( 219 | children: [ 220 | Text(userToolEntity.name), 221 | Text(userToolEntity.link) 222 | ], 223 | )), 224 | ], 225 | ), 226 | ), 227 | )), 228 | ); 229 | }); 230 | } 231 | 232 | @override 233 | bool get wantKeepAlive => true; 234 | } 235 | -------------------------------------------------------------------------------- /lib/pages/tabpage/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:banner_carousel/banner_carousel.dart'; 2 | import 'package:carousel_slider/carousel_slider.dart'; 3 | import 'package:easy_refresh/easy_refresh.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fluttertoast/fluttertoast.dart'; 6 | import 'package:get/get.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:wan_android_flutter/base/base_page.dart'; 9 | import 'package:wan_android_flutter/network/api.dart'; 10 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 11 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 12 | import 'package:wan_android_flutter/network/bean/banner_entity.dart'; 13 | import 'package:wan_android_flutter/network/request_util.dart'; 14 | import 'package:wan_android_flutter/pages/article_item_layout.dart'; 15 | import 'package:wan_android_flutter/pages/detail_page.dart'; 16 | import 'package:wan_android_flutter/user.dart'; 17 | import 'package:wan_android_flutter/utils/log_util.dart'; 18 | 19 | class HomePage extends StatefulWidget { 20 | const HomePage({super.key}); 21 | 22 | @override 23 | State createState() => _HomePageState(); 24 | } 25 | 26 | class _HomePageState extends State 27 | with BasePage, AutomaticKeepAliveClientMixin { 28 | var _pageIndex = 0; 29 | 30 | List _articleList = List.empty(); 31 | 32 | List? bannerData; 33 | 34 | var retryCount = 0.obs; 35 | 36 | var dataUpdate = 0.obs; 37 | 38 | final EasyRefreshController _refreshController = EasyRefreshController( 39 | controlFinishRefresh: true, controlFinishLoad: true); 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | super.build(context); 44 | 45 | return Consumer(builder: (context, user, child) { 46 | return Obx(() { 47 | WanLog.i("retry count: ${retryCount.value}"); 48 | return _build(context); 49 | }); 50 | }); 51 | } 52 | 53 | Widget _build(BuildContext context) { 54 | return FutureBuilder( 55 | future: _refreshRequest(), 56 | builder: (context, snapshot) { 57 | if (snapshot.connectionState == ConnectionState.done) { 58 | if (snapshot.data == false) { 59 | return RetryWidget(onTapRetry: () => retryCount.value++); 60 | } 61 | return Obx(() { 62 | WanLog.i("data update: ${dataUpdate.value}"); 63 | return Scaffold( 64 | body: EasyRefresh.builder( 65 | controller: _refreshController, 66 | onRefresh: _onRefresh, 67 | onLoad: _loadRequest, 68 | childBuilder: (context, physics) { 69 | return CustomScrollView( 70 | physics: physics, 71 | slivers: [ 72 | if (bannerData != null && bannerData!.isNotEmpty) 73 | SliverToBoxAdapter( 74 | child: Padding( 75 | padding: 76 | const EdgeInsets.fromLTRB(0, 16, 0, 0), 77 | child: CarouselSlider( 78 | options: CarouselOptions( 79 | enableInfiniteScroll: true, 80 | autoPlay: true, 81 | aspectRatio: 2.0, 82 | enlargeCenterPage: true, 83 | enlargeStrategy: 84 | CenterPageEnlargeStrategy.height), 85 | items: _bannerList(), 86 | ))), 87 | SliverList( 88 | delegate: 89 | SliverChildBuilderDelegate((context, index) { 90 | return GestureDetector( 91 | onTap: () { 92 | Get.to(() => DetailPage( 93 | _articleList[index].link, 94 | _articleList[index].title)); 95 | }, 96 | child: ArticleItemLayout( 97 | itemEntity: _articleList[index], 98 | onCollectTap: () { 99 | _onCollectClick(_articleList[index]); 100 | })); 101 | }, childCount: _articleList.length)) 102 | ], 103 | ); 104 | }, 105 | ), 106 | ); 107 | }); 108 | } else { 109 | return const Center( 110 | widthFactor: 1, 111 | heightFactor: 1, 112 | child: CircularProgressIndicator(), 113 | ); 114 | } 115 | }); 116 | } 117 | 118 | List _bannerList() => bannerData! 119 | .map((e) => Container( 120 | margin: const EdgeInsets.all(5), 121 | decoration: BoxDecoration( 122 | border: Border.all(color: Colors.grey, width: 0.5), 123 | borderRadius: const BorderRadius.all(Radius.circular(6)) 124 | ), 125 | child: ClipRRect( 126 | borderRadius: const BorderRadius.all(Radius.circular(6)), 127 | child: Image.network( 128 | e.imagePath, 129 | fit: BoxFit.cover, 130 | width: double.infinity, 131 | )))) 132 | .toList(); 133 | 134 | _onCollectClick(ArticleItemEntity itemEntity) async { 135 | bool collected = itemEntity.collect; 136 | AppResponse res = await (collected 137 | ? HttpGo.instance.post("${Api.uncollectArticel}${itemEntity.id}/json") 138 | : HttpGo.instance.post("${Api.collectArticle}${itemEntity.id}/json")); 139 | 140 | if (res.isSuccessful) { 141 | Fluttertoast.showToast(msg: collected ? "取消收藏!" : "收藏成功!"); 142 | itemEntity.collect = !itemEntity.collect; 143 | } else { 144 | Fluttertoast.showToast( 145 | msg: (collected ? "取消失败 -- " : "收藏失败 -- ") + 146 | (res.errorMsg ?? res.errorCode.toString())); 147 | } 148 | } 149 | 150 | void _onRefresh() async { 151 | await _refreshRequest(); 152 | _refreshController.finishRefresh(); 153 | dataUpdate.refresh(); 154 | } 155 | 156 | Future _refreshRequest() async { 157 | _pageIndex = 0; 158 | 159 | bool resultStatus = true; 160 | 161 | List result = []; 162 | 163 | AppResponse> bannerRes = 164 | await HttpGo.instance.get(Api.banner); 165 | bannerData = bannerRes.data; 166 | resultStatus &= bannerRes.isSuccessful; 167 | 168 | AppResponse> topRes = 169 | await HttpGo.instance.get(Api.topArticle); 170 | if (topRes.isSuccessful) { 171 | result.addAll(topRes.data ?? List.empty()); 172 | } 173 | resultStatus &= topRes.isSuccessful; 174 | 175 | AppResponse res = await HttpGo.instance 176 | .get("${Api.homePageArticle}$_pageIndex/json"); 177 | resultStatus &= res.isSuccessful; 178 | 179 | if (res.isSuccessful) { 180 | result.addAll(res.data?.datas ?? List.empty()); 181 | } 182 | _articleList = result; 183 | return resultStatus; 184 | } 185 | 186 | void _loadRequest() async { 187 | _pageIndex++; 188 | AppResponse res = await HttpGo.instance 189 | .get("${Api.homePageArticle}$_pageIndex/json"); 190 | if (res.isSuccessful) { 191 | _articleList.addAll(res.data?.datas ?? List.empty()); 192 | } 193 | _refreshController.finishLoad(); 194 | dataUpdate.refresh(); 195 | } 196 | 197 | @override 198 | bool get wantKeepAlive => true; 199 | } 200 | -------------------------------------------------------------------------------- /lib/generated/json/base/json_convert_content.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: non_constant_identifier_names 2 | // ignore_for_file: camel_case_types 3 | // ignore_for_file: prefer_single_quotes 4 | 5 | // This file is automatically generated. DO NOT EDIT, all your changes would be lost. 6 | import 'package:flutter/material.dart' show debugPrint; 7 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 8 | import 'package:wan_android_flutter/network/bean/banner_entity.dart'; 9 | import 'package:wan_android_flutter/network/bean/hot_keyword_entity.dart'; 10 | import 'package:wan_android_flutter/network/bean/my_shared_data_entity.dart'; 11 | import 'package:wan_android_flutter/network/bean/my_todo_data_entity.dart'; 12 | import 'package:wan_android_flutter/network/bean/project_category_entity.dart'; 13 | import 'package:wan_android_flutter/network/bean/project_list_data_entity.dart'; 14 | import 'package:wan_android_flutter/network/bean/user_info_entity.dart'; 15 | import 'package:wan_android_flutter/network/bean/user_tool_entity.dart'; 16 | 17 | JsonConvert jsonConvert = JsonConvert(); 18 | 19 | typedef JsonConvertFunction = T Function(Map json); 20 | typedef EnumConvertFunction = T Function(String value); 21 | typedef ConvertExceptionHandler = void Function(Object error, StackTrace stackTrace); 22 | 23 | class JsonConvert { 24 | static ConvertExceptionHandler? onError; 25 | 26 | static Map get convertFuncMap => 27 | { 28 | (ArticleDataEntity).toString(): ArticleDataEntity.fromJson, 29 | (ArticleItemEntity).toString(): ArticleItemEntity.fromJson, 30 | (BannerEntity).toString(): BannerEntity.fromJson, 31 | (HotKeywordEntity).toString(): HotKeywordEntity.fromJson, 32 | (MySharedDataEntity).toString(): MySharedDataEntity.fromJson, 33 | (MySharedDataCoinInfo).toString(): MySharedDataCoinInfo.fromJson, 34 | (MySharedDataShareArticles).toString(): MySharedDataShareArticles 35 | .fromJson, 36 | (MyTodoDataEntity).toString(): MyTodoDataEntity.fromJson, 37 | (MyTodoDataItem).toString(): MyTodoDataItem.fromJson, 38 | (ProjectCategoryEntity).toString(): ProjectCategoryEntity.fromJson, 39 | (ProjectListDataEntity).toString(): ProjectListDataEntity.fromJson, 40 | (ProjectListDataItemEntity).toString(): ProjectListDataItemEntity 41 | .fromJson, 42 | (ProjectListDataDatasTags).toString(): ProjectListDataDatasTags 43 | .fromJson, 44 | (UserInfoEntity).toString(): UserInfoEntity.fromJson, 45 | (UserToolEntity).toString(): UserToolEntity.fromJson, 46 | }; 47 | 48 | T? convert(dynamic value, {EnumConvertFunction? enumConvert}) { 49 | if (value == null) { 50 | return null; 51 | } 52 | if (value is T) { 53 | return value; 54 | } 55 | try { 56 | return _asT(value, enumConvert: enumConvert); 57 | } catch (e, stackTrace) { 58 | debugPrint('asT<$T> $e $stackTrace'); 59 | if (onError != null) { 60 | onError!(e, stackTrace); 61 | } 62 | return null; 63 | } 64 | } 65 | 66 | List? convertList(List? value, 67 | {EnumConvertFunction? enumConvert}) { 68 | if (value == null) { 69 | return null; 70 | } 71 | try { 72 | return value.map((dynamic e) => _asT(e, enumConvert: enumConvert)) 73 | .toList(); 74 | } catch (e, stackTrace) { 75 | debugPrint('asT<$T> $e $stackTrace'); 76 | if (onError != null) { 77 | onError!(e, stackTrace); 78 | } 79 | return []; 80 | } 81 | } 82 | 83 | List? convertListNotNull(dynamic value, 84 | {EnumConvertFunction? enumConvert}) { 85 | if (value == null) { 86 | return null; 87 | } 88 | try { 89 | return (value as List).map((dynamic e) => 90 | _asT(e, enumConvert: enumConvert)!).toList(); 91 | } catch (e, stackTrace) { 92 | debugPrint('asT<$T> $e $stackTrace'); 93 | if (onError != null) { 94 | onError!(e, stackTrace); 95 | } 96 | return []; 97 | } 98 | } 99 | 100 | T? _asT(dynamic value, 101 | {EnumConvertFunction? enumConvert}) { 102 | final String type = T.toString(); 103 | final String valueS = value.toString(); 104 | if (enumConvert != null) { 105 | return enumConvert(valueS) as T; 106 | } else if (type == "String") { 107 | return valueS as T; 108 | } else if (type == "int") { 109 | final int? intValue = int.tryParse(valueS); 110 | if (intValue == null) { 111 | return double.tryParse(valueS)?.toInt() as T?; 112 | } else { 113 | return intValue as T; 114 | } 115 | } else if (type == "double") { 116 | return double.parse(valueS) as T; 117 | } else if (type == "DateTime") { 118 | return DateTime.parse(valueS) as T; 119 | } else if (type == "bool") { 120 | if (valueS == '0' || valueS == '1') { 121 | return (valueS == '1') as T; 122 | } 123 | return (valueS == 'true') as T; 124 | } else if (type == "Map" || type.startsWith("Map<")) { 125 | return value as T; 126 | } else { 127 | if (convertFuncMap.containsKey(type)) { 128 | if (value == null) { 129 | return null; 130 | } 131 | return convertFuncMap[type]!(Map.from(value)) as T; 132 | } else { 133 | throw UnimplementedError('$type unimplemented'); 134 | } 135 | } 136 | } 137 | 138 | //list is returned by type 139 | static M? _getListChildType(List> data) { 140 | if ([] is M) { 141 | return data.map((Map e) => 142 | ArticleDataEntity.fromJson(e)).toList() as M; 143 | } 144 | if ([] is M) { 145 | return data.map((Map e) => 146 | ArticleItemEntity.fromJson(e)).toList() as M; 147 | } 148 | if ([] is M) { 149 | return data.map((Map e) => 150 | BannerEntity.fromJson(e)).toList() as M; 151 | } 152 | if ([] is M) { 153 | return data.map((Map e) => 154 | HotKeywordEntity.fromJson(e)).toList() as M; 155 | } 156 | if ([] is M) { 157 | return data.map((Map e) => 158 | MySharedDataEntity.fromJson(e)).toList() as M; 159 | } 160 | if ([] is M) { 161 | return data.map((Map e) => 162 | MySharedDataCoinInfo.fromJson(e)).toList() as M; 163 | } 164 | if ([] is M) { 165 | return data.map((Map e) => 166 | MySharedDataShareArticles.fromJson(e)).toList() as M; 167 | } 168 | if ([] is M) { 169 | return data.map((Map e) => 170 | MyTodoDataEntity.fromJson(e)).toList() as M; 171 | } 172 | if ([] is M) { 173 | return data.map((Map e) => 174 | MyTodoDataItem.fromJson(e)).toList() as M; 175 | } 176 | if ([] is M) { 177 | return data.map((Map e) => 178 | ProjectCategoryEntity.fromJson(e)).toList() as M; 179 | } 180 | if ([] is M) { 181 | return data.map((Map e) => 182 | ProjectListDataEntity.fromJson(e)).toList() as M; 183 | } 184 | if ([] is M) { 185 | return data.map((Map e) => 186 | ProjectListDataItemEntity.fromJson(e)).toList() as M; 187 | } 188 | if ([] is M) { 189 | return data.map((Map e) => 190 | ProjectListDataDatasTags.fromJson(e)).toList() as M; 191 | } 192 | if ([] is M) { 193 | return data.map((Map e) => 194 | UserInfoEntity.fromJson(e)).toList() as M; 195 | } 196 | if ([] is M) { 197 | return data.map((Map e) => 198 | UserToolEntity.fromJson(e)).toList() as M; 199 | } 200 | 201 | debugPrint("${M.toString()} not found"); 202 | 203 | return null; 204 | } 205 | 206 | static M? fromJsonAsT(dynamic json) { 207 | if (json is M) { 208 | return json; 209 | } 210 | if (json is List) { 211 | return _getListChildType( 212 | json.map((e) => e as Map).toList()); 213 | } else { 214 | return jsonConvert.convert(json); 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /lib/pages/tabpage/mine_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:wan_android_flutter/base/base_page.dart'; 5 | import 'package:wan_android_flutter/pages/login_register_page.dart'; 6 | import 'package:wan_android_flutter/pages/my_colllect_page.dart'; 7 | import 'package:wan_android_flutter/pages/my_shared_page.dart'; 8 | import 'package:wan_android_flutter/pages/my_todo_page.dart'; 9 | import 'package:wan_android_flutter/pages/setting_page.dart'; 10 | import 'package:wan_android_flutter/user.dart'; 11 | import 'package:wan_android_flutter/utils/log_util.dart'; 12 | 13 | class MinePage extends StatefulWidget { 14 | const MinePage({super.key}); 15 | 16 | @override 17 | State createState() => _MinePageState(); 18 | } 19 | 20 | class _MinePageState extends State 21 | with BasePage, AutomaticKeepAliveClientMixin { 22 | @override 23 | Widget build(BuildContext context) { 24 | super.build(context); 25 | WanLog.i("change mine"); 26 | return Scaffold( 27 | body: Container( 28 | decoration: const BoxDecoration( 29 | // color: Theme.of(context).primaryColor 30 | ), 31 | child: Column( 32 | children: [ 33 | Padding( 34 | padding: const EdgeInsets.all(16), 35 | child: Row( 36 | children: [ 37 | const CircleAvatar( 38 | radius: 24, 39 | backgroundImage: 40 | AssetImage("assets/images/ic_default_avatar.png"), 41 | ), 42 | GestureDetector( 43 | onTap: () { 44 | if (!User().isLoggedIn()) { 45 | Get.to(() => const LoginRegisterPage()); 46 | } 47 | }, 48 | behavior: HitTestBehavior.opaque, 49 | child: Container( 50 | alignment: Alignment.centerLeft, 51 | padding: const EdgeInsets.fromLTRB(16, 16, 0, 16), 52 | child: Text(context.select( 53 | (user) => user.isLoggedIn()) 54 | ? context 55 | .select((user) => user.userName) 56 | : "登录/注册"), 57 | )), 58 | Expanded( 59 | child: Align( 60 | alignment: Alignment.centerRight, 61 | child: Text(context.select( 62 | (user) => user.isLoggedIn()) 63 | ? "积分:${context.select((user) => user.userCoinCount.toString())}" 64 | : "积分:--")), 65 | ) 66 | ], 67 | )), 68 | Expanded( 69 | child: Container( 70 | decoration: BoxDecoration( 71 | color: Colors.white.withOpacity(0.95), 72 | boxShadow: [ 73 | BoxShadow( 74 | color: Colors.black.withOpacity(0.5), 75 | blurRadius: 8, 76 | ) 77 | ], 78 | borderRadius: const BorderRadius.only( 79 | topLeft: Radius.circular(24), 80 | topRight: Radius.circular(24)), 81 | ), 82 | child: Padding( 83 | padding: const EdgeInsets.symmetric(horizontal: 10), 84 | child: Column( 85 | children: [ 86 | GestureDetector( 87 | behavior: HitTestBehavior.opaque, 88 | onTap: () { 89 | Get.to(() => const MyColllectPage()); 90 | }, 91 | child: Container( 92 | padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), 93 | child: const Row( 94 | crossAxisAlignment: CrossAxisAlignment.center, 95 | children: [ 96 | Text( 97 | "我的收藏", 98 | style: TextStyle(fontSize: 16), 99 | ), 100 | Expanded( 101 | child: Align( 102 | alignment: Alignment.centerRight, 103 | child: Icon( 104 | Icons.arrow_forward_ios, 105 | size: 15, 106 | ))) 107 | ], 108 | ), 109 | )), 110 | GestureDetector( 111 | behavior: HitTestBehavior.opaque, 112 | onTap: () { 113 | Get.to(() => const MySharedPage()); 114 | }, 115 | child: Container( 116 | padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), 117 | child: const Row( 118 | crossAxisAlignment: CrossAxisAlignment.center, 119 | children: [ 120 | Text( 121 | "我的分享", 122 | style: TextStyle(fontSize: 16), 123 | ), 124 | Expanded( 125 | child: Align( 126 | alignment: Alignment.centerRight, 127 | child: Icon( 128 | Icons.arrow_forward_ios, 129 | size: 15, 130 | ))) 131 | ], 132 | ), 133 | )), 134 | GestureDetector( 135 | behavior: HitTestBehavior.opaque, 136 | onTap: () { 137 | Get.to(() => const MyTodoListPage()); 138 | }, 139 | child: Container( 140 | padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), 141 | child: const Row( 142 | crossAxisAlignment: CrossAxisAlignment.center, 143 | children: [ 144 | Text( 145 | "我的待办", 146 | style: TextStyle(fontSize: 16), 147 | ), 148 | Expanded( 149 | child: Align( 150 | alignment: Alignment.centerRight, 151 | child: Icon( 152 | Icons.arrow_forward_ios, 153 | size: 15, 154 | ))) 155 | ], 156 | ), 157 | )), 158 | GestureDetector( 159 | behavior: HitTestBehavior.opaque, 160 | onTap: () { 161 | Get.to(() => const SettingPage()); 162 | }, 163 | child: Container( 164 | padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), 165 | child: const Row( 166 | crossAxisAlignment: CrossAxisAlignment.center, 167 | children: [ 168 | Text( 169 | "系统设置", 170 | style: TextStyle(fontSize: 16), 171 | ), 172 | Expanded( 173 | child: Align( 174 | alignment: Alignment.centerRight, 175 | child: Icon( 176 | Icons.arrow_forward_ios, 177 | size: 15, 178 | ))) 179 | ], 180 | ), 181 | )) 182 | ], 183 | )), 184 | )) 185 | ], 186 | ), 187 | ), 188 | ); 189 | } 190 | 191 | @override 192 | bool get wantKeepAlive => true; 193 | } 194 | -------------------------------------------------------------------------------- /lib/pages/my_shared_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:wan_android_flutter/base/base_page.dart'; 6 | import 'package:wan_android_flutter/network/api.dart'; 7 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 8 | import 'package:wan_android_flutter/network/bean/article_data_entity.dart'; 9 | import 'package:wan_android_flutter/network/bean/my_shared_data_entity.dart'; 10 | import 'package:wan_android_flutter/network/request_util.dart'; 11 | import 'package:wan_android_flutter/pages/article_item_layout.dart'; 12 | import 'package:wan_android_flutter/pages/detail_page.dart'; 13 | 14 | class MySharedPage extends StatefulWidget { 15 | const MySharedPage({Key? key}) : super(key: key); 16 | 17 | @override 18 | State createState() => _MySharedPageState(); 19 | } 20 | 21 | class _MySharedPageState extends State { 22 | int _pageIndex = 1; 23 | 24 | final List _data = []; 25 | 26 | bool _over = true; 27 | 28 | late var dataObs = _data.obs; 29 | 30 | final TextEditingController _titleController = TextEditingController(); 31 | final TextEditingController _linkController = TextEditingController(); 32 | 33 | final EasyRefreshController _refreshController = EasyRefreshController( 34 | controlFinishRefresh: true, controlFinishLoad: true); 35 | 36 | Future _requestData() async { 37 | AppResponse res = 38 | await HttpGo.instance.get("${Api.sharedList}$_pageIndex/json"); 39 | if (res.isSuccessful) { 40 | _over = res.data!.shareArticles.over; 41 | if (_pageIndex == 1) { 42 | _data.clear(); 43 | } 44 | _data.addAll(res.data!.shareArticles.datas); 45 | return true; 46 | } 47 | return false; 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return Scaffold( 53 | appBar: AppBar( 54 | backgroundColor: Theme.of(context).primaryColor, 55 | title: const Text("我的分享", style: TextStyle(color: Colors.white)), 56 | iconTheme: const IconThemeData(color: Colors.white), 57 | actions: [ 58 | Padding( 59 | padding: const EdgeInsets.only(right: 16), 60 | child: GestureDetector( 61 | onTap: _showShareDialog, 62 | child: const Icon(Icons.add, color: Colors.white), 63 | )) 64 | ], 65 | ), 66 | body: FutureBuilder(future: (() async { 67 | _pageIndex = 1; 68 | return await _requestData(); 69 | })(), builder: (context, snapshot) { 70 | if (snapshot.connectionState == ConnectionState.done) { 71 | if (snapshot.data == false) { 72 | return RetryWidget(onTapRetry: () => setState(() {})); 73 | } else { 74 | return _build(); 75 | } 76 | } else { 77 | return Container( 78 | width: double.infinity, 79 | height: double.infinity, 80 | alignment: Alignment.center, 81 | child: const CircularProgressIndicator(), 82 | ); 83 | } 84 | }), 85 | ); 86 | } 87 | 88 | Widget _build() { 89 | return EasyRefresh.builder( 90 | onLoad: _onLoad, 91 | onRefresh: _onRefresh, 92 | controller: _refreshController, 93 | childBuilder: (context, physics) { 94 | return Obx(() { 95 | // ignore: invalid_use_of_protected_member 96 | dataObs.value; 97 | return ListView.builder( 98 | physics: physics, 99 | itemCount: _data.length, 100 | itemBuilder: (context, index) { 101 | ArticleItemEntity itemEntity = _data[index]; 102 | return GestureDetector( 103 | onTap: () { 104 | Get.to(()=> DetailPage(itemEntity.link, itemEntity.title)); 105 | }, 106 | behavior: HitTestBehavior.opaque, 107 | child: ArticleItemLayout( 108 | itemEntity: itemEntity, 109 | onCollectTap: () { 110 | _onCollectClick(itemEntity); 111 | }), 112 | ); 113 | }, 114 | ); 115 | }); 116 | }); 117 | } 118 | 119 | _onLoad() async { 120 | _pageIndex++; 121 | await _requestData(); 122 | _refreshController 123 | .finishLoad(_over ? IndicatorResult.noMore : IndicatorResult.success); 124 | dataObs.refresh(); 125 | } 126 | 127 | _onRefresh() async { 128 | _pageIndex = 1; 129 | await _requestData(); 130 | _refreshController.finishRefresh(); 131 | dataObs.refresh(); 132 | } 133 | 134 | _onCollectClick(ArticleItemEntity itemEntity) async { 135 | bool collected = itemEntity.collect; 136 | AppResponse res = await (collected 137 | ? HttpGo.instance.post("${Api.uncollectArticel}${itemEntity.id}/json") 138 | : HttpGo.instance.post("${Api.collectArticle}${itemEntity.id}/json")); 139 | 140 | if (res.isSuccessful) { 141 | Fluttertoast.showToast(msg: collected ? "取消收藏!" : "收藏成功!"); 142 | itemEntity.collect = !itemEntity.collect; 143 | } else { 144 | Fluttertoast.showToast( 145 | msg: (collected ? "取消失败 -- " : "收藏失败 -- ") + 146 | (res.errorMsg ?? res.errorCode.toString())); 147 | } 148 | } 149 | 150 | _showShareDialog() async { 151 | await showModalBottomSheet( 152 | isScrollControlled: true, 153 | context: context, 154 | builder: (context) { 155 | return AnimatedPadding( 156 | padding: MediaQuery.of(context).viewInsets, 157 | duration: Duration.zero, 158 | child: Column( 159 | mainAxisSize: MainAxisSize.min, 160 | crossAxisAlignment: CrossAxisAlignment.center, 161 | children: [ 162 | Padding( 163 | padding: 164 | const EdgeInsets.only(left: 16, right: 16, top: 32), 165 | child: TextField( 166 | controller: _titleController, 167 | decoration: const InputDecoration( 168 | contentPadding: EdgeInsets.symmetric( 169 | vertical: 4, horizontal: 8), 170 | hintText: "文章标题", 171 | border: OutlineInputBorder( 172 | borderRadius: 173 | BorderRadius.all(Radius.circular(16))))), 174 | ), 175 | Padding( 176 | padding: 177 | const EdgeInsets.only(left: 16, right: 16, top: 16), 178 | child: TextField( 179 | controller: _linkController, 180 | decoration: const InputDecoration( 181 | contentPadding: EdgeInsets.symmetric( 182 | vertical: 4, horizontal: 8), 183 | hintText: "文章链接", 184 | border: OutlineInputBorder( 185 | borderRadius: 186 | BorderRadius.all(Radius.circular(16)))))), 187 | Padding( 188 | padding: const EdgeInsets.only(top: 16, bottom: 16), 189 | child: TextButton( 190 | onPressed: () async { 191 | bool result = await _onShareClick(); 192 | if (result) { 193 | Get.back(); 194 | } 195 | }, 196 | child: Container( 197 | decoration: BoxDecoration( 198 | color: Theme.of(context).primaryColor, 199 | borderRadius: const BorderRadius.all( 200 | Radius.circular(16))), 201 | padding: const EdgeInsets.symmetric( 202 | vertical: 8, horizontal: 16), 203 | child: const Text( 204 | "分享", 205 | style: TextStyle(color: Colors.white), 206 | ), 207 | ))) 208 | ], 209 | )); 210 | }); 211 | } 212 | 213 | Future _onShareClick() async { 214 | FocusScope.of(context).unfocus(); 215 | AppResponse res = await HttpGo.instance.post(Api.shareArticle, 216 | data: {"title": _titleController.text, "link": _linkController.text}); 217 | if (res.isSuccessful) { 218 | // 分享成功 219 | Fluttertoast.showToast(msg: "分享成功!"); 220 | await _onRefresh(); 221 | return true; 222 | } else { 223 | Fluttertoast.showToast(msg: "分享失败--${res.errorMsg}"); 224 | return false; 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /lib/pages/search_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:get/get.dart'; 5 | import 'package:mmkv/mmkv.dart'; 6 | import 'package:wan_android_flutter/base/base_page.dart'; 7 | import 'package:wan_android_flutter/network/api.dart'; 8 | import 'package:wan_android_flutter/network/bean/AppResponse.dart'; 9 | import 'package:wan_android_flutter/network/request_util.dart'; 10 | import 'package:wan_android_flutter/pages/search_result_page.dart'; 11 | import 'package:wan_android_flutter/utils/log_util.dart'; 12 | 13 | import '../network/bean/hot_keyword_entity.dart'; 14 | 15 | class SearchPage extends StatefulWidget { 16 | const SearchPage({Key? key}) : super(key: key); 17 | 18 | @override 19 | State createState() => _SearchPageState(); 20 | } 21 | 22 | class _SearchPageState extends State with BasePage { 23 | static const String historyKey = "historyKey"; 24 | 25 | List histories = []; 26 | 27 | List hotKeywords = []; 28 | 29 | List keywordsColors = [ 30 | const Color(0xffe35454), 31 | const Color(0xff549A3A), 32 | const Color(0xff34856E), 33 | const Color(0xffB59B42), 34 | const Color(0xff9B4BAA), 35 | const Color(0xff4966B1), 36 | ]; 37 | 38 | @override 39 | void initState() { 40 | super.initState(); 41 | try { 42 | var mmkv = MMKV.defaultMMKV(); 43 | String historyContent = mmkv.decodeString(historyKey) ?? ""; 44 | if (historyKey.trim().isNotEmpty) { 45 | histories.addAll((json.decoder.convert(historyContent) as List) 46 | .map((e) => e as String)); 47 | } 48 | } catch (e) { 49 | WanLog.e("load history words error - ${e.toString()}"); 50 | } 51 | _getHotKeywords(); 52 | } 53 | 54 | _getHotKeywords() async { 55 | AppResponse> res = 56 | await HttpGo.instance.get(Api.hotKeywords); 57 | 58 | if (res.isSuccessful) { 59 | setState(() { 60 | hotKeywords.clear(); 61 | hotKeywords.addAll(res.data!); 62 | }); 63 | } 64 | } 65 | 66 | _saveHistoryToLocal() { 67 | try { 68 | var mmkv = MMKV.defaultMMKV(); 69 | String historyContent = json.encoder.convert(histories); 70 | mmkv.encodeString(historyKey, historyContent); 71 | } catch (e) { 72 | WanLog.e("load history words error - ${e.toString()}"); 73 | } 74 | } 75 | 76 | @override 77 | void dispose() { 78 | super.dispose(); 79 | _saveHistoryToLocal(); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | return Scaffold( 85 | appBar: AppBar( 86 | titleSpacing: 0, 87 | backgroundColor: Theme.of(context).primaryColor, 88 | title: Padding( 89 | padding: const EdgeInsets.only(right: 16), 90 | child: MySearchBar( 91 | onSubmit: _onSearch, 92 | )), 93 | iconTheme: const IconThemeData(color: Colors.white), 94 | ), 95 | body: Container( 96 | padding: const EdgeInsets.only(left: 16, right: 16, top: 8), 97 | child: Column( 98 | children: [ 99 | Container( 100 | alignment: Alignment.centerLeft, 101 | width: double.infinity, 102 | margin: const EdgeInsets.only(bottom: 16), 103 | child: Wrap( 104 | direction: Axis.horizontal, 105 | runSpacing: 8, 106 | alignment: WrapAlignment.start, 107 | spacing: 16, 108 | runAlignment: WrapAlignment.center, 109 | children: List.generate(hotKeywords.length, (index) { 110 | return GestureDetector( 111 | onTap: () { 112 | _onSearch(hotKeywords[index].name); 113 | }, 114 | child: Container( 115 | padding: const EdgeInsets.symmetric( 116 | horizontal: 10, vertical: 4), 117 | decoration: BoxDecoration( 118 | borderRadius: BorderRadius.circular(6), 119 | color: keywordsColors[ 120 | index % (keywordsColors.length)]), 121 | child: Text(hotKeywords[index].name, 122 | style: const TextStyle(color: Colors.white)))); 123 | }), 124 | ), 125 | ), 126 | Row( 127 | crossAxisAlignment: CrossAxisAlignment.center, 128 | children: [ 129 | const Text( 130 | "搜索记录", 131 | style: TextStyle(fontSize: 16), 132 | ), 133 | Expanded( 134 | child: Align( 135 | alignment: Alignment.centerRight, 136 | child: GestureDetector( 137 | onTap: _onClearClick, child: const Text("清空")), 138 | ), 139 | ) 140 | ], 141 | ), 142 | Container( 143 | height: 1, 144 | margin: const EdgeInsets.only(top: 16), 145 | width: double.infinity, 146 | decoration: const BoxDecoration(color: Colors.grey), 147 | ), 148 | Expanded( 149 | child: ListView.builder( 150 | itemCount: histories.length, 151 | itemBuilder: (context, index) { 152 | return GestureDetector( 153 | behavior: HitTestBehavior.opaque, 154 | onTap: () { 155 | _onSearch(histories[index]); 156 | }, 157 | child: Container( 158 | padding: const EdgeInsets.symmetric(vertical: 8), 159 | child: Row( 160 | children: [ 161 | Text(histories[index]), 162 | Expanded( 163 | child: GestureDetector( 164 | child: const Align( 165 | alignment: Alignment.centerRight, 166 | child: Icon(Icons.close)), 167 | onTap: () { 168 | _deleteHistory(index); 169 | }, 170 | ), 171 | ) 172 | ], 173 | ))); 174 | }, 175 | )) 176 | ], 177 | ), 178 | )); 179 | } 180 | 181 | _onClearClick() async { 182 | bool? result = await showDialog( 183 | context: context, 184 | builder: (context) { 185 | return AlertDialog( 186 | title: const Text("提示"), 187 | content: const Text("确定要清除所有搜索记录吗?"), 188 | actions: [ 189 | TextButton( 190 | onPressed: () { 191 | Get.back(result: false); 192 | }, 193 | child: const Text("取消")), 194 | TextButton( 195 | onPressed: () { 196 | Get.back(result: true); 197 | }, 198 | child: const Text("确定")), 199 | ], 200 | ); 201 | }); 202 | 203 | if (result == true) { 204 | setState(() { 205 | histories.clear(); 206 | _saveHistoryToLocal(); 207 | }); 208 | } 209 | } 210 | 211 | _deleteHistory(int index) { 212 | setState(() { 213 | histories.removeAt(index); 214 | }); 215 | } 216 | 217 | _onSearch(String content) { 218 | setState(() { 219 | if (histories.contains(content)) { 220 | histories.remove(content); 221 | } 222 | histories.insert(0, content); 223 | }); 224 | Get.to(() => SearchResultPage(keyword: content)); 225 | } 226 | } 227 | 228 | /// -------------------------------搜索框封装↓--------------------------------- 229 | class MySearchBar extends StatefulWidget { 230 | const MySearchBar({super.key, required this.onSubmit}); 231 | 232 | final void Function(String) onSubmit; 233 | 234 | @override 235 | State createState() => _MySearchState(); 236 | } 237 | 238 | class _MySearchState extends State { 239 | @override 240 | Widget build(BuildContext context) { 241 | return Container( 242 | width: double.infinity, 243 | height: 40, 244 | padding: const EdgeInsets.symmetric(horizontal: 16), 245 | alignment: Alignment.centerLeft, 246 | decoration: BoxDecoration( 247 | borderRadius: BorderRadius.circular(8), color: Colors.white), 248 | child: TextField( 249 | autofocus: true, 250 | decoration: const InputDecoration( 251 | hintText: "搜索", 252 | contentPadding: EdgeInsets.only(bottom: 10), 253 | border: InputBorder.none, 254 | icon: Icon(Icons.search), 255 | ), 256 | onSubmitted: (content) { 257 | widget.onSubmit(content); 258 | }, 259 | ), 260 | ); 261 | } 262 | } 263 | --------------------------------------------------------------------------------