├── lib ├── weiguan │ ├── usecase │ │ ├── port │ │ │ ├── port.dart │ │ │ └── service │ │ │ │ ├── service.dart │ │ │ │ └── weiguan.dart │ │ ├── usecase.dart │ │ ├── base.dart │ │ ├── user.dart │ │ ├── exception.dart │ │ └── post.dart │ ├── util │ │ ├── util.dart │ │ ├── number.dart │ │ └── string.dart │ ├── ui │ │ ├── form │ │ │ ├── form.dart │ │ │ ├── file.dart │ │ │ ├── post.dart │ │ │ ├── user.dart │ │ │ └── file.g.dart │ │ ├── redux │ │ │ ├── action │ │ │ │ ├── common.dart │ │ │ │ ├── action.dart │ │ │ │ ├── user.dart │ │ │ │ ├── page.dart │ │ │ │ ├── oauth2.dart │ │ │ │ └── post.dart │ │ │ ├── redux.dart │ │ │ ├── state │ │ │ │ ├── state.dart │ │ │ │ ├── user.dart │ │ │ │ ├── oauth2.dart │ │ │ │ ├── page.dart │ │ │ │ ├── post.dart │ │ │ │ ├── app.dart │ │ │ │ ├── user.g.dart │ │ │ │ ├── oauth2.g.dart │ │ │ │ ├── page.g.dart │ │ │ │ ├── app.g.dart │ │ │ │ └── post.g.dart │ │ │ ├── reducer │ │ │ │ ├── user.dart │ │ │ │ ├── page.dart │ │ │ │ ├── oauth2.dart │ │ │ │ ├── reducer.dart │ │ │ │ └── post.dart │ │ │ └── store.dart │ │ ├── ui.dart │ │ ├── component │ │ │ ├── component.dart │ │ │ ├── common │ │ │ │ ├── select_image_source.dart │ │ │ │ └── tab_bar.dart │ │ │ └── user │ │ │ │ └── user_tile.dart │ │ ├── theme.dart │ │ ├── page │ │ │ ├── vm │ │ │ │ ├── vm.dart │ │ │ │ └── vm.g.dart │ │ │ ├── page.dart │ │ │ ├── common │ │ │ │ ├── video_player.dart │ │ │ │ ├── image_player.dart │ │ │ │ └── text_input.dart │ │ │ ├── tab.dart │ │ │ ├── bootstrap.dart │ │ │ └── user │ │ │ │ ├── follower_users.dart │ │ │ │ ├── following_users.dart │ │ │ │ ├── liked_posts.dart │ │ │ │ ├── oauth2_login.dart │ │ │ │ └── register.dart │ │ └── app.dart │ ├── adapter │ │ ├── adapter.dart │ │ ├── presenter │ │ │ ├── presenter.dart │ │ │ ├── post.dart │ │ │ ├── user.dart │ │ │ └── base.dart │ │ └── service │ │ │ └── service.dart │ ├── entity │ │ ├── entity.dart │ │ ├── message.dart │ │ ├── user.dart │ │ ├── post.dart │ │ ├── stat.dart │ │ └── file.dart │ ├── main.dart │ ├── main_dev.dart │ └── config.dart ├── demo │ ├── components │ │ ├── components.dart │ │ └── counter.dart │ ├── pages │ │ ├── navigation │ │ │ ├── back.dart │ │ │ ├── named_route.dart │ │ │ ├── basic.dart │ │ │ ├── hero.dart │ │ │ ├── send_data.dart │ │ │ ├── return_data.dart │ │ │ └── nested.dart │ │ ├── home.dart │ │ ├── layout │ │ │ ├── hori_vert_align.dart │ │ │ ├── hori_vert_packing.dart │ │ │ ├── grid_view_extent.dart │ │ │ ├── stack.dart │ │ │ ├── hori_vert_sizing.dart │ │ │ ├── grid_view_count.dart │ │ │ ├── container.dart │ │ │ ├── card.dart │ │ │ ├── list_view.dart │ │ │ ├── lake.dart │ │ │ └── pavlova.dart │ │ ├── widget │ │ │ ├── material.dart │ │ │ └── basic.dart │ │ ├── pages.dart │ │ ├── interaction │ │ │ ├── refresh_indicator.dart │ │ │ ├── favorite_lake.dart │ │ │ └── silver_app_bar.dart │ │ └── state │ │ │ └── counter.dart │ └── main.dart └── main.dart ├── assets ├── demo │ ├── lake.jpg │ ├── pavlova.jpg │ ├── large-pic-1.jpg │ ├── large-pic-2.jpg │ ├── large-pic-3.jpg │ ├── middle-pic-1.jpg │ ├── middle-pic-2.jpg │ ├── middle-pic-3.jpg │ ├── middle-pic-4.jpg │ ├── middle-pic-5.jpg │ ├── middle-pic-6.jpg │ ├── middle-pic-7.jpg │ ├── middle-pic-8.jpg │ ├── middle-pic-9.jpg │ ├── small-pic-1.jpg │ ├── small-pic-2.jpg │ ├── small-pic-3.jpg │ ├── small-pic-4.jpg │ ├── small-pic-5.jpg │ ├── small-pic-6.jpg │ ├── small-pic-7.jpg │ ├── middle-pic-10.jpg │ ├── middle-pic-11.jpg │ ├── middle-pic-12.jpg │ ├── middle-pic-13.jpg │ ├── middle-pic-14.jpg │ ├── middle-pic-15.jpg │ ├── middle-pic-16.jpg │ ├── middle-pic-17.jpg │ ├── middle-pic-18.jpg │ ├── middle-pic-19.jpg │ ├── middle-pic-20.jpg │ ├── middle-pic-21.jpg │ ├── middle-pic-22.jpg │ ├── middle-pic-23.jpg │ ├── middle-pic-24.jpg │ ├── middle-pic-25.jpg │ ├── middle-pic-26.jpg │ ├── middle-pic-27.jpg │ ├── middle-pic-28.jpg │ ├── middle-pic-29.jpg │ └── middle-pic-30.jpg └── weiguan │ ├── weiguan.png │ ├── weiguan-bg.png │ ├── post_stats.json │ ├── user_stats.json │ ├── users.json │ └── posts.json ├── android ├── .settings │ └── org.eclipse.buildship.core.prefs ├── app │ ├── .settings │ │ ├── org.eclipse.buildship.core.prefs │ │ └── org.eclipse.jdt.core.prefs │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── java │ │ │ │ └── net │ │ │ │ │ └── jaggerwang │ │ │ │ │ └── fip │ │ │ │ │ └── MainActivity.java │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── .classpath │ ├── .project │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .project ├── settings.gradle └── build.gradle ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── AppDelegate.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── main.m │ ├── AppDelegate.m │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Podfile.lock └── Podfile ├── .metadata ├── test └── widget_test.dart ├── .gitignore └── pubspec.yaml /lib/weiguan/usecase/port/port.dart: -------------------------------------------------------------------------------- 1 | export 'service/service.dart'; 2 | -------------------------------------------------------------------------------- /lib/weiguan/usecase/port/service/service.dart: -------------------------------------------------------------------------------- 1 | export 'weiguan.dart'; 2 | -------------------------------------------------------------------------------- /lib/weiguan/util/util.dart: -------------------------------------------------------------------------------- 1 | export 'number.dart'; 2 | export 'string.dart'; 3 | -------------------------------------------------------------------------------- /lib/demo/components/components.dart: -------------------------------------------------------------------------------- 1 | export 'counter.dart'; 2 | export 'drawer.dart'; 3 | -------------------------------------------------------------------------------- /lib/weiguan/ui/form/form.dart: -------------------------------------------------------------------------------- 1 | export 'post.dart'; 2 | export 'file.dart'; 3 | export 'user.dart'; 4 | -------------------------------------------------------------------------------- /assets/demo/lake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/lake.jpg -------------------------------------------------------------------------------- /lib/weiguan/adapter/adapter.dart: -------------------------------------------------------------------------------- 1 | export 'presenter/presenter.dart'; 2 | export 'service/service.dart'; 3 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir= 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /assets/demo/pavlova.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/pavlova.jpg -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | connection.project.dir=.. 2 | eclipse.preferences.version=1 3 | -------------------------------------------------------------------------------- /assets/weiguan/weiguan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/weiguan/weiguan.png -------------------------------------------------------------------------------- /lib/weiguan/adapter/presenter/presenter.dart: -------------------------------------------------------------------------------- 1 | export 'base.dart'; 2 | export 'post.dart'; 3 | export 'user.dart'; 4 | -------------------------------------------------------------------------------- /assets/demo/large-pic-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/large-pic-1.jpg -------------------------------------------------------------------------------- /assets/demo/large-pic-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/large-pic-2.jpg -------------------------------------------------------------------------------- /assets/demo/large-pic-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/large-pic-3.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-1.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-2.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-3.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-4.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-5.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-6.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-7.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-8.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-9.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-1.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-2.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-3.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-4.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-5.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-6.jpg -------------------------------------------------------------------------------- /assets/demo/small-pic-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/small-pic-7.jpg -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/action/common.dart: -------------------------------------------------------------------------------- 1 | abstract class BaseAction {} 2 | 3 | class ResetAction extends BaseAction {} 4 | -------------------------------------------------------------------------------- /assets/demo/middle-pic-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-10.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-11.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-12.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-13.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-14.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-15.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-16.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-17.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-18.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-19.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-20.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-21.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-22.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-23.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-24.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-24.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-25.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-25.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-26.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-26.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-27.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-27.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-28.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-28.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-29.jpg -------------------------------------------------------------------------------- /assets/demo/middle-pic-30.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/demo/middle-pic-30.jpg -------------------------------------------------------------------------------- /assets/weiguan/weiguan-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/assets/weiguan/weiguan-bg.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/weiguan/adapter/service/service.dart: -------------------------------------------------------------------------------- 1 | export 'weiguan_graphql.dart'; 2 | export 'weiguan_mock.dart'; 3 | export 'weiguan_rest.dart'; 4 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.enableR8=true 4 | android.useAndroidX=true 5 | android.enableJetifier=true 6 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /lib/weiguan/entity/entity.dart: -------------------------------------------------------------------------------- 1 | export 'file.dart'; 2 | export 'message.dart'; 3 | export 'post.dart'; 4 | export 'stat.dart'; 5 | export 'user.dart'; 6 | -------------------------------------------------------------------------------- /lib/weiguan/entity/message.dart: -------------------------------------------------------------------------------- 1 | enum MessageLevel { INFO, WARNING, ERROR, SUCCESS } 2 | 3 | enum NotificationLevel { INFO, WARNING, ERROR, SUCCESS } 4 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/redux.dart: -------------------------------------------------------------------------------- 1 | export 'action/action.dart'; 2 | export 'reducer/reducer.dart'; 3 | export 'state/state.dart'; 4 | export 'store.dart'; 5 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/state.dart: -------------------------------------------------------------------------------- 1 | export 'app.dart'; 2 | export 'oauth2.dart'; 3 | export 'page.dart'; 4 | export 'post.dart'; 5 | export 'user.dart'; 6 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/action/action.dart: -------------------------------------------------------------------------------- 1 | export 'common.dart'; 2 | export 'oauth2.dart'; 3 | export 'page.dart'; 4 | export 'post.dart'; 5 | export 'user.dart'; 6 | -------------------------------------------------------------------------------- /lib/weiguan/usecase/usecase.dart: -------------------------------------------------------------------------------- 1 | export 'port/port.dart'; 2 | export 'base.dart'; 3 | export 'exception.dart'; 4 | export 'post.dart'; 5 | export 'user.dart'; 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/weiguan/usecase/base.dart: -------------------------------------------------------------------------------- 1 | import 'usecase.dart'; 2 | 3 | abstract class BaseUsecases { 4 | WeiguanService weiguanService; 5 | 6 | BaseUsecases(this.weiguanService); 7 | } 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /lib/weiguan/ui/ui.dart: -------------------------------------------------------------------------------- 1 | export 'component/component.dart'; 2 | export 'form/form.dart'; 3 | export 'page/page.dart'; 4 | export 'redux/redux.dart'; 5 | export 'app.dart'; 6 | export 'theme.dart'; 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaggerwang/flutter-in-practice/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 3 | org.eclipse.jdt.core.compiler.compliance=1.8 4 | org.eclipse.jdt.core.compiler.source=1.8 5 | -------------------------------------------------------------------------------- /lib/weiguan/ui/component/component.dart: -------------------------------------------------------------------------------- 1 | export 'common/select_image_source.dart'; 2 | export 'common/tab_bar.dart'; 3 | export 'common/video_player.dart'; 4 | 5 | export 'post/post_tile.dart'; 6 | 7 | export 'user/user_tile.dart'; 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Oct 17 12:13:22 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/action/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../../entity/entity.dart'; 4 | import '../../ui.dart'; 5 | 6 | class UserLoggedAction extends BaseAction { 7 | final UserEntity user; 8 | 9 | UserLoggedAction({ 10 | @required this.user, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/weiguan/util/number.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | String formatNumber(num n) { 4 | if (n >= pow(10, 6)) { 5 | return '${(n / pow(10, 6)).round().toString()}百万'; 6 | } else if (n >= pow(10, 4)) { 7 | return '${(n / pow(10, 4)).round().toString()}万'; 8 | } else { 9 | return n.round().toString(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/action/page.dart: -------------------------------------------------------------------------------- 1 | import '../../../entity/entity.dart'; 2 | import '../../ui.dart'; 3 | 4 | class PageStateAction extends BaseAction { 5 | final PostListType homePostListType; 6 | final PostPublishForm publishForm; 7 | 8 | PageStateAction({ 9 | this.homePostListType, 10 | this.publishForm, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /.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: 20e59316b8b8474554b38493b8ca888794b0234a 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/action/oauth2.dart: -------------------------------------------------------------------------------- 1 | import '../../ui.dart'; 2 | 3 | class OAuth2StateAction extends BaseAction { 4 | final String accessToken; 5 | final DateTime accessTokenExpireAt; 6 | final String refreshToken; 7 | 8 | OAuth2StateAction({ 9 | this.accessToken, 10 | this.accessTokenExpireAt, 11 | this.refreshToken, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/reducer/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | 3 | import '../../ui.dart'; 4 | 5 | final userReducer = combineReducers([ 6 | TypedReducer(_logged), 7 | ]); 8 | 9 | UserState _logged(UserState state, UserLoggedAction action) { 10 | return state.copyWith( 11 | logged: action.user, 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /android/app/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/reducer/page.dart: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | 3 | import '../../ui.dart'; 4 | 5 | final pageReducer = combineReducers([ 6 | TypedReducer(_publishForm), 7 | ]); 8 | 9 | PageState _publishForm(PageState state, PageStateAction action) { 10 | return state.copyWith( 11 | homeMode: action.homePostListType, 12 | publishForm: action.publishForm, 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/weiguan/usecase/user.dart: -------------------------------------------------------------------------------- 1 | import '../entity/entity.dart'; 2 | import 'usecase.dart'; 3 | 4 | class UserUsecases extends BaseUsecases { 5 | UserUsecases(WeiguanService weiguanService) : super(weiguanService); 6 | 7 | Future modifyAvatar(String localPath) async { 8 | final files = await weiguanService.fileUpload([localPath], path: 'avatar'); 9 | 10 | return weiguanService.userModify(UserEntity(avatarId: files.first.id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/java/net/jaggerwang/fip/MainActivity.java: -------------------------------------------------------------------------------- 1 | package net.jaggerwang.fip; 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 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /lib/demo/pages/navigation/back.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NavigationBackPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Back'), 9 | ), 10 | body: Center( 11 | child: RaisedButton( 12 | onPressed: () => Navigator.of(context).pop(), 13 | child: Text('Back'), 14 | ), 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/reducer/oauth2.dart: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | 3 | import '../../ui.dart'; 4 | 5 | final oauth2Reducer = combineReducers([ 6 | TypedReducer(_publishForm), 7 | ]); 8 | 9 | OAuth2State _publishForm(OAuth2State state, OAuth2StateAction action) { 10 | return state.copyWith( 11 | accessToken: action.accessToken, 12 | accessTokenExpireAt: action.accessTokenExpireAt, 13 | refreshToken: action.refreshToken, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/weiguan/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'ui/ui.dart'; 4 | import 'config.dart'; 5 | import 'container.dart'; 6 | 7 | void main() async { 8 | WidgetsFlutterBinding.ensureInitialized(); 9 | 10 | final container = WgContainer(WgConfig()); 11 | await container.onReady; 12 | 13 | runApp(WgApp( 14 | config: container.config, 15 | store: container.appStore, 16 | packageInfo: container.config.packageInfo, 17 | theme: container.theme.themeData, 18 | )); 19 | } 20 | -------------------------------------------------------------------------------- /lib/demo/pages/navigation/named_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NamedRoutePage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Named Route'), 9 | ), 10 | body: Center( 11 | child: RaisedButton( 12 | child: Text('Go'), 13 | onPressed: () => Navigator.of(context).pushNamed('/back'), 14 | ), 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/demo/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'pages/pages.dart'; 4 | 5 | class JWApp extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return MaterialApp( 9 | title: 'Flutter Demo', 10 | theme: ThemeData( 11 | primarySwatch: Colors.blueGrey, 12 | ), 13 | routes: { 14 | '/back': (context) => NavigationBackPage(), 15 | }, 16 | home: HomePage(), 17 | ); 18 | } 19 | } 20 | 21 | void main() => runApp(JWApp()); 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/demo/pages/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../components/components.dart'; 4 | 5 | class HomePage extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | appBar: AppBar( 10 | title: Text('Home'), 11 | ), 12 | drawer: JWFDDrawer(), 13 | body: Center( 14 | child: Text( 15 | 'Flutter Demo', 16 | style: Theme.of(context).textTheme.display1, 17 | ), 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | import '../../../entity/entity.dart'; 5 | 6 | part 'user.g.dart'; 7 | 8 | @JsonSerializable() 9 | @FunctionalData() 10 | class UserState extends $UserState { 11 | final UserEntity logged; 12 | 13 | UserState({ 14 | this.logged, 15 | }); 16 | 17 | factory UserState.fromJson(Map json) => 18 | _$UserStateFromJson(json); 19 | 20 | Map toJson() => _$UserStateToJson(this); 21 | } 22 | -------------------------------------------------------------------------------- /lib/weiguan/ui/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class WgTheme { 4 | final themeData = ThemeData( 5 | primarySwatch: Colors.teal, 6 | textTheme: Typography.dense2018, 7 | ); 8 | 9 | final double paddingSizeSmall = 4; 10 | final double paddingSizeNormal = 8; 11 | final double paddingSizeLarge = 16; 12 | 13 | final double marginSizeSmall = 4; 14 | final double marginSizeNormal = 8; 15 | final double marginSizeLarge = 16; 16 | 17 | final double fontSizeSmall = 12; 18 | final double fontSizeNormal = 14; 19 | final double fontSizeLarge = 16; 20 | } 21 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.3' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/vm/vm.dart: -------------------------------------------------------------------------------- 1 | import 'package:functional_data/functional_data.dart'; 2 | 3 | import '../../../entity/entity.dart'; 4 | 5 | part 'vm.g.dart'; 6 | 7 | @FunctionalData() 8 | class HomeVM extends $HomeVM { 9 | final PostListType postListType; 10 | final List followingPosts; 11 | final bool followingPostsAllLoaded; 12 | final List hotPosts; 13 | final bool hotPostsAllLoaded; 14 | 15 | HomeVM({ 16 | this.postListType, 17 | this.followingPosts, 18 | this.followingPostsAllLoaded, 19 | this.hotPosts, 20 | this.hotPostsAllLoaded, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/reducer/reducer.dart: -------------------------------------------------------------------------------- 1 | import '../../../container.dart'; 2 | import '../../ui.dart'; 3 | import 'oauth2.dart'; 4 | import 'page.dart'; 5 | import 'user.dart'; 6 | import 'post.dart'; 7 | 8 | AppState appReducer(AppState state, dynamic action) { 9 | if (action is ResetAction) { 10 | return WgContainer().initialAppState; 11 | } else { 12 | return state.copyWith( 13 | oauth2: oauth2Reducer(state.oauth2, action), 14 | page: pageReducer(state.page, action), 15 | user: userReducer(state.user, action), 16 | post: postReducer(state.post, action), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/page.dart: -------------------------------------------------------------------------------- 1 | export 'common/image_player.dart'; 2 | export 'common/text_input.dart'; 3 | export 'common/video_player.dart'; 4 | 5 | export 'user/detail.dart'; 6 | export 'user/follower_users.dart'; 7 | export 'user/following_users.dart'; 8 | export 'user/liked_posts.dart'; 9 | export 'user/login.dart'; 10 | export 'user/modify_mobile.dart'; 11 | export 'user/oauth2_login.dart'; 12 | export 'user/profile.dart'; 13 | export 'user/register.dart'; 14 | 15 | export 'vm/vm.dart'; 16 | 17 | export 'bootstrap.dart'; 18 | export 'home.dart'; 19 | export 'me.dart'; 20 | export 'publish.dart'; 21 | export 'tab.dart'; 22 | -------------------------------------------------------------------------------- /assets/weiguan/post_stats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "postId": 0, 5 | "likeCount": 1000, 6 | "createdAt": "2019-07-26T03:26:01+00:00", 7 | "updatedAt": "2019-07-26T03:26:01+00:00" 8 | }, 9 | { 10 | "id": 2, 11 | "postId": 0, 12 | "likeCount": 500, 13 | "createdAt": "2019-07-26T03:26:01+00:00", 14 | "updatedAt": "2019-07-26T03:26:01+00:00" 15 | }, 16 | { 17 | "id": 3, 18 | "postId": 0, 19 | "likeCount": 2000, 20 | "createdAt": "2019-07-26T03:26:01+00:00", 21 | "updatedAt": "2019-07-26T03:26:01+00:00" 22 | } 23 | ] -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/oauth2.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | part 'oauth2.g.dart'; 5 | 6 | @JsonSerializable() 7 | @FunctionalData() 8 | class OAuth2State extends $OAuth2State { 9 | final String accessToken; 10 | final DateTime accessTokenExpireAt; 11 | final String refreshToken; 12 | 13 | OAuth2State({ 14 | this.accessToken, 15 | this.accessTokenExpireAt, 16 | this.refreshToken, 17 | }); 18 | 19 | factory OAuth2State.fromJson(Map json) => 20 | _$OAuth2StateFromJson(json); 21 | 22 | Map toJson() => _$OAuth2StateToJson(this); 23 | } 24 | -------------------------------------------------------------------------------- /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_test/flutter_test.dart'; 9 | 10 | import 'package:flutter_in_practice/main.dart'; 11 | 12 | void main() { 13 | testWidgets('Run app', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(FipApp()); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /android/app/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app 4 | Project app created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/hori_vert_align.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HoriVertAlignPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Horizontal and Vertical Align'), 9 | ), 10 | body: Center( 11 | child: Row( 12 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 13 | children: [ 14 | Image.asset('assets/demo/small-pic-1.jpg'), 15 | Image.asset('assets/demo/small-pic-2.jpg'), 16 | Image.asset('assets/demo/small-pic-3.jpg'), 17 | ], 18 | ), 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/weiguan/ui/form/file.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | part 'file.g.dart'; 6 | 7 | @JsonSerializable() 8 | @FunctionalData() 9 | class FileUploadForm extends $FileUploadForm { 10 | String region; 11 | String bucket; 12 | String path; 13 | List files; 14 | 15 | FileUploadForm({ 16 | this.region, 17 | this.bucket, 18 | this.path, 19 | @required this.files, 20 | }); 21 | 22 | factory FileUploadForm.fromJson(Map json) => 23 | _$FileUploadFormFromJson(json); 24 | 25 | Map toJson() => _$FileUploadFormToJson(this); 26 | } 27 | -------------------------------------------------------------------------------- /lib/demo/pages/widget/material.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class WidgetMaterialPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Example Title'), 9 | actions: [ 10 | IconButton( 11 | icon: Icon(Icons.search), 12 | tooltip: 'Search', 13 | onPressed: null, 14 | ), 15 | ], 16 | ), 17 | body: Center( 18 | child: Text('Hello, world!'), 19 | ), 20 | floatingActionButton: FloatingActionButton( 21 | tooltip: 'Add', 22 | child: Icon(Icons.add), 23 | onPressed: null, 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/page.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | import '../../../entity/entity.dart'; 5 | import '../../ui.dart'; 6 | 7 | part 'page.g.dart'; 8 | 9 | @JsonSerializable() 10 | @FunctionalData() 11 | class PageState extends $PageState { 12 | final PostListType homeMode; 13 | final PostPublishForm publishForm; 14 | 15 | PageState({ 16 | this.homeMode = PostListType.FOLLOWING, 17 | PostPublishForm publishForm, 18 | }) : this.publishForm = publishForm ?? PostPublishForm(); 19 | 20 | factory PageState.fromJson(Map json) => 21 | _$PageStateFromJson(json); 22 | 23 | Map toJson() => _$PageStateToJson(this); 24 | } 25 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/hori_vert_packing.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HoriVertPackingPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Horizontal and Vertical Packing'), 9 | ), 10 | body: Center( 11 | child: Row( 12 | mainAxisSize: MainAxisSize.min, 13 | children: [ 14 | Icon(Icons.star, color: Colors.green[500]), 15 | Icon(Icons.star, color: Colors.green[500]), 16 | Icon(Icons.star, color: Colors.green[500]), 17 | Icon(Icons.star, color: Colors.black), 18 | Icon(Icons.star, color: Colors.black), 19 | ], 20 | ), 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/weiguan/usecase/exception.dart: -------------------------------------------------------------------------------- 1 | class UsecaseException implements Exception { 2 | final String message; 3 | 4 | UsecaseException(this.message); 5 | 6 | @override 7 | String toString() { 8 | return 'UsecaseException(message: $message)'; 9 | } 10 | } 11 | 12 | class UnauthenticatedException extends UsecaseException { 13 | UnauthenticatedException(String message) : super(message); 14 | } 15 | 16 | class UnauthorizedException extends UsecaseException { 17 | UnauthorizedException(String message) : super(message); 18 | } 19 | 20 | class ServiceException extends UsecaseException { 21 | final String code; 22 | 23 | ServiceException(this.code, String message) : super(message); 24 | 25 | @override 26 | String toString() { 27 | return 'UsecaseException(code: $code, message: $message)'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | import '../../../entity/entity.dart'; 5 | 6 | part 'post.g.dart'; 7 | 8 | @JsonSerializable() 9 | @FunctionalData() 10 | class PostState extends $PostState { 11 | final List followingPosts; 12 | final bool followingPostsAllLoaded; 13 | final List hotPosts; 14 | final bool hotPostsAllLoaded; 15 | 16 | PostState({ 17 | this.followingPosts = const [], 18 | this.followingPostsAllLoaded = false, 19 | this.hotPosts = const [], 20 | this.hotPostsAllLoaded = false, 21 | }); 22 | 23 | factory PostState.fromJson(Map json) => 24 | _$PostStateFromJson(json); 25 | 26 | Map toJson() => _$PostStateToJson(this); 27 | } 28 | -------------------------------------------------------------------------------- /lib/weiguan/ui/form/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | import '../../entity/entity.dart'; 5 | 6 | part 'post.g.dart'; 7 | 8 | @JsonSerializable() 9 | @FunctionalData() 10 | class PostPublishForm extends $PostPublishForm { 11 | PostType type; 12 | String text; 13 | List images; 14 | List imageIds; 15 | String video; 16 | int videoId; 17 | 18 | PostPublishForm({ 19 | this.type = PostType.IMAGE, 20 | this.text = '', 21 | this.images = const [], 22 | this.imageIds = const [], 23 | this.video, 24 | this.videoId, 25 | }); 26 | 27 | factory PostPublishForm.fromJson(Map json) => 28 | _$PostPublishFormFromJson(json); 29 | 30 | Map toJson() => _$PostPublishFormToJson(this); 31 | } 32 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/grid_view_extent.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridViewExtentPage extends StatelessWidget { 4 | Widget buildGrid() { 5 | return GridView.extent( 6 | maxCrossAxisExtent: 150, 7 | padding: const EdgeInsets.all(4), 8 | mainAxisSpacing: 4, 9 | crossAxisSpacing: 4, 10 | children: List.generate( 11 | 30, 12 | (index) => Container( 13 | child: Image.asset( 14 | 'assets/demo/middle-pic-${index + 1}.jpg', 15 | fit: BoxFit.cover, 16 | ), 17 | ), 18 | ), 19 | ); 20 | } 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Scaffold( 25 | appBar: AppBar( 26 | title: Text('Grid View Extent'), 27 | ), 28 | body: Center( 29 | child: buildGrid(), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/demo/components/counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Counter extends StatefulWidget { 4 | @override 5 | _CounterState createState() => _CounterState(); 6 | } 7 | 8 | class _CounterState extends State { 9 | var _num = 0; 10 | 11 | void _substract() { 12 | setState(() { 13 | _num -= 1; 14 | }); 15 | } 16 | 17 | void _add() { 18 | setState(() { 19 | _num += 1; 20 | }); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Row( 26 | mainAxisAlignment: MainAxisAlignment.spaceAround, 27 | children: [ 28 | RaisedButton( 29 | onPressed: _substract, 30 | child: Text('-'), 31 | ), 32 | Text(_num.toString()), 33 | RaisedButton( 34 | onPressed: _add, 35 | child: Text('+'), 36 | ), 37 | ], 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /assets/weiguan/user_stats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "userId": 0, 5 | "postCount": 100, 6 | "likeCount": 200, 7 | "followingCount": 200, 8 | "followerCount": 500, 9 | "createdAt": "2019-07-26T03:25:46+00:00", 10 | "updatedAt": "2019-07-26T03:25:46+00:00" 11 | }, 12 | { 13 | "id": 2, 14 | "userId": 0, 15 | "postCount": 50, 16 | "likeCount": 100, 17 | "followingCount": 100, 18 | "followerCount": 250, 19 | "createdAt": "2019-07-26T03:25:46+00:00", 20 | "updatedAt": "2019-07-26T03:25:46+00:00" 21 | }, 22 | { 23 | "id": 3, 24 | "userId": 0, 25 | "postCount": 200, 26 | "likeCount": 400, 27 | "followingCount": 400, 28 | "followerCount": 1000, 29 | "createdAt": "2019-07-26T03:25:46+00:00", 30 | "updatedAt": "2019-07-26T03:25:46+00:00" 31 | } 32 | ] -------------------------------------------------------------------------------- /assets/weiguan/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "username": "jaggerwang", 5 | "mobile": "18666668888", 6 | "email": "jaggerwang@gmail.com", 7 | "avatarId": 1, 8 | "intro": "Coding for Free", 9 | "createdAt": "2019-08-16T03:44:47+00:00", 10 | "updatedAt": "2019-08-16T03:44:47+00:00" 11 | }, 12 | { 13 | "id": 2, 14 | "username": "天火", 15 | "mobile": null, 16 | "email": null, 17 | "avatarId": null, 18 | "intro": "变形金刚", 19 | "createdAt": "2019-08-16T03:44:47+00:00", 20 | "updatedAt": "2019-08-16T03:44:47+00:00" 21 | }, 22 | { 23 | "id": 3, 24 | "username": "灭霸", 25 | "mobile": null, 26 | "email": null, 27 | "avatarId": null, 28 | "intro": "漫威宇宙", 29 | "createdAt": "2019-08-16T03:44:47+00:00", 30 | "updatedAt": "2019-08-16T03:44:47+00:00" 31 | } 32 | ] -------------------------------------------------------------------------------- /lib/weiguan/ui/page/common/video_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:video_player/video_player.dart'; 5 | 6 | import '../../../entity/entity.dart'; 7 | import '../../ui.dart'; 8 | 9 | class VideoPlayerPage extends StatelessWidget { 10 | final FileEntity video; 11 | final File file; 12 | final VideoPlayerController controller; 13 | 14 | VideoPlayerPage({ 15 | this.video, 16 | this.file, 17 | this.controller, 18 | }) : assert(video != null || file != null); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | body: Container( 24 | alignment: Alignment.center, 25 | color: Colors.black, 26 | child: VideoPlayerWithControlBar( 27 | video: video, 28 | file: file, 29 | isAutoPlay: true, 30 | isFull: true, 31 | controller: controller, 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/demo/pages/navigation/basic.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BasicNavigationPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Basic Navigation'), 9 | ), 10 | body: Center( 11 | child: RaisedButton( 12 | child: Text('Go'), 13 | onPressed: () => Navigator.of(context).push(MaterialPageRoute( 14 | builder: (context) => _BackPage(), 15 | )), 16 | ), 17 | ), 18 | ); 19 | } 20 | } 21 | 22 | class _BackPage extends StatelessWidget { 23 | @override 24 | Widget build(BuildContext context) { 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: Text('Back'), 28 | ), 29 | body: Center( 30 | child: RaisedButton( 31 | onPressed: () => Navigator.of(context).pop(), 32 | child: Text('Back'), 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/weiguan/usecase/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../entity/entity.dart'; 4 | import 'usecase.dart'; 5 | 6 | class PostUsecases extends BaseUsecases { 7 | PostUsecases(WeiguanService weiguanService) : super(weiguanService); 8 | 9 | Future publish( 10 | {@required PostType type, 11 | String text, 12 | List localImagePaths, 13 | String localVideoPath}) async { 14 | List imageIds; 15 | int videoId; 16 | if (type == PostType.IMAGE) { 17 | final files = 18 | await weiguanService.fileUpload(localImagePaths, path: 'post'); 19 | imageIds = files.map((v) => v.id).toList(); 20 | } else if (type == PostType.VIDEO) { 21 | final files = 22 | await weiguanService.fileUpload([localVideoPath], path: 'post'); 23 | videoId = files.first.id; 24 | } 25 | 26 | return weiguanService.postPublish(PostEntity( 27 | type: type, text: text, imageIds: imageIds, videoId: videoId)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/demo/pages/pages.dart: -------------------------------------------------------------------------------- 1 | export 'interaction/favorite_lake.dart'; 2 | export 'interaction/refresh_indicator.dart'; 3 | export 'interaction/silver_app_bar.dart'; 4 | 5 | export 'layout/card.dart'; 6 | export 'layout/container.dart'; 7 | export 'layout/grid_view_count.dart'; 8 | export 'layout/grid_view_extent.dart'; 9 | export 'layout/hori_vert_align.dart'; 10 | export 'layout/hori_vert_packing.dart'; 11 | export 'layout/hori_vert_sizing.dart'; 12 | export 'layout/lake.dart'; 13 | export 'layout/list_view.dart'; 14 | export 'layout/pavlova.dart'; 15 | export 'layout/stack.dart'; 16 | 17 | export 'navigation/back.dart'; 18 | export 'navigation/basic.dart'; 19 | export 'navigation/hero.dart'; 20 | export 'navigation/named_route.dart'; 21 | export 'navigation/nested.dart'; 22 | export 'navigation/return_data.dart'; 23 | export 'navigation/send_data.dart'; 24 | export 'navigation/tab_bar.dart'; 25 | 26 | export 'state/counter.dart'; 27 | export 'state/tapbox.dart'; 28 | 29 | export 'widget/basic.dart'; 30 | export 'widget/material.dart'; 31 | 32 | export 'home.dart'; 33 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/stack.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StackPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Stack'), 9 | ), 10 | body: Center( 11 | child: Stack( 12 | alignment: const Alignment(0.6, 0.6), 13 | children: [ 14 | CircleAvatar( 15 | backgroundImage: AssetImage('assets/demo/middle-pic-1.jpg'), 16 | radius: 100, 17 | ), 18 | Container( 19 | decoration: BoxDecoration( 20 | color: Colors.black45, 21 | ), 22 | child: Text( 23 | 'Mia B', 24 | style: TextStyle( 25 | fontSize: 20, 26 | fontWeight: FontWeight.bold, 27 | color: Colors.white, 28 | ), 29 | ), 30 | ), 31 | ], 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/hori_vert_sizing.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HoriVertSizingPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Horizontal and Vertical Sizing'), 9 | ), 10 | body: Center( 11 | child: Row( 12 | children: [ 13 | Expanded( 14 | child: Image.asset( 15 | 'assets/demo/small-pic-1.jpg', 16 | fit: BoxFit.cover, 17 | ), 18 | ), 19 | Expanded( 20 | flex: 2, 21 | child: Image.asset( 22 | 'assets/demo/small-pic-2.jpg', 23 | fit: BoxFit.cover, 24 | ), 25 | ), 26 | Expanded( 27 | child: Image.asset( 28 | 'assets/demo/small-pic-3.jpg', 29 | fit: BoxFit.cover, 30 | ), 31 | ), 32 | ], 33 | ), 34 | ), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_in_practice/weiguan/ui/redux/state/oauth2.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | import 'package:functional_data/functional_data.dart'; 5 | 6 | import '../../ui.dart'; 7 | 8 | part 'app.g.dart'; 9 | 10 | @JsonSerializable() 11 | @FunctionalData() 12 | class AppState extends $AppState { 13 | final String version; 14 | final OAuth2State oauth2; 15 | final PageState page; 16 | final UserState user; 17 | final PostState post; 18 | 19 | AppState({ 20 | @required this.version, 21 | OAuth2State oauth2, 22 | PageState page, 23 | UserState user, 24 | PostState post, 25 | }) : this.oauth2 = oauth2 ?? OAuth2State(), 26 | this.page = page ?? PageState(), 27 | this.user = user ?? UserState(), 28 | this.post = post ?? PostState(); 29 | 30 | factory AppState.fromJson(Map json) => 31 | _$AppStateFromJson(json); 32 | 33 | Map toJson() => _$AppStateToJson(this); 34 | } 35 | -------------------------------------------------------------------------------- /lib/demo/pages/navigation/hero.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HeroPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Hero'), 9 | ), 10 | body: GestureDetector( 11 | child: Hero( 12 | tag: 'imageHero', 13 | child: Image.asset('assets/demo/lake.jpg'), 14 | ), 15 | onTap: () => Navigator.of(context).push(MaterialPageRoute( 16 | builder: (context) => _DetailPage(), 17 | )), 18 | ), 19 | ); 20 | } 21 | } 22 | 23 | class _DetailPage extends StatelessWidget { 24 | @override 25 | Widget build(BuildContext context) { 26 | return Scaffold( 27 | appBar: AppBar( 28 | title: Text('Detail'), 29 | ), 30 | body: GestureDetector( 31 | child: Center( 32 | child: Hero( 33 | tag: 'imageHero', 34 | child: Image.asset('assets/demo/lake.jpg'), 35 | ), 36 | ), 37 | onTap: () => Navigator.of(context).pop(), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/weiguan/main_dev.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:logging/logging.dart'; 3 | 4 | import 'ui/ui.dart'; 5 | import 'config.dart'; 6 | import 'container.dart'; 7 | 8 | void main() async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | 11 | final container = WgContainer(WgConfig( 12 | debug: true, 13 | loggerLevel: Level.ALL, 14 | logAction: true, 15 | logApi: true, 16 | enableRestApi: false, 17 | enableGraphQLApi: false, 18 | apiBaseUrl: 'http://localhost:8080', 19 | enableOAuth2Login: false, 20 | oAuth2Config: OAuth2Config( 21 | clientId: 'fip', 22 | redirectUrl: 'net.jaggerwang.fip:/login/oauth2/code/hydra', 23 | authorizationEndpoint: 'http://localhost:4444/oauth2/auth', 24 | tokenEndpoint: 'http://localhost:4444/oauth2/token', 25 | scopes: ['offline', 'user', 'post', 'file', 'stat'], 26 | ), 27 | )); 28 | await container.onReady; 29 | 30 | runApp(WgApp( 31 | config: container.config, 32 | store: container.appStore, 33 | packageInfo: container.config.packageInfo, 34 | theme: container.theme.themeData, 35 | )); 36 | } 37 | -------------------------------------------------------------------------------- /lib/weiguan/util/string.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | String randomString([int length = 8, String letters]) { 4 | if (letters == null) { 5 | letters = String.fromCharCodes( 6 | List.generate(10, (i) => '0'.codeUnitAt(0) + i) + 7 | List.generate(26, (i) => 'A'.codeUnitAt(0) + i) + 8 | List.generate(26, (i) => 'a'.codeUnitAt(0) + i)); 9 | } 10 | 11 | final random = Random(); 12 | return List.generate(length, (i) => letters[random.nextInt(letters.length)]) 13 | .join(); 14 | } 15 | 16 | int compareVersion(String version1, String version2, [int length = 3]) { 17 | final v1 = version1.split('.') 18 | ..remove('') 19 | ..addAll(List.filled(length, '0')) 20 | ..sublist(0, length); 21 | final v2 = version2.split('.') 22 | ..remove('') 23 | ..addAll(List.filled(length, '0')) 24 | ..sublist(0, length); 25 | 26 | for (final i in List.generate(length, (i) => i)) { 27 | final v11 = int.parse(v1[i]); 28 | final v22 = int.parse(v2[i]); 29 | if (v11 > v22) { 30 | return 1; 31 | } else if (v11 < v22) { 32 | return -1; 33 | } 34 | } 35 | return 0; 36 | } 37 | -------------------------------------------------------------------------------- /lib/weiguan/entity/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | import 'entity.dart'; 5 | 6 | part 'user.g.dart'; 7 | 8 | @JsonSerializable() 9 | @FunctionalData() 10 | class UserEntity extends $UserEntity { 11 | final int id; 12 | final String username; 13 | final String password; 14 | final String mobile; 15 | final String email; 16 | final int avatarId; 17 | final String intro; 18 | final DateTime createdAt; 19 | final DateTime updatedAt; 20 | final FileEntity avatar; 21 | final UserStatEntity stat; 22 | @JsonKey(defaultValue: false) 23 | final bool following; 24 | 25 | const UserEntity({ 26 | this.id, 27 | this.username, 28 | this.password, 29 | this.mobile, 30 | this.email, 31 | this.avatarId, 32 | this.intro, 33 | this.createdAt, 34 | this.updatedAt, 35 | this.avatar, 36 | this.stat, 37 | this.following, 38 | }); 39 | 40 | factory UserEntity.fromJson(Map json) => 41 | _$UserEntityFromJson(json); 42 | 43 | Map toJson() => _$UserEntityToJson(this); 44 | } 45 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/action/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | 3 | import '../../../entity/entity.dart'; 4 | import '../../ui.dart'; 5 | 6 | class PostDeleteAction extends BaseAction { 7 | final int postId; 8 | 9 | PostDeleteAction({ 10 | @required this.postId, 11 | }); 12 | } 13 | 14 | class PostLikeAction extends BaseAction { 15 | final int postId; 16 | 17 | PostLikeAction({ 18 | @required this.postId, 19 | }); 20 | } 21 | 22 | class PostUnlikeAction extends BaseAction { 23 | final int postId; 24 | 25 | PostUnlikeAction({ 26 | @required this.postId, 27 | }); 28 | } 29 | 30 | class PostFollowingAction extends BaseAction { 31 | final List posts; 32 | final bool append; 33 | final bool allLoaded; 34 | 35 | PostFollowingAction({ 36 | @required this.posts, 37 | this.append = true, 38 | this.allLoaded = false, 39 | }); 40 | } 41 | 42 | class PostHotAction extends BaseAction { 43 | final List posts; 44 | final bool clearAll; 45 | final bool allLoaded; 46 | 47 | PostHotAction({ 48 | @required this.posts, 49 | this.clearAll = false, 50 | this.allLoaded = false, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/weiguan/ui/component/common/select_image_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:image_picker/image_picker.dart'; 3 | 4 | import '../../../container.dart'; 5 | 6 | class SelectImageSource extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Column( 10 | mainAxisSize: MainAxisSize.min, 11 | crossAxisAlignment: CrossAxisAlignment.stretch, 12 | children: [ 13 | FlatButton( 14 | onPressed: () => WgContainer() 15 | .basePresenter 16 | .navigator(context) 17 | .pop(ImageSource.gallery), 18 | textColor: Theme.of(context).primaryColor, 19 | child: Text('从相册选取'), 20 | ), 21 | Divider(), 22 | FlatButton( 23 | onPressed: () => WgContainer() 24 | .basePresenter 25 | .navigator(context) 26 | .pop(ImageSource.camera), 27 | textColor: Theme.of(context).primaryColor, 28 | child: Text('用相机拍摄'), 29 | ), 30 | Divider(), 31 | FlatButton( 32 | onPressed: () => WgContainer().basePresenter.navigator(context).pop(), 33 | child: Text('取消'), 34 | ), 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/weiguan/config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:logging/logging.dart'; 5 | import 'package:package_info/package_info.dart'; 6 | 7 | class OAuth2Config { 8 | String clientId; 9 | String redirectUrl; 10 | String authorizationEndpoint; 11 | String tokenEndpoint; 12 | List scopes; 13 | 14 | OAuth2Config({ 15 | this.clientId, 16 | this.redirectUrl, 17 | this.authorizationEndpoint, 18 | this.tokenEndpoint, 19 | this.scopes, 20 | }); 21 | } 22 | 23 | class WgConfig { 24 | bool debug; 25 | Level loggerLevel; 26 | bool logAction; 27 | bool logApi; 28 | bool enableRestApi; 29 | bool enableGraphQLApi; 30 | String apiBaseUrl; 31 | bool enableOAuth2Login; 32 | OAuth2Config oAuth2Config; 33 | bool persistState; 34 | PackageInfo packageInfo; 35 | Directory appDocDir; 36 | GlobalKey rootNavigatorKey = GlobalKey(); 37 | 38 | WgConfig({ 39 | this.debug = false, 40 | this.loggerLevel = Level.INFO, 41 | this.logAction = false, 42 | this.logApi = false, 43 | this.enableRestApi = false, 44 | this.enableGraphQLApi = false, 45 | this.apiBaseUrl = '', 46 | this.enableOAuth2Login = false, 47 | this.oAuth2Config, 48 | this.persistState = true, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /lib/weiguan/ui/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_redux/flutter_redux.dart'; 3 | import 'package:package_info/package_info.dart'; 4 | import 'package:redux/redux.dart'; 5 | import 'package:bot_toast/bot_toast.dart'; 6 | 7 | import '../config.dart'; 8 | import 'ui.dart'; 9 | 10 | class WgApp extends StatelessWidget { 11 | final WgConfig config; 12 | final Store store; 13 | final PackageInfo packageInfo; 14 | final ThemeData theme; 15 | 16 | WgApp({ 17 | @required this.config, 18 | @required this.store, 19 | @required this.packageInfo, 20 | @required this.theme, 21 | }); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return StoreProvider( 26 | store: store, 27 | child: BotToastInit( 28 | child: MaterialApp( 29 | title: packageInfo.appName, 30 | theme: theme, 31 | navigatorKey: config.rootNavigatorKey, 32 | navigatorObservers: [BotToastNavigatorObserver()], 33 | routes: { 34 | '/': (context) => BootstrapPage(), 35 | '/register': (context) => RegisterPage(), 36 | '/login': (context) => LoginPage(), 37 | '/oauth2_login': (context) => OAuth2LoginPage(), 38 | '/tab': (context) => TabPage(), 39 | }, 40 | ), 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/weiguan/entity/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | import 'entity.dart'; 5 | 6 | part 'post.g.dart'; 7 | 8 | enum PostType { TEXT, IMAGE, VIDEO } 9 | 10 | enum PostListType { FOLLOWING, HOT } 11 | 12 | @JsonSerializable() 13 | @FunctionalData() 14 | class PostEntity extends $PostEntity { 15 | static final typeNames = { 16 | PostType.TEXT: '文字', 17 | PostType.IMAGE: '图片', 18 | PostType.VIDEO: '视频', 19 | }; 20 | 21 | final int id; 22 | final int userId; 23 | final PostType type; 24 | final String text; 25 | final List imageIds; 26 | final int videoId; 27 | final DateTime createdAt; 28 | final DateTime updatedAt; 29 | final UserEntity user; 30 | final List images; 31 | final FileEntity video; 32 | final PostStatEntity stat; 33 | @JsonKey(defaultValue: false) 34 | final bool liked; 35 | 36 | const PostEntity({ 37 | this.id, 38 | this.userId, 39 | this.type, 40 | this.text, 41 | this.imageIds, 42 | this.videoId, 43 | this.createdAt, 44 | this.updatedAt, 45 | this.user, 46 | this.images, 47 | this.video, 48 | this.stat, 49 | this.liked, 50 | }); 51 | 52 | factory PostEntity.fromJson(Map json) => 53 | _$PostEntityFromJson(json); 54 | 55 | Map toJson() => _$PostEntityToJson(this); 56 | } 57 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/grid_view_count.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GridViewCountPage extends StatelessWidget { 4 | Widget buildGrid(BuildContext context) { 5 | final orientation = MediaQuery.of(context).orientation; 6 | 7 | return GridView.count( 8 | crossAxisCount: (orientation == Orientation.portrait) ? 2 : 3, 9 | padding: const EdgeInsets.all(4), 10 | mainAxisSpacing: 4, 11 | crossAxisSpacing: 4, 12 | childAspectRatio: (orientation == Orientation.portrait) ? 1 : 1.3, 13 | children: List.generate( 14 | 30, 15 | (index) => GridTile( 16 | child: Image.asset( 17 | 'assets/demo/middle-pic-${index + 1}.jpg', 18 | fit: BoxFit.cover, 19 | ), 20 | footer: GridTileBar( 21 | backgroundColor: Colors.black45, 22 | title: Text('Picture ${index + 1}'), 23 | subtitle: Text('Description of ${index + 1}'), 24 | trailing: Icon( 25 | Icons.star_border, 26 | color: Colors.white, 27 | ), 28 | ), 29 | ), 30 | ), 31 | ); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: Text('Grid View Count'), 39 | ), 40 | body: Center( 41 | child: buildGrid(context), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/demo/pages/navigation/send_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class _Todo { 4 | final String title; 5 | final String description; 6 | 7 | _Todo(this.title, this.description); 8 | } 9 | 10 | class SendDataPage extends StatelessWidget { 11 | final todos = List.generate( 12 | 20, 13 | (i) => _Todo( 14 | 'Todo $i', 15 | 'A description of what needs to be done for Todo $i', 16 | ), 17 | ); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | appBar: AppBar( 23 | title: Text('Send Data'), 24 | ), 25 | body: ListView.builder( 26 | itemCount: todos.length, 27 | itemBuilder: (context, index) => ListTile( 28 | title: Text(todos[index].title), 29 | onTap: () => Navigator.of(context).push(MaterialPageRoute( 30 | builder: (context) => _DetailPage(todo: todos[index]), 31 | )), 32 | ), 33 | ), 34 | ); 35 | } 36 | } 37 | 38 | class _DetailPage extends StatelessWidget { 39 | final _Todo todo; 40 | 41 | _DetailPage({ 42 | Key key, 43 | @required this.todo, 44 | }) : super(key: key); 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Scaffold( 49 | appBar: AppBar( 50 | title: Text("${todo.title}"), 51 | ), 52 | body: Center( 53 | child: Text('${todo.description}'), 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContainerPage extends StatelessWidget { 4 | Widget _buildImage(String name) { 5 | return Expanded( 6 | child: Container( 7 | decoration: BoxDecoration( 8 | border: Border.all(width: 10, color: Colors.black38), 9 | borderRadius: const BorderRadius.all(const Radius.circular(8)), 10 | ), 11 | margin: const EdgeInsets.all(4), 12 | child: Image.asset( 13 | 'assets/demo/$name.jpg', 14 | fit: BoxFit.cover, 15 | ), 16 | ), 17 | ); 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | var container = Container( 23 | decoration: BoxDecoration( 24 | color: Colors.black26, 25 | ), 26 | child: Column( 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | Row( 30 | children: [ 31 | _buildImage('small-pic-1'), 32 | _buildImage('small-pic-2'), 33 | ], 34 | ), 35 | Row( 36 | children: [ 37 | _buildImage('small-pic-3'), 38 | _buildImage('small-pic-4'), 39 | ], 40 | ), 41 | ], 42 | ), 43 | ); 44 | 45 | return Scaffold( 46 | appBar: AppBar( 47 | title: Text('Container'), 48 | ), 49 | body: Center( 50 | child: container, 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/demo/pages/widget/basic.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class WidgetBasicPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Material( 7 | child: Column( 8 | children: [ 9 | _AppBar( 10 | title: Text( 11 | 'Example Title', 12 | style: Theme.of(context).primaryTextTheme.title, 13 | ), 14 | ), 15 | Expanded( 16 | child: Center( 17 | child: Text('Hello, world!'), 18 | ), 19 | ), 20 | ], 21 | ), 22 | ); 23 | } 24 | } 25 | 26 | class _AppBar extends StatelessWidget { 27 | final Widget title; 28 | 29 | _AppBar({this.title}); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Container( 34 | height: 56, 35 | padding: const EdgeInsets.symmetric(horizontal: 8), 36 | decoration: BoxDecoration(color: Colors.blue[500]), 37 | child: Row( 38 | children: [ 39 | IconButton( 40 | icon: Icon(Icons.menu), 41 | tooltip: 'Navigation menu', 42 | onPressed: null, 43 | ), 44 | Expanded( 45 | child: title, 46 | ), 47 | IconButton( 48 | icon: Icon(Icons.search), 49 | tooltip: 'Search', 50 | onPressed: null, 51 | ), 52 | ], 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/weiguan/entity/stat.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | part 'stat.g.dart'; 5 | 6 | @JsonSerializable() 7 | @FunctionalData() 8 | class UserStatEntity extends $UserStatEntity { 9 | final int id; 10 | final int userId; 11 | final int postCount; 12 | final int likeCount; 13 | final int followingCount; 14 | final int followerCount; 15 | final DateTime createdAt; 16 | final DateTime updatedAt; 17 | 18 | const UserStatEntity({ 19 | this.id, 20 | this.userId, 21 | this.postCount, 22 | this.likeCount, 23 | this.followingCount, 24 | this.followerCount, 25 | this.createdAt, 26 | this.updatedAt, 27 | }); 28 | 29 | factory UserStatEntity.fromJson(Map json) => 30 | _$UserStatEntityFromJson(json); 31 | 32 | Map toJson() => _$UserStatEntityToJson(this); 33 | } 34 | 35 | @JsonSerializable() 36 | @FunctionalData() 37 | class PostStatEntity extends $PostStatEntity { 38 | final int id; 39 | final int postId; 40 | final int likeCount; 41 | final DateTime createdAt; 42 | final DateTime updatedAt; 43 | 44 | const PostStatEntity({ 45 | this.id, 46 | this.postId, 47 | this.likeCount, 48 | this.createdAt, 49 | this.updatedAt, 50 | }); 51 | 52 | factory PostStatEntity.fromJson(Map json) => 53 | _$PostStatEntityFromJson(json); 54 | 55 | Map toJson() => _$PostStatEntityToJson(this); 56 | } 57 | -------------------------------------------------------------------------------- /lib/demo/pages/interaction/refresh_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class RefreshIndicatorPage extends StatefulWidget { 6 | @override 7 | _RefreshIndicatorPageState createState() => _RefreshIndicatorPageState(); 8 | } 9 | 10 | class _RefreshIndicatorPageState extends State { 11 | final _controller = ScrollController(); 12 | 13 | Future _refresh() { 14 | final completer = Completer(); 15 | Future.delayed(Duration(seconds: 2), () { 16 | completer.complete(); 17 | }); 18 | return completer.future; 19 | } 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final items = List.generate(100, (i) => i + 1).toList(); 24 | 25 | return Scaffold( 26 | appBar: AppBar( 27 | title: Text('Refresh Indicator'), 28 | ), 29 | body: RefreshIndicator( 30 | onRefresh: _refresh, 31 | child: ListView.builder( 32 | controller: _controller, 33 | itemCount: items.length, 34 | itemBuilder: (context, index) => ListTile( 35 | title: Text('Item ${items[index]}'), 36 | ), 37 | ), 38 | ), 39 | floatingActionButton: FloatingActionButton( 40 | onPressed: () => _controller.animateTo( 41 | 0, 42 | curve: Curves.easeOut, 43 | duration: const Duration(milliseconds: 300), 44 | ), 45 | child: Icon(Icons.arrow_upward, color: Colors.white), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'user.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $UserState { 10 | UserEntity get logged; 11 | const $UserState(); 12 | UserState copyWith({UserEntity logged}) => 13 | UserState(logged: logged ?? this.logged); 14 | String toString() => "UserState(logged: $logged)"; 15 | bool operator ==(dynamic other) => 16 | other.runtimeType == runtimeType && logged == other.logged; 17 | @override 18 | int get hashCode { 19 | var result = 17; 20 | result = 37 * result + logged.hashCode; 21 | return result; 22 | } 23 | } 24 | 25 | class UserState$ { 26 | static final logged = Lens( 27 | (s_) => s_.logged, (s_, logged) => s_.copyWith(logged: logged)); 28 | } 29 | 30 | // ************************************************************************** 31 | // JsonSerializableGenerator 32 | // ************************************************************************** 33 | 34 | UserState _$UserStateFromJson(Map json) { 35 | return UserState( 36 | logged: json['logged'] == null 37 | ? null 38 | : UserEntity.fromJson(json['logged'] as Map), 39 | ); 40 | } 41 | 42 | Map _$UserStateToJson(UserState instance) => { 43 | 'logged': instance.logged, 44 | }; 45 | -------------------------------------------------------------------------------- /lib/weiguan/usecase/port/service/weiguan.dart: -------------------------------------------------------------------------------- 1 | import '../../../entity/entity.dart'; 2 | 3 | abstract class WeiguanService { 4 | Future authLogin(String username, String password); 5 | 6 | Future authLogged(); 7 | 8 | Future authLogout(); 9 | 10 | Future userRegister(UserEntity userEntity); 11 | 12 | Future userModify(UserEntity userEntity, [String code]); 13 | 14 | Future userInfo(int id); 15 | 16 | Future userFollow(int userId); 17 | 18 | Future userUnfollow(int userId); 19 | 20 | Future> userFollowing( 21 | {int userId, int limit = 10, int offset = 0}); 22 | 23 | Future> userFollower( 24 | {int userId, int limit = 10, int offset = 0}); 25 | 26 | Future userSendMobileVerifyCode(String type, String mobile); 27 | 28 | Future postPublish(PostEntity postEntity); 29 | 30 | Future postDelete(int id); 31 | 32 | Future postInfo(int id); 33 | 34 | Future> postPublished( 35 | {int userId, int limit = 10, int offset = 0}); 36 | 37 | Future postLike(int postId); 38 | 39 | Future postUnlike(int postId); 40 | 41 | Future> postLiked( 42 | {int userId, int limit = 10, int offset = 0}); 43 | 44 | Future> postFollowing( 45 | {int limit = 10, int beforeId, int afterId}); 46 | 47 | Future> fileUpload(List files, 48 | {String region, String bucket, String path}); 49 | } 50 | -------------------------------------------------------------------------------- /lib/weiguan/ui/component/common/tab_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../ui.dart'; 4 | 5 | class WgTabBar extends StatelessWidget { 6 | static final tabs = [ 7 | { 8 | 'title': Text('首页'), 9 | 'icon': Icon(Icons.home), 10 | 'builder': (BuildContext context) => HomePage(), 11 | 'refresh': HomePage.refresh, 12 | }, 13 | { 14 | 'title': Text('发布'), 15 | 'icon': Icon(Icons.add), 16 | 'builder': (BuildContext context) => PublishPage(), 17 | }, 18 | { 19 | 'title': Text('我的'), 20 | 'icon': Icon(Icons.account_circle), 21 | 'builder': (BuildContext context) => MePage(), 22 | }, 23 | ]; 24 | 25 | final int currentIndex; 26 | 27 | WgTabBar({ 28 | this.currentIndex = 0, 29 | }); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return BottomNavigationBar( 34 | currentIndex: currentIndex, 35 | type: BottomNavigationBarType.fixed, 36 | onTap: (tab) { 37 | SwitchTabNotification(tab).dispatch(context); 38 | 39 | final refresh = tabs[tab]['refresh'] as void Function(); 40 | if (refresh != null && tab == this.currentIndex) refresh(); 41 | }, 42 | items: tabs 43 | .map( 44 | (v) => BottomNavigationBarItem( 45 | icon: v['icon'], 46 | title: v['title'], 47 | ), 48 | ) 49 | .toList(), 50 | ); 51 | } 52 | } 53 | 54 | class SwitchTabNotification extends Notification { 55 | final int tab; 56 | 57 | SwitchTabNotification(this.tab); 58 | } 59 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/store.dart: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | import 'package:redux_persist/redux_persist.dart'; 3 | import 'package:redux_persist_flutter/redux_persist_flutter.dart'; 4 | import 'package:redux_logging/redux_logging.dart'; 5 | 6 | import '../../util/util.dart'; 7 | import '../../container.dart'; 8 | import 'redux.dart'; 9 | 10 | Future> createStore() async { 11 | final config = WgContainer().config; 12 | var initialState = WgContainer().initialAppState; 13 | final actionLogger = WgContainer().actionLogger; 14 | 15 | final List> wms = []; 16 | if (config.logAction) { 17 | wms.add(LoggingMiddleware(logger: actionLogger)); 18 | } 19 | 20 | if (config.persistState) { 21 | final persistor = Persistor( 22 | storage: FlutterStorage(key: config.packageInfo.packageName), 23 | serializer: JsonSerializer((json) { 24 | if (json == null) { 25 | return initialState; 26 | } 27 | return AppState.fromJson(json); 28 | }), 29 | transforms: Transforms( 30 | onLoad: [ 31 | (state) { 32 | if (compareVersion(state.version, config.packageInfo.version, 2) != 33 | 0) { 34 | state = initialState; 35 | } 36 | return state; 37 | } 38 | ], 39 | ), 40 | ); 41 | 42 | initialState = await persistor.load(); 43 | 44 | wms.add(persistor.createMiddleware()); 45 | } 46 | 47 | return Store( 48 | appReducer, 49 | initialState: initialState, 50 | middleware: wms, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CardPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Card'), 9 | ), 10 | body: Center( 11 | child: SizedBox( 12 | height: 210, 13 | child: Card( 14 | child: Column( 15 | children: [ 16 | ListTile( 17 | title: Text( 18 | '1625 Main Street', 19 | style: TextStyle(fontWeight: FontWeight.w500), 20 | ), 21 | subtitle: Text('My City, CA 99984'), 22 | leading: Icon( 23 | Icons.restaurant_menu, 24 | color: Colors.blue[500], 25 | ), 26 | ), 27 | Divider(), 28 | ListTile( 29 | title: Text( 30 | '(408) 555-1212', 31 | style: TextStyle(fontWeight: FontWeight.w500), 32 | ), 33 | leading: Icon( 34 | Icons.contact_phone, 35 | color: Colors.blue[500], 36 | ), 37 | ), 38 | ListTile( 39 | title: Text('costa@example.com'), 40 | leading: Icon( 41 | Icons.contact_mail, 42 | color: Colors.blue[500], 43 | ), 44 | ), 45 | ], 46 | ), 47 | ), 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/demo/pages/state/counter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | class _CounterModel with ChangeNotifier { 5 | int value = 0; 6 | 7 | void increment() { 8 | value += 1; 9 | notifyListeners(); 10 | } 11 | } 12 | 13 | class _Body extends StatelessWidget { 14 | @override 15 | Widget build(BuildContext context) { 16 | return Center( 17 | child: Column( 18 | mainAxisAlignment: MainAxisAlignment.center, 19 | children: [ 20 | Text('You have pushed the button this many times:'), 21 | Consumer<_CounterModel>( 22 | builder: (context, counter, child) => Text( 23 | '${counter.value}', 24 | style: Theme.of(context).textTheme.display1, 25 | ), 26 | ), 27 | ], 28 | ), 29 | ); 30 | } 31 | } 32 | 33 | class _ActionButton extends StatelessWidget { 34 | @override 35 | Widget build(BuildContext context) { 36 | return FloatingActionButton( 37 | onPressed: () => 38 | Provider.of<_CounterModel>(context, listen: false).increment(), 39 | tooltip: 'Increment', 40 | child: Icon(Icons.add), 41 | ); 42 | } 43 | } 44 | 45 | class StateCounterPage extends StatelessWidget { 46 | @override 47 | Widget build(BuildContext context) { 48 | return ChangeNotifierProvider( 49 | builder: (context) => _CounterModel(), 50 | child: Scaffold( 51 | appBar: AppBar( 52 | title: Text('Counter with Provider'), 53 | ), 54 | body: _Body(), 55 | floatingActionButton: _ActionButton(), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/weiguan/ui/form/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | part 'user.g.dart'; 5 | 6 | @JsonSerializable() 7 | @FunctionalData() 8 | class UserLoginForm extends $UserLoginForm { 9 | String username; 10 | String password; 11 | 12 | UserLoginForm({ 13 | this.username, 14 | this.password, 15 | }); 16 | 17 | factory UserLoginForm.fromJson(Map json) => 18 | _$UserLoginFormFromJson(json); 19 | 20 | Map toJson() => _$UserLoginFormToJson(this); 21 | } 22 | 23 | @JsonSerializable() 24 | @FunctionalData() 25 | class UserRegisterForm extends $UserRegisterForm { 26 | String username; 27 | String password; 28 | 29 | UserRegisterForm({ 30 | this.username, 31 | this.password, 32 | }); 33 | 34 | factory UserRegisterForm.fromJson(Map json) => 35 | _$UserRegisterFormFromJson(json); 36 | 37 | Map toJson() => _$UserRegisterFormToJson(this); 38 | } 39 | 40 | @JsonSerializable() 41 | @FunctionalData() 42 | class UserProfileForm extends $UserProfileForm { 43 | String username; 44 | String password; 45 | String mobile; 46 | String email; 47 | int avatarId; 48 | String intro; 49 | String code; 50 | 51 | UserProfileForm({ 52 | this.username, 53 | this.password, 54 | this.mobile, 55 | this.email, 56 | this.avatarId, 57 | this.intro, 58 | this.code, 59 | }); 60 | 61 | factory UserProfileForm.fromJson(Map json) => 62 | _$UserProfileFormFromJson(json); 63 | 64 | Map toJson() => _$UserProfileFormToJson(this); 65 | } 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/weiguan/posts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "userId": 1, 5 | "type": "TEXT", 6 | "text": "Flutter 是 Google 在 2017 I/O 大会上推出的全新 UI 框架,目前主打移动平台,包括 Android 和 iOS,未来还会扩展到其它平台,包括桌面平台。经过一年多时间的发展,Flutter 团队于 2018/12/4 号正式释放出了 1.0 版本", 7 | "imageIds": [], 8 | "videoId": null, 9 | "createdAt": "2019-07-24T07:24:22+00:00", 10 | "updatedAt": "2019-07-24T07:24:22+00:00" 11 | }, 12 | { 13 | "id": 2, 14 | "userId": 1, 15 | "type": "IMAGE", 16 | "text": "", 17 | "imageIds": [ 18 | 2 19 | ], 20 | "videoId": null, 21 | "createdAt": "2019-07-24T07:24:22+00:00", 22 | "updatedAt": "2019-07-24T07:24:22+00:00" 23 | }, 24 | { 25 | "id": 3, 26 | "userId": 2, 27 | "type": "IMAGE", 28 | "text": "", 29 | "imageIds": [ 30 | 3, 31 | 4 32 | ], 33 | "videoId": null, 34 | "createdAt": "2019-07-24T07:24:22+00:00", 35 | "updatedAt": "2019-07-24T07:24:22+00:00" 36 | }, 37 | { 38 | "id": 4, 39 | "userId": 3, 40 | "type": "IMAGE", 41 | "text": "", 42 | "imageIds": [ 43 | 5, 44 | 6, 45 | 7 46 | ], 47 | "videoId": null, 48 | "createdAt": "2019-07-24T07:24:22+00:00", 49 | "updatedAt": "2019-07-24T07:24:22+00:00" 50 | }, 51 | { 52 | "id": 5, 53 | "userId": 1, 54 | "type": "VIDEO", 55 | "text": "", 56 | "imageIds": [], 57 | "videoId": 8, 58 | "createdAt": "2019-07-24T07:24:22+00:00", 59 | "updatedAt": "2019-07-24T07:24:22+00:00" 60 | } 61 | ] -------------------------------------------------------------------------------- /lib/demo/pages/navigation/return_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ReturnDataPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Return Data'), 9 | ), 10 | body: Builder( 11 | builder: (context) => Center( 12 | child: RaisedButton( 13 | onPressed: () async { 14 | final result = await Navigator.of(context).push(MaterialPageRoute( 15 | builder: (context) => _SelectionPage(), 16 | )); 17 | 18 | Scaffold.of(context) 19 | .showSnackBar(SnackBar(content: Text('$result'))); 20 | }, 21 | child: Text('Pick an option, any option!'), 22 | ), 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | class _SelectionPage extends StatelessWidget { 30 | @override 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: Text('Pick an option'), 35 | ), 36 | body: Center( 37 | child: Column( 38 | mainAxisAlignment: MainAxisAlignment.center, 39 | children: [ 40 | Padding( 41 | padding: const EdgeInsets.all(8), 42 | child: RaisedButton( 43 | onPressed: () => Navigator.of(context).pop('Yep!'), 44 | child: Text('Yep!'), 45 | ), 46 | ), 47 | Padding( 48 | padding: const EdgeInsets.all(8), 49 | child: RaisedButton( 50 | onPressed: () => Navigator.of(context).pop('Nope.'), 51 | child: Text('Nope.'), 52 | ), 53 | ) 54 | ], 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/ServiceDefinitions.json 67 | **/ios/Runner/GeneratedPluginRegistrant.* 68 | **/ios/Flutter/flutter_export_environment.sh 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | 77 | .vscode/ -------------------------------------------------------------------------------- /lib/weiguan/ui/page/common/image_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:carousel_slider/carousel_slider.dart'; 5 | import 'package:cached_network_image/cached_network_image.dart'; 6 | 7 | import '../../../entity/entity.dart'; 8 | import '../../../container.dart'; 9 | 10 | class ImagePlayerPage extends StatelessWidget { 11 | final List images; 12 | final List files; 13 | final int initialIndex; 14 | 15 | ImagePlayerPage({ 16 | this.images = const [], 17 | this.files = const [], 18 | this.initialIndex = 0, 19 | }) : assert(images.isNotEmpty || files.isNotEmpty); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | final screenSize = MediaQuery.of(context).size; 24 | 25 | return Scaffold( 26 | body: GestureDetector( 27 | onTap: Feedback.wrapForTap( 28 | () => WgContainer().basePresenter.navigator().pop(), context), 29 | child: Container( 30 | color: Colors.black, 31 | child: CarouselSlider( 32 | items: images.isNotEmpty 33 | ? images 34 | .map((image) => CachedNetworkImage( 35 | imageUrl: image.url, 36 | placeholder: (context, url) => Center( 37 | child: CircularProgressIndicator(), 38 | ), 39 | )) 40 | .toList() 41 | : files 42 | .map((image) => Image.file( 43 | image, 44 | fit: BoxFit.contain, 45 | )) 46 | .toList(), 47 | viewportFraction: 1.0, 48 | height: screenSize.height, 49 | initialPage: initialIndex, 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/weiguan/entity/file.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | import 'package:functional_data/functional_data.dart'; 3 | 4 | part 'file.g.dart'; 5 | 6 | enum FileThumbType { SMALL, MIDDLE, LARGE, HUGE } 7 | 8 | @JsonSerializable() 9 | @FunctionalData() 10 | class FileEntity extends $FileEntity { 11 | final int id; 12 | final int userId; 13 | final String region; 14 | final String bucket; 15 | final String path; 16 | final FileMetaEntity meta; 17 | final String url; 18 | final Map thumbs; 19 | final DateTime createdAt; 20 | final DateTime updatedAt; 21 | 22 | const FileEntity({ 23 | this.id, 24 | this.userId, 25 | this.region, 26 | this.bucket, 27 | this.path, 28 | this.meta, 29 | this.url, 30 | this.thumbs, 31 | this.createdAt, 32 | this.updatedAt, 33 | }); 34 | 35 | factory FileEntity.fromJson(Map json) => 36 | _$FileEntityFromJson(json); 37 | 38 | Map toJson() => _$FileEntityToJson(this); 39 | 40 | double get ratio { 41 | return meta.width != 0 && meta.height != 0 42 | ? meta.width / meta.height 43 | : 16 / 9; 44 | } 45 | } 46 | 47 | @JsonSerializable() 48 | @FunctionalData() 49 | class FileMetaEntity extends $FileMetaEntity { 50 | final String name; 51 | final int size; 52 | final String type; 53 | @JsonKey(defaultValue: 0) 54 | final int width; 55 | @JsonKey(defaultValue: 0) 56 | final int height; 57 | @JsonKey(defaultValue: 0) 58 | final int duration; 59 | 60 | const FileMetaEntity({ 61 | this.name, 62 | this.size, 63 | this.type, 64 | this.width, 65 | this.height, 66 | this.duration, 67 | }); 68 | 69 | factory FileMetaEntity.fromJson(Map json) => 70 | _$FileMetaEntityFromJson(json); 71 | 72 | Map toJson() => _$FileMetaEntityToJson(this); 73 | } 74 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 15 | 22 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HomePage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Scaffold( 7 | appBar: AppBar( 8 | title: Text('Flutter in Practice'), 9 | ), 10 | body: Column( 11 | mainAxisAlignment: MainAxisAlignment.center, 12 | crossAxisAlignment: CrossAxisAlignment.stretch, 13 | children: [ 14 | Spacer(flex: 4), 15 | Text( 16 | '叽歪课程 - Flutter 移动应用开发实战', 17 | textAlign: TextAlign.center, 18 | style: Theme.of(context).textTheme.title, 19 | ), 20 | Text( 21 | 'by 天火@blog.jaggerwang.net', 22 | textAlign: TextAlign.center, 23 | style: Theme.of(context).textTheme.caption, 24 | ), 25 | Spacer(), 26 | Text( 27 | 'run flutter demo', 28 | textAlign: TextAlign.center, 29 | style: Theme.of(context).textTheme.caption, 30 | ), 31 | Text( 32 | 'flutter run -t lib/demo/main.dart', 33 | textAlign: TextAlign.center, 34 | style: Theme.of(context).textTheme.subhead, 35 | ), 36 | Spacer(), 37 | Text( 38 | 'run weiguan', 39 | textAlign: TextAlign.center, 40 | style: Theme.of(context).textTheme.caption, 41 | ), 42 | Text( 43 | 'flutter run -t lib/weiguan/mobile/main.dart', 44 | textAlign: TextAlign.center, 45 | style: Theme.of(context).textTheme.subhead, 46 | ), 47 | Spacer(flex: 4), 48 | ], 49 | ), 50 | ); 51 | } 52 | } 53 | 54 | class FipApp extends StatelessWidget { 55 | @override 56 | Widget build(BuildContext context) { 57 | return MaterialApp( 58 | title: 'Flutter in Practice', 59 | home: HomePage(), 60 | ); 61 | } 62 | } 63 | 64 | void main() => runApp(FipApp()); 65 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/tab.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import '../ui.dart'; 6 | 7 | class TabPage extends StatefulWidget { 8 | @override 9 | _TabPageState createState() => _TabPageState(); 10 | } 11 | 12 | class _TabPageState extends State { 13 | final _navigatorKeys = 14 | WgTabBar.tabs.map((v) => GlobalKey()).toList(); 15 | var _tab = 0; 16 | 17 | bool _handleSwitchTabNotification(SwitchTabNotification notification) { 18 | setState(() { 19 | _tab = notification.tab; 20 | }); 21 | return true; 22 | } 23 | 24 | Future _onWillPop() async { 25 | final maybePop = await _navigatorKeys[_tab].currentState.maybePop(); 26 | return Future.value(!maybePop); 27 | } 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return WillPopScope( 32 | onWillPop: _onWillPop, 33 | child: NotificationListener( 34 | onNotification: _handleSwitchTabNotification, 35 | child: IndexedStack( 36 | index: _tab, 37 | children: WgTabBar.tabs 38 | .asMap() 39 | .entries 40 | .map( 41 | (entry) => Navigator( 42 | key: _navigatorKeys[entry.key], 43 | onGenerateRoute: (settings) { 44 | WidgetBuilder builder; 45 | switch (settings.name) { 46 | case '/': 47 | builder = entry.value['builder']; 48 | break; 49 | default: 50 | throw Exception('Unknown route: ${settings.name}'); 51 | } 52 | return MaterialPageRoute( 53 | builder: builder, 54 | settings: settings, 55 | ); 56 | }, 57 | ), 58 | ) 59 | .toList(), 60 | ), 61 | ), 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /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 28 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 "net.jaggerwang.fip" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | manifestPlaceholders = [ 42 | 'appAuthRedirectScheme': 'net.jaggerwang.fip' 43 | ] 44 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 45 | } 46 | 47 | buildTypes { 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source '../..' 58 | } 59 | 60 | dependencies { 61 | testImplementation 'junit:junit:4.12' 62 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 63 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 64 | } 65 | -------------------------------------------------------------------------------- /lib/weiguan/ui/component/user/user_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:cached_network_image/cached_network_image.dart'; 3 | 4 | import '../../../entity/entity.dart'; 5 | import '../../../container.dart'; 6 | import '../../ui.dart'; 7 | 8 | class UserTile extends StatelessWidget { 9 | final UserEntity user; 10 | final void Function() onFollow; 11 | final void Function() onUnfollow; 12 | 13 | UserTile({ 14 | Key key, 15 | @required this.user, 16 | this.onFollow, 17 | this.onUnfollow, 18 | }) : super(key: key); 19 | 20 | void _followUser(BuildContext context) async { 21 | WgContainer().basePresenter.doWithLoading(() async { 22 | await WgContainer().userPresenter.follow(user.id); 23 | 24 | if (onFollow != null) onFollow(); 25 | }); 26 | } 27 | 28 | void _unfollowUser(BuildContext context) async { 29 | WgContainer().basePresenter.doWithLoading(() async { 30 | await WgContainer().userPresenter.unfollow(user.id); 31 | 32 | if (onUnfollow != null) onUnfollow(); 33 | }); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return ListTile( 39 | onTap: () => 40 | WgContainer().basePresenter.navigator().push(MaterialPageRoute( 41 | builder: (context) => UserDetailPage(userId: user.id), 42 | )), 43 | leading: CircleAvatar( 44 | radius: 25, 45 | backgroundImage: user.avatar == null 46 | ? null 47 | : CachedNetworkImageProvider( 48 | user.avatar.thumbs[FileThumbType.SMALL]), 49 | child: user.avatar == null ? Icon(Icons.person) : null, 50 | ), 51 | title: Text(user.username), 52 | subtitle: Text(user.intro), 53 | trailing: user.following 54 | ? FlatButton( 55 | onPressed: () => _unfollowUser(context), 56 | textColor: Theme.of(context).primaryColor, 57 | child: Text('取消关注'), 58 | ) 59 | : FlatButton( 60 | onPressed: () => _followUser(context), 61 | textColor: Theme.of(context).primaryColor, 62 | child: Text('关注'), 63 | ), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/bootstrap.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../container.dart'; 4 | 5 | class BootstrapPage extends StatelessWidget { 6 | final bool needLogout; 7 | 8 | BootstrapPage({this.needLogout = false}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return Scaffold( 13 | body: _Body(needLogout: needLogout), 14 | ); 15 | } 16 | } 17 | 18 | class _Body extends StatefulWidget { 19 | final bool needLogout; 20 | 21 | _Body({this.needLogout = false}); 22 | 23 | @override 24 | _BodyState createState() => _BodyState(); 25 | } 26 | 27 | class _BodyState extends State<_Body> { 28 | @override 29 | void initState() { 30 | super.initState(); 31 | 32 | _bootstrap(); 33 | } 34 | 35 | void _bootstrap() async { 36 | WgContainer().basePresenter.doWithLoading(() async { 37 | if (widget.needLogout) { 38 | await WgContainer().userPresenter.logout(); 39 | } 40 | 41 | final user = await WgContainer().userPresenter.logged(); 42 | if (user != null) { 43 | WgContainer() 44 | .basePresenter 45 | .navigator() 46 | .pushNamedAndRemoveUntil('/tab', (route) => false); 47 | return; 48 | } 49 | 50 | final config = WgContainer().config; 51 | WgContainer().basePresenter.navigator().pushNamedAndRemoveUntil( 52 | config.enableOAuth2Login ? '/oauth2_login' : '/login', 53 | (route) => false); 54 | }); 55 | } 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return Center( 60 | child: Column( 61 | children: [ 62 | Spacer(flex: 5), 63 | FractionallySizedBox( 64 | widthFactor: 0.5, 65 | child: Image( 66 | image: AssetImage('assets/weiguan/weiguan.png'), 67 | ), 68 | ), 69 | Spacer(), 70 | Text( 71 | '正在执行网络请求,如果出错请重启应用。', 72 | style: Theme.of(context) 73 | .textTheme 74 | .body1 75 | .copyWith(color: Theme.of(context).hintColor), 76 | ), 77 | Spacer(flex: 5), 78 | ], 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | FIP 15 | CFBundleDisplayName 16 | FlutterInPractice 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | NSPhotoLibraryUsageDescription 47 | 发布动态时选取相片或视频 48 | NSCameraUsageDescription 49 | 发布动态时拍摄相片或视频 50 | NSMicrophoneUsageDescription 51 | 发布动态时拍摄视频 52 | CFBundleURLTypes 53 | 54 | 55 | CFBundleTypeRole 56 | Editor 57 | CFBundleURLSchemes 58 | 59 | net.jaggerwang.fip 60 | 61 | 62 | 63 | NSAppTransportSecurity 64 | 65 | NSAllowsArbitraryLoads 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ListViewPage extends StatelessWidget { 4 | final theaters = [ 5 | { 6 | 'title': 'CineArts at the Empire', 7 | 'subtitle': '85 W Portal Ave', 8 | }, 9 | { 10 | 'title': 'The Castro Theater', 11 | 'subtitle': '429 Castro St', 12 | }, 13 | { 14 | 'title': 'Alamo Drafthouse Cinema', 15 | 'subtitle': '2550 Mission St', 16 | }, 17 | { 18 | 'title': 'Roxie Theater', 19 | 'subtitle': '3117 16th St', 20 | }, 21 | { 22 | 'title': 'United Artists Stonestown Twin', 23 | 'subtitle': '501 Buckingham Way', 24 | }, 25 | { 26 | 'title': 'AMC Metreon 16', 27 | 'subtitle': '135 4th St #3000', 28 | }, 29 | ]; 30 | 31 | final restaurants = [ 32 | { 33 | 'title': 'K\'s Kitchen', 34 | 'subtitle': '757 Monterey Blvd', 35 | }, 36 | { 37 | 'title': 'Emmy\'s Restaurant', 38 | 'subtitle': '1923 Ocean Ave', 39 | }, 40 | { 41 | 'title': 'Chaiya Thai Restaurant', 42 | 'subtitle': '272 Claremont Blvd', 43 | }, 44 | { 45 | 'title': 'La Ciccia', 46 | 'subtitle': '291 30th St', 47 | }, 48 | ]; 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | final items = theaters 53 | .map((theater) => ListTile( 54 | title: Text( 55 | theater['title'], 56 | style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), 57 | ), 58 | subtitle: Text(theater['subtitle']), 59 | leading: Icon( 60 | Icons.theaters, 61 | color: Colors.blue[500], 62 | ), 63 | )) 64 | .toList(); 65 | 66 | items.add(Divider()); 67 | 68 | items.addAll(restaurants.map((restaurant) => ListTile( 69 | title: Text( 70 | restaurant['title'], 71 | style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), 72 | ), 73 | subtitle: Text(restaurant['subtitle']), 74 | leading: Icon( 75 | Icons.restaurant, 76 | color: Colors.blue[500], 77 | ), 78 | ))); 79 | 80 | return Scaffold( 81 | appBar: AppBar( 82 | title: Text('List View'), 83 | elevation: 5, 84 | ), 85 | body: Center( 86 | child: ListView( 87 | children: items, 88 | ), 89 | ), 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AppAuth (1.2.0): 3 | - AppAuth/Core (= 1.2.0) 4 | - AppAuth/ExternalUserAgent (= 1.2.0) 5 | - AppAuth/Core (1.2.0) 6 | - AppAuth/ExternalUserAgent (1.2.0) 7 | - Flutter (1.0.0) 8 | - flutter_appauth (0.0.1): 9 | - AppAuth (= 1.2.0) 10 | - Flutter 11 | - FMDB (2.7.5): 12 | - FMDB/standard (= 2.7.5) 13 | - FMDB/standard (2.7.5) 14 | - image_picker (0.0.1): 15 | - Flutter 16 | - package_info (0.0.1): 17 | - Flutter 18 | - path_provider (0.0.1): 19 | - Flutter 20 | - shared_preferences (0.0.1): 21 | - Flutter 22 | - sqflite (0.0.1): 23 | - Flutter 24 | - FMDB (~> 2.7.2) 25 | - video_player (0.0.1): 26 | - Flutter 27 | 28 | DEPENDENCIES: 29 | - Flutter (from `Flutter`) 30 | - flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`) 31 | - image_picker (from `.symlinks/plugins/image_picker/ios`) 32 | - package_info (from `.symlinks/plugins/package_info/ios`) 33 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 34 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 35 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 36 | - video_player (from `.symlinks/plugins/video_player/ios`) 37 | 38 | SPEC REPOS: 39 | https://github.com/CocoaPods/Specs.git: 40 | - FMDB 41 | trunk: 42 | - AppAuth 43 | 44 | EXTERNAL SOURCES: 45 | Flutter: 46 | :path: Flutter 47 | flutter_appauth: 48 | :path: ".symlinks/plugins/flutter_appauth/ios" 49 | image_picker: 50 | :path: ".symlinks/plugins/image_picker/ios" 51 | package_info: 52 | :path: ".symlinks/plugins/package_info/ios" 53 | path_provider: 54 | :path: ".symlinks/plugins/path_provider/ios" 55 | shared_preferences: 56 | :path: ".symlinks/plugins/shared_preferences/ios" 57 | sqflite: 58 | :path: ".symlinks/plugins/sqflite/ios" 59 | video_player: 60 | :path: ".symlinks/plugins/video_player/ios" 61 | 62 | SPEC CHECKSUMS: 63 | AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4 64 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 65 | flutter_appauth: df12d7d65012a380f1ab11311866cfcbfd55370f 66 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 67 | image_picker: 16e5fec1fbc87fd3b297c53e4048521eaf17cd06 68 | package_info: 78cabb3c322943c55d39676f4a5bfc748c01d055 69 | path_provider: f96fff6166a8867510d2c25fdcc346327cc4b259 70 | shared_preferences: 1feebfa37bb57264736e16865e7ffae7fc99b523 71 | sqflite: ff1d9da63c06588cc8d1faf7256d741f16989d5a 72 | video_player: 3964090a33353060ed7f58aa6427c7b4b208ec21 73 | 74 | PODFILE CHECKSUM: 3dbe063e9c90a5d7c9e4e76e70a821b9e2c1d271 75 | 76 | COCOAPODS: 1.8.3 77 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/reducer/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:redux/redux.dart'; 2 | 3 | import '../../../entity/entity.dart'; 4 | import '../../ui.dart'; 5 | 6 | final postReducer = combineReducers([ 7 | TypedReducer(_delete), 8 | TypedReducer(_like), 9 | TypedReducer(_unlike), 10 | TypedReducer(_following), 11 | TypedReducer(_hot), 12 | ]); 13 | 14 | PostState _delete(PostState state, PostDeleteAction action) { 15 | final followingPosts = List.from(state.followingPosts); 16 | followingPosts.removeWhere((v) => v.id == action.postId); 17 | 18 | final hotPosts = List.from(state.hotPosts); 19 | hotPosts.removeWhere((v) => v.id == action.postId); 20 | 21 | return state.copyWith( 22 | followingPosts: followingPosts, 23 | hotPosts: hotPosts, 24 | ); 25 | } 26 | 27 | PostState _like(PostState state, PostLikeAction action) { 28 | final followingPosts = state.followingPosts.map((post) { 29 | if (post.id == action.postId) { 30 | post = post.copyWith(liked: true); 31 | } 32 | return post; 33 | }).toList(); 34 | 35 | final hotPosts = state.hotPosts.map((post) { 36 | if (post.id == action.postId) { 37 | post = post.copyWith(liked: true); 38 | } 39 | return post; 40 | }).toList(); 41 | 42 | return state.copyWith( 43 | followingPosts: followingPosts, 44 | hotPosts: hotPosts, 45 | ); 46 | } 47 | 48 | PostState _unlike(PostState state, PostUnlikeAction action) { 49 | final followingPosts = state.followingPosts.map((post) { 50 | if (post.id == action.postId) { 51 | post = post.copyWith(liked: false); 52 | } 53 | return post; 54 | }).toList(); 55 | 56 | final hotPosts = state.hotPosts.map((post) { 57 | if (post.id == action.postId) { 58 | post = post.copyWith(liked: false); 59 | } 60 | return post; 61 | }).toList(); 62 | 63 | return state.copyWith( 64 | followingPosts: followingPosts, 65 | hotPosts: hotPosts, 66 | ); 67 | } 68 | 69 | PostState _following(PostState state, PostFollowingAction action) { 70 | return state.copyWith( 71 | followingPosts: action.append 72 | ? state.followingPosts + action.posts 73 | : action.posts + state.followingPosts, 74 | followingPostsAllLoaded: action.allLoaded, 75 | ); 76 | } 77 | 78 | PostState _hot(PostState state, PostHotAction action) { 79 | return state.copyWith( 80 | hotPosts: action.clearAll ? action.posts : state.hotPosts + action.posts, 81 | hotPostsAllLoaded: action.allLoaded, 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/user/follower_users.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../entity/entity.dart'; 4 | import '../../../container.dart'; 5 | import '../../ui.dart'; 6 | 7 | class FollowerUsersPage extends StatelessWidget { 8 | final int userId; 9 | 10 | FollowerUsersPage({ 11 | @required this.userId, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: Text('粉丝'), 19 | ), 20 | body: _Body( 21 | userId: userId, 22 | ), 23 | bottomNavigationBar: WgTabBar(currentIndex: 2), 24 | ); 25 | } 26 | } 27 | 28 | class _Body extends StatefulWidget { 29 | final int userId; 30 | 31 | _Body({ 32 | @required this.userId, 33 | }); 34 | 35 | @override 36 | _BodyState createState() => _BodyState(); 37 | } 38 | 39 | class _BodyState extends State<_Body> { 40 | var _loaded = false; 41 | List _followerUsers = []; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | 47 | _loadFollowerUsers(); 48 | } 49 | 50 | void _loadFollowerUsers() async { 51 | if (_loaded) { 52 | return; 53 | } 54 | 55 | WgContainer().basePresenter.doWithLoading(() async { 56 | final users = await WgContainer().userPresenter.follower( 57 | userId: widget.userId, limit: 20, offset: _followerUsers.length); 58 | 59 | setState(() { 60 | _loaded = users.length < 20; 61 | _followerUsers.addAll(users); 62 | }); 63 | }); 64 | } 65 | 66 | bool _handleScrollNotification(ScrollNotification notification) { 67 | if (notification is ScrollEndNotification) { 68 | if (notification.metrics.extentAfter < 500) { 69 | _loadFollowerUsers(); 70 | } 71 | } 72 | return true; 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return NotificationListener( 78 | onNotification: _handleScrollNotification, 79 | child: ListView.separated( 80 | separatorBuilder: (context, index) => Divider(), 81 | itemCount: _followerUsers.length, 82 | itemBuilder: (context, index) => UserTile( 83 | key: ValueKey(_followerUsers[index].id), 84 | user: _followerUsers[index], 85 | onFollow: () => setState(() { 86 | _followerUsers[index] = 87 | _followerUsers[index].copyWith(following: true); 88 | }), 89 | onUnfollow: () => setState(() { 90 | _followerUsers[index] = 91 | _followerUsers[index].copyWith(following: false); 92 | }), 93 | ), 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/user/following_users.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../entity/entity.dart'; 4 | import '../../../container.dart'; 5 | import '../../ui.dart'; 6 | 7 | class FollowingUsersPage extends StatelessWidget { 8 | final int userId; 9 | 10 | FollowingUsersPage({ 11 | @required this.userId, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: Text('关注'), 19 | ), 20 | body: _Body( 21 | userId: userId, 22 | ), 23 | bottomNavigationBar: WgTabBar(currentIndex: 2), 24 | ); 25 | } 26 | } 27 | 28 | class _Body extends StatefulWidget { 29 | final int userId; 30 | 31 | _Body({ 32 | @required this.userId, 33 | }); 34 | 35 | @override 36 | _BodyState createState() => _BodyState(); 37 | } 38 | 39 | class _BodyState extends State<_Body> { 40 | var _loaded = false; 41 | List _followingUsers = []; 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | 47 | _loadFollowingUsers(); 48 | } 49 | 50 | void _loadFollowingUsers() async { 51 | if (_loaded) { 52 | return; 53 | } 54 | 55 | WgContainer().basePresenter.doWithLoading(() async { 56 | final users = await WgContainer().userPresenter.following( 57 | userId: widget.userId, limit: 20, offset: _followingUsers.length); 58 | 59 | setState(() { 60 | _loaded = users.length < 20; 61 | _followingUsers.addAll(users); 62 | }); 63 | }); 64 | } 65 | 66 | bool _handleScrollNotification(ScrollNotification notification) { 67 | if (notification is ScrollEndNotification) { 68 | if (notification.metrics.extentAfter < 500) { 69 | _loadFollowingUsers(); 70 | } 71 | } 72 | return true; 73 | } 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return NotificationListener( 78 | onNotification: _handleScrollNotification, 79 | child: ListView.separated( 80 | separatorBuilder: (context, index) => Divider(), 81 | itemCount: _followingUsers.length, 82 | itemBuilder: (context, index) => UserTile( 83 | key: ValueKey(_followingUsers[index].id), 84 | user: _followingUsers[index], 85 | onFollow: () => setState(() { 86 | _followingUsers[index] = 87 | _followingUsers[index].copyWith(following: true); 88 | }), 89 | onUnfollow: () => setState(() { 90 | _followingUsers[index] = 91 | _followingUsers[index].copyWith(following: false); 92 | }), 93 | ), 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/weiguan/ui/form/file.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'file.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $FileUploadForm { 10 | String get region; 11 | String get bucket; 12 | String get path; 13 | List get files; 14 | const $FileUploadForm(); 15 | FileUploadForm copyWith( 16 | {String region, String bucket, String path, List files}) => 17 | FileUploadForm( 18 | region: region ?? this.region, 19 | bucket: bucket ?? this.bucket, 20 | path: path ?? this.path, 21 | files: files ?? this.files); 22 | String toString() => 23 | "FileUploadForm(region: $region, bucket: $bucket, path: $path, files: $files)"; 24 | bool operator ==(dynamic other) => 25 | other.runtimeType == runtimeType && 26 | region == other.region && 27 | bucket == other.bucket && 28 | path == other.path && 29 | files == other.files; 30 | @override 31 | int get hashCode { 32 | var result = 17; 33 | result = 37 * result + region.hashCode; 34 | result = 37 * result + bucket.hashCode; 35 | result = 37 * result + path.hashCode; 36 | result = 37 * result + files.hashCode; 37 | return result; 38 | } 39 | } 40 | 41 | class FileUploadForm$ { 42 | static final region = Lens( 43 | (s_) => s_.region, (s_, region) => s_.copyWith(region: region)); 44 | static final bucket = Lens( 45 | (s_) => s_.bucket, (s_, bucket) => s_.copyWith(bucket: bucket)); 46 | static final path = Lens( 47 | (s_) => s_.path, (s_, path) => s_.copyWith(path: path)); 48 | static final files = Lens>( 49 | (s_) => s_.files, (s_, files) => s_.copyWith(files: files)); 50 | } 51 | 52 | // ************************************************************************** 53 | // JsonSerializableGenerator 54 | // ************************************************************************** 55 | 56 | FileUploadForm _$FileUploadFormFromJson(Map json) { 57 | return FileUploadForm( 58 | region: json['region'] as String, 59 | bucket: json['bucket'] as String, 60 | path: json['path'] as String, 61 | files: (json['files'] as List)?.map((e) => e as String)?.toList(), 62 | ); 63 | } 64 | 65 | Map _$FileUploadFormToJson(FileUploadForm instance) => 66 | { 67 | 'region': instance.region, 68 | 'bucket': instance.bucket, 69 | 'path': instance.path, 70 | 'files': instance.files, 71 | }; 72 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/user/liked_posts.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:meta/meta.dart'; 3 | 4 | import '../../../entity/entity.dart'; 5 | import '../../../container.dart'; 6 | import '../../ui.dart'; 7 | 8 | class LikedPostsPage extends StatelessWidget { 9 | final int userId; 10 | 11 | LikedPostsPage({ 12 | @required this.userId, 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Scaffold( 18 | appBar: AppBar( 19 | title: Text('喜欢'), 20 | ), 21 | body: _Body( 22 | userId: userId, 23 | ), 24 | bottomNavigationBar: WgTabBar(currentIndex: 2), 25 | ); 26 | } 27 | } 28 | 29 | class _Body extends StatefulWidget { 30 | final int userId; 31 | 32 | _Body({ 33 | @required this.userId, 34 | }); 35 | 36 | @override 37 | _BodyState createState() => _BodyState(); 38 | } 39 | 40 | class _BodyState extends State<_Body> { 41 | var _loaded = false; 42 | List _likedPosts = []; 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | 48 | _loadLikedPosts(); 49 | } 50 | 51 | void _loadLikedPosts() async { 52 | if (_loaded) { 53 | return; 54 | } 55 | 56 | WgContainer().basePresenter.doWithLoading(() async { 57 | final posts = await WgContainer() 58 | .postPresenter 59 | .liked(userId: widget.userId, limit: 10, offset: _likedPosts.length); 60 | 61 | setState(() { 62 | _loaded = posts.length < 10; 63 | _likedPosts.addAll(posts); 64 | }); 65 | }); 66 | } 67 | 68 | bool _handleScrollNotification(ScrollNotification notification) { 69 | if (notification is ScrollEndNotification) { 70 | if (notification.metrics.extentAfter < 500) { 71 | _loadLikedPosts(); 72 | } 73 | } 74 | return true; 75 | } 76 | 77 | @override 78 | Widget build(BuildContext context) { 79 | return NotificationListener( 80 | onNotification: _handleScrollNotification, 81 | child: ListView.builder( 82 | itemCount: _likedPosts.length, 83 | itemBuilder: (context, index) => PostTile( 84 | key: ValueKey(_likedPosts[index].id), 85 | post: _likedPosts[index], 86 | onLike: () => setState(() { 87 | _likedPosts[index] = _likedPosts[index].copyWith(liked: true); 88 | }), 89 | onUnlike: () => setState(() { 90 | _likedPosts[index] = _likedPosts[index].copyWith(liked: false); 91 | }), 92 | onDelete: () => setState(() { 93 | _likedPosts.removeAt(index); 94 | }), 95 | ), 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/oauth2.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'oauth2.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $OAuth2State { 10 | String get accessToken; 11 | DateTime get accessTokenExpireAt; 12 | String get refreshToken; 13 | const $OAuth2State(); 14 | OAuth2State copyWith( 15 | {String accessToken, 16 | DateTime accessTokenExpireAt, 17 | String refreshToken}) => 18 | OAuth2State( 19 | accessToken: accessToken ?? this.accessToken, 20 | accessTokenExpireAt: accessTokenExpireAt ?? this.accessTokenExpireAt, 21 | refreshToken: refreshToken ?? this.refreshToken); 22 | String toString() => 23 | "OAuth2State(accessToken: $accessToken, accessTokenExpireAt: $accessTokenExpireAt, refreshToken: $refreshToken)"; 24 | bool operator ==(dynamic other) => 25 | other.runtimeType == runtimeType && 26 | accessToken == other.accessToken && 27 | accessTokenExpireAt == other.accessTokenExpireAt && 28 | refreshToken == other.refreshToken; 29 | @override 30 | int get hashCode { 31 | var result = 17; 32 | result = 37 * result + accessToken.hashCode; 33 | result = 37 * result + accessTokenExpireAt.hashCode; 34 | result = 37 * result + refreshToken.hashCode; 35 | return result; 36 | } 37 | } 38 | 39 | class OAuth2State$ { 40 | static final accessToken = Lens((s_) => s_.accessToken, 41 | (s_, accessToken) => s_.copyWith(accessToken: accessToken)); 42 | static final accessTokenExpireAt = Lens( 43 | (s_) => s_.accessTokenExpireAt, 44 | (s_, accessTokenExpireAt) => 45 | s_.copyWith(accessTokenExpireAt: accessTokenExpireAt)); 46 | static final refreshToken = Lens((s_) => s_.refreshToken, 47 | (s_, refreshToken) => s_.copyWith(refreshToken: refreshToken)); 48 | } 49 | 50 | // ************************************************************************** 51 | // JsonSerializableGenerator 52 | // ************************************************************************** 53 | 54 | OAuth2State _$OAuth2StateFromJson(Map json) { 55 | return OAuth2State( 56 | accessToken: json['accessToken'] as String, 57 | accessTokenExpireAt: json['accessTokenExpireAt'] == null 58 | ? null 59 | : DateTime.parse(json['accessTokenExpireAt'] as String), 60 | refreshToken: json['refreshToken'] as String, 61 | ); 62 | } 63 | 64 | Map _$OAuth2StateToJson(OAuth2State instance) => 65 | { 66 | 'accessToken': instance.accessToken, 67 | 'accessTokenExpireAt': instance.accessTokenExpireAt?.toIso8601String(), 68 | 'refreshToken': instance.refreshToken, 69 | }; 70 | -------------------------------------------------------------------------------- /lib/weiguan/adapter/presenter/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:redux/redux.dart'; 3 | import 'package:logging/logging.dart'; 4 | 5 | import '../../entity/entity.dart'; 6 | import '../../ui/ui.dart'; 7 | import '../../usecase/usecase.dart'; 8 | import '../../config.dart'; 9 | import 'base.dart'; 10 | 11 | class PostPresenter extends BasePresenter { 12 | WeiguanService weiguanService; 13 | PostUsecases postUsecases; 14 | 15 | PostPresenter({ 16 | @required WgConfig config, 17 | @required Store appStore, 18 | @required Logger logger, 19 | @required this.weiguanService, 20 | @required this.postUsecases, 21 | }) : super(config: config, appStore: appStore, logger: logger); 22 | 23 | Future publish(PostPublishForm form) async { 24 | final post = await postUsecases.publish( 25 | type: form.type, 26 | text: form.text, 27 | localImagePaths: form.images, 28 | localVideoPath: form.video); 29 | return post; 30 | } 31 | 32 | Future delete(int postId) async { 33 | await weiguanService.postDelete(postId); 34 | 35 | dispatchAction(PostDeleteAction(postId: postId)); 36 | } 37 | 38 | Future info(int postId) async { 39 | final post = await weiguanService.postInfo(postId); 40 | return post; 41 | } 42 | 43 | Future> published( 44 | {int userId, int limit = 10, int offset = 0}) async { 45 | final posts = await weiguanService.postPublished( 46 | userId: userId, limit: limit, offset: offset); 47 | return posts; 48 | } 49 | 50 | Future like(int postId) async { 51 | await weiguanService.postLike(postId); 52 | 53 | dispatchAction(PostLikeAction(postId: postId)); 54 | } 55 | 56 | Future unlike(int postId) async { 57 | await weiguanService.postUnlike(postId); 58 | 59 | dispatchAction(PostUnlikeAction(postId: postId)); 60 | } 61 | 62 | Future> liked( 63 | {int userId, int limit = 10, int offset = 0}) async { 64 | final posts = await weiguanService.postLiked( 65 | userId: userId, limit: limit, offset: offset); 66 | return posts; 67 | } 68 | 69 | Future> following( 70 | {int limit = 10, int beforeId, int afterId}) async { 71 | final posts = await weiguanService.postFollowing( 72 | limit: limit, beforeId: beforeId, afterId: afterId); 73 | 74 | dispatchAction(PostFollowingAction( 75 | posts: posts, 76 | append: afterId == null, 77 | allLoaded: posts.length < limit, 78 | )); 79 | 80 | return posts; 81 | } 82 | 83 | Future> hot({int limit = 10, int offset = 0}) async { 84 | final posts = 85 | await weiguanService.postPublished(limit: limit, offset: offset); 86 | 87 | dispatchAction(PostHotAction( 88 | posts: posts, 89 | clearAll: offset == 0, 90 | allLoaded: posts.length < limit, 91 | )); 92 | 93 | return posts; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/vm/vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'vm.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $HomeVM { 10 | PostListType get postListType; 11 | List get followingPosts; 12 | bool get followingPostsAllLoaded; 13 | List get hotPosts; 14 | bool get hotPostsAllLoaded; 15 | const $HomeVM(); 16 | HomeVM copyWith( 17 | {PostListType postListType, 18 | List followingPosts, 19 | bool followingPostsAllLoaded, 20 | List hotPosts, 21 | bool hotPostsAllLoaded}) => 22 | HomeVM( 23 | postListType: postListType ?? this.postListType, 24 | followingPosts: followingPosts ?? this.followingPosts, 25 | followingPostsAllLoaded: 26 | followingPostsAllLoaded ?? this.followingPostsAllLoaded, 27 | hotPosts: hotPosts ?? this.hotPosts, 28 | hotPostsAllLoaded: hotPostsAllLoaded ?? this.hotPostsAllLoaded); 29 | String toString() => 30 | "HomeVM(postListType: $postListType, followingPosts: $followingPosts, followingPostsAllLoaded: $followingPostsAllLoaded, hotPosts: $hotPosts, hotPostsAllLoaded: $hotPostsAllLoaded)"; 31 | bool operator ==(dynamic other) => 32 | other.runtimeType == runtimeType && 33 | postListType == other.postListType && 34 | followingPosts == other.followingPosts && 35 | followingPostsAllLoaded == other.followingPostsAllLoaded && 36 | hotPosts == other.hotPosts && 37 | hotPostsAllLoaded == other.hotPostsAllLoaded; 38 | @override 39 | int get hashCode { 40 | var result = 17; 41 | result = 37 * result + postListType.hashCode; 42 | result = 37 * result + followingPosts.hashCode; 43 | result = 37 * result + followingPostsAllLoaded.hashCode; 44 | result = 37 * result + hotPosts.hashCode; 45 | result = 37 * result + hotPostsAllLoaded.hashCode; 46 | return result; 47 | } 48 | } 49 | 50 | class HomeVM$ { 51 | static final postListType = Lens( 52 | (s_) => s_.postListType, 53 | (s_, postListType) => s_.copyWith(postListType: postListType)); 54 | static final followingPosts = Lens>( 55 | (s_) => s_.followingPosts, 56 | (s_, followingPosts) => s_.copyWith(followingPosts: followingPosts)); 57 | static final followingPostsAllLoaded = Lens( 58 | (s_) => s_.followingPostsAllLoaded, 59 | (s_, followingPostsAllLoaded) => 60 | s_.copyWith(followingPostsAllLoaded: followingPostsAllLoaded)); 61 | static final hotPosts = Lens>( 62 | (s_) => s_.hotPosts, (s_, hotPosts) => s_.copyWith(hotPosts: hotPosts)); 63 | static final hotPostsAllLoaded = Lens( 64 | (s_) => s_.hotPostsAllLoaded, 65 | (s_, hotPostsAllLoaded) => 66 | s_.copyWith(hotPostsAllLoaded: hotPostsAllLoaded)); 67 | } 68 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/page.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'page.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $PageState { 10 | PostListType get homeMode; 11 | PostPublishForm get publishForm; 12 | const $PageState(); 13 | PageState copyWith({PostListType homeMode, PostPublishForm publishForm}) => 14 | PageState( 15 | homeMode: homeMode ?? this.homeMode, 16 | publishForm: publishForm ?? this.publishForm); 17 | String toString() => 18 | "PageState(homeMode: $homeMode, publishForm: $publishForm)"; 19 | bool operator ==(dynamic other) => 20 | other.runtimeType == runtimeType && 21 | homeMode == other.homeMode && 22 | publishForm == other.publishForm; 23 | @override 24 | int get hashCode { 25 | var result = 17; 26 | result = 37 * result + homeMode.hashCode; 27 | result = 37 * result + publishForm.hashCode; 28 | return result; 29 | } 30 | } 31 | 32 | class PageState$ { 33 | static final homeMode = Lens( 34 | (s_) => s_.homeMode, (s_, homeMode) => s_.copyWith(homeMode: homeMode)); 35 | static final publishForm = Lens( 36 | (s_) => s_.publishForm, 37 | (s_, publishForm) => s_.copyWith(publishForm: publishForm)); 38 | } 39 | 40 | // ************************************************************************** 41 | // JsonSerializableGenerator 42 | // ************************************************************************** 43 | 44 | PageState _$PageStateFromJson(Map json) { 45 | return PageState( 46 | homeMode: _$enumDecodeNullable(_$PostListTypeEnumMap, json['homeMode']), 47 | publishForm: json['publishForm'] == null 48 | ? null 49 | : PostPublishForm.fromJson(json['publishForm'] as Map), 50 | ); 51 | } 52 | 53 | Map _$PageStateToJson(PageState instance) => { 54 | 'homeMode': _$PostListTypeEnumMap[instance.homeMode], 55 | 'publishForm': instance.publishForm, 56 | }; 57 | 58 | T _$enumDecode( 59 | Map enumValues, 60 | dynamic source, { 61 | T unknownValue, 62 | }) { 63 | if (source == null) { 64 | throw ArgumentError('A value must be provided. Supported values: ' 65 | '${enumValues.values.join(', ')}'); 66 | } 67 | 68 | final value = enumValues.entries 69 | .singleWhere((e) => e.value == source, orElse: () => null) 70 | ?.key; 71 | 72 | if (value == null && unknownValue == null) { 73 | throw ArgumentError('`$source` is not one of the supported values: ' 74 | '${enumValues.values.join(', ')}'); 75 | } 76 | return value ?? unknownValue; 77 | } 78 | 79 | T _$enumDecodeNullable( 80 | Map enumValues, 81 | dynamic source, { 82 | T unknownValue, 83 | }) { 84 | if (source == null) { 85 | return null; 86 | } 87 | return _$enumDecode(enumValues, source, unknownValue: unknownValue); 88 | } 89 | 90 | const _$PostListTypeEnumMap = { 91 | PostListType.FOLLOWING: 'FOLLOWING', 92 | PostListType.HOT: 'HOT', 93 | }; 94 | -------------------------------------------------------------------------------- /lib/weiguan/adapter/presenter/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:redux/redux.dart'; 3 | import 'package:logging/logging.dart'; 4 | 5 | import '../../entity/entity.dart'; 6 | import '../../ui/ui.dart'; 7 | import '../../usecase/usecase.dart'; 8 | import '../../config.dart'; 9 | import 'base.dart'; 10 | 11 | class UserPresenter extends BasePresenter { 12 | WeiguanService weiguanService; 13 | UserUsecases userUsecases; 14 | 15 | UserPresenter({ 16 | @required WgConfig config, 17 | @required Store appStore, 18 | @required Logger logger, 19 | @required this.weiguanService, 20 | @required this.userUsecases, 21 | }) : super(config: config, appStore: appStore, logger: logger); 22 | 23 | Future login(UserLoginForm form) async { 24 | return await weiguanService.authLogin(form.username, form.password); 25 | } 26 | 27 | Future logout() async { 28 | if (!config.enableOAuth2Login) { 29 | await weiguanService.authLogout(); 30 | } 31 | 32 | dispatchAction(ResetAction()); 33 | } 34 | 35 | Future logged() async { 36 | final user = await weiguanService.authLogged(); 37 | 38 | dispatchAction(UserLoggedAction(user: user)); 39 | 40 | return user; 41 | } 42 | 43 | Future register(UserRegisterForm form) async { 44 | return await weiguanService.userRegister( 45 | UserEntity(username: form.username, password: form.password)); 46 | } 47 | 48 | Future modify(UserProfileForm form) async { 49 | final user = await weiguanService.userModify( 50 | UserEntity( 51 | username: form.username, 52 | password: form.password, 53 | mobile: form.mobile, 54 | email: form.email, 55 | avatarId: form.avatarId, 56 | intro: form.intro), 57 | form.code); 58 | 59 | await logged(); 60 | 61 | return user; 62 | } 63 | 64 | Future modifyAvatar(String path) async { 65 | final user = await userUsecases.modifyAvatar(path); 66 | 67 | await logged(); 68 | 69 | return user; 70 | } 71 | 72 | Future sendMobileVerifyCode(String type, String mobile) async { 73 | final verifyCode = 74 | await weiguanService.userSendMobileVerifyCode(type, mobile); 75 | return verifyCode; 76 | } 77 | 78 | Future info(int userId) async { 79 | final user = await weiguanService.userInfo(userId); 80 | return user; 81 | } 82 | 83 | Future follow(int userId) async { 84 | await weiguanService.userFollow(userId); 85 | } 86 | 87 | Future unfollow(int userId) async { 88 | await weiguanService.userUnfollow(userId); 89 | } 90 | 91 | Future> following( 92 | {int userId, int limit = 10, int offset = 0}) async { 93 | final users = await weiguanService.userFollowing( 94 | userId: userId, limit: limit, offset: offset); 95 | return users; 96 | } 97 | 98 | Future> follower( 99 | {int userId, int limit = 10, int offset = 0}) async { 100 | final users = await weiguanService.userFollower( 101 | userId: userId, limit: limit, offset: offset); 102 | return users; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/lake.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LakePage extends StatelessWidget { 4 | Widget _buildButtonColumn(Color color, IconData icon, String label) { 5 | return Column( 6 | children: [ 7 | Icon(icon, color: color), 8 | Container( 9 | margin: const EdgeInsets.only(top: 8), 10 | child: Text( 11 | label, 12 | style: TextStyle( 13 | fontSize: 12, 14 | fontWeight: FontWeight.w400, 15 | color: color, 16 | ), 17 | ), 18 | ), 19 | ], 20 | ); 21 | } 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | Widget titleSection = Container( 26 | padding: const EdgeInsets.all(32), 27 | child: Row( 28 | children: [ 29 | Expanded( 30 | child: Column( 31 | crossAxisAlignment: CrossAxisAlignment.start, 32 | children: [ 33 | Container( 34 | padding: const EdgeInsets.only(bottom: 8), 35 | child: Text( 36 | 'Oeschinen Lake Campground', 37 | style: TextStyle(fontWeight: FontWeight.bold), 38 | ), 39 | ), 40 | Text( 41 | 'Kandersteg, Switzerland', 42 | style: TextStyle(color: Colors.grey[500]), 43 | ), 44 | ], 45 | ), 46 | ), 47 | Icon( 48 | Icons.star, 49 | color: Colors.red[500], 50 | ), 51 | Text('41'), 52 | ], 53 | ), 54 | ); 55 | 56 | Widget buttonSection = Container( 57 | child: Row( 58 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 59 | children: [ 60 | _buildButtonColumn( 61 | Theme.of(context).primaryColor, Icons.call, 'CALL'), 62 | _buildButtonColumn( 63 | Theme.of(context).primaryColor, Icons.near_me, 'ROUTE'), 64 | _buildButtonColumn( 65 | Theme.of(context).primaryColor, Icons.share, 'SHARE'), 66 | ], 67 | ), 68 | ); 69 | 70 | Widget textSection = Container( 71 | padding: const EdgeInsets.all(32), 72 | child: Text( 73 | 'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese ' 74 | 'Alps. Situated 1,578 meters above sea level, it is one of the ' 75 | 'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a ' 76 | 'half-hour walk through pastures and pine forest, leads you to the ' 77 | 'lake, which warms to 20 degrees Celsius in the summer. Activities ' 78 | 'enjoyed here include rowing, and riding the summer toboggan run.'), 79 | ); 80 | 81 | return Scaffold( 82 | appBar: AppBar( 83 | title: Text('Lake'), 84 | ), 85 | body: ListView( 86 | children: [ 87 | Image.asset( 88 | 'assets/demo/lake.jpg', 89 | height: 240, 90 | fit: BoxFit.cover, 91 | ), 92 | titleSection, 93 | buttonSection, 94 | textSection, 95 | ], 96 | ), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/app.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $AppState { 10 | String get version; 11 | OAuth2State get oauth2; 12 | PageState get page; 13 | UserState get user; 14 | PostState get post; 15 | const $AppState(); 16 | AppState copyWith( 17 | {String version, 18 | OAuth2State oauth2, 19 | PageState page, 20 | UserState user, 21 | PostState post}) => 22 | AppState( 23 | version: version ?? this.version, 24 | oauth2: oauth2 ?? this.oauth2, 25 | page: page ?? this.page, 26 | user: user ?? this.user, 27 | post: post ?? this.post); 28 | String toString() => 29 | "AppState(version: $version, oauth2: $oauth2, page: $page, user: $user, post: $post)"; 30 | bool operator ==(dynamic other) => 31 | other.runtimeType == runtimeType && 32 | version == other.version && 33 | oauth2 == other.oauth2 && 34 | page == other.page && 35 | user == other.user && 36 | post == other.post; 37 | @override 38 | int get hashCode { 39 | var result = 17; 40 | result = 37 * result + version.hashCode; 41 | result = 37 * result + oauth2.hashCode; 42 | result = 37 * result + page.hashCode; 43 | result = 37 * result + user.hashCode; 44 | result = 37 * result + post.hashCode; 45 | return result; 46 | } 47 | } 48 | 49 | class AppState$ { 50 | static final version = Lens( 51 | (s_) => s_.version, (s_, version) => s_.copyWith(version: version)); 52 | static final oauth2 = Lens( 53 | (s_) => s_.oauth2, (s_, oauth2) => s_.copyWith(oauth2: oauth2)); 54 | static final page = Lens( 55 | (s_) => s_.page, (s_, page) => s_.copyWith(page: page)); 56 | static final user = Lens( 57 | (s_) => s_.user, (s_, user) => s_.copyWith(user: user)); 58 | static final post = Lens( 59 | (s_) => s_.post, (s_, post) => s_.copyWith(post: post)); 60 | } 61 | 62 | // ************************************************************************** 63 | // JsonSerializableGenerator 64 | // ************************************************************************** 65 | 66 | AppState _$AppStateFromJson(Map json) { 67 | return AppState( 68 | version: json['version'] as String, 69 | oauth2: json['oauth2'] == null 70 | ? null 71 | : OAuth2State.fromJson(json['oauth2'] as Map), 72 | page: json['page'] == null 73 | ? null 74 | : PageState.fromJson(json['page'] as Map), 75 | user: json['user'] == null 76 | ? null 77 | : UserState.fromJson(json['user'] as Map), 78 | post: json['post'] == null 79 | ? null 80 | : PostState.fromJson(json['post'] as Map), 81 | ); 82 | } 83 | 84 | Map _$AppStateToJson(AppState instance) => { 85 | 'version': instance.version, 86 | 'oauth2': instance.oauth2, 87 | 'page': instance.page, 88 | 'user': instance.user, 89 | 'post': instance.post, 90 | }; 91 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | # Flutter Pod 37 | 38 | copied_flutter_dir = File.join(__dir__, 'Flutter') 39 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 40 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 41 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 42 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 43 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 44 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 45 | 46 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 47 | unless File.exist?(generated_xcode_build_settings_path) 48 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 49 | end 50 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 51 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 52 | 53 | unless File.exist?(copied_framework_path) 54 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 55 | end 56 | unless File.exist?(copied_podspec_path) 57 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 58 | end 59 | end 60 | 61 | # Keep pod path relative so it can be checked into Podfile.lock. 62 | pod 'Flutter', :path => 'Flutter' 63 | 64 | # Plugin Pods 65 | 66 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 67 | # referring to absolute paths on developers' machines. 68 | system('rm -rf .symlinks') 69 | system('mkdir -p .symlinks/plugins') 70 | plugin_pods = parse_KV_file('../.flutter-plugins') 71 | plugin_pods.each do |name, path| 72 | symlink = File.join('.symlinks', 'plugins', name) 73 | File.symlink(path, symlink) 74 | pod name, :path => File.join(symlink, 'ios') 75 | end 76 | end 77 | 78 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. 79 | install! 'cocoapods', :disable_input_output_paths => true 80 | 81 | post_install do |installer| 82 | installer.pods_project.targets.each do |target| 83 | target.build_configurations.each do |config| 84 | config.build_settings['ENABLE_BITCODE'] = 'NO' 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/demo/pages/navigation/nested.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class NestedNavigationPage extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | appBar: AppBar( 10 | title: Text('Nested Navigation'), 11 | ), 12 | body: Center( 13 | child: RaisedButton( 14 | child: Text('Sign Up'), 15 | onPressed: () => Navigator.of(context).push(MaterialPageRoute( 16 | builder: (context) => _SignUpPage(), 17 | )), 18 | ), 19 | ), 20 | ); 21 | } 22 | } 23 | 24 | class _SignUpPage extends StatelessWidget { 25 | final _navigatorKey = GlobalKey(); 26 | 27 | Future _onWillPop() async { 28 | final maybePop = await _navigatorKey.currentState.maybePop(); 29 | return Future.value(!maybePop); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return WillPopScope( 35 | onWillPop: _onWillPop, 36 | child: Navigator( 37 | key: _navigatorKey, 38 | initialRoute: 'signup/username', 39 | onGenerateRoute: (settings) { 40 | WidgetBuilder builder; 41 | switch (settings.name) { 42 | case 'signup/username': 43 | builder = (_) => _UsernamePage(); 44 | break; 45 | case 'signup/password': 46 | builder = (_) => _PasswordPage(); 47 | break; 48 | default: 49 | throw Exception('Invalid route: ${settings.name}'); 50 | } 51 | return MaterialPageRoute(builder: builder, settings: settings); 52 | }, 53 | ), 54 | ); 55 | } 56 | } 57 | 58 | class _UsernamePage extends StatelessWidget { 59 | @override 60 | Widget build(BuildContext context) { 61 | return Scaffold( 62 | appBar: AppBar( 63 | title: Text('Username'), 64 | ), 65 | body: Column( 66 | mainAxisAlignment: MainAxisAlignment.spaceAround, 67 | children: [ 68 | Text('Please input username'), 69 | Row( 70 | mainAxisAlignment: MainAxisAlignment.spaceAround, 71 | children: [ 72 | RaisedButton( 73 | onPressed: () => 74 | Navigator.of(context, rootNavigator: true).pop(), 75 | child: Text('Back'), 76 | ), 77 | RaisedButton( 78 | onPressed: () => 79 | Navigator.of(context).pushNamed('signup/password'), 80 | child: Text('Next'), 81 | ), 82 | ], 83 | ), 84 | ], 85 | ), 86 | ); 87 | } 88 | } 89 | 90 | class _PasswordPage extends StatelessWidget { 91 | @override 92 | Widget build(BuildContext context) { 93 | return Scaffold( 94 | appBar: AppBar( 95 | title: Text('Password'), 96 | ), 97 | body: Column( 98 | mainAxisAlignment: MainAxisAlignment.spaceAround, 99 | children: [ 100 | Text('Please input password'), 101 | Row( 102 | mainAxisAlignment: MainAxisAlignment.spaceAround, 103 | children: [ 104 | RaisedButton( 105 | onPressed: () => Navigator.of(context).pop(), 106 | child: Text('Back'), 107 | ), 108 | RaisedButton( 109 | onPressed: () => 110 | Navigator.of(context, rootNavigator: true).pop(), 111 | child: Text('Finish'), 112 | ), 113 | ], 114 | ), 115 | ], 116 | ), 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/weiguan/ui/redux/state/post.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'post.dart'; 4 | 5 | // ************************************************************************** 6 | // FunctionalDataGenerator 7 | // ************************************************************************** 8 | 9 | abstract class $PostState { 10 | List get followingPosts; 11 | bool get followingPostsAllLoaded; 12 | List get hotPosts; 13 | bool get hotPostsAllLoaded; 14 | const $PostState(); 15 | PostState copyWith( 16 | {List followingPosts, 17 | bool followingPostsAllLoaded, 18 | List hotPosts, 19 | bool hotPostsAllLoaded}) => 20 | PostState( 21 | followingPosts: followingPosts ?? this.followingPosts, 22 | followingPostsAllLoaded: 23 | followingPostsAllLoaded ?? this.followingPostsAllLoaded, 24 | hotPosts: hotPosts ?? this.hotPosts, 25 | hotPostsAllLoaded: hotPostsAllLoaded ?? this.hotPostsAllLoaded); 26 | String toString() => 27 | "PostState(followingPosts: $followingPosts, followingPostsAllLoaded: $followingPostsAllLoaded, hotPosts: $hotPosts, hotPostsAllLoaded: $hotPostsAllLoaded)"; 28 | bool operator ==(dynamic other) => 29 | other.runtimeType == runtimeType && 30 | followingPosts == other.followingPosts && 31 | followingPostsAllLoaded == other.followingPostsAllLoaded && 32 | hotPosts == other.hotPosts && 33 | hotPostsAllLoaded == other.hotPostsAllLoaded; 34 | @override 35 | int get hashCode { 36 | var result = 17; 37 | result = 37 * result + followingPosts.hashCode; 38 | result = 37 * result + followingPostsAllLoaded.hashCode; 39 | result = 37 * result + hotPosts.hashCode; 40 | result = 37 * result + hotPostsAllLoaded.hashCode; 41 | return result; 42 | } 43 | } 44 | 45 | class PostState$ { 46 | static final followingPosts = Lens>( 47 | (s_) => s_.followingPosts, 48 | (s_, followingPosts) => s_.copyWith(followingPosts: followingPosts)); 49 | static final followingPostsAllLoaded = Lens( 50 | (s_) => s_.followingPostsAllLoaded, 51 | (s_, followingPostsAllLoaded) => 52 | s_.copyWith(followingPostsAllLoaded: followingPostsAllLoaded)); 53 | static final hotPosts = Lens>( 54 | (s_) => s_.hotPosts, (s_, hotPosts) => s_.copyWith(hotPosts: hotPosts)); 55 | static final hotPostsAllLoaded = Lens( 56 | (s_) => s_.hotPostsAllLoaded, 57 | (s_, hotPostsAllLoaded) => 58 | s_.copyWith(hotPostsAllLoaded: hotPostsAllLoaded)); 59 | } 60 | 61 | // ************************************************************************** 62 | // JsonSerializableGenerator 63 | // ************************************************************************** 64 | 65 | PostState _$PostStateFromJson(Map json) { 66 | return PostState( 67 | followingPosts: (json['followingPosts'] as List) 68 | ?.map((e) => 69 | e == null ? null : PostEntity.fromJson(e as Map)) 70 | ?.toList(), 71 | followingPostsAllLoaded: json['followingPostsAllLoaded'] as bool, 72 | hotPosts: (json['hotPosts'] as List) 73 | ?.map((e) => 74 | e == null ? null : PostEntity.fromJson(e as Map)) 75 | ?.toList(), 76 | hotPostsAllLoaded: json['hotPostsAllLoaded'] as bool, 77 | ); 78 | } 79 | 80 | Map _$PostStateToJson(PostState instance) => { 81 | 'followingPosts': instance.followingPosts, 82 | 'followingPostsAllLoaded': instance.followingPostsAllLoaded, 83 | 'hotPosts': instance.hotPosts, 84 | 'hotPostsAllLoaded': instance.hotPostsAllLoaded, 85 | }; 86 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_in_practice 2 | description: Flutter in practice. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.5.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | bot_toast: ^2.1.1 24 | cached_network_image: ^2.0.0-rc.1 25 | carousel_slider: ^1.3.1 26 | cookie_jar: ^1.0.1 27 | dio: ^2.1.13 28 | flutter_appauth: ^0.8.1 29 | flutter_redux: ^0.5.3 30 | functional_data: ^0.2.3 31 | image_picker: ^0.6.1+3 32 | injector: ^1.0.8 33 | json_annotation: ^3.0.1 34 | logging: ^0.11.3+2 35 | meta: ^1.1.6 36 | package_info: ^0.4.0+6 37 | provider: ^3.1.0 38 | redux: ^3.0.0 39 | redux_logging: ^0.3.0 40 | redux_persist: ^0.8.2 41 | redux_persist_flutter: ^0.8.1 42 | video_player: ^0.10.1+6 43 | 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | analyzer: ^0.39.4 49 | build_runner: ^1.7.4 50 | flutter_launcher_icons: ^0.7.4 51 | functional_data_generator: ^0.2.5 52 | json_serializable: ^3.2.5 53 | 54 | dependency_overrides: 55 | analyzer: ^0.39.4 56 | 57 | # For information on the generic Dart part of this file, see the 58 | # following page: https://dart.dev/tools/pub/pubspec 59 | 60 | # The following section is specific to Flutter. 61 | flutter: 62 | 63 | # The following line ensures that the Material Icons font is 64 | # included with your application, so that you can use the icons in 65 | # the material Icons class. 66 | uses-material-design: true 67 | 68 | # To add assets to your application, add an assets section, like this: 69 | # assets: 70 | # - images/a_dot_burr.jpeg 71 | # - images/a_dot_ham.jpeg 72 | assets: 73 | - assets/demo/ 74 | - assets/weiguan/ 75 | - assets/ 76 | 77 | # An image asset can refer to one or more resolution-specific "variants", see 78 | # https://flutter.dev/assets-and-images/#resolution-aware. 79 | 80 | # For details regarding adding assets from package dependencies, see 81 | # https://flutter.dev/assets-and-images/#from-packages 82 | 83 | # To add custom fonts to your application, add a fonts section here, 84 | # in this "flutter" section. Each entry in this list should have a 85 | # "family" key with the font family name, and a "fonts" key with a 86 | # list giving the asset and other descriptors for the font. For 87 | # example: 88 | # fonts: 89 | # - family: Schyler 90 | # fonts: 91 | # - asset: fonts/Schyler-Regular.ttf 92 | # - asset: fonts/Schyler-Italic.ttf 93 | # style: italic 94 | # - family: Trajan Pro 95 | # fonts: 96 | # - asset: fonts/TrajanPro.ttf 97 | # - asset: fonts/TrajanPro_Bold.ttf 98 | # weight: 700 99 | # 100 | # For details regarding fonts from package dependencies, 101 | # see https://flutter.dev/custom-fonts/#from-packages 102 | 103 | flutter_icons: 104 | android: true 105 | ios: true 106 | image_path: assets/weiguan/weiguan-bg.png -------------------------------------------------------------------------------- /lib/weiguan/adapter/presenter/base.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:bot_toast/bot_toast.dart'; 3 | import 'package:flutter_in_practice/weiguan/config.dart'; 4 | import 'package:redux/redux.dart'; 5 | import 'package:logging/logging.dart'; 6 | 7 | import '../../entity/entity.dart'; 8 | import '../../ui/ui.dart'; 9 | import '../../usecase/usecase.dart'; 10 | 11 | class BasePresenter { 12 | WgConfig config; 13 | Store appStore; 14 | Logger logger; 15 | 16 | BasePresenter({ 17 | @required this.config, 18 | @required this.appStore, 19 | @required this.logger, 20 | }); 21 | 22 | NavigatorState navigator([BuildContext context]) { 23 | return context == null 24 | ? config.rootNavigatorKey.currentState 25 | : Navigator.of(context); 26 | } 27 | 28 | void dispatchAction(BaseAction action) { 29 | appStore.dispatch(action); 30 | } 31 | 32 | void handleException(Exception e) { 33 | logger.severe(e.toString()); 34 | if (e is UnauthenticatedException) { 35 | navigator().pushNamedAndRemoveUntil( 36 | config.enableOAuth2Login ? '/oauth2_login' : '/login', 37 | (route) => false); 38 | } else if (e is UsecaseException) { 39 | showMessage(e.message); 40 | } else { 41 | showMessage(e.toString()); 42 | } 43 | } 44 | 45 | void showMessage( 46 | String text, { 47 | MessageLevel level = MessageLevel.ERROR, 48 | Duration duration = const Duration(seconds: 4), 49 | }) { 50 | Color backgroundColor; 51 | switch (level) { 52 | case MessageLevel.INFO: 53 | backgroundColor = Colors.blue[100]; 54 | break; 55 | case MessageLevel.WARNING: 56 | backgroundColor = Colors.yellow[100]; 57 | break; 58 | case MessageLevel.SUCCESS: 59 | backgroundColor = Colors.green[100]; 60 | break; 61 | default: 62 | backgroundColor = Colors.red[100]; 63 | } 64 | 65 | BotToast.showText( 66 | text: text, 67 | duration: duration, 68 | contentColor: backgroundColor, 69 | textStyle: TextStyle(fontSize: 16, color: Colors.black87), 70 | ); 71 | } 72 | 73 | void showNotification( 74 | String title, { 75 | String subtitle, 76 | NotificationLevel level = NotificationLevel.ERROR, 77 | Duration duration = const Duration(seconds: 4), 78 | VoidCallback onTap, 79 | }) { 80 | IconData iconData; 81 | switch (level) { 82 | case NotificationLevel.INFO: 83 | iconData = Icons.info; 84 | break; 85 | case NotificationLevel.WARNING: 86 | iconData = Icons.warning; 87 | break; 88 | case NotificationLevel.SUCCESS: 89 | iconData = Icons.check_circle; 90 | break; 91 | default: 92 | iconData = Icons.error; 93 | } 94 | 95 | Color iconColor; 96 | switch (level) { 97 | case NotificationLevel.INFO: 98 | iconColor = Colors.blue[500]; 99 | break; 100 | case NotificationLevel.WARNING: 101 | iconColor = Colors.yellow[500]; 102 | break; 103 | case NotificationLevel.SUCCESS: 104 | iconColor = Colors.green[500]; 105 | break; 106 | default: 107 | iconColor = Colors.red[500]; 108 | } 109 | 110 | BotToast.showNotification( 111 | title: (_) => Text(title), 112 | subtitle: (_) => subtitle == null ? null : Text(subtitle), 113 | leading: (_) => Icon(iconData, color: iconColor), 114 | trailing: (cancel) => 115 | IconButton(icon: Icon(Icons.close), onPressed: cancel), 116 | duration: duration, 117 | onTap: onTap, 118 | ); 119 | } 120 | 121 | VoidCallback showLoading() { 122 | return BotToast.showLoading(); 123 | } 124 | 125 | Future doWithLoading(Future Function() doSomething) async { 126 | final cancelLoading = showLoading(); 127 | T result; 128 | try { 129 | result = await doSomething(); 130 | } on Exception catch (e) { 131 | handleException(e); 132 | } finally { 133 | cancelLoading(); 134 | } 135 | return result; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/demo/pages/layout/pavlova.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class PavlovaPage extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | final titleText = Container( 7 | padding: EdgeInsets.all(20), 8 | child: Text( 9 | 'Strawberry Pavlova', 10 | style: TextStyle( 11 | fontWeight: FontWeight.w800, 12 | letterSpacing: 0.5, 13 | fontSize: 30, 14 | ), 15 | ), 16 | ); 17 | 18 | final subTitle = Text( 19 | ''' 20 | Pavlova is a meringue-based dessert named after the Russian ballerina Anna Pavlova. Pavlova features a crisp crust and soft, light inside, topped with fruit and whipped cream. 21 | ''', 22 | textAlign: TextAlign.center, 23 | style: TextStyle( 24 | fontFamily: 'Georgia', 25 | fontSize: 25, 26 | ), 27 | ); 28 | 29 | final ratings = Container( 30 | padding: EdgeInsets.all(20), 31 | child: Row( 32 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 33 | children: [ 34 | Row( 35 | mainAxisSize: MainAxisSize.min, 36 | children: [ 37 | Icon(Icons.star, color: Colors.black), 38 | Icon(Icons.star, color: Colors.black), 39 | Icon(Icons.star, color: Colors.black), 40 | Icon(Icons.star, color: Colors.black), 41 | Icon(Icons.star, color: Colors.black), 42 | ], 43 | ), 44 | Text( 45 | '170 Reviews', 46 | style: TextStyle( 47 | color: Colors.black, 48 | fontWeight: FontWeight.w800, 49 | fontFamily: 'Roboto', 50 | letterSpacing: 0.5, 51 | fontSize: 20, 52 | ), 53 | ), 54 | ], 55 | ), 56 | ); 57 | 58 | final descTextStyle = TextStyle( 59 | color: Colors.black, 60 | fontWeight: FontWeight.w800, 61 | fontFamily: 'Roboto', 62 | letterSpacing: 0.5, 63 | fontSize: 18, 64 | height: 2, 65 | ); 66 | 67 | final iconList = DefaultTextStyle.merge( 68 | style: descTextStyle, 69 | child: Container( 70 | padding: EdgeInsets.all(20), 71 | child: Row( 72 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 73 | children: [ 74 | Column( 75 | children: [ 76 | Icon(Icons.kitchen, color: Colors.green[500]), 77 | Text('PREP:'), 78 | Text('25 min'), 79 | ], 80 | ), 81 | Column( 82 | children: [ 83 | Icon(Icons.timer, color: Colors.green[500]), 84 | Text('COOK:'), 85 | Text('1 hr'), 86 | ], 87 | ), 88 | Column( 89 | children: [ 90 | Icon(Icons.restaurant, color: Colors.green[500]), 91 | Text('FEEDS:'), 92 | Text('4-6'), 93 | ], 94 | ), 95 | ], 96 | ), 97 | ), 98 | ); 99 | 100 | final leftColumn = Container( 101 | padding: EdgeInsets.symmetric(horizontal: 20, vertical: 30), 102 | child: Column( 103 | children: [ 104 | titleText, 105 | subTitle, 106 | ratings, 107 | iconList, 108 | ], 109 | ), 110 | ); 111 | 112 | final mainImage = Image.asset( 113 | 'assets/demo/pavlova.jpg', 114 | fit: BoxFit.cover, 115 | ); 116 | 117 | return Scaffold( 118 | appBar: AppBar( 119 | title: Text('Pavlova'), 120 | ), 121 | body: Center( 122 | child: Container( 123 | margin: EdgeInsets.all(5), 124 | height: 600, 125 | child: Card( 126 | child: Row( 127 | children: [ 128 | Container( 129 | width: 440, 130 | child: leftColumn, 131 | ), 132 | mainImage, 133 | ], 134 | ), 135 | ), 136 | ), 137 | ), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/demo/pages/interaction/favorite_lake.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class _FavoriteWidget extends StatefulWidget { 4 | @override 5 | _FavoriteWidgetState createState() => _FavoriteWidgetState(); 6 | } 7 | 8 | class _FavoriteWidgetState extends State<_FavoriteWidget> { 9 | var _isFavorited = true; 10 | var _favoriteCount = 41; 11 | 12 | void _toggleFavorite() { 13 | setState(() { 14 | if (_isFavorited) { 15 | _favoriteCount -= 1; 16 | _isFavorited = false; 17 | } else { 18 | _favoriteCount += 1; 19 | _isFavorited = true; 20 | } 21 | }); 22 | } 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Row( 27 | mainAxisSize: MainAxisSize.min, 28 | children: [ 29 | IconButton( 30 | icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)), 31 | color: Colors.red[500], 32 | onPressed: _toggleFavorite, 33 | ), 34 | SizedBox( 35 | width: 18, 36 | child: Text('$_favoriteCount'), 37 | ), 38 | ], 39 | ); 40 | } 41 | } 42 | 43 | class FavoriteLakePage extends StatelessWidget { 44 | Widget _buildButtonColumn(Color color, IconData icon, String label) { 45 | return Column( 46 | children: [ 47 | Icon(icon, color: color), 48 | Container( 49 | margin: const EdgeInsets.only(top: 8), 50 | child: Text( 51 | label, 52 | style: TextStyle( 53 | fontSize: 12, 54 | fontWeight: FontWeight.w400, 55 | color: color, 56 | ), 57 | ), 58 | ), 59 | ], 60 | ); 61 | } 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | Widget titleSection = Container( 66 | padding: const EdgeInsets.all(32), 67 | child: Row( 68 | children: [ 69 | Expanded( 70 | child: Column( 71 | crossAxisAlignment: CrossAxisAlignment.start, 72 | children: [ 73 | Container( 74 | padding: const EdgeInsets.only(bottom: 8), 75 | child: Text( 76 | 'Oeschinen Lake Campground', 77 | style: TextStyle(fontWeight: FontWeight.bold), 78 | ), 79 | ), 80 | Text( 81 | 'Kandersteg, Switzerland', 82 | style: TextStyle(color: Colors.grey[500]), 83 | ), 84 | ], 85 | ), 86 | ), 87 | _FavoriteWidget(), 88 | ], 89 | ), 90 | ); 91 | 92 | Widget buttonSection = Container( 93 | child: Row( 94 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 95 | children: [ 96 | _buildButtonColumn( 97 | Theme.of(context).primaryColor, Icons.call, 'CALL'), 98 | _buildButtonColumn( 99 | Theme.of(context).primaryColor, Icons.near_me, 'ROUTE'), 100 | _buildButtonColumn( 101 | Theme.of(context).primaryColor, Icons.share, 'SHARE'), 102 | ], 103 | ), 104 | ); 105 | 106 | Widget textSection = Container( 107 | padding: const EdgeInsets.all(32), 108 | child: Text( 109 | 'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese ' 110 | 'Alps. Situated 1,578 meters above sea level, it is one of the ' 111 | 'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a ' 112 | 'half-hour walk through pastures and pine forest, leads you to the ' 113 | 'lake, which warms to 20 degrees Celsius in the summer. Activities ' 114 | 'enjoyed here include rowing, and riding the summer toboggan run.'), 115 | ); 116 | 117 | return Scaffold( 118 | appBar: AppBar( 119 | title: Text('Lake'), 120 | ), 121 | body: ListView( 122 | children: [ 123 | Image.asset( 124 | 'assets/demo/lake.jpg', 125 | height: 240, 126 | fit: BoxFit.cover, 127 | ), 128 | titleSection, 129 | buttonSection, 130 | textSection, 131 | ], 132 | ), 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/user/oauth2_login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_appauth/flutter_appauth.dart'; 3 | 4 | import '../../../container.dart'; 5 | import '../../ui.dart'; 6 | 7 | class OAuth2LoginPage extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Scaffold( 11 | appBar: AppBar( 12 | title: Text('OAuth2 登录'), 13 | ), 14 | body: _Body(), 15 | ); 16 | } 17 | } 18 | 19 | class _Body extends StatefulWidget { 20 | @override 21 | _BodyState createState() => _BodyState(); 22 | } 23 | 24 | class _BodyState extends State<_Body> { 25 | void _login() async { 26 | final config = WgContainer().config; 27 | final appAuth = FlutterAppAuth(); 28 | final appStore = WgContainer().appStore; 29 | final oAuth2State = appStore.state.oauth2; 30 | if (oAuth2State.refreshToken != null) { 31 | final response = await appAuth.token(TokenRequest( 32 | config.oAuth2Config.clientId, 33 | config.oAuth2Config.redirectUrl, 34 | serviceConfiguration: AuthorizationServiceConfiguration( 35 | config.oAuth2Config.authorizationEndpoint, 36 | config.oAuth2Config.tokenEndpoint, 37 | ), 38 | scopes: config.oAuth2Config.scopes, 39 | refreshToken: oAuth2State.refreshToken, 40 | allowInsecureConnections: true, 41 | )); 42 | 43 | if (response.accessToken != null) { 44 | WgContainer().basePresenter.dispatchAction(OAuth2StateAction( 45 | accessToken: response.accessToken, 46 | accessTokenExpireAt: response.accessTokenExpirationDateTime, 47 | refreshToken: response.refreshToken, 48 | )); 49 | WgContainer() 50 | .basePresenter 51 | .navigator() 52 | .pushNamedAndRemoveUntil('/', (route) => false); 53 | return; 54 | } 55 | } 56 | 57 | final response = 58 | await appAuth.authorizeAndExchangeCode(AuthorizationTokenRequest( 59 | config.oAuth2Config.clientId, 60 | config.oAuth2Config.redirectUrl, 61 | serviceConfiguration: AuthorizationServiceConfiguration( 62 | config.oAuth2Config.authorizationEndpoint, 63 | config.oAuth2Config.tokenEndpoint, 64 | ), 65 | scopes: config.oAuth2Config.scopes, 66 | allowInsecureConnections: true, 67 | )); 68 | if (response.accessToken != null) { 69 | WgContainer().basePresenter.dispatchAction(OAuth2StateAction( 70 | accessToken: response.accessToken, 71 | accessTokenExpireAt: response.accessTokenExpirationDateTime, 72 | refreshToken: response.refreshToken, 73 | )); 74 | WgContainer() 75 | .basePresenter 76 | .navigator() 77 | .pushNamedAndRemoveUntil('/', (route) => false); 78 | return; 79 | } 80 | 81 | WgContainer().basePresenter.showMessage('OAuth2 登录失败'); 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | return Padding( 87 | padding: EdgeInsets.all(WgContainer().theme.paddingSizeLarge), 88 | child: Column( 89 | crossAxisAlignment: CrossAxisAlignment.stretch, 90 | children: [ 91 | Spacer(flex: 5), 92 | RaisedButton( 93 | padding: EdgeInsets.all(WgContainer().theme.paddingSizeNormal), 94 | onPressed: _login, 95 | color: Theme.of(context).primaryColorDark, 96 | child: Text( 97 | 'OAuth2 登录', 98 | style: Theme.of(context) 99 | .primaryTextTheme 100 | .button 101 | .copyWith(fontSize: 16), 102 | ), 103 | ), 104 | Spacer(), 105 | RaisedButton( 106 | padding: EdgeInsets.all(WgContainer().theme.paddingSizeNormal), 107 | onPressed: () => 108 | WgContainer().basePresenter.navigator().pushNamed('/register'), 109 | color: Theme.of(context).primaryColor, 110 | child: Text( 111 | '注册新帐号', 112 | style: Theme.of(context) 113 | .primaryTextTheme 114 | .button 115 | .copyWith(fontSize: 16), 116 | ), 117 | ), 118 | Spacer(flex: 5), 119 | ], 120 | ), 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/common/text_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:meta/meta.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import '../../../container.dart'; 5 | 6 | class TextInputPage extends StatelessWidget { 7 | static final _bodyKey = GlobalKey<_BodyState>(); 8 | 9 | final String title; 10 | final String initialValue; 11 | final String hintText; 12 | final int maxLength; 13 | final int maxLines; 14 | final bool obscureText; 15 | final FormFieldValidator validator; 16 | final void Function(String value, BuildContext context) onSubmit; 17 | 18 | TextInputPage({ 19 | this.title = '', 20 | this.initialValue = '', 21 | this.hintText = '', 22 | this.maxLength, 23 | this.maxLines = 1, 24 | this.obscureText = false, 25 | this.validator, 26 | @required this.onSubmit, 27 | }); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | return Scaffold( 32 | appBar: AppBar( 33 | title: Text(title), 34 | actions: [ 35 | FlatButton( 36 | onPressed: () => _bodyKey.currentState._submit(), 37 | child: Text( 38 | '完成', 39 | style: Theme.of(context).primaryTextTheme.subhead, 40 | ), 41 | ), 42 | ], 43 | ), 44 | body: _Body( 45 | key: _bodyKey, 46 | initialValue: initialValue, 47 | hintText: hintText, 48 | maxLength: maxLength, 49 | maxLines: maxLines, 50 | obscureText: obscureText, 51 | validator: validator, 52 | onSubmit: onSubmit, 53 | ), 54 | ); 55 | } 56 | } 57 | 58 | class _Body extends StatefulWidget { 59 | final String initialValue; 60 | final String hintText; 61 | final int maxLength; 62 | final int maxLines; 63 | final bool obscureText; 64 | final FormFieldValidator validator; 65 | final void Function(String value, BuildContext context) onSubmit; 66 | 67 | _Body({ 68 | Key key, 69 | this.initialValue, 70 | this.hintText, 71 | this.maxLength, 72 | this.maxLines, 73 | this.obscureText, 74 | this.validator, 75 | @required this.onSubmit, 76 | }) : super(key: key); 77 | 78 | @override 79 | _BodyState createState() => _BodyState(); 80 | } 81 | 82 | class _BodyState extends State<_Body> { 83 | static final _formKey = GlobalKey(); 84 | 85 | TextEditingController _textEditingController; 86 | String _value; 87 | 88 | @override 89 | void initState() { 90 | super.initState(); 91 | _textEditingController = TextEditingController(text: widget.initialValue); 92 | } 93 | 94 | @override 95 | void dispose() { 96 | _textEditingController.dispose(); 97 | super.dispose(); 98 | } 99 | 100 | void _submit() { 101 | if (_formKey.currentState.validate()) { 102 | _formKey.currentState.save(); 103 | 104 | widget.onSubmit(_value, context); 105 | } 106 | } 107 | 108 | @override 109 | Widget build(BuildContext context) { 110 | return Container( 111 | padding: EdgeInsets.all(WgContainer().theme.paddingSizeNormal), 112 | child: GestureDetector( 113 | onTap: () => FocusScope.of(context).unfocus(), 114 | child: ListView( 115 | children: [ 116 | Form( 117 | key: _formKey, 118 | child: Column( 119 | children: [ 120 | TextFormField( 121 | controller: _textEditingController, 122 | autofocus: true, 123 | maxLength: widget.maxLength, 124 | maxLengthEnforced: true, 125 | maxLines: widget.maxLines, 126 | obscureText: widget.obscureText, 127 | validator: widget.validator, 128 | onSaved: (value) => _value = value, 129 | onFieldSubmitted: (value) => _submit(), 130 | decoration: InputDecoration( 131 | hintText: widget.hintText, 132 | suffixIcon: IconButton( 133 | onPressed: () => _textEditingController.clear(), 134 | icon: Icon(Icons.clear), 135 | ), 136 | ), 137 | ), 138 | ], 139 | ), 140 | ), 141 | ], 142 | ), 143 | ), 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /lib/demo/pages/interaction/silver_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SilverAppBarScrollPage extends StatelessWidget { 4 | static final _tabs = ['Tab1', 'Tab2', 'Tab3']; 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return Scaffold( 9 | body: DefaultTabController( 10 | length: _tabs.length, 11 | child: _Body( 12 | tabs: _tabs, 13 | ), 14 | ), 15 | ); 16 | } 17 | } 18 | 19 | class _Body extends StatefulWidget { 20 | final List tabs; 21 | 22 | _Body({ 23 | Key key, 24 | @required this.tabs, 25 | }) : super(key: key); 26 | 27 | @override 28 | _BodyState createState() => _BodyState(); 29 | } 30 | 31 | class _BodyState extends State<_Body> { 32 | final _items = { 33 | 'Tab1': [], 34 | 'Tab2': [], 35 | 'Tab3': [], 36 | }; 37 | final _scrollController = ScrollController(); 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | 43 | _scrollController.addListener(_scrollListener); 44 | 45 | _loadItems(widget.tabs[0]); 46 | } 47 | 48 | @override 49 | void dispose() { 50 | _scrollController.removeListener(_scrollListener); 51 | 52 | super.dispose(); 53 | } 54 | 55 | void _scrollListener() { 56 | final index = DefaultTabController.of(context).index; 57 | if (_scrollController.position.pixels == 58 | _scrollController.position.maxScrollExtent) { 59 | _loadItems(widget.tabs[index]); 60 | } 61 | } 62 | 63 | void _loadItems(String name) { 64 | setState(() { 65 | final start = _items[name].length; 66 | _items[name].addAll(List.generate(20, (i) => 'Item ${start + i}')); 67 | }); 68 | } 69 | 70 | Widget _buildSilverAppBar(BuildContext context, bool innerBoxIsScrolled) { 71 | return SliverOverlapAbsorber( 72 | handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), 73 | child: SliverAppBar( 74 | expandedHeight: 240, 75 | forceElevated: innerBoxIsScrolled, 76 | flexibleSpace: Container( 77 | padding: EdgeInsets.all(5), 78 | child: Column( 79 | mainAxisAlignment: MainAxisAlignment.center, 80 | children: [ 81 | Spacer(flex: 5), 82 | Text( 83 | 'Silver App Bar', 84 | style: TextStyle( 85 | color: Colors.white, 86 | fontSize: 24, 87 | fontWeight: FontWeight.bold, 88 | ), 89 | ), 90 | Spacer(), 91 | Text( 92 | 'Intro', 93 | style: TextStyle( 94 | color: Colors.white70, 95 | fontSize: 16, 96 | ), 97 | ), 98 | Spacer(flex: 5), 99 | ], 100 | ), 101 | ), 102 | bottom: TabBar( 103 | tabs: widget.tabs.map((String name) => Tab(text: name)).toList(), 104 | ), 105 | ), 106 | ); 107 | } 108 | 109 | Widget _buildTabBarView() { 110 | return TabBarView( 111 | children: widget.tabs 112 | .map((name) => SafeArea( 113 | top: false, 114 | bottom: false, 115 | child: Builder( 116 | builder: (context) => CustomScrollView( 117 | key: PageStorageKey(name), 118 | slivers: [ 119 | SliverOverlapInjector( 120 | handle: NestedScrollView.sliverOverlapAbsorberHandleFor( 121 | context), 122 | ), 123 | SliverList( 124 | delegate: SliverChildBuilderDelegate( 125 | (context, index) => 126 | ListTile(title: Text('$name Item $index')), 127 | childCount: _items[name].length, 128 | ), 129 | ), 130 | ], 131 | ), 132 | ), 133 | )) 134 | .toList(), 135 | ); 136 | } 137 | 138 | @override 139 | Widget build(BuildContext context) { 140 | return NestedScrollView( 141 | controller: _scrollController, 142 | headerSliverBuilder: (context, innerBoxIsScrolled) => [ 143 | _buildSilverAppBar(context, innerBoxIsScrolled), 144 | ], 145 | body: _buildTabBarView(), 146 | ); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lib/weiguan/ui/page/user/register.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../../container.dart'; 4 | import '../../ui.dart'; 5 | 6 | class RegisterPage extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | appBar: AppBar( 11 | title: Text('注册'), 12 | ), 13 | body: _Body(), 14 | ); 15 | } 16 | } 17 | 18 | class _Body extends StatefulWidget { 19 | @override 20 | _BodyState createState() => _BodyState(); 21 | } 22 | 23 | class _BodyState extends State<_Body> { 24 | static final _formKey = GlobalKey(); 25 | 26 | FocusNode _passwordFocusNode; 27 | var _form = UserRegisterForm(); 28 | 29 | @override 30 | void initState() { 31 | super.initState(); 32 | 33 | _passwordFocusNode = FocusNode(); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | _passwordFocusNode.dispose(); 39 | 40 | super.dispose(); 41 | } 42 | 43 | void _submit() async { 44 | if (_formKey.currentState.validate()) { 45 | _formKey.currentState.save(); 46 | 47 | WgContainer().basePresenter.doWithLoading(() async { 48 | await WgContainer().userPresenter.register(_form); 49 | 50 | WgContainer().basePresenter.navigator().pushReplacementNamed('/'); 51 | }); 52 | } 53 | } 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | return GestureDetector( 58 | onTap: () => FocusScope.of(context).unfocus(), 59 | child: ListView( 60 | children: [ 61 | Card( 62 | child: Padding( 63 | padding: EdgeInsets.all(WgContainer().theme.paddingSizeSmall), 64 | child: Form( 65 | key: _formKey, 66 | child: Column( 67 | crossAxisAlignment: CrossAxisAlignment.stretch, 68 | children: [ 69 | TextFormField( 70 | autofocus: true, 71 | textInputAction: TextInputAction.next, 72 | onSaved: (value) => _form.username = value, 73 | onFieldSubmitted: (value) => FocusScope.of(context) 74 | .requestFocus(_passwordFocusNode), 75 | decoration: InputDecoration( 76 | labelText: '用户名', 77 | hintText: '2-20 个中英文字符', 78 | ), 79 | validator: (value) { 80 | if (value.length < 2 || value.length > 20) { 81 | return '长度不符合要求'; 82 | } 83 | return null; 84 | }, 85 | ), 86 | TextFormField( 87 | focusNode: _passwordFocusNode, 88 | textInputAction: TextInputAction.done, 89 | obscureText: true, 90 | onSaved: (value) => _form.password = value, 91 | onFieldSubmitted: (value) => _submit(), 92 | decoration: InputDecoration( 93 | labelText: '密码', 94 | hintText: '6-20 个字符', 95 | ), 96 | validator: (value) { 97 | if (value.length < 6 || value.length > 20) { 98 | return '长度不符合要求'; 99 | } 100 | return null; 101 | }, 102 | ), 103 | Container( 104 | margin: EdgeInsets.only( 105 | top: WgContainer().theme.marginSizeNormal), 106 | child: RaisedButton( 107 | padding: EdgeInsets.all( 108 | WgContainer().theme.paddingSizeNormal), 109 | onPressed: _submit, 110 | color: Theme.of(context).primaryColor, 111 | child: Text( 112 | '注册', 113 | style: Theme.of(context) 114 | .primaryTextTheme 115 | .button 116 | .copyWith( 117 | fontSize: 16, 118 | letterSpacing: 32, 119 | ), 120 | ), 121 | ), 122 | ), 123 | ], 124 | ), 125 | ), 126 | ), 127 | ) 128 | ], 129 | ), 130 | ); 131 | } 132 | } 133 | --------------------------------------------------------------------------------