├── .gitignore ├── .metadata ├── README.md ├── android ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── judou │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── author_sub.jpg ├── avatar_placeholder.png ├── descovery.png ├── detail_page.jpg ├── discovery_dis.jpg ├── discovery_recommand.jpg ├── discovery_subscribe.jpg ├── home.png ├── index_page.jpg ├── judou.png ├── topics.jpg ├── user_collections.jpg └── user_sentences.jpg ├── fonts ├── Apple-LiSung-Light.ttf └── PingFang-SC-Regular.ttf ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-60.png │ │ ├── Icon-60@2x.png │ │ ├── Icon-60@3x.png │ │ ├── Icon-76.png │ │ ├── Icon-76@2x.png │ │ ├── Icon-Small-1.png │ │ ├── Icon-Small.png │ │ ├── Icon-Small@2x-1.png │ │ ├── Icon-Small@2x.png │ │ ├── Icon-Small@3x.png │ │ ├── Icon-Spotlight-40.png │ │ ├── Icon-Spotlight-40@2x-1.png │ │ ├── Icon-Spotlight-40@2x.png │ │ ├── Icon-Spotlight-40@3x.png │ │ ├── Icon-Spotlight-41.png │ │ ├── Icon-Spotlight-42.png │ │ ├── Icon-Spotlight-43.png │ │ └── Icon-iPadPro@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── JudouTestPlugin.h │ ├── JudouTestPlugin.m │ └── main.m ├── json └── daily.json ├── lib ├── bloc_provider.dart ├── discovery │ ├── BLoc │ │ ├── discovery_bloc.dart │ │ ├── recommand_bloc.dart │ │ └── subscribe_bloc.dart │ ├── models │ │ ├── carousel_model.dart │ │ ├── jotting_model.dart │ │ ├── post_model.dart │ │ ├── subject_model.dart │ │ ├── tabs_model.dart │ │ ├── topic_model.dart │ │ └── video_model.dart │ ├── pages │ │ ├── discovery_page.dart │ │ └── include_page.dart │ └── widget │ │ ├── discovery_card.dart │ │ ├── discovery_widget.dart │ │ ├── recommand_cell.dart │ │ ├── recommand_widget.dart │ │ ├── subscribe_widget.dart │ │ └── topic_header.dart ├── index │ ├── BLoc │ │ ├── detail_bloc.dart │ │ └── index_bloc.dart │ ├── models │ │ ├── author_model.dart │ │ ├── comment_model.dart │ │ ├── image_model.dart │ │ ├── judou_model.dart │ │ ├── tag_model.dart │ │ └── user_model.dart │ ├── pages │ │ ├── detail_page.dart │ │ └── index_page.dart │ └── widgets │ │ ├── detail_label.dart │ │ ├── index_item.dart │ │ └── vertical_text.dart ├── main.dart ├── main_page.dart ├── network │ ├── network.dart │ ├── path.dart │ └── request.dart ├── profile │ ├── Bloc │ │ └── profile_detail_bloc.dart │ ├── models │ │ └── collections_model.dart │ ├── pages │ │ ├── message_page.dart │ │ ├── profile_detail.dart │ │ ├── profile_page.dart │ │ └── subscribes_page.dart │ └── widgets │ │ ├── list_cell.dart │ │ ├── normal_header.dart │ │ ├── subscribes_cell.dart │ │ └── verify_header.dart ├── utils │ ├── color_util.dart │ ├── date_util.dart │ └── ui_util.dart └── widgets │ ├── SliverAppBarDelegate.dart │ ├── blank.dart │ ├── button_subscript.dart │ ├── collection_cell.dart │ ├── comment_cell.dart │ ├── end_cell.dart │ ├── image_preview.dart │ ├── jottings_cell.dart │ ├── judou_cell.dart │ ├── label.dart │ ├── loading.dart │ ├── radius_image.dart │ └── user_info_tile.dart ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | gank.io 3 |

4 |

5 | 6 | 7 | 8 | 9 | 10 |

11 | 12 | ## JuDou 13 | 14 | Judou is my favorite app,so I implemented it in flutter 15 | 16 | ## UI 17 | 18 | | ![1](https://github.com/CrazyCoderShi/judou/blob/master/assets/index_page.jpg) | ![2](https://github.com/CrazyCoderShi/judou/blob/master/assets/detail_page.jpg) | ![3](https://github.com/CrazyCoderShi/judou/blob/master/assets/discovery_subscribe.jpg) | ![4](https://github.com/CrazyCoderShi/judou/blob/master/assets/discovery_dis.jpg) | 19 | | :--: | :--: | :--: | :--: | 20 | | 首页 | 详情 | 订阅 | 发现 | 21 | 22 | | ![1](https://github.com/CrazyCoderShi/judou/blob/master/assets/author_sub.jpg) | ![2](https://github.com/CrazyCoderShi/judou/blob/master/assets/user_sentences.jpg) | ![3](https://github.com/CrazyCoderShi/judou/blob/master/assets/user_collections.jpg) | ![4](https://github.com/CrazyCoderShi/judou/blob/master/assets/topics.jpg) | 23 | | :--: | :--: | :--: | :--: | 24 | | 订阅作者 | 用户句子 | 用户收藏 | 话题 | 25 | 26 | ## Getting Started 27 | 28 | This project is a starting point for a Flutter application. 29 | 30 | A few resources to get you started if this is your first Flutter project: 31 | 32 | - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) 33 | - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) 34 | 35 | For help getting started with Flutter, view our 36 | [online documentation](https://flutter.io/docs), which offers tutorials, 37 | samples, guidance on mobile development, and a full API reference. 38 | 39 | ## BLoc Architecture 40 | 41 | Fully implemented using the BLoc architecture, 42 | If you are not familiar with BLoc, the following resources can help you. 43 | 44 | - [Architect your Flutter project using BLOC pattern](https://medium.com/flutterpub/architecting-your-flutter-project-bd04e144a8f1) 45 | - [Reactive Programming - Streams - BLoC](https://www.didierboelens.com/2018/08/reactive-programming---streams---bloc/) 46 | - [Technical Debt and Streams/BLoC](https://www.youtube.com/watch?v=fahC3ky_zW0)🎬 47 | - [Flutter / AngularDart – Code sharing, better together](https://www.youtube.com/watch?v=PLHln7wHgPE)🎬 48 | 49 | ## Desktop 50 | 51 | Currently only supports macOS 52 | 53 | Open FeatherApp.xcodeproj file with Xcode, then cmd + R 54 | 55 | ## Thanks 56 | 57 | - [RxDart](https://github.com/ReactiveX/rxdart) 58 | - [RxCommand](https://github.com/fluttercommunity/rx_command) 59 | - [Dio](https://github.com/flutterchina/dio) 60 | - [flutter_page_transition](https://github.com/kalismeras61/flutter_page_transition) 61 | 62 | ## License 63 | 64 | MIT License 65 | 66 | Copyright (c) 2018 CrazyCoderShi 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining a copy 69 | of this software and associated documentation files (the "Software"), to deal 70 | in the Software without restriction, including without limitation the rights 71 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 72 | copies of the Software, and to permit persons to whom the Software is 73 | furnished to do so, subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all 76 | copies or substantial portions of the Software. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 80 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 81 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 82 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 83 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 84 | SOFTWARE. 85 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 27 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.example.judou" 37 | minSdkVersion 16 38 | targetSdkVersion 27 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/judou/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.judou; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends FlutterActivity { 8 | @Override 9 | protected void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | GeneratedPluginRegistrant.registerWith(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.2.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/author_sub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/author_sub.jpg -------------------------------------------------------------------------------- /assets/avatar_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/avatar_placeholder.png -------------------------------------------------------------------------------- /assets/descovery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/descovery.png -------------------------------------------------------------------------------- /assets/detail_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/detail_page.jpg -------------------------------------------------------------------------------- /assets/discovery_dis.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/discovery_dis.jpg -------------------------------------------------------------------------------- /assets/discovery_recommand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/discovery_recommand.jpg -------------------------------------------------------------------------------- /assets/discovery_subscribe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/discovery_subscribe.jpg -------------------------------------------------------------------------------- /assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/home.png -------------------------------------------------------------------------------- /assets/index_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/index_page.jpg -------------------------------------------------------------------------------- /assets/judou.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/judou.png -------------------------------------------------------------------------------- /assets/topics.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/topics.jpg -------------------------------------------------------------------------------- /assets/user_collections.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/user_collections.jpg -------------------------------------------------------------------------------- /assets/user_sentences.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/assets/user_sentences.jpg -------------------------------------------------------------------------------- /fonts/Apple-LiSung-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/fonts/Apple-LiSung-Light.ttf -------------------------------------------------------------------------------- /fonts/PingFang-SC-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/fonts/PingFang-SC-Regular.ttf -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | pods_ary = [] 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) { |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | pods_ary.push({:name => podname, :path => podpath}); 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | } 32 | return pods_ary 33 | end 34 | 35 | target 'Runner' do 36 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 37 | # referring to absolute paths on developers' machines. 38 | system('rm -rf .symlinks') 39 | system('mkdir -p .symlinks/plugins') 40 | 41 | # Flutter Pods 42 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig') 43 | if generated_xcode_build_settings.empty? 44 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first." 45 | end 46 | generated_xcode_build_settings.map { |p| 47 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR' 48 | symlink = File.join('.symlinks', 'flutter') 49 | File.symlink(File.dirname(p[:path]), symlink) 50 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path])) 51 | end 52 | } 53 | 54 | # Plugin Pods 55 | plugin_pods = parse_KV_file('../.flutter-plugins') 56 | plugin_pods.map { |p| 57 | symlink = File.join('.symlinks', 'plugins', p[:name]) 58 | File.symlink(p[:path], symlink) 59 | pod p[:name], :path => File.join(symlink, 'ios') 60 | } 61 | end 62 | 63 | post_install do |installer| 64 | installer.pods_project.targets.each do |target| 65 | target.build_configurations.each do |config| 66 | config.build_settings['ENABLE_BITCODE'] = 'NO' 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | #include 4 | 5 | @implementation AppDelegate 6 | 7 | - (BOOL)application:(UIApplication *)application 8 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 9 | [GeneratedPluginRegistrant registerWithRegistry:self]; 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-Spotlight-40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-Small-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-Small@2x-1.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-Small@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-Spotlight-40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-Spotlight-40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-Spotlight-43.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-Spotlight-42.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-Small.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-Small@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-Spotlight-41.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-Spotlight-40@2x-1.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-76.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-iPadPro@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "idiom" : "ios-marketing", 113 | "size" : "1024x1024", 114 | "scale" : "1x" 115 | } 116 | ], 117 | "info" : { 118 | "version" : 1, 119 | "author" : "xcode" 120 | } 121 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-42.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-43.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrazyCoderShi/judou/7a00e1e30c3cb3f87f8c7fd6cc4b545b6c902742/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | 句读 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | judou 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 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/JudouTestPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // JudouTestPlugin.h 3 | // Runner 4 | // 5 | // Created by 天南 on 2019/2/18. 6 | // Copyright © 2019 The Chromium Authors. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface JudouTestPlugin : NSObject 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /ios/Runner/JudouTestPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // JudouTestPlugin.m 3 | // Runner 4 | // 5 | // Created by 天南 on 2019/2/18. 6 | // Copyright © 2019 The Chromium Authors. All rights reserved. 7 | // 8 | 9 | #import "JudouTestPlugin.h" 10 | 11 | @implementation JudouTestPlugin 12 | 13 | + (void)registerWithRegistrar:(NSObject *)registrar { 14 | FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"judou.test" binaryMessenger:registrar.messenger]; 15 | [channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) { 16 | if ([call.method isEqualToString:@"getString"]) { 17 | result([self getString]); 18 | } else { 19 | result(FlutterMethodNotImplemented); 20 | } 21 | }]; 22 | } 23 | 24 | + (NSString *)getString { 25 | NSLog(@"调用回来之前..."); 26 | return @"我回来了..."; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/bloc_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | abstract class BlocBase { 4 | void dispose(); 5 | } 6 | 7 | // Generic BLoC provider 8 | class BlocProvider extends StatefulWidget { 9 | BlocProvider({ 10 | Key key, 11 | @required this.child, 12 | @required this.bloc, 13 | }) : super(key: key); 14 | 15 | final T bloc; 16 | final Widget child; 17 | 18 | @override 19 | _BlocProviderState createState() => _BlocProviderState(); 20 | 21 | static T of(BuildContext context) { 22 | final type = _typeOf>(); 23 | BlocProvider provider = context.ancestorWidgetOfExactType(type); 24 | return provider.bloc; 25 | } 26 | 27 | static Type _typeOf() => T; 28 | } 29 | 30 | class _BlocProviderState extends State> { 31 | @override 32 | void dispose() { 33 | widget.bloc.dispose(); 34 | super.dispose(); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return widget.child; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/discovery/BLoc/discovery_bloc.dart: -------------------------------------------------------------------------------- 1 | import '../models/topic_model.dart'; 2 | import '../../network/network.dart'; 3 | import '../../index/models/tag_model.dart'; 4 | import '../../index/models/judou_model.dart'; 5 | import 'dart:async'; 6 | 7 | class DiscoveryBloc implements BlocBase { 8 | final _discoverySubject = PublishSubject>(); 9 | List _topics; 10 | List tags; 11 | List _tagListData; 12 | DiscoveryBloc() { 13 | _fetchData(); 14 | } 15 | 16 | /// 获取tab标题相关数据 17 | /// 整个方法已经在最新的版本中废弃 18 | // void _fetchTitle() async { 19 | // List tabs = await Request.instance.dio 20 | // .get(RequestPath.channels()) 21 | // .then((response) => response.data['data'] as List) 22 | // .then((response) => 23 | // response.map((item) => TabModel.fromJSON(item)).toList()); 24 | // tabSubject.sink.add(tabs); 25 | // } 26 | 27 | Stream> get stream => _discoverySubject.stream; 28 | 29 | /// 拉取当前页面的数据, 并进行组装, 最终返回的是一个Map 30 | /// topics -> 话题数据 31 | /// tags -> 中间tag标题 32 | /// tagListData -> 某一个tag的数据 33 | void _fetchData() async { 34 | _topics = await Request.instance.dio 35 | .get(RequestPath.topicData()) 36 | .then((response) => response.data['data'] as List) 37 | .then((response) => 38 | response.map((item) => TopicModel.fromJSON(item)).toList()); 39 | 40 | tags = await Request.instance.dio 41 | .get(RequestPath.discoveryTags()) 42 | .then((response) => response.data['data'] as List) 43 | .then((response) => 44 | response.map((item) => TagModel.fromJSON(item)).toList()); 45 | 46 | fetchTagListDataWithId('${tags[0].id}'); 47 | } 48 | 49 | /// 根据[id]获取某个tag下的数据 50 | /// [id] -> tagId 51 | void fetchTagListDataWithId(String id) async { 52 | _tagListData = await Request.instance.dio 53 | .get(RequestPath.dataWithTagId(id)) 54 | .then((response) => response.data['data'] as List) 55 | .then((response) => response.where((item) => !item['is_ad']).toList()) 56 | .then((response) => 57 | response.map((item) => JuDouModel.fromJson(item)).toList()); 58 | Map map = { 59 | 'topics': _topics, 60 | 'tags': tags, 61 | 'tagListData': _tagListData 62 | }; 63 | if (!_discoverySubject.isClosed) { 64 | _discoverySubject.sink.add(map); 65 | } 66 | } 67 | 68 | @override 69 | dispose() { 70 | if (!_discoverySubject.isClosed) _discoverySubject.close(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/discovery/BLoc/recommand_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import '../../network/network.dart'; 3 | import '../models/post_model.dart'; 4 | import '../models/video_model.dart'; 5 | import '../models/subject_model.dart'; 6 | import '../models/carousel_model.dart'; 7 | import '../../index/models/judou_model.dart'; 8 | 9 | class RecommandBloc implements BlocBase { 10 | final _fetchSubject = PublishSubject>(); 11 | 12 | RecommandBloc() { 13 | _fetchData(); 14 | } 15 | 16 | Stream> get stream => _fetchSubject.stream; 17 | 18 | void _fetchData() async { 19 | // 推荐 20 | Map recommands = await Request.instance.dio 21 | .get(RequestPath.recommand()) 22 | .then((response) => response.data) 23 | .then((response) { 24 | List l1 = response['posts'] as List; 25 | List l2 = response['subjects'] as List; 26 | List l3 = response['videos'] as List; 27 | 28 | List posts = 29 | l1.map((item) => PostModel.fromJSON(item)).toList(); 30 | List subjects = 31 | l2.map((item) => SubjectModel.fromJSON(item)).toList(); 32 | List videos = 33 | l3.map((item) => VideoModel.fromJSON(item)).toList(); 34 | return {'posts': posts, 'subjects': subjects, 'videos': videos}; 35 | }); 36 | // 轮播 37 | List carousels = await Request.instance.dio 38 | .get(RequestPath.carousels()) 39 | .then((response) => response.data) 40 | .then((response) => response['data'] as List) 41 | .then((response) => 42 | response.map((item) => CarouselModel.fromJSON(item)).toList()); 43 | 44 | // 今日哲思 45 | List today = await Request.instance.dio 46 | .get(RequestPath.todayThink()) 47 | .then((response) => response.data) 48 | .then((response) => response['data'] as List) 49 | .then((response) => 50 | response.map((item) => JuDouModel.fromJson(item)).toList()); 51 | 52 | /// 最后的所有数据组装完毕 53 | /// posts -> List 54 | /// subjects -> List 55 | /// videos -> List 56 | /// carousels -> List 57 | recommands.addAll({'carousels': carousels, 'today': today}); 58 | if (!_fetchSubject.isClosed) { 59 | _fetchSubject.sink.add(recommands); 60 | } 61 | } 62 | 63 | @override 64 | dispose() { 65 | if (!_fetchSubject.isClosed) { 66 | _fetchSubject.close(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/discovery/BLoc/subscribe_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import '../../network/network.dart'; 3 | import '../../index/pages/detail_page.dart'; 4 | import 'package:flutter/material.dart'; 5 | import '../../index/models/judou_model.dart'; 6 | 7 | class SubscribeBloc implements BlocBase { 8 | final _fetchSubject = PublishSubject>(); 9 | 10 | SubscribeBloc() { 11 | this._fetchData(); 12 | } 13 | 14 | Stream> get stream => _fetchSubject.stream; 15 | 16 | void _fetchData() async { 17 | List dataList = await Request.instance.dio 18 | .get(RequestPath.channelWithId('12')) 19 | .then((response) => response.data['data'] as List) 20 | .then((response) => response.where((item) => !item['is_ad']).toList()) 21 | .then((response) => 22 | response.map((item) => JuDouModel.fromJson(item)).toList()); 23 | if (!_fetchSubject.isClosed) { 24 | _fetchSubject.sink.add(dataList); 25 | } 26 | } 27 | 28 | /// to detail page 29 | void toDetailPage(BuildContext context, JuDouModel model) { 30 | Navigator.push( 31 | context, 32 | MaterialPageRoute(builder: (context) => DetailPage(model: model)), 33 | ); 34 | } 35 | 36 | @override 37 | dispose() { 38 | if (!_fetchSubject.isClosed) _fetchSubject.close(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/discovery/models/carousel_model.dart: -------------------------------------------------------------------------------- 1 | class CarouselModel { 2 | final int id; 3 | final String title; 4 | final String summary; 5 | final String cover; 6 | final String schemeUrl; 7 | final String actionType; 8 | final int weight; 9 | final String createdAt; 10 | 11 | CarouselModel({ 12 | this.id, 13 | this.title, 14 | this.summary, 15 | this.cover, 16 | this.schemeUrl, 17 | this.actionType, 18 | this.weight, 19 | this.createdAt, 20 | }); 21 | 22 | factory CarouselModel.fromJSON(Map json) { 23 | return CarouselModel( 24 | id: json['id'], 25 | title: json['title'], 26 | summary: json['summary'], 27 | cover: json['cover'], 28 | schemeUrl: json['scheme_url'], 29 | actionType: json['action_type'], 30 | weight: json['weight'], 31 | createdAt: json['created_at'], 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/discovery/models/jotting_model.dart: -------------------------------------------------------------------------------- 1 | class JuttingModel { 2 | final String summary; 3 | final int id; 4 | final String author; 5 | final int hits; 6 | final String publishedAt; 7 | final String uuid; 8 | final String title; 9 | final String url; 10 | final String banner; 11 | final bool isShowBanner; 12 | 13 | JuttingModel( 14 | {this.summary, 15 | this.id, 16 | this.author, 17 | this.hits, 18 | this.publishedAt, 19 | this.uuid, 20 | this.title, 21 | this.url, 22 | this.banner, 23 | this.isShowBanner}); 24 | 25 | factory JuttingModel.fromJSON(Map json) { 26 | return JuttingModel( 27 | summary: json['summary'] as String, 28 | id: json['id'] as int, 29 | author: json['author'] as String, 30 | hits: json['hits'] as int, 31 | publishedAt: json['published_at'] as String, 32 | uuid: json['uuid'] as String, 33 | title: json['title'] as String, 34 | url: json['url'] as String, 35 | banner: json['banner'] as String, 36 | isShowBanner: json['is_show_banner'] as bool, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/discovery/models/post_model.dart: -------------------------------------------------------------------------------- 1 | // "id": 547, 2 | // "summary": "我也许感到有一点寂寞,回想我刚才瞥见的这种幸福家庭生活,心里不无艳羡之感。", 3 | // "banner": "https://judou.oss-cn-beijing.aliyuncs.com/images/post/2019/1/7/ablrxblg5.jpeg", 4 | // "author": "毛姆", 5 | // "is_show_banner": true, 6 | // "published_at": "2019-01-09T00:00:00.000+08:00", 7 | // "title": "幸福", 8 | // "url": "https://judouapp.com/p/547", 9 | // "hits": 258, 10 | // "comment_count": 3, 11 | // "like_count": 6, 12 | // "is_liked": false, 13 | // "is_disabled_comment": false 14 | 15 | class PostModel { 16 | final int id; 17 | final String title; 18 | final int commentCount; 19 | final int hits; 20 | final String author; 21 | final String summary; 22 | final String banner; 23 | final String url; 24 | final int likeCount; 25 | final bool isLiked; 26 | final String publishedAt; 27 | 28 | PostModel({ 29 | this.id, 30 | this.title, 31 | this.author, 32 | this.commentCount, 33 | this.likeCount, 34 | this.summary, 35 | this.banner, 36 | this.url, 37 | this.hits, 38 | this.isLiked, 39 | this.publishedAt, 40 | }); 41 | 42 | factory PostModel.fromJSON(Map json) { 43 | return PostModel( 44 | id: json['id'], 45 | title: json['title'], 46 | author: json['author'], 47 | commentCount: json['comment_count'], 48 | likeCount: json['like_count'], 49 | summary: json['summary'], 50 | banner: json['banner'], 51 | url: json['url'], 52 | hits: json['hits'], 53 | isLiked: json['is_liked'], 54 | publishedAt: json['published_at'], 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/discovery/models/subject_model.dart: -------------------------------------------------------------------------------- 1 | // "id": 124, 2 | // "uuid": "3e18be3f-3994-4e51-a3ee-f26955fc40c0", 3 | // "title": "哪一句话,让你突然就放弃Ta了?", 4 | // "summary": "你有没有喜欢过一个人,很久很久,以为自己会一直坚持下去,结果却在多年后的某一天,因为Ta或者别人的一句话,你就突然释怀了,选择了放弃。谁都懂坚持的甜蜜,但不是谁能理解放手的心酸。", 5 | // "attendees_count": 488, 6 | // "comments_count": 318, 7 | // "published_at": "2019-01-07T00:00:00.000+08:00", 8 | // "share_url": "http://m.judouapp.com/subjects/124", 9 | // "cover": "https://judou.oss-cn-beijing.aliyuncs.com/images/subject/2019/1/2/inx9nz81.jpeg", 10 | // "hits": 8 11 | 12 | class SubjectModel { 13 | final int id; 14 | final String title; 15 | final String uuid; 16 | final int attendeesCount; 17 | final int commentsCount; 18 | final String summary; 19 | final String cover; 20 | final int hits; 21 | final String shareUrl; 22 | final String publishedAt; 23 | 24 | SubjectModel({ 25 | this.id, 26 | this.title, 27 | this.uuid, 28 | this.attendeesCount, 29 | this.commentsCount, 30 | this.summary, 31 | this.cover, 32 | this.hits, 33 | this.shareUrl, 34 | this.publishedAt, 35 | }); 36 | 37 | factory SubjectModel.fromJSON(Map json) { 38 | return SubjectModel( 39 | id: json['id'], 40 | title: json['title'], 41 | uuid: json['uuid'], 42 | attendeesCount: json['attendees_count'], 43 | commentsCount: json['comments_count'], 44 | summary: json['summary'], 45 | cover: json['cover'], 46 | hits: json['hits'], 47 | shareUrl: json['share_url'], 48 | publishedAt: json['published_at'], 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/discovery/models/tabs_model.dart: -------------------------------------------------------------------------------- 1 | class TabModel { 2 | final int id; 3 | final String name; 4 | 5 | TabModel({this.id, this.name}); 6 | 7 | factory TabModel.fromJSON(Map json) { 8 | return TabModel(id: json['id'] as int, name: json['name'] as String); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/discovery/models/topic_model.dart: -------------------------------------------------------------------------------- 1 | class TopicModel { 2 | final String schemeUrl; 3 | final String name; 4 | final String uuid; 5 | final String cover; 6 | final String description; 7 | 8 | TopicModel({ 9 | this.schemeUrl, 10 | this.name, 11 | this.uuid, 12 | this.cover, 13 | this.description, 14 | }); 15 | 16 | factory TopicModel.fromJSON(Map json) { 17 | return TopicModel( 18 | schemeUrl: json['scheme_url'] as String, 19 | name: json['name'] as String, 20 | uuid: json['uuid'] as String, 21 | cover: json['cover'] as String, 22 | description: json['description'] as String, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/discovery/models/video_model.dart: -------------------------------------------------------------------------------- 1 | // "id": 58, 2 | // "title": "分享才是快乐的意义", 3 | // "source": "", 4 | // "source_link": "", 5 | // "is_published": true, 6 | // "comments_count": 1, 7 | // "likes_count": 0, 8 | // "video_length": 339, 9 | // "summary": "短片中的主人公每天重复着同样的生活,也包括天天买彩票都中头奖。当他明白和他人分享才能获得快乐之前,他已经对于天天中头奖这件事感到厌倦了。你心里的 OS 是不是:他不要放着我来!", 10 | // "cover": "https://judou.oss-cn-beijing.aliyuncs.com/images/video/2019/1/3/1p33xa522u.jpeg", 11 | // "orig_source": "http://video.judouapp.com/sv/33e01ad-16812081b1e/33e01ad-16812081b1e.mp4?auth_key=1547040618-0-0-8a5d82ba60d7a30de4873862930a5814", 12 | // "user": null, 13 | // "share_url": "https://judouapp.com", 14 | // "like_count": 9, 15 | // "is_liked": false 16 | class VideoModel { 17 | final int id; 18 | final String title; 19 | final String source; 20 | final String sourceLink; 21 | final bool isPublished; 22 | final int commentsCount; 23 | final int likesCount; 24 | final int videoLength; 25 | final String summary; 26 | final String cover; 27 | final String origSource; 28 | final String shareUrl; 29 | final int likeCount; 30 | final bool isLiked; 31 | 32 | VideoModel({ 33 | this.id, 34 | this.title, 35 | this.source, 36 | this.sourceLink, 37 | this.isPublished, 38 | this.commentsCount, 39 | this.likeCount, 40 | this.videoLength, 41 | this.summary, 42 | this.cover, 43 | this.origSource, 44 | this.shareUrl, 45 | this.likesCount, 46 | this.isLiked, 47 | }); 48 | 49 | factory VideoModel.fromJSON(Map json) { 50 | return VideoModel( 51 | id: json['id'], 52 | title: json['title'], 53 | source: json['source'], 54 | sourceLink: json['source_link'], 55 | isPublished: json['is_published'], 56 | commentsCount: json['comments_count'], 57 | likeCount: json['like_count'], 58 | videoLength: json['video_length'], 59 | summary: json['summary'], 60 | cover: json['cover'], 61 | origSource: json['orig_source'], 62 | shareUrl: json['share_url'], 63 | likesCount: json['likes_count'], 64 | isLiked: json['is_liked'], 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/discovery/pages/discovery_page.dart: -------------------------------------------------------------------------------- 1 | import './include_page.dart'; 2 | import '../../utils/color_util.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import '../widget/subscribe_widget.dart'; 6 | import '../widget/recommand_widget.dart'; 7 | import '../widget/discovery_widget.dart'; 8 | import 'package:page_transition/page_transition.dart'; 9 | 10 | class DiscoveryPage extends StatefulWidget { 11 | @override 12 | _DiscoveryPageState createState() => _DiscoveryPageState(); 13 | } 14 | 15 | class _DiscoveryPageState extends State 16 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 17 | TabController _topController; 18 | List tabs = [Tab(text: '订阅'), Tab(text: '发现'), Tab(text: '推荐')]; 19 | 20 | @override 21 | void initState() { 22 | super.initState(); 23 | _topController = TabController(vsync: this, length: 3); 24 | _topController.index = 1; 25 | } 26 | 27 | @override 28 | bool get wantKeepAlive => true; 29 | 30 | @override 31 | void dispose() { 32 | _topController.dispose(); 33 | super.dispose(); 34 | } 35 | 36 | void _toIncludePage() { 37 | Navigator.of(context).push(PageTransition( 38 | type: PageTransitionType.downToUp, 39 | child: IncludePage(), 40 | )); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | super.build(context); 46 | return Scaffold( 47 | appBar: AppBar( 48 | centerTitle: true, 49 | title: TabBar( 50 | controller: _topController, 51 | tabs: tabs, 52 | indicatorColor: Colors.yellow, 53 | indicatorSize: TabBarIndicatorSize.label, 54 | unselectedLabelColor: ColorUtils.textGreyColor, 55 | labelStyle: TextStyle(fontSize: 16), 56 | labelPadding: EdgeInsets.symmetric(horizontal: 4), 57 | ), 58 | actions: [ 59 | IconButton( 60 | icon: Icon(Icons.search), 61 | onPressed: () => print('搜索'), 62 | ), 63 | ], 64 | ), 65 | body: TabBarView( 66 | controller: _topController, 67 | children: [ 68 | DiscoverySubscribe(), 69 | Discovery(), 70 | DiscoveryRecommand() 71 | ], 72 | ), 73 | floatingActionButton: Container( 74 | width: 40, 75 | height: 40, 76 | decoration: BoxDecoration( 77 | shape: BoxShape.rectangle, 78 | color: ColorUtils.textPrimaryColor, 79 | borderRadius: BorderRadius.circular(20), 80 | ), 81 | child: IconButton( 82 | icon: Icon(Icons.add), 83 | color: Colors.white, 84 | onPressed: _toIncludePage, 85 | ), 86 | ), 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/discovery/pages/include_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class IncludePage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('收录'), 9 | leading: IconButton( 10 | icon: Icon(Icons.close), 11 | onPressed: () => Navigator.pop(context), 12 | ), 13 | ), 14 | body: Center( 15 | child: Text('收录句子'), 16 | ), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/discovery/widget/discovery_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../widgets/radius_image.dart'; 3 | import '../../profile/pages/profile_detail.dart'; 4 | 5 | class DiscoveryCard extends StatelessWidget { 6 | DiscoveryCard({ 7 | Key key, 8 | this.isLeading, 9 | this.isTrailing, 10 | this.title, 11 | this.imageUrl, 12 | this.height, 13 | this.width, 14 | this.id, 15 | }) : super(key: key); 16 | 17 | final bool isLeading; 18 | final bool isTrailing; 19 | final String title; 20 | final String imageUrl; 21 | final double height; 22 | final double width; 23 | final String id; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Card( 28 | margin: 29 | EdgeInsets.only(left: isLeading ? 15 : 4, right: isTrailing ? 15 : 4), 30 | child: GestureDetector( 31 | child: Stack( 32 | children: [ 33 | SizedBox( 34 | child: Container( 35 | foregroundDecoration: BoxDecoration( 36 | gradient: LinearGradient( 37 | colors: [ 38 | Colors.black, 39 | Colors.black26, 40 | Colors.black12, 41 | Colors.transparent 42 | ], 43 | begin: Alignment.bottomCenter, 44 | end: Alignment.topCenter, 45 | ), 46 | borderRadius: BorderRadius.all( 47 | Radius.circular(3), 48 | ), 49 | ), 50 | width: width, 51 | child: RadiusImage( 52 | imageUrl: imageUrl, 53 | radius: 3, 54 | width: width, 55 | height: height, 56 | ), 57 | ), 58 | ), 59 | Positioned( 60 | bottom: 8, 61 | left: 8, 62 | right: 8, 63 | child: Row( 64 | mainAxisSize: MainAxisSize.max, 65 | mainAxisAlignment: MainAxisAlignment.center, 66 | children: [ 67 | Container( 68 | width: 84, 69 | child: Text( 70 | title, 71 | style: TextStyle(color: Colors.white, fontSize: 12), 72 | textAlign: TextAlign.center, 73 | maxLines: 2, 74 | ), 75 | ) 76 | ], 77 | ), 78 | ), 79 | ], 80 | ), 81 | onTap: () { 82 | Navigator.push( 83 | context, 84 | MaterialPageRoute( 85 | builder: (_) => ProfileDetailPage(type: 2, id: id)), 86 | ); 87 | }, 88 | ), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/discovery/widget/discovery_widget.dart: -------------------------------------------------------------------------------- 1 | import './discovery_card.dart'; 2 | import '../../widgets/blank.dart'; 3 | import '../../bloc_provider.dart'; 4 | import '../../widgets/loading.dart'; 5 | import '../models/topic_model.dart'; 6 | import '../BLoc/discovery_bloc.dart'; 7 | import '../../utils/color_util.dart'; 8 | import 'package:flutter/material.dart'; 9 | import '../../widgets/judou_cell.dart'; 10 | import '../../index/models/tag_model.dart'; 11 | import '../../index/models/judou_model.dart'; 12 | 13 | class Discovery extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return BlocProvider( 17 | bloc: DiscoveryBloc(), 18 | child: DiscoveryWidget(), 19 | ); 20 | } 21 | } 22 | 23 | class DiscoveryWidget extends StatefulWidget { 24 | @override 25 | _DiscoveryWidgetState createState() => _DiscoveryWidgetState(); 26 | } 27 | 28 | class _DiscoveryWidgetState extends State 29 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 30 | ScrollController _scrollController; 31 | TabController _controller; 32 | DiscoveryBloc _bloc; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _bloc = BlocProvider.of(context); 38 | _scrollController = ScrollController(); 39 | _scrollController.addListener(_scrollListener); 40 | 41 | _controller = TabController(vsync: this, length: 9); 42 | _controller.addListener(() { 43 | if (_controller.indexIsChanging) { 44 | _bloc.fetchTagListDataWithId('${_bloc.tags[_controller.index].id}'); 45 | } 46 | }); 47 | } 48 | 49 | @override 50 | bool get wantKeepAlive => true; 51 | 52 | @override 53 | void dispose() { 54 | _controller.dispose(); 55 | _bloc.dispose(); 56 | super.dispose(); 57 | } 58 | 59 | _scrollListener() { 60 | // print(_scrollController.position.extentInside); 61 | } 62 | 63 | List _tagWidgets(List tags) { 64 | return tags 65 | .map((item) => Tab( 66 | text: item.name, 67 | )) 68 | .toList(); 69 | } 70 | 71 | List _tabBarViews(List tags, List tagListData) { 72 | return _tagWidgets(tags).map( 73 | (item) { 74 | return ListView.builder( 75 | itemBuilder: (context, index) => JuDouCell( 76 | model: tagListData[index], 77 | divider: Blank(height: 10), 78 | tag: 'discovery$index', 79 | isCell: true, 80 | ), 81 | itemCount: tagListData.length, 82 | physics: AlwaysScrollableScrollPhysics(), 83 | controller: _scrollController, 84 | ); 85 | }, 86 | ).toList(); 87 | } 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | super.build(context); 92 | return StreamBuilder( 93 | stream: _bloc.stream, 94 | builder: (context, snapshot) { 95 | if (snapshot.connectionState != ConnectionState.active) { 96 | return Loading(); 97 | } else { 98 | return Container( 99 | color: Colors.white, 100 | child: Column( 101 | children: [ 102 | _DiscoverTopicsWidget( 103 | topics: snapshot.data['topics'], 104 | ), 105 | Blank(height: 5), 106 | Container( 107 | height: 35, 108 | child: TabBar( 109 | isScrollable: true, 110 | controller: _controller, 111 | tabs: _tagWidgets(snapshot.data['tags']), 112 | indicatorSize: TabBarIndicatorSize.label, 113 | indicatorColor: Colors.white, 114 | unselectedLabelColor: ColorUtils.textGreyColor, 115 | labelColor: ColorUtils.textPrimaryColor, 116 | labelStyle: TextStyle(fontSize: 14), 117 | ), 118 | ), 119 | Blank(height: 1), 120 | Expanded( 121 | child: TabBarView( 122 | controller: _controller, 123 | children: _tabBarViews( 124 | snapshot.data['tags'], 125 | snapshot.data['tagListData'], 126 | ), 127 | ), 128 | ) 129 | ], 130 | ), 131 | ); 132 | } 133 | }, 134 | ); 135 | } 136 | } 137 | 138 | class _DiscoverTopicsWidget extends StatelessWidget { 139 | _DiscoverTopicsWidget({this.topics}); 140 | 141 | final List topics; 142 | 143 | @override 144 | Widget build(BuildContext context) { 145 | return Container( 146 | padding: EdgeInsets.only(top: 15, bottom: 15), 147 | height: 100, 148 | color: Colors.white, 149 | child: ListView.builder( 150 | scrollDirection: Axis.horizontal, 151 | itemCount: topics.length, 152 | physics: AlwaysScrollableScrollPhysics(), 153 | itemBuilder: ((context, index) { 154 | TopicModel model = topics[index]; 155 | return DiscoveryCard( 156 | isLeading: index == 0, 157 | isTrailing: index == topics.length - 1, 158 | title: model.name, 159 | imageUrl: model.cover, 160 | height: 70, 161 | width: 100, 162 | id: model.uuid, 163 | ); 164 | }), 165 | ), 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/discovery/widget/recommand_cell.dart: -------------------------------------------------------------------------------- 1 | import '../../widgets/blank.dart'; 2 | import '../../utils/color_util.dart'; 3 | import 'package:flutter/material.dart'; 4 | import '../../widgets/radius_image.dart'; 5 | 6 | final TextStyle _textStyle = 7 | TextStyle(fontSize: 12, color: ColorUtils.textGreyColor); 8 | 9 | class RecommandCell extends StatelessWidget { 10 | RecommandCell({ 11 | this.isVideo, 12 | this.imageUrl, 13 | this.title, 14 | this.subTitle, 15 | this.content, 16 | }); 17 | 18 | final bool isVideo; 19 | final String imageUrl; 20 | final String title; 21 | final String subTitle; 22 | final String content; 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Column( 27 | children: [ 28 | Row( 29 | children: [ 30 | Stack( 31 | children: [ 32 | RadiusImage( 33 | imageUrl: imageUrl, 34 | width: 100, 35 | height: 70, 36 | radius: 0, 37 | ), 38 | isVideo 39 | ? Positioned( 40 | left: 38, 41 | top: 23, 42 | child: Icon( 43 | Icons.play_circle_filled, 44 | color: Colors.white54, 45 | )) 46 | : Container() 47 | ], 48 | ), 49 | Container( 50 | width: MediaQuery.of(context).size.width - 145, 51 | padding: EdgeInsets.only(left: 15), 52 | child: Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 55 | children: [ 56 | Padding( 57 | child: Text( 58 | title, 59 | style: TextStyle(fontSize: 14), 60 | maxLines: 1, 61 | softWrap: true, 62 | overflow: TextOverflow.ellipsis, 63 | ), 64 | padding: EdgeInsets.only(bottom: 3), 65 | ), 66 | Padding( 67 | padding: EdgeInsets.only(bottom: 3), 68 | child: Text( 69 | '$subTitle 著', 70 | style: _textStyle, 71 | maxLines: 2, 72 | softWrap: true, 73 | overflow: TextOverflow.ellipsis, 74 | ), 75 | ), 76 | Text( 77 | content, 78 | style: _textStyle, 79 | maxLines: 2, 80 | softWrap: true, 81 | overflow: TextOverflow.ellipsis, 82 | ), 83 | ], 84 | ), 85 | ), 86 | ], 87 | ), 88 | Blank(color: Colors.white, height: 15), 89 | ], 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/discovery/widget/recommand_widget.dart: -------------------------------------------------------------------------------- 1 | import './discovery_card.dart'; 2 | import './recommand_cell.dart'; 3 | import '../../bloc_provider.dart'; 4 | import '../../widgets/blank.dart'; 5 | import '../models/post_model.dart'; 6 | import '../models/video_model.dart'; 7 | import '../../widgets/loading.dart'; 8 | import '../BLoc/recommand_bloc.dart'; 9 | import '../../utils/color_util.dart'; 10 | import '../models/subject_model.dart'; 11 | import '../models/carousel_model.dart'; 12 | import 'package:flutter/material.dart'; 13 | import '../../index/models/judou_model.dart'; 14 | import 'package:flutter_swiper/flutter_swiper.dart'; 15 | 16 | final TextStyle _textStyle = 17 | TextStyle(fontSize: 12, color: ColorUtils.textGreyColor); 18 | 19 | class DiscoveryRecommand extends StatelessWidget { 20 | @override 21 | Widget build(BuildContext context) { 22 | return BlocProvider( 23 | bloc: RecommandBloc(), 24 | child: RecommandWidget(), 25 | ); 26 | } 27 | } 28 | 29 | class RecommandWidget extends StatefulWidget { 30 | @override 31 | _RecommandWidgetState createState() => _RecommandWidgetState(); 32 | } 33 | 34 | class _RecommandWidgetState extends State 35 | with AutomaticKeepAliveClientMixin { 36 | RecommandBloc bloc; 37 | @override 38 | void initState() { 39 | super.initState(); 40 | bloc = BlocProvider.of(context); 41 | } 42 | 43 | @override 44 | void dispose() { 45 | bloc.dispose(); 46 | super.dispose(); 47 | } 48 | 49 | @override 50 | bool get wantKeepAlive => true; 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | super.build(context); 55 | return StreamBuilder( 56 | stream: bloc.stream, 57 | builder: (context, snapshot) { 58 | if (snapshot.connectionState != ConnectionState.active) { 59 | return Loading(); 60 | } 61 | List carousels = snapshot.data['carousels'] as List; 62 | return Container( 63 | color: Colors.white, 64 | child: ListView( 65 | children: [ 66 | SizedBox( 67 | width: MediaQuery.of(context).size.width, 68 | height: 180, 69 | child: Swiper( 70 | itemBuilder: (context, index) { 71 | return Image.network( 72 | carousels[index].cover, 73 | fit: BoxFit.cover, 74 | ); 75 | }, 76 | itemCount: carousels.length, 77 | autoplay: true, 78 | outer: false, 79 | controller: SwiperController(), 80 | ), 81 | ), 82 | Blank(height: 10), 83 | _RecommandThink(models: snapshot.data['today']), 84 | Blank(height: 10), 85 | _SectionTitle(title: '文章', moreAction: () => print('更多')), 86 | _ArticleList(posts: snapshot.data['posts']), 87 | Blank(height: 10), 88 | _SectionTitle(title: '话题', moreAction: () => print('更多')), 89 | _SubjectList(subjects: snapshot.data['subjects']), 90 | Blank(height: 10), 91 | _SectionTitle(title: '视频', moreAction: () => print('更多')), 92 | _VideoList(videos: snapshot.data['videos']), 93 | Blank(height: 10), 94 | ], 95 | ), 96 | ); 97 | }, 98 | ); 99 | } 100 | } 101 | 102 | class _RecommandThink extends StatelessWidget { 103 | /// [onTap] 点击回调 104 | _RecommandThink({this.onTap, this.models}); 105 | final VoidCallback onTap; 106 | final List models; 107 | @override 108 | Widget build(BuildContext context) { 109 | return Container( 110 | height: 180, 111 | padding: EdgeInsets.all(10), 112 | child: GestureDetector( 113 | onTap: onTap, 114 | child: Container( 115 | height: 180, 116 | padding: EdgeInsets.symmetric(vertical: 10, horizontal: 15), 117 | decoration: BoxDecoration( 118 | border: Border.all(color: ColorUtils.dividerColor, width: 0.5), 119 | ), 120 | child: Column( 121 | crossAxisAlignment: CrossAxisAlignment.start, 122 | mainAxisAlignment: MainAxisAlignment.spaceAround, 123 | children: [ 124 | Row( 125 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 126 | children: [ 127 | Text( 128 | '今日哲思', 129 | style: TextStyle(fontSize: 25.0, fontFamily: 'LiSung'), 130 | ), 131 | IconButton( 132 | icon: Icon(Icons.share), 133 | color: ColorUtils.iconColor, 134 | onPressed: () => print('分享今日哲思'), 135 | ), 136 | ], 137 | ), 138 | Text( 139 | '${models[0].content}', 140 | style: TextStyle(color: ColorUtils.textPrimaryColor), 141 | ), 142 | Align( 143 | alignment: Alignment.bottomRight, 144 | child: Text( 145 | '${models[0].subHeading}', 146 | style: TextStyle(color: ColorUtils.textPrimaryColor), 147 | ), 148 | ) 149 | ], 150 | ), 151 | ), 152 | ), 153 | ); 154 | } 155 | } 156 | 157 | class _ArticleList extends StatelessWidget { 158 | _ArticleList({this.posts}); 159 | 160 | final List posts; 161 | 162 | @override 163 | Widget build(BuildContext context) { 164 | return Container( 165 | padding: EdgeInsets.symmetric(horizontal: 15), 166 | child: Column( 167 | children: posts 168 | .map((item) => RecommandCell( 169 | isVideo: false, 170 | title: item.title, 171 | subTitle: item.author, 172 | content: item.summary, 173 | imageUrl: item.banner, 174 | )) 175 | .toList(), 176 | ), 177 | ); 178 | } 179 | } 180 | 181 | class _SubjectList extends StatelessWidget { 182 | _SubjectList({this.subjects}); 183 | final List subjects; 184 | @override 185 | Widget build(BuildContext context) { 186 | return Container( 187 | height: 90, 188 | padding: EdgeInsets.only(bottom: 10), 189 | child: ListView.builder( 190 | scrollDirection: Axis.horizontal, 191 | itemCount: subjects.length, 192 | physics: AlwaysScrollableScrollPhysics(), 193 | itemBuilder: ((context, index) { 194 | SubjectModel model = subjects[index]; 195 | return DiscoveryCard( 196 | isLeading: index == 0, 197 | isTrailing: index == subjects.length - 1, 198 | title: model.title, 199 | imageUrl: model.cover, 200 | height: 90, 201 | width: 150, 202 | ); 203 | }), 204 | ), 205 | ); 206 | } 207 | } 208 | 209 | class _VideoList extends StatelessWidget { 210 | _VideoList({this.videos}); 211 | 212 | final List videos; 213 | 214 | @override 215 | Widget build(BuildContext context) { 216 | return Container( 217 | padding: EdgeInsets.symmetric(horizontal: 15), 218 | child: Column( 219 | children: videos 220 | .map((item) => RecommandCell( 221 | isVideo: true, 222 | title: item.title, 223 | subTitle: item.summary, 224 | content: '时长: ${item.videoLength}', 225 | imageUrl: item.cover, 226 | )) 227 | .toList(), 228 | ), 229 | ); 230 | } 231 | } 232 | 233 | class _SectionTitle extends StatelessWidget { 234 | _SectionTitle({ 235 | this.title, 236 | this.moreAction, 237 | }); 238 | 239 | final String title; 240 | final VoidCallback moreAction; 241 | 242 | @override 243 | Widget build(BuildContext context) { 244 | return Padding( 245 | padding: EdgeInsets.symmetric(horizontal: 15), 246 | child: Row( 247 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 248 | children: [ 249 | Text( 250 | title, 251 | style: TextStyle(fontSize: 18), 252 | ), 253 | FlatButton( 254 | child: Text('更多', style: _textStyle), 255 | onPressed: moreAction, 256 | ) 257 | ], 258 | ), 259 | ); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /lib/discovery/widget/subscribe_widget.dart: -------------------------------------------------------------------------------- 1 | import '../../bloc_provider.dart'; 2 | import '../../widgets/blank.dart'; 3 | import '../../widgets/loading.dart'; 4 | import '../BLoc/subscribe_bloc.dart'; 5 | import 'package:flutter/material.dart'; 6 | import '../../widgets/judou_cell.dart'; 7 | import '../../index/models/judou_model.dart'; 8 | 9 | class DiscoverySubscribe extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return BlocProvider( 13 | bloc: SubscribeBloc(), 14 | child: SubscribeWidget(), 15 | ); 16 | } 17 | } 18 | 19 | class SubscribeWidget extends StatefulWidget { 20 | @override 21 | _SubscribeWidgetState createState() => _SubscribeWidgetState(); 22 | } 23 | 24 | class _SubscribeWidgetState extends State 25 | with AutomaticKeepAliveClientMixin { 26 | SubscribeBloc bloc; 27 | 28 | @override 29 | void initState() { 30 | super.initState(); 31 | bloc = BlocProvider.of(context); 32 | } 33 | 34 | @override 35 | bool get wantKeepAlive => true; 36 | 37 | @override 38 | void dispose() { 39 | bloc.dispose(); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | super.build(context); 46 | return StreamBuilder( 47 | stream: bloc.stream, 48 | builder: (context, AsyncSnapshot> snapshot) { 49 | List dataList = snapshot.data; 50 | if (snapshot.connectionState != ConnectionState.active) { 51 | return Center( 52 | child: Loading(), 53 | ); 54 | } 55 | 56 | return ListView.builder( 57 | itemBuilder: (context, index) => JuDouCell( 58 | model: dataList[index], 59 | tag: 'DiscoveryPageSubscribe$index', 60 | divider: Blank(), 61 | isCell: true, 62 | ), 63 | itemCount: dataList.length, 64 | physics: AlwaysScrollableScrollPhysics(), 65 | ); 66 | }, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /lib/discovery/widget/topic_header.dart: -------------------------------------------------------------------------------- 1 | // 话题列表页header 2 | import 'dart:ui'; 3 | import '../../utils/ui_util.dart'; 4 | import '../../widgets/blank.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | class TopicsHeader extends StatelessWidget { 8 | TopicsHeader({this.data}); 9 | 10 | final Map data; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final sreenWidth = MediaQuery.of(context).size.width; 15 | TextStyle textStyle(double size) { 16 | return TextStyle( 17 | color: Colors.black, 18 | fontSize: size, 19 | fontWeight: FontWeight.w300, 20 | letterSpacing: 1); 21 | } 22 | 23 | return Column( 24 | crossAxisAlignment: CrossAxisAlignment.start, 25 | children: [ 26 | Container( 27 | width: sreenWidth, 28 | height: DeviceUtils.iPhoneXAbove(context) ? 190 : 170, 29 | child: Image.network( 30 | data['cover'], 31 | fit: BoxFit.cover, 32 | ), 33 | ), 34 | Container( 35 | padding: EdgeInsets.all(15), 36 | height: 90, 37 | child: Column( 38 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | Text( 42 | data['name'], 43 | style: textStyle(16), 44 | textAlign: TextAlign.left, 45 | ), 46 | Text( 47 | data['description'], 48 | style: textStyle(13), 49 | textAlign: TextAlign.left, 50 | maxLines: 2, 51 | softWrap: true, 52 | overflow: TextOverflow.ellipsis, 53 | ) 54 | ], 55 | ), 56 | ), 57 | Blank() 58 | ], 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/index/BLoc/detail_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import '../../network/network.dart'; 3 | import '../../index/models/judou_model.dart'; 4 | import '../../index/models/comment_model.dart'; 5 | 6 | class DetailBloc implements BlocBase { 7 | /// 数据流中的类型 8 | /// Map> 9 | /// { 10 | /// "hot": List, 11 | /// "latest": List 12 | /// "detail": JuDouModel 13 | /// } 14 | final _fetchComments = PublishSubject>(); 15 | final String uuid; 16 | 17 | DetailBloc({this.uuid}) { 18 | fetchData(); 19 | } 20 | 21 | Stream> get commentSteam => _fetchComments.stream; 22 | 23 | void fetchData() async { 24 | // JuDouModel 25 | Map hot = await sentenceHot(uuid); 26 | if (!_fetchComments.isClosed) { 27 | _fetchComments.sink.add(hot); 28 | } 29 | } 30 | 31 | @override 32 | dispose() { 33 | if (!_fetchComments.isClosed) _fetchComments.close(); 34 | } 35 | 36 | /// 每个句子的热评 37 | /// 每个句子的最新评论 38 | /// 每个句子的详情 39 | Future> sentenceHot(String uuid) async { 40 | List hot = await Request.instance.dio 41 | .get(RequestPath.sentenceHot(uuid)) 42 | .then((response) => response.data['data'] as List) 43 | .then((response) => 44 | response.map((item) => CommentModel.fromJSON(item)).toList()); 45 | List latest = await Request.instance.dio 46 | .get(RequestPath.sentenceLatest(uuid)) 47 | .then((response) => response.data['data'] as List) 48 | .then((response) => 49 | response.map((item) => CommentModel.fromJSON(item)).toList()); 50 | JuDouModel detailModel = await Request.instance.dio 51 | .get(RequestPath.sentence(uuid)) 52 | .then((response) => JuDouModel.fromJson(response.data)); 53 | return {'hot': hot, 'latest': latest, 'detail': detailModel}; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/index/BLoc/index_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/judou_model.dart'; 3 | import '../../index/pages/detail_page.dart'; 4 | import '../../network/network.dart'; 5 | import 'dart:async'; 6 | 7 | class IndexBloc implements BlocBase { 8 | /// 存放所有的model 9 | final _fetchDaily = PublishSubject>(); 10 | 11 | /// 顶部的角标数据 12 | /// [0] 评论数 13 | /// [1] 喜欢数 14 | /// [2] '1' 喜欢, '0' 不喜欢 15 | final _badges = PublishSubject>(); 16 | JuDouModel model = JuDouModel(); 17 | List _dataList = List(); 18 | 19 | IndexBloc() { 20 | _fetchDailyJson(); 21 | } 22 | 23 | /// JudouModel数据流 24 | /// 一次性返回 25 | Stream> get dailyStream => _fetchDaily.stream; 26 | 27 | /// 角标数据流 28 | /// 每次发送一个数组,同时包含like和commnet 29 | Stream> get badgesSteam => _badges.stream; 30 | 31 | /// pageview页面切换回调 32 | void onPageChanged(index) { 33 | model = _dataList[index]; 34 | double l = model.likeCount / 1000; 35 | double c = model.commentCount / 1000; 36 | String likeNum = 37 | (l > 1) ? l.toStringAsFixed(1) + 'k' : '${model.likeCount}'; 38 | String commentNum = 39 | (c > 1) ? c.toStringAsFixed(1) : '${model.commentCount}'; 40 | if (!_badges.isClosed) { 41 | _badges.sink.add([commentNum, likeNum, model.isLiked ? '1' : '0']); 42 | } 43 | } 44 | 45 | /// to detail page 46 | void toDetailPage(BuildContext context) { 47 | Navigator.push( 48 | context, 49 | MaterialPageRoute(builder: (context) => DetailPage(model: model)), 50 | ); 51 | } 52 | 53 | @override 54 | dispose() { 55 | if (!_fetchDaily.isClosed) _fetchDaily.close(); 56 | if (!_badges.isClosed) _badges.close(); 57 | } 58 | 59 | void _fetchDailyJson() async { 60 | List list = await daily(); 61 | if (!_fetchDaily.isClosed) { 62 | _fetchDaily.sink.add(list); 63 | } 64 | _dataList = list; 65 | this.onPageChanged(0); 66 | } 67 | 68 | /// 首页网络请求 69 | Future> daily() async { 70 | List list = await Request.instance.dio 71 | .get(RequestPath.daily) 72 | .then((response) => response.data['data'] as List) 73 | .then((response) => response.where((item) => !item['is_ad']).toList()) 74 | .then((response) => 75 | response.map((item) => JuDouModel.fromJson(item)).toList()); 76 | 77 | // var dio = Dio(); 78 | // dio.onHttpClientCreate = (HttpClient client) { 79 | // client.idleTimeout = Duration(seconds: 1); 80 | // }; 81 | 82 | // Directory appDocDir = await getApplicationDocumentsDirectory(); 83 | // String appDocPath = appDocDir.path; 84 | 85 | // try { 86 | // Response response = await dio.download( 87 | // 'http://flv2.bn.netease.com/videolib3/1707/07/liHAU2643/HD/liHAU2643-mobile.mp4', 88 | // '$appDocPath/test.mp4', onProgress: (received, total) { 89 | // print('received----- $received total******** $total'); 90 | // }); 91 | // print(response.data); 92 | // } catch (e) { 93 | // print('error -> $e'); 94 | // } 95 | 96 | // print("download succeed!"); 97 | 98 | return list; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/index/models/author_model.dart: -------------------------------------------------------------------------------- 1 | 2 | class AuthorModel { 3 | final bool isVerified; 4 | final String createDate; 5 | final int id; 6 | final String name; 7 | final int sentencesCount; 8 | final bool isLocked; 9 | final String forbidDate; 10 | final String description; 11 | final String type; 12 | final String coverUrl; 13 | 14 | AuthorModel({ 15 | this.isVerified, 16 | this.createDate, 17 | this.id, 18 | this.name, 19 | this.sentencesCount, 20 | this.isLocked, 21 | this.forbidDate, 22 | this.description, 23 | this.type, 24 | this.coverUrl 25 | }); 26 | 27 | factory AuthorModel.fromJson(Map json) { 28 | return AuthorModel( 29 | isVerified: json['is_verified'] as bool, 30 | createDate: json['created_at'] as String, 31 | id: json['id'] as int, 32 | name: json['name'] as String, 33 | sentencesCount: json['sentences_count'] as int, 34 | isLocked: json['is_locked'] as bool, 35 | forbidDate: json['forbided_at'] as String, 36 | description: json['description'] as String, 37 | type: json['type'] as String, 38 | coverUrl: json['cover'] as String 39 | ); 40 | } 41 | } -------------------------------------------------------------------------------- /lib/index/models/comment_model.dart: -------------------------------------------------------------------------------- 1 | import 'user_model.dart'; 2 | 3 | class CommentModel { 4 | final bool isSelf; 5 | final int upCount; 6 | final int id; 7 | final String content; 8 | final String threadId; 9 | final bool isLiked; 10 | final String commentableType; 11 | final UserModel user; 12 | final Map replyToComment; 13 | final bool isBlock; 14 | final String commentableId; 15 | final String createdAt; 16 | 17 | CommentModel({ 18 | this.isSelf, 19 | this.upCount, 20 | this.id, 21 | this.content, 22 | this.threadId, 23 | this.isLiked, 24 | this.commentableType, 25 | this.user, 26 | this.replyToComment, 27 | this.isBlock, 28 | this.commentableId, 29 | this.createdAt, 30 | }); 31 | 32 | factory CommentModel.fromJSON(Map json) { 33 | return CommentModel( 34 | isSelf: json['is_self'], 35 | upCount: json['up_count'], 36 | id: json['id'], 37 | content: json['content'], 38 | threadId: json['thread_id'], 39 | isLiked: json['is_liked'], 40 | commentableType: json['commentable_type'], 41 | user: UserModel.fromJson(json['user'] ?? Map()), 42 | replyToComment: json['reply_to_comment'] ?? Map(), 43 | isBlock: json['is_block'], 44 | commentableId: json['commentable_id'], 45 | createdAt: json['created_at'], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/index/models/image_model.dart: -------------------------------------------------------------------------------- 1 | 2 | class ImageModel { 3 | final String url; 4 | final String copyRight; 5 | final String color; 6 | final int id; 7 | 8 | ImageModel({this.url, this.copyRight, this.color, this.id}); 9 | 10 | factory ImageModel.fromJson(Map json) { 11 | return ImageModel( 12 | url: json['url'] as String, 13 | copyRight: json['copyright'] as String, 14 | color: json['color'] as String, 15 | id: json['id'] as int 16 | ); 17 | } 18 | } -------------------------------------------------------------------------------- /lib/index/models/judou_model.dart: -------------------------------------------------------------------------------- 1 | import 'author_model.dart'; 2 | import 'image_model.dart'; 3 | import 'user_model.dart'; 4 | import 'tag_model.dart'; 5 | 6 | class JuDouModel { 7 | final bool isPrivate; 8 | final List tags; 9 | final String dailyDate; 10 | final String publishedDate; 11 | final bool isAd; 12 | final bool isUsedByWechat; 13 | final bool isEditable; 14 | final AuthorModel author; 15 | final bool isOriginal; 16 | final UserModel user; 17 | final ImageModel image; 18 | final bool isCollected; 19 | final int likeCount; 20 | final bool isUsedByWeibo; 21 | final String shareUrl; 22 | final String maskColor; 23 | final List pictures; 24 | final bool isLiked; 25 | final bool isRandomable; 26 | final String content; 27 | final int commentCount; 28 | final bool isDisabledComment; 29 | final String maskTransparent; 30 | final String weiboUsedAt; 31 | final String uuid; 32 | final String subHeading; 33 | final bool isUgc; 34 | 35 | JuDouModel( 36 | {this.isPrivate, 37 | this.tags, 38 | this.dailyDate, 39 | this.publishedDate, 40 | this.isAd, 41 | this.isUsedByWechat, 42 | this.isEditable, 43 | this.author, 44 | this.isOriginal, 45 | this.user, 46 | this.image, 47 | this.isCollected, 48 | this.likeCount, 49 | this.isUsedByWeibo, 50 | this.shareUrl, 51 | this.maskColor, 52 | this.pictures, 53 | this.isLiked = false, 54 | this.isRandomable, 55 | this.content, 56 | this.commentCount, 57 | this.isDisabledComment, 58 | this.maskTransparent, 59 | this.weiboUsedAt, 60 | this.uuid, 61 | this.subHeading, 62 | this.isUgc}); 63 | 64 | factory JuDouModel.fromJson(Map json) { 65 | var picturesList = json['pictures'] as List; 66 | List imageList; 67 | if (picturesList != null) { 68 | imageList = picturesList.map((i) => ImageModel.fromJson(i)).toList(); 69 | } 70 | 71 | var tList = json['tags'] as List; 72 | List tagList; 73 | if (tList != null) { 74 | tagList = tList.map((i) => TagModel.fromJSON(i)).toList(); 75 | } 76 | 77 | return JuDouModel( 78 | isPrivate: json['is_private'] as bool, 79 | tags: tagList, 80 | dailyDate: json['daily_date'] as String, 81 | publishedDate: json['published_at'] as String, 82 | isAd: json['is_ad'] as bool, 83 | isUsedByWechat: json['is_used_by_wechat'] as bool, 84 | isEditable: json['is_editable'] as bool, 85 | author: json['author'] != null 86 | ? AuthorModel.fromJson(json['author']) 87 | : null, 88 | isOriginal: json['is_original'] as bool, 89 | user: json['user'] != null ? UserModel.fromJson(json['user']) : null, 90 | image: 91 | json['image'] != null ? ImageModel.fromJson(json['image']) : null, 92 | isCollected: json['is_collected'] as bool, 93 | likeCount: json['like_count'] as int, 94 | isUsedByWeibo: json['is_used_by_weibo'] as bool, 95 | shareUrl: json['share_url'] as String, 96 | maskColor: json['mask_color'] as String, 97 | pictures: imageList, 98 | isLiked: json['is_liked'] ?? false, 99 | isRandomable: json['is_randomable'] as bool, 100 | content: json['content'] as String, 101 | commentCount: json['comment_count'] as int, 102 | isDisabledComment: json['is_disabled_comment'] as bool, 103 | maskTransparent: json['mask_transparent'] as String, 104 | weiboUsedAt: json['weibo_used_at'] as String, 105 | uuid: json['uuid'] as String, 106 | subHeading: json['subheading'] as String, 107 | isUgc: json['is_ugc'] as bool); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/index/models/tag_model.dart: -------------------------------------------------------------------------------- 1 | class TagModel { 2 | final String name; 3 | final int id; 4 | final String cover; 5 | final String summary; 6 | final int weight; 7 | final bool isCloseable; 8 | final String publishedAt; 9 | 10 | TagModel({ 11 | this.name, 12 | this.id, 13 | this.cover, 14 | this.summary, 15 | this.weight, 16 | this.isCloseable, 17 | this.publishedAt, 18 | }); 19 | 20 | factory TagModel.fromJSON(Map json) { 21 | return TagModel(name: json['name'], id: json['id']); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/index/models/user_model.dart: -------------------------------------------------------------------------------- 1 | class UserModel { 2 | final String nickname; 3 | final String avatar; 4 | final String uid; 5 | 6 | UserModel({this.nickname, this.avatar, this.uid}); 7 | 8 | factory UserModel.fromJson(Map json) { 9 | return UserModel( 10 | nickname: json['nickname'] as String, 11 | avatar: json['avatar'] as String, 12 | uid: '${json['uid']}'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/index/pages/detail_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import '../../utils/ui_util.dart'; 4 | import '../../widgets/blank.dart'; 5 | import '../../utils/color_util.dart'; 6 | import '../../widgets/judou_cell.dart'; 7 | import '../../widgets/comment_cell.dart'; 8 | import '../../widgets/end_cell.dart'; 9 | import '../widgets/detail_label.dart'; 10 | import '../models/judou_model.dart'; 11 | import '../models/comment_model.dart'; 12 | import '../../bloc_provider.dart'; 13 | import '../BLoc/detail_bloc.dart'; 14 | import '../../widgets/loading.dart'; 15 | 16 | class DetailPage extends StatelessWidget { 17 | DetailPage({Key key, this.model}) : super(key: key); 18 | final JuDouModel model; 19 | @override 20 | Widget build(BuildContext context) { 21 | return BlocProvider( 22 | bloc: DetailBloc(uuid: model.uuid), 23 | child: DetailWidget(uuid: model.uuid), 24 | ); 25 | } 26 | } 27 | 28 | class DetailWidget extends StatefulWidget { 29 | DetailWidget({Key key, this.uuid}); 30 | 31 | final String uuid; 32 | @override 33 | State createState() { 34 | return _DetailWidgetStateful(); 35 | } 36 | } 37 | 38 | class _DetailWidgetStateful extends State { 39 | DetailBloc detailBloc; 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | detailBloc = BlocProvider.of(context); 45 | } 46 | 47 | @override 48 | dispose() { 49 | detailBloc.dispose(); 50 | super.dispose(); 51 | } 52 | 53 | Widget sectionHeader(String title) { 54 | return Container( 55 | color: Colors.white, 56 | child: Column( 57 | crossAxisAlignment: CrossAxisAlignment.start, 58 | children: [ 59 | Padding( 60 | padding: EdgeInsets.only(left: 15, top: 10), 61 | child: Text(title), 62 | ), 63 | Divider(), 64 | ], 65 | ), 66 | ); 67 | } 68 | 69 | Widget hotCommnets(List hotList) { 70 | List listCell = List(); 71 | for (var i = 0; i < hotList.length; i++) { 72 | listCell.add( 73 | CommentCell( 74 | divider: i == hotList.length - 1 ? Container() : Divider(), 75 | model: hotList[i]), 76 | ); 77 | } 78 | return Container( 79 | color: Colors.white, 80 | child: Column( 81 | crossAxisAlignment: CrossAxisAlignment.start, 82 | children: [ 83 | sectionHeader('热门评论'), 84 | Column( 85 | children: listCell, 86 | ), 87 | Blank(), 88 | ], 89 | ), 90 | ); 91 | } 92 | 93 | Widget body( 94 | BuildContext context, AsyncSnapshot> snapshot) { 95 | if (snapshot.connectionState != ConnectionState.active) { 96 | return Center( 97 | child: Loading(), 98 | ); 99 | } 100 | List hot = snapshot.data['hot']; 101 | List latest = snapshot.data['latest']; 102 | JuDouModel model = snapshot.data['detail']; 103 | int itemCount = latest.length + 5; 104 | String endString = latest.isNotEmpty ? '- END -' : '快来添加第一条评论吧'; 105 | return Column( 106 | children: [ 107 | Container( 108 | width: MediaQuery.of(context).size.width, 109 | height: MediaQuery.of(context).size.height - 100, 110 | child: ListView.builder( 111 | itemBuilder: (context, index) { 112 | if (index == 0) 113 | return JuDouCell( 114 | divider: Blank(), 115 | tag: 'index_detail', 116 | model: model, 117 | isCell: false, 118 | ); 119 | if (index == 1) 120 | return model.tags != null 121 | ? DetailLabel(labelTitle: model.tags[0].name ?? '爱情') 122 | : Container(); 123 | if (index == 2) 124 | return hot.isNotEmpty ? hotCommnets(hot) : Container(); 125 | if (index == 3) 126 | return latest.isNotEmpty ? sectionHeader('最新评论') : Container(); 127 | if (index == itemCount - 1) return EndCell(text: endString); 128 | return CommentCell( 129 | divider: index == itemCount - 2 ? Container() : Divider(), 130 | model: latest[index - 4]); 131 | }, 132 | itemCount: itemCount, 133 | ), 134 | ) 135 | ], 136 | ); 137 | } 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | return StreamBuilder( 142 | stream: detailBloc.commentSteam, 143 | builder: (context, AsyncSnapshot> snapshot) { 144 | return Scaffold( 145 | appBar: AppBarUtils.appBar('详情', context), 146 | backgroundColor: ColorUtils.blankColor, 147 | body: body(context, snapshot), 148 | floatingActionButton: _BottomInput(), 149 | floatingActionButtonLocation: 150 | FloatingActionButtonLocation.centerDocked, 151 | resizeToAvoidBottomPadding: true, 152 | ); 153 | }, 154 | ); 155 | } 156 | } 157 | 158 | class _BottomInput extends StatelessWidget { 159 | @override 160 | Widget build(BuildContext context) { 161 | return SafeArea( 162 | right: false, 163 | child: Container( 164 | height: 50, 165 | color: Colors.white, 166 | child: Column( 167 | children: [ 168 | Blank(height: 0.5, color: Colors.black12), 169 | Padding( 170 | padding: EdgeInsets.only(top: 5, left: 15, right: 15), 171 | child: CupertinoTextField( 172 | placeholder: '说点什么...', 173 | textAlign: TextAlign.center, 174 | decoration: BoxDecoration( 175 | color: ColorUtils.dividerColor, 176 | border: Border.all(color: Colors.transparent), 177 | shape: BoxShape.rectangle, 178 | borderRadius: BorderRadius.circular(20.0), 179 | ), 180 | style: TextStyle( 181 | height: 1, fontSize: 12, color: ColorUtils.textGreyColor), 182 | ), 183 | ) 184 | ], 185 | ), 186 | ), 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /lib/index/pages/index_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../utils/color_util.dart'; 3 | import '../widgets/index_item.dart'; 4 | import '../../widgets/button_subscript.dart'; 5 | import '../models/judou_model.dart'; 6 | import '../BLoc/index_bloc.dart'; 7 | import '../../bloc_provider.dart'; 8 | import 'package:flutter/services.dart'; 9 | 10 | class IndexPage extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return BlocProvider( 14 | bloc: IndexBloc(), 15 | child: IndexWidget(), 16 | ); 17 | } 18 | } 19 | 20 | class IndexWidget extends StatefulWidget { 21 | @override 22 | _IndexWidgetState createState() => _IndexWidgetState(); 23 | } 24 | 25 | class _IndexWidgetState extends State 26 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 27 | PageController _pageController = PageController(); 28 | IndexBloc indexBloc; 29 | String _like = ''; 30 | String _comment = ''; 31 | String _isLike = ''; 32 | 33 | @override 34 | void initState() { 35 | super.initState(); 36 | indexBloc = BlocProvider.of(context); 37 | indexBloc.badgesSteam.listen((List data) { 38 | setState(() { 39 | _like = data[1]; 40 | _comment = data[0]; 41 | _isLike = data[2]; 42 | }); 43 | }); 44 | } 45 | 46 | // void _test() { 47 | // print('-------1-'); 48 | // var platform = MethodChannel('judou.test'); 49 | // platform.invokeMethod('getString').then((result) { 50 | // print('------- $result'); 51 | // }); 52 | // } 53 | 54 | @override 55 | void dispose() { 56 | _pageController.dispose(); 57 | indexBloc.dispose(); 58 | super.dispose(); 59 | } 60 | 61 | @override 62 | bool get wantKeepAlive => true; 63 | 64 | Icon likeIcon() => _isLike == '0' 65 | ? Icon( 66 | Icons.favorite_border, 67 | color: ColorUtils.iconColor, 68 | ) 69 | : Icon( 70 | Icons.favorite, 71 | color: Colors.redAccent, 72 | ); 73 | 74 | Widget indexAppBar() => AppBar( 75 | iconTheme: IconThemeData(color: ColorUtils.iconColor), 76 | centerTitle: true, 77 | leading: Container( 78 | alignment: Alignment.center, 79 | child: Text( 80 | '句子', 81 | style: TextStyle(fontSize: 22.0, fontFamily: 'LiSung'), 82 | ), 83 | ), 84 | actions: [ 85 | SubscriptButton( 86 | icon: Icon(Icons.message), 87 | subscript: _comment, 88 | onPressed: () => indexBloc.toDetailPage(context), 89 | ), 90 | SubscriptButton( 91 | icon: likeIcon(), 92 | subscript: _like, 93 | ), 94 | IconButton( 95 | icon: Icon(Icons.share, color: ColorUtils.iconColor), 96 | onPressed: () => indexBloc.toDetailPage(context), 97 | ), 98 | ], 99 | ); 100 | 101 | Widget buildBody(AsyncSnapshot> snapshot) { 102 | if (snapshot.connectionState != ConnectionState.active) { 103 | return Center( 104 | child: CircularProgressIndicator(), 105 | ); 106 | } 107 | return PageView.builder( 108 | itemBuilder: (context, index) { 109 | return IndexPageItem( 110 | onTap: () => indexBloc.toDetailPage(context), 111 | model: snapshot.data[index], 112 | ); 113 | }, 114 | itemCount: snapshot.data.length, 115 | controller: this._pageController, 116 | onPageChanged: indexBloc.onPageChanged, 117 | ); 118 | } 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | super.build(context); 123 | return StreamBuilder( 124 | stream: indexBloc.dailyStream, 125 | builder: 126 | (BuildContext context, AsyncSnapshot> snapshot) { 127 | return Scaffold( 128 | appBar: indexAppBar(), 129 | backgroundColor: Colors.white, 130 | body: buildBody(snapshot), 131 | ); 132 | }, 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/index/widgets/detail_label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../widgets/blank.dart'; 3 | import '../../widgets/label.dart'; 4 | 5 | class DetailLabel extends StatelessWidget { 6 | DetailLabel({Key key, this.labelTitle}) : super(key: key); 7 | 8 | final String labelTitle; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Container( 13 | color: Colors.white, 14 | child: Column( 15 | children: [ 16 | Row(children: [ 17 | Container( 18 | padding: EdgeInsets.symmetric(vertical: 10, horizontal: 15), 19 | child: Label( 20 | width: 40, 21 | height: 20, 22 | radius: 10, 23 | title: labelTitle, 24 | onTap: () => print('爱情'), 25 | ), 26 | ) 27 | ]), 28 | Blank() 29 | ], 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/index/widgets/index_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../models/judou_model.dart'; 3 | import '../widgets/vertical_text.dart'; 4 | 5 | class IndexPageItem extends StatefulWidget { 6 | IndexPageItem({Key key, this.onTap, this.model}) : super(key: key); 7 | 8 | final VoidCallback onTap; 9 | final JuDouModel model; 10 | 11 | @override 12 | _IndexPageItemState createState() => _IndexPageItemState(); 13 | } 14 | 15 | class _IndexPageItemState extends State 16 | with AutomaticKeepAliveClientMixin { 17 | String day = ''; 18 | String dailyDate = ''; 19 | 20 | @override 21 | bool get wantKeepAlive => true; 22 | 23 | @override 24 | initState() { 25 | super.initState(); 26 | // 日期转换 27 | String dayString = widget.model.dailyDate.toString(); 28 | String weekday = ''; 29 | String dailyString = ''; 30 | if (dayString != '' || dailyString != 'null') { 31 | dailyString = dayString.substring(0, 7).replaceAll(RegExp(r'-'), '.'); 32 | var date = DateTime.parse(widget.model.dailyDate); 33 | List dayList = ['一', '二', '三', '四', '五', '六', '日']; 34 | weekday = dayList[date.weekday - 1]; 35 | day = '$date'.substring(8, 10); 36 | dailyDate = dailyString + '星期' + '$weekday'; 37 | } 38 | } 39 | 40 | // 字体设置 41 | TextStyle textStyle(double fontSize, bool isSpace) => TextStyle( 42 | fontSize: fontSize, 43 | fontFamily: 'PingFang', 44 | fontWeight: FontWeight.w200, 45 | letterSpacing: isSpace ? 1 : 0, 46 | ); 47 | 48 | Widget verticalText(String text, double rightPosition) => Positioned( 49 | child: Container( 50 | padding: EdgeInsets.only(left: 2), 51 | decoration: BoxDecoration( 52 | border: Border(left: BorderSide(color: Colors.white, width: 0.5)), 53 | ), 54 | child: VerticalText( 55 | text: text, 56 | color: Colors.white, 57 | size: 10, 58 | ), 59 | ), 60 | top: 10, 61 | right: rightPosition + 10, 62 | ); 63 | 64 | // 顶部大图部分 65 | Stack headerView() => Stack( 66 | children: [ 67 | SizedBox( 68 | child: Image.network(widget.model.pictures[0].url, 69 | fit: BoxFit.cover, 70 | width: MediaQuery.of(context).size.width, 71 | height: 260, 72 | gaplessPlayback: true), 73 | ), 74 | Positioned( 75 | child: 76 | Text(day, style: TextStyle(fontSize: 99, color: Colors.white)), 77 | bottom: -50, 78 | left: 20, 79 | ), 80 | verticalText('戊戌狗年', 15), 81 | verticalText('甲子月庚寅日', 35), 82 | verticalText('腊月甘十八', 55), 83 | ], 84 | ); 85 | 86 | // 文章和作者部分 87 | Container contentView() => Container( 88 | padding: EdgeInsets.all(20), 89 | child: Column( 90 | mainAxisAlignment: MainAxisAlignment.center, 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | children: [ 93 | Text(widget.model.content, 94 | style: textStyle(17, true), textAlign: TextAlign.start), 95 | Row( 96 | mainAxisAlignment: MainAxisAlignment.end, 97 | children: [ 98 | Container( 99 | padding: EdgeInsets.only(top: 10), 100 | child: Text(widget.model.subHeading, 101 | style: textStyle(17, true), textAlign: TextAlign.end), 102 | ) 103 | ], 104 | ) 105 | ], 106 | ), 107 | ); 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | super.build(context); 112 | return GestureDetector( 113 | onTap: widget.onTap, 114 | child: Column( 115 | children: [ 116 | headerView(), 117 | Expanded( 118 | child: Stack( 119 | children: [ 120 | SizedBox(child: contentView()), 121 | Positioned( 122 | child: Text(day, 123 | style: TextStyle(fontSize: 99, color: Colors.black)), 124 | top: -70, 125 | left: 20), 126 | Positioned( 127 | child: Text(dailyDate, 128 | style: textStyle(12, false), textAlign: TextAlign.end), 129 | right: 20, 130 | top: 5) 131 | ], 132 | ), 133 | ), 134 | ], 135 | ), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/index/widgets/vertical_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class VerticalText extends StatelessWidget { 4 | VerticalText({Key key, this.text, this.color, this.size}) : super(key: key); 5 | 6 | final double size; 7 | final String text; 8 | final Color color; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Container( 13 | width: size, 14 | child: Text( 15 | text, 16 | style: TextStyle( 17 | fontSize: size, 18 | color: color, 19 | height: 0.85, 20 | fontWeight: FontWeight.w300, 21 | ), 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'main_page.dart'; 4 | 5 | void main() { 6 | // debugPaintSizeEnabled = true; 7 | debugDefaultTargetPlatformOverride = TargetPlatform.iOS; 8 | runApp(JuDouApp()); 9 | } 10 | 11 | class JuDouApp extends StatelessWidget { 12 | /// 强制设置splashColor和highlightColor为transparent 13 | /// 可以去除material的点击波纹效果 14 | @override 15 | Widget build(BuildContext context) { 16 | return MaterialApp( 17 | home: MainPage(), 18 | theme: ThemeData( 19 | primaryColor: Colors.white, 20 | splashColor: Colors.transparent, 21 | highlightColor: Colors.transparent, 22 | ), 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/main_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import './index/pages/index_page.dart'; 3 | import './discovery/pages/discovery_page.dart'; 4 | import './profile/pages/profile_page.dart'; 5 | 6 | class MainPage extends StatefulWidget { 7 | MainPage({Key key}) : super(key: key); 8 | 9 | @override 10 | _MainPageState createState() => _MainPageState(); 11 | } 12 | 13 | class _MainPageState extends State 14 | with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { 15 | int _selectedIndex = 0; 16 | static IndexPage _indexPage = IndexPage(); 17 | static DiscoveryPage _discoveryPage = DiscoveryPage(); 18 | static ProfilePage _profilePage = ProfilePage(); 19 | final _pages = [_indexPage, _discoveryPage, _profilePage]; 20 | 21 | @override 22 | void initState() { 23 | super.initState(); 24 | } 25 | 26 | void _onItemTapped(int index) { 27 | setState(() { 28 | _selectedIndex = index; 29 | }); 30 | } 31 | 32 | @override 33 | bool get wantKeepAlive => true; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | super.build(context); 38 | return Scaffold( 39 | body: IndexedStack( 40 | index: _selectedIndex, 41 | children: _pages, 42 | ), 43 | bottomNavigationBar: BottomAppBar( 44 | child: Row( 45 | mainAxisAlignment: MainAxisAlignment.spaceAround, 46 | children: [ 47 | IconButton( 48 | icon: _selectedIndex == 0 49 | ? Icon(Icons.autorenew) 50 | : Icon(Icons.adjust), 51 | onPressed: () => this._onItemTapped(0), 52 | ), 53 | IconButton( 54 | icon: _selectedIndex == 1 55 | ? Icon(Icons.explore) 56 | : ImageIcon(AssetImage('assets/descovery.png')), 57 | onPressed: () => this._onItemTapped(1), 58 | ), 59 | IconButton( 60 | icon: _selectedIndex == 2 61 | ? Icon(Icons.person) 62 | : Icon(Icons.person_outline), 63 | onPressed: () => this._onItemTapped(2), 64 | ) 65 | ], 66 | ), 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/network/network.dart: -------------------------------------------------------------------------------- 1 | export 'package:rxdart/rxdart.dart'; 2 | export '../bloc_provider.dart'; 3 | export 'package:dio/dio.dart'; 4 | export './request.dart'; 5 | export './path.dart'; 6 | -------------------------------------------------------------------------------- /lib/network/path.dart: -------------------------------------------------------------------------------- 1 | class RequestPath { 2 | /// 首页接口地址 3 | static const String daily = 4 | '/v6/op/sentences/daily?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=45&platform=ios&signature=6d52830af89222da917a0161d3d32c70&system_version=12.1×tamp=1546019663&token=249d880e4ba539c6edc04f9e35ff46a3&version=3.5.7&version_code=41'; 5 | 6 | /// 句子详情 7 | /// [uuid] 句子id 8 | static String sentence(String uuid) { 9 | return '/v6/op/sentences/$uuid?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=5351b762de0d27b2c2db3c4eebdaca41&system_version=12.1×tamp=1546215975&token=249d880e4ba539c6edc04f9e35ff46a3&version=3.5.7&version_code=41'; 10 | } 11 | 12 | /// 句子热评 13 | static String sentenceHot(String uuid) { 14 | return '/v6/op/sentences/$uuid/comments/hot?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=78bffaeb4feeebdd5427dd810bc11e3d&system_version=12.1×tamp=1546215975&token=249d880e4ba539c6edc04f9e35ff46a3&version=3.5.7&version_code=41'; 15 | } 16 | 17 | /// 最新评论 18 | static String sentenceLatest(String uuid) { 19 | return '/v6/op/sentences/$uuid/comments/latest?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=78bffaeb4feeebdd5427dd810bc11e3d&system_version=12.1×tamp=1546215975&token=249d880e4ba539c6edc04f9e35ff46a3&version=3.5.7&version_code=41'; 20 | } 21 | 22 | /// 所有频道id + title 23 | static String channels() { 24 | return 'v6/op/channels/12?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=9eb7dd623af816981a9947e017fb95e9&system_version=12.3×tamp=1559000076&token=5c445d2b07a7e9ee6a84e9a4a1533891&version=3.8.0&version_code=51'; 25 | } 26 | 27 | /// 每个子频道的数据 28 | /// 12 -> 订阅 29 | static String channelWithId(String id) { 30 | return '/v6/op/channels/12?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=44afc59250d46d93fdde07ba6579e889&system_version=12.1.2×tamp=1551393195&token=bf803413c8e020a23f9c15f4c4237ee6&version=3.7.0&version_code=45'; 31 | } 32 | 33 | /// 发现页面 34 | /// 底部Tags 35 | static String discoveryTags() { 36 | return '/v5/tags/list?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=75f4b199a5ed71b7bb9c85679839ae7d&system_version=12.1.2×tamp=1546822867&version=3.6.1&version_code=44'; 37 | } 38 | 39 | /// 发现页面 40 | /// 话题数据 41 | static String topicData() { 42 | return '/v5/topics?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=75f4b199a5ed71b7bb9c85679839ae7d&system_version=12.1.2×tamp=1546822867&version=3.6.1&version_code=44'; 43 | } 44 | 45 | /// 发现页面 46 | /// 根据Tag id获取数据 47 | /// [id] tag id 48 | static String dataWithTagId(String id) { 49 | return '/v5/tags/$id/ordered_sentences?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=af6b8753c7eed7177746f0b326de350d&system_version=12.1.2×tamp=1546822893&token=5fbeffff6f6d92c4902139d2619852b0&version=3.6.1&version_code=44'; 50 | } 51 | 52 | /// 发现页面 53 | /// 推荐轮播 54 | static String carousels() { 55 | return '/v5/recommends/carousels?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=39acb567bf5b1792f58b58ff46ad6003&system_version=12.1.2×tamp=1547011818&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 56 | } 57 | 58 | /// 发现页面 59 | /// 今日哲思考 60 | static String todayThink() { 61 | return '/v5/recommends/today?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=39acb567bf5b1792f58b58ff46ad6003&system_version=12.1.2×tamp=1547011818&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 62 | } 63 | 64 | /// 发现页面 65 | /// 三段list数据 66 | /// posts 67 | /// subjects 68 | /// videos 69 | static String recommand() { 70 | return '/v5/recommends?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=39acb567bf5b1792f58b58ff46ad6003&system_version=12.1.2×tamp=1547011818&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 71 | } 72 | 73 | /// ProfileDetail 74 | /// Author Info 75 | static String authorInfo(String id) { 76 | return '/v6/op/authors/$id?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=a00f384c42d33a23334baef74690628b&system_version=12.1.2×tamp=1547494083&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 77 | } 78 | 79 | /// ProfileDetail 80 | /// Author Sentences Latest 81 | static String authorInfoLatest(String id) { 82 | return '/v6/op/authors/$id/sentences?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&order_by=latest&page=1&per_page=20&platform=ios&signature=e53538fc8afbeee370c06f4ac9e88862&system_version=12.1.2×tamp=1547494908&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 83 | } 84 | 85 | /// ProfileDetail 86 | /// Author Sentences Hot 87 | static String authorInfoHot(String id) { 88 | return '/v6/op/authors/$id/sentences?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&order_by=hot&page=1&per_page=20&platform=ios&signature=b93170717513bd9ea3aba395911fb740&system_version=12.1.2×tamp=1547494915&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 89 | } 90 | 91 | /// ProfileDetail 92 | /// Topics info 93 | static String topicsInfo(String uuid) { 94 | return '/v5/topics/$uuid?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=816ca877b7a4f8ebae64fc5fbd2cf81f&system_version=12.1.2×tamp=1547497222&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 95 | } 96 | 97 | /// ProfileDetail 98 | /// Topics Latest 99 | static String topicsInfoLatest(String uuid) { 100 | return '/v5/topics/$uuid/sentences?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=6c86664041fa6268b446f716cc0d84be&system_version=12.1.2×tamp=1547497222&token=8783e97df8f4a954663a0674fb38ffe0&type=latest&version=3.6.1&version_code=44'; 101 | } 102 | 103 | /// ProfileDetail 104 | /// Topics Hot 105 | static String topicsInfoHot(String uuid) { 106 | return '/v5/topics/$uuid/sentences?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=8131b95f324e83bde06ecd60d7511966&system_version=12.1.2×tamp=1547497366&token=8783e97df8f4a954663a0674fb38ffe0&type=hot&version=3.6.1&version_code=44'; 107 | } 108 | 109 | /// ProfileDetail 110 | /// User 111 | static String userInfo(String uid) { 112 | return '/v5/users/$uid?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&platform=ios&signature=34e8a31d21c37d78557486df1e4aa07a&system_version=12.1.2×tamp=1547669808&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 113 | } 114 | 115 | /// ProfileDetail 116 | /// User Sentences 117 | static String userInfoSentences(String uid) { 118 | return '/v6/op/users/$uid/sentences?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=0d73839987a308ebfa7019f071968b05&system_version=12.1.2×tamp=1547669808&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 119 | } 120 | 121 | /// ProfileDetail 122 | /// User Collections 123 | static String userInfoCollections(String uid) { 124 | return '/v6/op/users/$uid/collections?app_key=af66b896-665e-415c-a119-6ca5233a6963&channel=App%20Store&device_id=9f5e19d3dd08667400da31ae0e045e1b&device_type=iPhone9%2C1&page=1&per_page=20&platform=ios&signature=b4fdc33ea9ac41325261a7ccf2a4dbc1&system_version=12.1.2×tamp=1547497622&token=8783e97df8f4a954663a0674fb38ffe0&version=3.6.1&version_code=44'; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/network/request.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class Request { 4 | /// instance 5 | static final Request _request = Request._internal(); 6 | Request._internal(); 7 | static Request get instance => _request; 8 | 9 | /// Dio通用配置 10 | /// baseUrl -> https://judouapp.com/api 11 | /// connectTimeout 12 | /// receiveTimeout 13 | /// 14 | final Dio dio = Dio( 15 | BaseOptions( 16 | baseUrl: 'https://judouapp.com/api', 17 | connectTimeout: 5000, 18 | receiveTimeout: 3000, 19 | responseType: ResponseType.json, 20 | ), 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /lib/profile/Bloc/profile_detail_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import '../../bloc_provider.dart'; 3 | import '../../network/network.dart'; 4 | import '../models/collections_model.dart'; 5 | import '../../index/models/judou_model.dart'; 6 | 7 | class ProfileDetailBloc implements BlocBase { 8 | int type = 0; 9 | PublishSubject> _dataSubject = PublishSubject(); 10 | 11 | /// prfile header wiget type 12 | /// 0 Normal 13 | /// 1 Verify 14 | /// 2 Topics 15 | ProfileDetailBloc(int type, String id) { 16 | this.type = type; 17 | _fetchData(type, id); 18 | } 19 | 20 | Stream> get stream => _dataSubject.stream; 21 | 22 | void _fetchData(int type, String id) async { 23 | switch (type) { 24 | case 0: 25 | { 26 | _fetchUserInfo(id); 27 | } 28 | break; 29 | case 1: 30 | { 31 | _fetchAuthorInfo(id); 32 | } 33 | break; 34 | case 2: 35 | { 36 | _fetchTopicsInfo(id); 37 | } 38 | break; 39 | } 40 | } 41 | 42 | _fetchUserInfo(String uid) async { 43 | Map header = await Request.instance.dio 44 | .get(RequestPath.userInfo(uid)) 45 | .then((response) => response.data); 46 | 47 | List sentences = await Request.instance.dio 48 | .get(RequestPath.userInfoSentences(uid)) 49 | .then((response) => response.data['data'] as List) 50 | .then((response) => response.where((item) => !item['is_ad']).toList()) 51 | .then((response) => 52 | response.map((item) => JuDouModel.fromJson(item)).toList()); 53 | 54 | List collections = await Request.instance.dio 55 | .get(RequestPath.userInfoCollections(uid)) 56 | .then((response) => response.data['data'] as List) 57 | .then((response) => 58 | response.map((item) => CollectionModel.fromJSON(item)).toList()); 59 | if (!_dataSubject.isClosed) { 60 | _dataSubject.sink.add({ 61 | 'header': header, 62 | 'sentences': sentences, 63 | 'collections': collections, 64 | }); 65 | } 66 | } 67 | 68 | _fetchAuthorInfo(String uid) async { 69 | Map user = await Request.instance.dio 70 | .get(RequestPath.authorInfo(uid)) 71 | .then((response) => response.data); 72 | 73 | List sentences = await Request.instance.dio 74 | .get(RequestPath.authorInfoLatest(uid)) 75 | .then((response) => response.data['data'] as List) 76 | .then((response) => response.where((item) => !item['is_ad']).toList()) 77 | .then((response) => 78 | response.map((item) => JuDouModel.fromJson(item)).toList()); 79 | 80 | List hot = await Request.instance.dio 81 | .get(RequestPath.authorInfoHot(uid)) 82 | .then((response) => response.data['data'] as List) 83 | .then((response) => response.where((item) => !item['is_ad']).toList()) 84 | .then((response) => 85 | response.map((item) => JuDouModel.fromJson(item)).toList()); 86 | if (!_dataSubject.isClosed) { 87 | _dataSubject.sink.add({ 88 | 'header': user, 89 | 'sentences': sentences, 90 | 'collections': hot, 91 | }); 92 | } 93 | } 94 | 95 | _fetchTopicsInfo(String uid) async { 96 | Map user = await Request.instance.dio 97 | .get(RequestPath.topicsInfo(uid)) 98 | .then((response) => response.data); 99 | 100 | List sentences = await Request.instance.dio 101 | .get(RequestPath.topicsInfoLatest(uid)) 102 | .then((response) => response.data['data'] as List) 103 | .then((response) => response.where((item) => !item['is_ad']).toList()) 104 | .then((response) => 105 | response.map((item) => JuDouModel.fromJson(item)).toList()); 106 | 107 | List hot = await Request.instance.dio 108 | .get(RequestPath.topicsInfoHot(uid)) 109 | .then((response) => response.data['data'] as List) 110 | .then((response) => response.where((item) => !item['is_ad']).toList()) 111 | .then((response) => 112 | response.map((item) => JuDouModel.fromJson(item)).toList()); 113 | 114 | if (!_dataSubject.isClosed) { 115 | _dataSubject.sink.add({ 116 | 'header': user, 117 | 'sentences': sentences, 118 | 'collections': hot, 119 | }); 120 | } 121 | } 122 | 123 | @override 124 | dispose() { 125 | if (!_dataSubject.isClosed) { 126 | _dataSubject.close(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/profile/models/collections_model.dart: -------------------------------------------------------------------------------- 1 | class CollectionModel { 2 | final int id; 3 | final String name; 4 | final bool isDefault; 5 | final bool isSelf; 6 | final String type; 7 | final String description; 8 | final String sentencesCount; 9 | final String cover; 10 | 11 | CollectionModel({ 12 | this.id, 13 | this.name, 14 | this.isDefault, 15 | this.isSelf, 16 | this.type, 17 | this.description, 18 | this.sentencesCount, 19 | this.cover, 20 | }); 21 | 22 | factory CollectionModel.fromJSON(Map json) { 23 | return CollectionModel( 24 | id: json['id'], 25 | name: json['name'], 26 | isDefault: json['is_default'], 27 | isSelf: json['is_self'], 28 | type: json['type'], 29 | description: json['description'], 30 | sentencesCount: json['sentences_count'], 31 | cover: json['cover']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/profile/pages/message_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../widgets/blank.dart'; 3 | import '../widgets/list_cell.dart'; 4 | 5 | class MessagePage extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | backgroundColor: Colors.grey[100], 10 | appBar: AppBar( 11 | title: Text('我的消息'), 12 | actions: [ 13 | FlatButton(child: Text('清空'), onPressed: () => debugPrint('消息清空')) 14 | ], 15 | ), 16 | body: ListView(children: [ 17 | Blank(height: 15), 18 | ListCell( 19 | leading: Icons.alarm, 20 | title: '通知', 21 | trailing: Icons.arrow_forward_ios, 22 | isDivider: true), 23 | ListCell( 24 | leading: Icons.person_add, 25 | title: '关注', 26 | trailing: Icons.arrow_forward_ios, 27 | isDivider: true), 28 | ListCell( 29 | leading: Icons.thumb_up, 30 | title: '点赞', 31 | trailing: Icons.arrow_forward_ios, 32 | isDivider: true), 33 | ListCell( 34 | leading: Icons.message, 35 | title: '评论', 36 | trailing: Icons.arrow_forward_ios, 37 | isDivider: true), 38 | ListCell( 39 | leading: Icons.bookmark_border, 40 | title: '收藏', 41 | trailing: Icons.arrow_forward_ios), 42 | ]), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/profile/pages/profile_detail.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import '../../widgets/blank.dart'; 3 | import '../../widgets/judou_cell.dart'; 4 | import '../models/collections_model.dart'; 5 | import '../../bloc_provider.dart'; 6 | import '../widgets/verify_header.dart'; 7 | import 'package:flutter/services.dart'; 8 | import '../../utils/color_util.dart'; 9 | import 'package:flutter/material.dart'; 10 | import '../widgets/normal_header.dart'; 11 | import '../Bloc/profile_detail_bloc.dart'; 12 | import '../../widgets/loading.dart'; 13 | import '../../widgets/collection_cell.dart'; 14 | import '../../index/models/judou_model.dart'; 15 | import '../../discovery/widget/topic_header.dart'; 16 | 17 | class ProfileDetailPage extends StatelessWidget { 18 | /// prfile header wiget type 19 | /// 0 Normal 20 | /// 1 Verify 21 | /// 2 Topics 22 | ProfileDetailPage({ 23 | @required this.type, 24 | @required this.id, 25 | }); 26 | 27 | final String id; 28 | final int type; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | return BlocProvider( 33 | bloc: ProfileDetailBloc(type, id), 34 | child: ProfilDetail(), 35 | ); 36 | } 37 | } 38 | 39 | class ProfilDetail extends StatefulWidget { 40 | @override 41 | _ProfilDetailState createState() => _ProfilDetailState(); 42 | } 43 | 44 | class _ProfilDetailState extends State { 45 | Color _titleColor = Colors.transparent; 46 | Color _iconColor = Colors.white; 47 | ScrollController _controller = ScrollController(); 48 | ProfileDetailBloc _bloc; 49 | Brightness _brightness = Brightness.dark; 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | _bloc = BlocProvider.of(context); 55 | _controller.addListener(() { 56 | bool isTop = _controller.offset >= 135; 57 | setState(() { 58 | _titleColor = isTop ? Colors.black : Colors.transparent; 59 | _iconColor = isTop ? Colors.black : Colors.white; 60 | _brightness = isTop ? Brightness.light : Brightness.dark; 61 | }); 62 | }); 63 | } 64 | 65 | Widget _header(Map map, int type) { 66 | List list = [ 67 | NormalHeader(data: map), 68 | VerfiyHeader(data: map), 69 | TopicsHeader(data: map) 70 | ]; 71 | return list[type]; 72 | } 73 | 74 | /// verify,topics -> 350 75 | /// normal -> 286 76 | @override 77 | Widget build(BuildContext context) { 78 | return DefaultTabController( 79 | length: 2, 80 | child: StreamBuilder( 81 | stream: _bloc.stream, 82 | builder: (context, snapshot) { 83 | if (snapshot.connectionState != ConnectionState.active) { 84 | return Container( 85 | color: Colors.white, 86 | child: Loading(), 87 | ); 88 | } 89 | 90 | Map header = snapshot.data['header']; 91 | List sentences = snapshot.data['sentences']; 92 | List collections; 93 | List hot; 94 | String title; 95 | if (_bloc.type == 0) { 96 | title = header['nickname']; 97 | collections = snapshot.data['collections']; 98 | } else { 99 | title = header['name']; 100 | hot = snapshot.data['collections']; 101 | } 102 | 103 | return Scaffold( 104 | backgroundColor: Colors.white, 105 | body: NestedScrollView( 106 | controller: _controller, 107 | body: TabBarView( 108 | children: [ 109 | ListView.builder( 110 | itemBuilder: (context, index) { 111 | return JuDouCell( 112 | divider: Blank(), 113 | tag: 'profile_detail_$index', 114 | model: sentences[index], 115 | isCell: true, 116 | ); 117 | }, 118 | itemCount: sentences.length, 119 | ), 120 | _bloc.type == 0 121 | ? GridView.builder( 122 | itemBuilder: (context, index) { 123 | return CollectionCell(model: collections[index]); 124 | }, 125 | itemCount: collections.length, 126 | gridDelegate: 127 | SliverGridDelegateWithFixedCrossAxisCount( 128 | crossAxisCount: 2), 129 | ) 130 | : ListView.builder( 131 | itemBuilder: (context, index) { 132 | return JuDouCell( 133 | divider: Blank(), 134 | tag: 'profile_detail_hot$index', 135 | model: hot[index], 136 | isCell: true, 137 | ); 138 | }, 139 | itemCount: hot.length, 140 | ), 141 | ], 142 | ), 143 | headerSliverBuilder: (context, innerBoxIsScrolled) { 144 | return [ 145 | SliverAppBar( 146 | brightness: _brightness, 147 | backgroundColor: Colors.white, 148 | expandedHeight: _bloc.type == 1 ? 350 : 286, 149 | bottom: TabBar( 150 | indicatorColor: Colors.yellow, 151 | indicatorSize: TabBarIndicatorSize.label, 152 | unselectedLabelColor: ColorUtils.textGreyColor, 153 | tabs: [ 154 | Tab( 155 | text: _bloc.type == 0 156 | ? '句子 ${sentences.length}' 157 | : '最新'), 158 | Tab( 159 | text: _bloc.type == 0 160 | ? '收藏夹 ${collections.length}' 161 | : '最热') 162 | ], 163 | ), 164 | iconTheme: IconThemeData(color: _iconColor), 165 | pinned: true, 166 | flexibleSpace: FlexibleSpaceBar( 167 | background: _header(header, _bloc.type), 168 | collapseMode: CollapseMode.pin, 169 | ), 170 | title: Text( 171 | title, 172 | style: TextStyle(color: _titleColor), 173 | ), 174 | leading: IconButton( 175 | icon: Icon(Icons.arrow_back_ios, 176 | color: _iconColor, size: 20), 177 | onPressed: () { 178 | Navigator.maybePop(context); 179 | }, 180 | ), 181 | ) 182 | ]; 183 | }, 184 | ), 185 | ); 186 | }, 187 | ), 188 | ); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /lib/profile/pages/profile_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../widgets/list_cell.dart'; 3 | import 'message_page.dart'; 4 | import '../../widgets/blank.dart'; 5 | import '../../utils/color_util.dart'; 6 | import 'subscribes_page.dart'; 7 | 8 | class ProfilePage extends StatefulWidget { 9 | ProfilePage({Key key}) : super(key: key); 10 | 11 | @override 12 | _ProfilePageState createState() => _ProfilePageState(); 13 | } 14 | 15 | class _ProfilePageState extends State { 16 | // 头像 + 昵称 17 | Widget header() => Container( 18 | padding: EdgeInsets.only(top: 25, left: 15, right: 15, bottom: 25), 19 | child: Row( 20 | children: [ 21 | Container( 22 | child: CircleAvatar(backgroundColor: Colors.orange, radius: 40), 23 | padding: EdgeInsets.only(left: 15, right: 15)), 24 | Column( 25 | children: [ 26 | Text( 27 | 'CrazyCoderShi', 28 | style: TextStyle(fontSize: 20, fontFamily: 'PingFang'), 29 | ), 30 | GestureDetector( 31 | child: Text( 32 | '点击查看个人主页', 33 | style: TextStyle( 34 | fontSize: 14, color: ColorUtils.textGreyColor), 35 | ), 36 | ), 37 | ], 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | ), 40 | ], 41 | ), 42 | ); 43 | 44 | // 订阅-句子-喜欢 45 | Widget subscribe() => Container( 46 | padding: EdgeInsets.only(bottom: 15, left: 15, right: 15), 47 | child: Row( 48 | mainAxisAlignment: MainAxisAlignment.spaceAround, 49 | children: [ 50 | columnText('订阅', () => this.pushPage(Subscribes())), 51 | SizedBox( 52 | width: 1, 53 | height: 25, 54 | child: Container(color: ColorUtils.dividerColor)), 55 | columnText('句子', () => {}), 56 | SizedBox( 57 | width: 1, 58 | height: 25, 59 | child: Container(color: ColorUtils.dividerColor)), 60 | columnText('喜欢', () => {}), 61 | ])); 62 | 63 | Widget columnText(String title, VoidCallback onTap) => GestureDetector( 64 | child: Column( 65 | children: [ 66 | Text('0', style: TextStyle(fontSize: 16)), 67 | Text( 68 | title, 69 | style: TextStyle(fontSize: 14, color: ColorUtils.textGreyColor), 70 | ), 71 | ], 72 | ), 73 | onTap: onTap, 74 | ); 75 | 76 | void pushPage(Widget page) { 77 | Navigator.push(context, MaterialPageRoute(builder: (context) => page)); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return Scaffold( 83 | backgroundColor: Colors.white, 84 | body: SafeArea( 85 | child: ListView( 86 | children: [ 87 | header(), 88 | subscribe(), 89 | Blank(), 90 | ListCell( 91 | title: '我的消息', 92 | leading: Icons.add_alert, 93 | trailing: Icons.arrow_forward_ios, 94 | isDivider: true, 95 | onTap: () => this.pushPage(MessagePage()), 96 | ), 97 | ListCell( 98 | title: '我的收藏夹', 99 | leading: Icons.bookmark, 100 | trailing: Icons.arrow_forward_ios, 101 | isDivider: true, 102 | onTap: () => debugPrint('点击'), 103 | ), 104 | ListCell( 105 | title: '我的评论', 106 | leading: Icons.insert_comment, 107 | trailing: Icons.arrow_forward_ios, 108 | onTap: () => debugPrint('点击'), 109 | ), 110 | Blank(), 111 | ListCell( 112 | title: '常见问题', 113 | leading: Icons.assistant_photo, 114 | trailing: Icons.arrow_forward_ios, 115 | isDivider: true, 116 | onTap: () => debugPrint('点击'), 117 | ), 118 | ListCell( 119 | title: '我要反馈', 120 | leading: Icons.feedback, 121 | trailing: Icons.arrow_forward_ios, 122 | isDivider: true, 123 | onTap: () => debugPrint('点击'), 124 | ), 125 | ListCell( 126 | title: '推荐句读', 127 | leading: Icons.thumb_up, 128 | trailing: Icons.arrow_forward_ios, 129 | onTap: () => debugPrint('点击'), 130 | ), 131 | Blank(), 132 | ListCell( 133 | title: '设置', 134 | leading: Icons.settings, 135 | trailing: Icons.arrow_forward_ios, 136 | onTap: () => debugPrint('点击'), 137 | ), 138 | Blank(), 139 | ], 140 | ), 141 | top: true, 142 | ), 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/profile/pages/subscribes_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../utils/ui_util.dart'; 3 | import '../../widgets/end_cell.dart'; 4 | import '../widgets/subscribes_cell.dart'; 5 | 6 | class Subscribes extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | appBar: AppBarUtils.appBar('我的订阅', context), 11 | body: ListView.builder( 12 | itemBuilder: (context, index) { 13 | if (index == 9) { 14 | return EndCell(); 15 | } 16 | return SubscribesCell(); 17 | }, 18 | itemCount: 10, 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/profile/widgets/list_cell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import '../../utils/color_util.dart'; 4 | 5 | class ListCell extends StatelessWidget { 6 | ListCell( 7 | {Key key, 8 | this.leading, 9 | this.title, 10 | this.trailing, 11 | this.onTap, 12 | this.isDivider = false}) 13 | : super(key: key); 14 | 15 | final bool isDivider; 16 | final IconData leading; 17 | final String title; 18 | final IconData trailing; 19 | final VoidCallback onTap; 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return GestureDetector( 24 | child: Container( 25 | child: Column( 26 | children: [ 27 | Container( 28 | padding: EdgeInsets.only(top: 10, bottom: 10), 29 | color: Colors.white, 30 | child: Row( 31 | children: [ 32 | Row( 33 | children: [ 34 | Container( 35 | child: Icon(this.leading, color: ColorUtils.iconColor), 36 | padding: EdgeInsets.only(left: 15, right: 15), 37 | ), 38 | Text( 39 | this.title, 40 | style: TextStyle(fontSize: 16), 41 | ), 42 | ], 43 | ), 44 | Container( 45 | child: Icon(this.trailing, 46 | color: ColorUtils.dividerColor, size: 16), 47 | padding: EdgeInsets.only(left: 15, right: 15), 48 | ) 49 | ], 50 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 51 | ), 52 | ), 53 | Divider( 54 | color: 55 | this.isDivider ? ColorUtils.dividerColor : Colors.transparent, 56 | indent: 54, 57 | height: 1, 58 | ) 59 | ], 60 | ), 61 | color: Colors.white, 62 | ), 63 | onTap: this.onTap ?? () => {}, 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/profile/widgets/normal_header.dart: -------------------------------------------------------------------------------- 1 | // 普通用户Profile 2 | import 'dart:ui'; 3 | import '../../utils/ui_util.dart'; 4 | import 'package:flutter/material.dart'; 5 | import '../../widgets/radius_image.dart'; 6 | 7 | class NormalHeader extends StatelessWidget { 8 | NormalHeader({this.data}); 9 | 10 | final Map data; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final sreenWidth = MediaQuery.of(context).size.width; 15 | TextStyle textStyle(double size) { 16 | return TextStyle( 17 | color: Colors.white, fontSize: size, fontWeight: FontWeight.w300); 18 | } 19 | 20 | return Stack( 21 | children: [ 22 | SizedBox( 23 | width: sreenWidth, 24 | height: DeviceUtils.iPhoneXAbove(context) ? 286 : 264, 25 | child: Image.network( 26 | data['avatar'], 27 | fit: BoxFit.cover, 28 | ), 29 | ), 30 | BackdropFilter( 31 | filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), 32 | child: Container( 33 | height: DeviceUtils.iPhoneXAbove(context) ? 286 : 264, 34 | width: sreenWidth, 35 | color: Colors.black.withOpacity(0.4), 36 | child: Align( 37 | alignment: Alignment.center, 38 | child: Column( 39 | mainAxisAlignment: MainAxisAlignment.center, 40 | children: [ 41 | Padding( 42 | padding: EdgeInsets.only(top: 80), 43 | child: RadiusImage( 44 | imageUrl: data['avatar'], 45 | width: 70, 46 | height: 70, 47 | radius: 35, 48 | ), 49 | ), 50 | Padding( 51 | padding: EdgeInsets.symmetric(vertical: 8), 52 | child: Text(data['nickname'], style: textStyle(16)), 53 | ), 54 | Padding( 55 | padding: EdgeInsets.only(bottom: 10), 56 | child: Text( 57 | '关注 ${data['followings_count']} | 粉丝 ${data['followers_count']}', 58 | style: textStyle(12)), 59 | ), 60 | Container( 61 | decoration: BoxDecoration( 62 | border: Border.all(width: 0.5, color: Colors.white), 63 | borderRadius: BorderRadius.all(Radius.circular(12))), 64 | height: 24, 65 | width: 100, 66 | child: Center( 67 | child: Text( 68 | data['is_self'] ? '编辑' : '关注', 69 | style: textStyle(12), 70 | ), 71 | ), 72 | ) 73 | ], 74 | ), 75 | ), 76 | ), 77 | ), 78 | ], 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/profile/widgets/subscribes_cell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../../widgets/radius_image.dart'; 3 | import '../../utils/color_util.dart'; 4 | 5 | class SubscribesCell extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | padding: EdgeInsets.symmetric(horizontal: 15, vertical: 8), 10 | margin: EdgeInsets.only(bottom: 10), 11 | color: Colors.white, 12 | child: Row( 13 | children: [ 14 | RadiusImage( 15 | imageUrl: 16 | 'http:\/\/judou.b0.upaiyun.com\/avatar\/2018\/04\/2E9998D6-7BC8-4D1A-B2D2-77B2D664910A.JPG', 17 | radius: 3.0, 18 | width: 60, 19 | height: 60, 20 | ), 21 | Padding( 22 | padding: EdgeInsets.only(left: 8), 23 | child: Column( 24 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Row( 28 | mainAxisAlignment: MainAxisAlignment.start, 29 | children: [ 30 | Text( 31 | '薛之谦', 32 | style: TextStyle( 33 | fontSize: 14, 34 | color: ColorUtils.textPrimaryColor, 35 | ), 36 | ), 37 | Padding( 38 | padding: EdgeInsets.only(left: 3), 39 | child: Icon( 40 | Icons.stars, 41 | size: 16, 42 | color: Colors.blue, 43 | ), 44 | ) 45 | ], 46 | ), 47 | Container( 48 | width: MediaQuery.of(context).size.width - 98, 49 | child: Text( 50 | '华语流行乐男歌手,影视演员,音乐制作人,代表作品认真的雪、演员、丑八怪 || 华语流行乐男歌手,影视演员,音乐制作人,代表作品认真的雪、演员、丑八怪', 51 | style: TextStyle( 52 | fontSize: 12, 53 | color: ColorUtils.textGreyColor, 54 | ), 55 | softWrap: true, 56 | overflow: TextOverflow.ellipsis, 57 | maxLines: 2, 58 | ), 59 | ) 60 | ], 61 | ), 62 | ) 63 | ], 64 | ), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/profile/widgets/verify_header.dart: -------------------------------------------------------------------------------- 1 | // 认证用户的Profile 2 | import 'dart:ui'; 3 | import '../../index/models/user_model.dart'; 4 | import '../../utils/ui_util.dart'; 5 | import '../../widgets/blank.dart'; 6 | import '../../utils/color_util.dart'; 7 | import 'package:flutter/material.dart'; 8 | import '../../widgets/radius_image.dart'; 9 | import '../../widgets/user_info_tile.dart'; 10 | 11 | class VerfiyHeader extends StatelessWidget { 12 | VerfiyHeader({this.data}); 13 | 14 | final Map data; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final sreenWidth = MediaQuery.of(context).size.width; 19 | UserModel user = UserModel.fromJson(data['user'] ?? Map()); 20 | return Stack( 21 | children: [ 22 | SizedBox( 23 | width: sreenWidth, 24 | height: 180, 25 | child: Image.network( 26 | data['cover'], 27 | fit: BoxFit.cover, 28 | ), 29 | ), 30 | BackdropFilter( 31 | filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), 32 | child: Container( 33 | height: 180, 34 | width: sreenWidth, 35 | color: Colors.grey.withOpacity(0.1), 36 | ), 37 | ), 38 | Positioned( 39 | top: 160, 40 | child: Padding( 41 | padding: EdgeInsets.only(left: 15), 42 | child: Text( 43 | '2380人订阅', 44 | style: TextStyle(color: Colors.white70, fontSize: 12), 45 | ), 46 | ), 47 | ), 48 | Positioned( 49 | top: 120, 50 | left: (sreenWidth - 80) / 2, 51 | child: RadiusImage( 52 | imageUrl: data['cover'], 53 | width: 80, 54 | height: 80, 55 | radius: 3, 56 | ), 57 | ), 58 | Positioned( 59 | child: Container( 60 | padding: EdgeInsets.symmetric(vertical: 10, horizontal: 15), 61 | height: DeviceUtils.iPhoneXAbove(context) ? 140 : 110, 62 | width: MediaQuery.of(context).size.width, 63 | child: Column( 64 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 65 | children: [ 66 | Row( 67 | mainAxisAlignment: MainAxisAlignment.center, 68 | children: [ 69 | Text( 70 | data['name'], 71 | style: TextStyle(fontSize: 13), 72 | ), 73 | Icon(Icons.stars, size: 16, color: Colors.blue) 74 | ], 75 | ), 76 | Align( 77 | alignment: Alignment.centerLeft, 78 | child: Text( 79 | data['description'], 80 | style: TextStyle( 81 | fontSize: 12, color: ColorUtils.textGreyColor), 82 | maxLines: DeviceUtils.iPhoneXAbove(context) ? 3 : 2, 83 | overflow: TextOverflow.ellipsis, 84 | softWrap: true, 85 | ), 86 | ), 87 | Row( 88 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 89 | children: [ 90 | data['user'] != null 91 | ? UserInfoTile( 92 | avatar: user.avatar, 93 | name: user.nickname, 94 | trailName: '创建', 95 | ) 96 | : Container(), 97 | Container( 98 | decoration: BoxDecoration( 99 | border: Border.all(width: 0.5), 100 | borderRadius: BorderRadius.all(Radius.circular(3)), 101 | ), 102 | height: 24, 103 | width: 60, 104 | child: Center( 105 | child: Text( 106 | '订阅', 107 | style: TextStyle(fontSize: 13), 108 | ), 109 | ), 110 | ) 111 | ], 112 | ), 113 | ], 114 | ), 115 | ), 116 | top: 200, 117 | ), 118 | Positioned( 119 | bottom: 44, 120 | child: Blank(height: 10), 121 | ) 122 | ], 123 | ); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/utils/color_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ColorUtils { 4 | /// icon颜色 5 | static const Color iconColor = Colors.black54; 6 | 7 | /// 分割线颜色 8 | static const Color dividerColor = Color(0xFFEEEEEE); 9 | 10 | /// 空白分割条颜色 11 | static const Color blankColor = Color(0xFFFAFAFA); 12 | 13 | /// 文字主色 14 | static const Color textPrimaryColor = Color(0xFF333333); 15 | 16 | /// 灰色文字颜色 17 | static const Color textGreyColor = Color(0x42000000); 18 | 19 | /// 用户信息的用户名颜色 20 | static const Color textUserNameColor = Color(0x73000000); 21 | } 22 | -------------------------------------------------------------------------------- /lib/utils/date_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | class DateUtils { 4 | static String fromNow(int timeStamp) { 5 | /// 因为dart里面并没有实现时区的设置,只能手动设置了 6 | 7 | int now = DateTime.now().millisecondsSinceEpoch + 8 * 3600000; 8 | double distance = (now - timeStamp) / 60000; 9 | // 大于24小时就直接显示日期 10 | if (distance > 24 * 60) { 11 | DateTime time = 12 | DateTime.fromMillisecondsSinceEpoch((timeStamp + 8 * 3600) * 1000); 13 | return DateFormat('yyyy/MM/dd HH:mm').format(time); 14 | } 15 | 16 | if (distance > 60 && distance < 24 * 60) { 17 | return '${(distance / 60).toStringAsFixed(0)}小时前'; 18 | } 19 | 20 | if (distance < 60 && distance > 1) { 21 | return '${distance.toStringAsFixed(0)}分钟前'; 22 | } else { 23 | DateTime time = 24 | DateTime.fromMillisecondsSinceEpoch((timeStamp + 8 * 3600) * 1000); 25 | return DateFormat('yyyy/MM/dd HH:mm').format(time); 26 | } 27 | } 28 | 29 | static String replaceLineWithDot(String date) { 30 | return date.replaceAll(RegExp(r'-'), '.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/utils/ui_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'color_util.dart'; 3 | 4 | class AppBarUtils { 5 | static AppBar appBar(String title, BuildContext context, 6 | [Widget leading, List actions]) { 7 | void backAction() { 8 | Navigator.pop(context); 9 | } 10 | 11 | return AppBar( 12 | title: Text( 13 | title, 14 | style: TextStyle( 15 | color: Color.fromARGB(255, 45, 45, 45), 16 | fontWeight: FontWeight.w300, 17 | fontSize: 18, 18 | fontFamily: 'PingFang'), 19 | ), 20 | centerTitle: true, 21 | leading: leading ?? 22 | IconButton( 23 | icon: Icon(Icons.arrow_back_ios, 24 | color: ColorUtils.iconColor, size: 20), 25 | onPressed: backAction, 26 | ), 27 | actions: actions, 28 | ); 29 | } 30 | } 31 | 32 | class DeviceUtils { 33 | static bool iPhoneXAbove(BuildContext context) { 34 | return (DeviceUtils.sreenWidth(context) >= 375 && 35 | DeviceUtils.sreenHeight(context) >= 812); 36 | } 37 | 38 | static double sreenWidth(BuildContext context) { 39 | return (MediaQuery.of(context).size.width); 40 | } 41 | 42 | static double sreenHeight(BuildContext context) { 43 | return (MediaQuery.of(context).size.height); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/widgets/SliverAppBarDelegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math' as math; 3 | 4 | class SliverAppBarDelegate extends SliverPersistentHeaderDelegate { 5 | SliverAppBarDelegate({ 6 | @required this.minHeight, 7 | @required this.maxHeight, 8 | @required this.child, 9 | }); 10 | 11 | final double minHeight; 12 | final double maxHeight; 13 | final Widget child; 14 | 15 | @override 16 | double get minExtent => minHeight; 17 | 18 | @override 19 | double get maxExtent => math.max(minHeight, maxHeight); 20 | 21 | @override 22 | bool shouldRebuild(SliverAppBarDelegate oldDelegate) { 23 | return maxHeight != oldDelegate.maxHeight || 24 | minHeight != oldDelegate.minHeight || 25 | child != oldDelegate.child; 26 | } 27 | 28 | @override 29 | Widget build( 30 | BuildContext context, double shrinkOffset, bool overlapsContent) { 31 | return SizedBox.expand(child: child); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/widgets/blank.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../utils/color_util.dart'; 3 | 4 | class Blank extends StatelessWidget { 5 | Blank({Key key, this.height = 10, this.color = ColorUtils.blankColor}) 6 | : super(key: key); 7 | 8 | final double height; 9 | final Color color; 10 | @override 11 | Widget build(BuildContext context) { 12 | return SizedBox( 13 | width: MediaQuery.of(context).size.width, 14 | height: height, 15 | child: Container(color: color), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/widgets/button_subscript.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../utils/color_util.dart'; 3 | 4 | class SubscriptButton extends StatefulWidget { 5 | SubscriptButton( 6 | {Key key, 7 | @required this.icon, 8 | this.color = ColorUtils.iconColor, 9 | @required this.subscript, 10 | this.onPressed}) 11 | : super(key: key); 12 | 13 | final Icon icon; 14 | final Color color; 15 | final String subscript; 16 | final VoidCallback onPressed; 17 | 18 | @override 19 | _SubscriptButtonState createState() => _SubscriptButtonState(); 20 | } 21 | 22 | class _SubscriptButtonState extends State { 23 | @override 24 | Widget build(BuildContext context) { 25 | return Stack( 26 | alignment: AlignmentDirectional.topEnd, 27 | children: [ 28 | SizedBox( 29 | child: Container( 30 | padding: EdgeInsets.only(top: 5, right: 5), 31 | child: IconButton( 32 | icon: widget.icon, 33 | onPressed: widget.onPressed, 34 | color: widget.color))), 35 | Positioned( 36 | top: 12, 37 | child: Text(widget.subscript, 38 | style: TextStyle( 39 | fontSize: 10, color: widget.color, fontFamily: 'PingFang'), 40 | textAlign: TextAlign.left)) 41 | ], 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/widgets/collection_cell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import './radius_image.dart'; 3 | import '../profile/models/collections_model.dart'; 4 | 5 | class CollectionCell extends StatelessWidget { 6 | CollectionCell({ 7 | Key key, 8 | this.model, 9 | }) : super(key: key); 10 | 11 | final CollectionModel model; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | var width = MediaQuery.of(context).size.width / 2; 16 | return Container( 17 | color: Colors.white, 18 | child: Stack( 19 | children: [ 20 | SizedBox( 21 | width: width, 22 | height: width, 23 | child: Container( 24 | padding: EdgeInsets.only(top: 0, bottom: 10, left: 20, right: 10), 25 | child: RadiusImage( 26 | imageUrl: model.cover, 27 | radius: 5, 28 | width: width - 30, 29 | height: width - 30, 30 | ), 31 | ), 32 | ), 33 | Center( 34 | child: Text( 35 | model.name, 36 | style: TextStyle(color: Colors.white), 37 | ), 38 | ) 39 | ], 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/widgets/comment_cell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../widgets/radius_image.dart'; 3 | import '../index/models/comment_model.dart'; 4 | import '../utils/color_util.dart'; 5 | import '../utils/date_util.dart'; 6 | import '../profile/pages/profile_detail.dart'; 7 | 8 | class CommentCell extends StatelessWidget { 9 | CommentCell({Key key, @required this.divider, this.model}) : super(key: key); 10 | 11 | final Widget divider; 12 | final CommentModel model; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | Widget commentContent() => Padding( 17 | padding: EdgeInsets.only(bottom: 10), 18 | child: Text( 19 | model.content, 20 | style: TextStyle( 21 | color: ColorUtils.textPrimaryColor, 22 | fontSize: 13, 23 | height: 1.2, 24 | ), 25 | ), 26 | ); 27 | return Container( 28 | padding: EdgeInsets.only(left: 15), 29 | color: Colors.white, 30 | child: Column( 31 | children: [ 32 | _UserInfo(model: model), 33 | Padding( 34 | padding: EdgeInsets.only( 35 | left: 35, 36 | right: 15, 37 | bottom: model.replyToComment.isEmpty ? 0 : 8), 38 | child: Column( 39 | crossAxisAlignment: CrossAxisAlignment.start, 40 | children: [ 41 | commentContent(), 42 | model.replyToComment.isEmpty 43 | ? Container() 44 | : _ReplyContent( 45 | replyModel: CommentModel.fromJSON(model.replyToComment), 46 | ), 47 | ], 48 | ), 49 | ), 50 | Padding( 51 | padding: EdgeInsets.only(left: 35), 52 | child: divider, 53 | ), 54 | ], 55 | ), 56 | ); 57 | } 58 | } 59 | 60 | // 点赞及数字 61 | class _UpCount extends StatelessWidget { 62 | _UpCount({this.isLiked, this.countStr}); 63 | 64 | final bool isLiked; 65 | final String countStr; 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | var color = isLiked ? Colors.black : ColorUtils.textGreyColor; 70 | return Row( 71 | mainAxisAlignment: MainAxisAlignment.spaceAround, 72 | crossAxisAlignment: CrossAxisAlignment.center, 73 | children: [ 74 | Text( 75 | countStr, 76 | style: TextStyle(fontSize: 10, color: color), 77 | textAlign: TextAlign.end, 78 | ), 79 | IconButton( 80 | alignment: Alignment.centerLeft, 81 | icon: Icon( 82 | Icons.thumb_up, 83 | color: color, 84 | ), 85 | onPressed: null, 86 | iconSize: 12, 87 | ), 88 | ], 89 | ); 90 | } 91 | } 92 | 93 | // user-info 94 | class _UserInfo extends StatelessWidget { 95 | _UserInfo({this.model}); 96 | 97 | final CommentModel model; 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | return Row( 102 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 103 | children: [ 104 | GestureDetector( 105 | onTap: () => Navigator.push( 106 | context, 107 | MaterialPageRoute( 108 | builder: (_) => 109 | ProfileDetailPage(type: 0, id: '${model.user.uid}')), 110 | ), 111 | child: Row( 112 | children: [ 113 | RadiusImage( 114 | width: 30, 115 | height: 30, 116 | radius: 15, 117 | imageUrl: model.user.avatar, 118 | ), 119 | Container( 120 | padding: EdgeInsets.only(left: 5), 121 | child: Column( 122 | crossAxisAlignment: CrossAxisAlignment.start, 123 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 124 | children: [ 125 | Text( 126 | model.user.nickname, 127 | style: TextStyle(fontSize: 13), 128 | ), 129 | Text( 130 | DateUtils.fromNow(int.parse(model.createdAt)), 131 | style: TextStyle( 132 | fontSize: 10, 133 | color: ColorUtils.textGreyColor, 134 | ), 135 | softWrap: true, 136 | maxLines: 999, 137 | ) 138 | ], 139 | ), 140 | ), 141 | ], 142 | ), 143 | ), 144 | _UpCount(isLiked: model.isLiked, countStr: '${model.upCount}'), 145 | ], 146 | ); 147 | } 148 | } 149 | 150 | // 回复引用 151 | class _ReplyContent extends StatelessWidget { 152 | _ReplyContent({this.replyModel}); 153 | final CommentModel replyModel; 154 | @override 155 | Widget build(BuildContext context) { 156 | return Container( 157 | width: 999, 158 | padding: EdgeInsets.all(8), 159 | color: ColorUtils.blankColor, 160 | child: RichText( 161 | text: TextSpan( 162 | children: [ 163 | TextSpan( 164 | text: '${replyModel.user.nickname}:', 165 | style: TextStyle( 166 | color: ColorUtils.textGreyColor, 167 | fontSize: 12, 168 | height: 1.2, 169 | ), 170 | ), 171 | TextSpan( 172 | text: replyModel.content, 173 | style: TextStyle( 174 | fontSize: 12, 175 | height: 1.2, 176 | color: ColorUtils.textPrimaryColor), 177 | ) 178 | ], 179 | ), 180 | ), 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/widgets/end_cell.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../utils/color_util.dart'; 3 | 4 | class EndCell extends StatelessWidget { 5 | EndCell({Key key, this.text}) : super(key: key); 6 | 7 | final String text; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Container( 12 | padding: EdgeInsets.only(top: 10, bottom: 50), 13 | child: Align( 14 | alignment: AlignmentDirectional.center, 15 | child: Text( 16 | text, 17 | style: TextStyle(color: ColorUtils.textGreyColor), 18 | ), 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/widgets/image_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ImagePreview extends StatelessWidget { 4 | ImagePreview({Key key, this.imageUrl, this.tag}) : super(key: key); 5 | 6 | final String imageUrl; 7 | final String tag; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Scaffold( 12 | backgroundColor: Colors.black, 13 | body: Center( 14 | child: Hero( 15 | tag: this.tag, 16 | child: GestureDetector( 17 | child: Image.network(this.imageUrl), 18 | onTap: () => Navigator.pop(context)), 19 | transitionOnUserGestures: true), 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/widgets/jottings_cell.dart: -------------------------------------------------------------------------------- 1 | /// 随笔cell 2 | import 'package:flutter/material.dart'; 3 | import '../utils/color_util.dart'; 4 | 5 | class JottingsCell extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Container( 9 | padding: EdgeInsets.all(15), 10 | color: Colors.white, 11 | margin: EdgeInsets.only(bottom: 10), 12 | child: Column( 13 | crossAxisAlignment: CrossAxisAlignment.start, 14 | children: [ 15 | Text( 16 | '等我一年半', 17 | style: TextStyle(color: ColorUtils.textPrimaryColor, fontSize: 14), 18 | ), 19 | Padding( 20 | child: Text( 21 | '平日喧闹的告诉公路,在这圣诞之夜出奇地安静,隔着车窗外望,四野一片迷茫', 22 | style: TextStyle( 23 | color: Colors.grey[500], 24 | fontSize: 12, 25 | fontWeight: FontWeight.w300, 26 | ), 27 | ), 28 | padding: EdgeInsets.only(top: 5, bottom: 5), 29 | ), 30 | Row( 31 | crossAxisAlignment: CrossAxisAlignment.end, 32 | children: [ 33 | Text( 34 | '天南', 35 | style: TextStyle(fontSize: 10, color: Colors.brown[200]), 36 | ), 37 | Padding( 38 | padding: EdgeInsets.only(left: 5, right: 5), 39 | child: Icon( 40 | Icons.remove_red_eye, 41 | size: 12, 42 | color: ColorUtils.textGreyColor, 43 | ), 44 | ), 45 | Text( 46 | '666', 47 | style: TextStyle( 48 | fontSize: 10, color: ColorUtils.textUserNameColor), 49 | ), 50 | ], 51 | ), 52 | Padding( 53 | padding: EdgeInsets.only(top: 10), 54 | child: Image.network( 55 | 'https:\/\/judou.oss-cn-beijing.aliyuncs.com\/images\/sentence\/2018\/12\/24\/2ucj7v8q0r.jpeg', 56 | fit: BoxFit.cover, 57 | height: 200, 58 | width: 999, 59 | ), 60 | ) 61 | ], 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/widgets/judou_cell.dart: -------------------------------------------------------------------------------- 1 | import '../widgets/blank.dart'; 2 | import './user_info_tile.dart'; 3 | import '../utils/color_util.dart'; 4 | import '../widgets/radius_image.dart'; 5 | import '../widgets/image_preview.dart'; 6 | import 'package:flutter/material.dart'; 7 | import '../index/pages/detail_page.dart'; 8 | import '../index/models/judou_model.dart'; 9 | import '../profile/pages/profile_detail.dart'; 10 | import 'package:page_transition/page_transition.dart'; 11 | 12 | class JuDouCell extends StatefulWidget { 13 | JuDouCell({ 14 | Key key, 15 | this.divider, 16 | this.tag, 17 | this.model, 18 | this.isCell = true, 19 | }) : super(key: key); 20 | 21 | final Widget divider; 22 | // 每一个tag必须是唯一的 23 | final String tag; 24 | final JuDouModel model; 25 | 26 | /// 如果是作为Cell时content文字只显示三行 27 | final bool isCell; 28 | 29 | @override 30 | _JuDouCellState createState() => _JuDouCellState(); 31 | } 32 | 33 | class _JuDouCellState extends State with SingleTickerProviderStateMixin { 34 | JuDouModel model; 35 | AnimationController controller; 36 | 37 | @override 38 | void initState() { 39 | super.initState(); 40 | model = widget.model; 41 | controller = AnimationController(vsync: this, value: 1.0); 42 | } 43 | 44 | // 点击右侧下箭头 45 | void _moreAction() { 46 | showModalBottomSheet( 47 | context: context, 48 | builder: (builder) => SafeArea( 49 | child: Container( 50 | color: Colors.white, 51 | height: 101, 52 | child: Column( 53 | children: [ 54 | FlatButton(child: Text('复制'), onPressed: () => print('复制')), 55 | Blank(height: 5), 56 | FlatButton(child: Text('取消'), onPressed: () => print('取消')), 57 | ], 58 | ), 59 | ), 60 | ), 61 | ); 62 | } 63 | 64 | // 中间大图 65 | Widget _midImage(BuildContext context) => Hero( 66 | tag: widget.tag, 67 | transitionOnUserGestures: true, 68 | child: Padding( 69 | padding: EdgeInsets.only(top: 10, bottom: 10), 70 | child: RadiusImage( 71 | imageUrl: model.pictures[0].url, 72 | width: MediaQuery.of(context).size.width, 73 | height: 200, 74 | radius: 8.0, 75 | ), 76 | ), 77 | ); 78 | 79 | // 大图预览 80 | void _toImagePreview(BuildContext context) { 81 | Navigator.push( 82 | context, 83 | PageTransition( 84 | type: PageTransitionType.fade, 85 | child: ImagePreview( 86 | imageUrl: model.pictures[0].url, 87 | tag: widget.tag, 88 | ), 89 | ), 90 | ); 91 | } 92 | 93 | void _toDetailPage() { 94 | if (!widget.isCell) return; 95 | Navigator.push( 96 | context, 97 | MaterialPageRoute( 98 | builder: (context) => DetailPage(model: model), 99 | ), 100 | ); 101 | } 102 | 103 | @override 104 | Widget build(BuildContext context) { 105 | return GestureDetector( 106 | child: Column(children: [ 107 | Container( 108 | padding: EdgeInsets.only(top: 15, bottom: 10, left: 15, right: 15), 109 | child: Column( 110 | crossAxisAlignment: CrossAxisAlignment.start, 111 | children: [ 112 | _AuthorInfo(model: model, moreAction: _moreAction), 113 | Text( 114 | model.content, 115 | style: TextStyle(color: ColorUtils.textPrimaryColor, fontSize: 14, height: 1.2), 116 | maxLines: widget.isCell ? 4 : 999, 117 | overflow: TextOverflow.ellipsis, 118 | softWrap: true, 119 | ), 120 | model.pictures.isNotEmpty 121 | ? GestureDetector( 122 | child: _midImage(context), 123 | onTap: () => _toImagePreview(context), 124 | ) 125 | : Container(height: 10), 126 | _ReferenceAuthorInfo(model: model), 127 | Divider(color: ColorUtils.dividerColor), 128 | _BottomButtonRow(model: model, commentAction: _toDetailPage) 129 | ], 130 | ), 131 | color: Colors.white, 132 | ), 133 | widget.divider, 134 | ]), 135 | onTap: _toDetailPage, 136 | ); 137 | } 138 | } 139 | 140 | /// 顶部作者信息 141 | class _AuthorInfo extends StatelessWidget { 142 | _AuthorInfo({Key key, this.model, this.moreAction}) : super(key: key); 143 | 144 | final JuDouModel model; 145 | final VoidCallback moreAction; 146 | @override 147 | Widget build(BuildContext context) { 148 | return Row( 149 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 150 | children: [ 151 | Container( 152 | child: GestureDetector( 153 | child: Row( 154 | children: [ 155 | RadiusImage( 156 | radius: 3.0, 157 | imageUrl: model.author != null ? model.author.coverUrl : model.user.avatar ?? '', 158 | width: 30, 159 | height: 30), 160 | Padding( 161 | padding: EdgeInsets.only(left: 10), 162 | child: Text( 163 | model.author != null ? model.author.name : model.user.nickname, 164 | style: TextStyle(fontSize: 15, fontWeight: FontWeight.w300, color: ColorUtils.textUserNameColor), 165 | ), 166 | ), 167 | Padding( 168 | padding: EdgeInsets.only(left: 10), 169 | child: (model.author != null ? model.author.isVerified : false) 170 | ? Icon(Icons.stars, size: 16, color: Colors.blue) 171 | : Container()), 172 | ], 173 | ), 174 | onTap: () { 175 | Navigator.push( 176 | context, 177 | MaterialPageRoute( 178 | builder: (_) => ProfileDetailPage(type: 1, id: '${model.author.id ?? model.user.uid}')), 179 | ); 180 | }, 181 | ), 182 | ), 183 | IconButton(icon: Icon(Icons.keyboard_arrow_down), onPressed: moreAction), 184 | ], 185 | ); 186 | } 187 | } 188 | 189 | /// 收录者信息 190 | class _ReferenceAuthorInfo extends StatelessWidget { 191 | _ReferenceAuthorInfo({Key key, this.model}) : super(key: key); 192 | 193 | final JuDouModel model; 194 | @override 195 | Widget build(BuildContext context) { 196 | return model.user != null 197 | ? UserInfoTile( 198 | avatar: model.user.avatar, 199 | name: model.user.nickname, 200 | trailName: '收录', 201 | ) 202 | : Container(); 203 | } 204 | } 205 | 206 | /// 最底部一排icon 207 | class _BottomButtonRow extends StatelessWidget { 208 | _BottomButtonRow({Key key, this.model, this.commentAction}) : super(key: key); 209 | 210 | final JuDouModel model; 211 | final VoidCallback commentAction; 212 | 213 | Widget btn(IconData iconData, VoidCallback onTap, [String rightTitle]) => GestureDetector( 214 | child: Row( 215 | crossAxisAlignment: CrossAxisAlignment.center, 216 | children: [ 217 | Icon(iconData, color: ColorUtils.textUserNameColor), 218 | Padding( 219 | padding: EdgeInsets.only(left: 2), 220 | child: Text( 221 | rightTitle ?? '', 222 | style: TextStyle(color: ColorUtils.textUserNameColor, fontSize: 10), 223 | ), 224 | ) 225 | ], 226 | ), 227 | onTap: onTap, 228 | ); 229 | @override 230 | Widget build(BuildContext context) { 231 | return Row( 232 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 233 | children: [ 234 | btn(Icons.favorite_border, () => print('11111'), '${model.likeCount}'), 235 | btn(Icons.insert_comment, commentAction, '${model.commentCount}'), 236 | btn(Icons.bookmark_border, null), 237 | btn(Icons.share, null) 238 | ], 239 | ); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /lib/widgets/label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import '../utils/color_util.dart'; 3 | 4 | class Label extends StatelessWidget { 5 | Label( 6 | {Key key, 7 | this.title, 8 | @required this.width, 9 | @required this.height, 10 | this.radius, 11 | this.onTap}) 12 | : super(key: key); 13 | 14 | final double width; 15 | final double height; 16 | final double radius; 17 | final String title; 18 | final VoidCallback onTap; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return GestureDetector( 23 | child: SizedBox( 24 | width: this.width, 25 | height: this.height, 26 | child: Container( 27 | decoration: BoxDecoration( 28 | border: Border.all(color: ColorUtils.textGreyColor), 29 | borderRadius: BorderRadius.all( 30 | Radius.circular(this.radius ?? 0), 31 | ), 32 | shape: BoxShape.rectangle, 33 | ), 34 | child: Center( 35 | child: Text( 36 | this.title, 37 | style: TextStyle(color: Colors.black45, fontSize: 12), 38 | ), 39 | ), 40 | ), 41 | ), 42 | onTap: this.onTap ?? () => {}, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/widgets/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'dart:io'; 4 | 5 | class Loading extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Center( 9 | child: Platform.isIOS 10 | ? CupertinoActivityIndicator( 11 | radius: 16, 12 | ) 13 | : CircularProgressIndicator(), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/widgets/radius_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RadiusImage extends StatelessWidget { 4 | RadiusImage({ 5 | Key key, 6 | this.radius, 7 | @required this.imageUrl, 8 | @required this.width, 9 | @required this.height, 10 | }) : super(key: key); 11 | 12 | final double radius; 13 | final String imageUrl; 14 | final double width; 15 | final double height; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return ClipRRect( 20 | borderRadius: BorderRadius.all(Radius.circular(this.radius ?? 0)), 21 | child: imageUrl == null || imageUrl == '' 22 | ? Image( 23 | image: AssetImage('assets/avatar_placeholder.png'), 24 | width: this.width, 25 | height: this.height, 26 | ) 27 | : Image.network( 28 | this.imageUrl, 29 | width: this.width, 30 | height: this.height, 31 | fit: BoxFit.cover, 32 | gaplessPlayback: true, 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/widgets/user_info_tile.dart: -------------------------------------------------------------------------------- 1 | import './radius_image.dart'; 2 | import '../utils/color_util.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class UserInfoTile extends StatelessWidget { 6 | UserInfoTile({ 7 | @required this.avatar, 8 | @required this.name, 9 | @required this.trailName, 10 | }); 11 | 12 | final String avatar; 13 | final String name; 14 | final String trailName; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Row( 19 | children: [ 20 | RadiusImage( 21 | imageUrl: avatar, 22 | width: 20, 23 | height: 20, 24 | radius: 10, 25 | ), 26 | Padding( 27 | padding: EdgeInsets.only(left: 5), 28 | child: Text( 29 | name, 30 | style: TextStyle(fontSize: 10), 31 | ), 32 | ), 33 | Padding( 34 | padding: EdgeInsets.only(left: 10), 35 | child: Text( 36 | trailName, 37 | style: TextStyle(fontSize: 10, color: ColorUtils.textGreyColor), 38 | ), 39 | ), 40 | ], 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: judou 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+1 11 | 12 | environment: 13 | sdk: ">=2.0.0-dev.68.0 <3.0.0" 14 | 15 | dependencies: 16 | flutter: 17 | sdk: flutter 18 | 19 | # The following adds the Cupertino Icons font to your application. 20 | # Use with the CupertinoIcons class for iOS style icons. 21 | cupertino_icons: ^0.1.2 22 | dio: ^2.1.13 23 | page_transition: ^1.1.4 24 | rx_command: 4.3.1+1 25 | intl: ^0.15.8 26 | flutter_swiper : ^1.1.6 27 | video_player: ^0.8.0 28 | path_provider: ^0.4.1 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | 34 | 35 | # For information on the generic Dart part of this file, see the 36 | # following page: https://www.dartlang.org/tools/pub/pubspec 37 | 38 | # The following section is specific to Flutter. 39 | flutter: 40 | 41 | # The following line ensures that the Material Icons font is 42 | # included with your application, so that you can use the icons in 43 | # the material Icons class. 44 | uses-material-design: true 45 | 46 | # To add assets to your application, add an assets section, like this: 47 | # assets: 48 | # - images/a_dot_burr.jpeg 49 | # - images/a_dot_ham.jpeg 50 | 51 | # An image asset can refer to one or more resolution-specific "variants", see 52 | # https://flutter.io/assets-and-images/#resolution-aware. 53 | 54 | # For details regarding adding assets from package dependencies, see 55 | # https://flutter.io/assets-and-images/#from-packages 56 | 57 | # To add custom fonts to your application, add a fonts section here, 58 | # in this "flutter" section. Each entry in this list should have a 59 | # "family" key with the font family name, and a "fonts" key with a 60 | # list giving the asset and other descriptors for the font. For 61 | # example: 62 | # fonts: 63 | # - family: Schyler 64 | # fonts: 65 | # - asset: fonts/Schyler-Regular.ttf 66 | # - asset: fonts/Schyler-Italic.ttf 67 | # style: italic 68 | # - family: Trajan Pro 69 | # fonts: 70 | # - asset: fonts/TrajanPro.ttf 71 | # - asset: fonts/TrajanPro_Bold.ttf 72 | # weight: 700 73 | # 74 | # For details regarding fonts from package dependencies, 75 | # see https://flutter.io/custom-fonts/#from-packages 76 | 77 | assets: 78 | - assets/home.png 79 | - assets/descovery.png 80 | - assets/avatar_placeholder.png 81 | - json/daily.json 82 | 83 | fonts: 84 | - family: LiSung 85 | fonts: 86 | - asset: fonts/Apple-LiSung-Light.ttf 87 | - family: PingFang 88 | fonts: 89 | - asset: fonts/PingFang-SC-Regular.ttf -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:judou/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(JuDouApp()); 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 | --------------------------------------------------------------------------------