├── .gitignore ├── .metadata ├── CHANGELOG.md ├── README-EN.md ├── README.md ├── android ├── app │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── cn │ │ │ │ └── phoenixsky │ │ │ │ └── wan_android │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable-night │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── launch_image.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launch_image.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── launch_image.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── settings_aar.gradle ├── assets ├── animations │ └── like.flr ├── fonts │ ├── ZCOOLKuaiLe-Regular.ttf │ └── iconfont.ttf └── images │ ├── home_second_floor_builder.png │ ├── login_logo.png │ ├── logo_wechat.png │ ├── logo_weibo.png │ ├── splash_android.png │ ├── splash_bg.png │ ├── splash_bg_dark.png │ ├── splash_flutter.png │ ├── splash_fun.png │ └── user_avatar.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ ├── appicon-0.png │ │ ├── appicon-1.png │ │ ├── appicon-2.png │ │ ├── appicon-3.png │ │ ├── appicon-4.png │ │ ├── appicon-5.png │ │ ├── appicon-6.png │ │ ├── appicon-7.png │ │ ├── appicon-8.png │ │ └── appicon.png │ ├── Contents.json │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── README.md │ │ ├── appicon-1.png │ │ ├── appicon-2.png │ │ ├── appicon-3.png │ │ ├── appicon-4.png │ │ ├── appicon-5.png │ │ └── appicon.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── main.m │ └── zh-Hans.lproj │ ├── LaunchScreen.strings │ └── Main.strings ├── lib ├── config │ ├── net │ │ ├── api.dart │ │ ├── pgyer_api.dart │ │ └── wan_android_api.dart │ ├── provider_manager.dart │ ├── resource_mananger.dart │ ├── router_manger.dart │ ├── storage_manager.dart │ └── ui_adapter_config.dart ├── flutter │ ├── dropdown.dart │ ├── refresh_animatedlist.dart │ └── search.dart ├── generated │ ├── intl │ │ ├── messages_all.dart │ │ ├── messages_en.dart │ │ └── messages_zh.dart │ └── l10n.dart ├── l10n │ ├── intl_en.arb │ └── intl_zh.arb ├── main.dart ├── model │ ├── article.dart │ ├── banner.dart │ ├── coin_record.dart │ ├── navigation_site.dart │ ├── search.dart │ ├── tree.dart │ └── user.dart ├── provider │ ├── provider_widget.dart │ ├── provider_widget_selector.dart │ ├── view_state.dart │ ├── view_state_list_model.dart │ ├── view_state_model.dart │ ├── view_state_refresh_list_model.dart │ └── view_state_widget.dart ├── service │ ├── app_repository.dart │ └── wan_android_repository.dart ├── ui │ ├── helper │ │ ├── dialog_helper.dart │ │ ├── favourite_helper.dart │ │ ├── popup_helper.dart │ │ ├── refresh_helper.dart │ │ └── theme_helper.dart │ ├── page │ │ ├── article │ │ │ ├── article_detail_page.dart │ │ │ ├── article_detail_plugin_page.dart │ │ │ └── article_list_page.dart │ │ ├── change_log_page.dart │ │ ├── coin │ │ │ ├── coin_ranking_list_page.dart │ │ │ └── coin_record_list_page.dart │ │ ├── favourite_list_page.dart │ │ ├── search │ │ │ ├── search_delegate.dart │ │ │ ├── search_results.dart │ │ │ └── search_suggestions.dart │ │ ├── setting_page.dart │ │ ├── splash.dart │ │ ├── tab │ │ │ ├── home_page.dart │ │ │ ├── home_second_floor_page.dart │ │ │ ├── project_page.dart │ │ │ ├── structure_page.dart │ │ │ ├── tab_navigator.dart │ │ │ ├── user_page.dart │ │ │ └── wechat_account_page.dart │ │ └── user │ │ │ ├── login_field_widget.dart │ │ │ ├── login_page.dart │ │ │ ├── login_widget.dart │ │ │ └── register_page.dart │ └── widget │ │ ├── activity_indicator.dart │ │ ├── animated_provider.dart │ │ ├── app_bar.dart │ │ ├── app_update.dart │ │ ├── article_list_Item.dart │ │ ├── article_skeleton.dart │ │ ├── article_tag.dart │ │ ├── banner_image.dart │ │ ├── bottom_clipper.dart │ │ ├── button_progress_indicator.dart │ │ ├── favourite_animation.dart │ │ ├── image.dart │ │ ├── page_route_anim.dart │ │ ├── skeleton.dart │ │ └── third_component.dart ├── utils │ ├── animation_utils.dart │ ├── platform_utils.dart │ ├── status_bar_utils.dart │ ├── string_utils.dart │ └── third_app_utils.dart └── view_model │ ├── app_model.dart │ ├── coin_model.dart │ ├── favourite_model.dart │ ├── home_model.dart │ ├── locale_model.dart │ ├── login_model.dart │ ├── project_model.dart │ ├── register_model.dart │ ├── scroll_controller_model.dart │ ├── search_model.dart │ ├── setting_model.dart │ ├── structure_model.dart │ ├── theme_model.dart │ ├── user_model.dart │ └── wechat_account_model.dart ├── pubspec.yaml └── test └── widget_test.dart /.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 | .vscode/settings.json 23 | **/.settings 24 | **/.project 25 | **/.classpath 26 | 27 | # Flutter/Dart/Pub related 28 | **/doc/api/ 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Android related 38 | **/android/**/gradle-wrapper.jar 39 | **/android/.gradle 40 | **/android/captures/ 41 | **/android/gradlew 42 | **/android/gradlew.bat 43 | **/android/local.properties 44 | **/android/**/GeneratedPluginRegistrant.java 45 | **/android/signing 46 | 47 | # iOS/XCode related 48 | **/ios/**/*.mode1v3 49 | **/ios/**/*.mode2v3 50 | **/ios/**/*.moved-aside 51 | **/ios/**/*.pbxuser 52 | **/ios/**/*.perspectivev3 53 | **/ios/**/*sync/ 54 | **/ios/**/.sconsign.dblite 55 | **/ios/**/.tags* 56 | **/ios/**/.vagrant/ 57 | **/ios/**/DerivedData/ 58 | **/ios/**/Icon? 59 | **/ios/**/Pods/ 60 | **/ios/**/.symlinks/ 61 | **/ios/**/profile 62 | **/ios/**/xcuserdata 63 | **/ios/.generated/ 64 | **/ios/Flutter/App.framework 65 | **/ios/Flutter/Flutter.framework 66 | **/ios/Flutter/Generated.xcconfig 67 | **/ios/Flutter/app.flx 68 | **/ios/Flutter/app.zip 69 | **/ios/Flutter/flutter_assets/ 70 | **/ios/ServiceDefinitions.json 71 | **/ios/Runner/GeneratedPluginRegistrant.* 72 | **/ios/Flutter/flutter_export_environment.sh 73 | 74 | # Exceptions to above rules. 75 | !**/ios/**/default.mode1v3 76 | !**/ios/**/default.mode2v3 77 | !**/ios/**/default.pbxuser 78 | !**/ios/**/default.perspectivev3 79 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 80 | /.fvm/ 81 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 编译环境 2 | > Channel stable, v1.12.13+hotfix.5 3 | 4 | 5 | # 最近更新 6 | 7 | ## V0.1.15 `2020-02-24` (未打包) 8 | - 优化ViewStateError 9 | - ViewState状态重命名 10 | - 升级部分依赖库 11 | 12 | ## V0.1.14 `2020-02-13` (未打包) 13 | - 更新SDK版本为`stable, v1.12.13+hotfix.8` 14 | - 适配Provider4.x 15 | - 迁移国际化方案到`flutter_intl`,使用说明见[FunFlutter系列之国际化Intl方案 \- 掘金](https://juejin.im/post/5e4536d0e51d4526ef5f85a9) 16 | 17 | ## V0.1.13 `2019-12-20` (未打包) 18 | - 修复之前未上传签名文件导致编译出错的问题 19 | - 更新SDK版本为`stable, v1.12.13+hotfix.5` 20 | - 更新Provider版本到3.2.0 21 | - 更新Cache_Network_Image到2.0.0RC 22 | - 隐藏部分重写导致import冲突的widget 23 | - 增加部分ignore配置 24 | - 感谢[liyujiang-gzu](https://github.com/liyujiang-gzu)的pr 25 | 26 | ## V0.1.12 `2019-10-21` 27 | - 下拉刷新列表在加载失败时,如果当前页没有数据显示错误提示页,有数据则弹出toast提示 28 | 29 | ## V0.1.11 `2019-10-17` 30 | - 增加网络加载失败的提示 31 | 32 | 33 | ## V0.1.10 `2019-10-16` 34 | - 修复收藏页面'shareUser'字段为空导致报错的bug 35 | 36 | ## V0.1.9 `2019-10-14` 37 | - 极致黑(Native的闪屏页面适配darkMode) 38 | - 首页banner高度根据屏幕宽高适应 39 | - 签名文件调整 40 | 41 | ## V0.1.8 `2019-10-13` 42 | - 文章列表加入分享人 43 | - 首页加入数据为空的逻辑判断 44 | - ViewStateModel中逻辑优化,bug fix 45 | - 状态栏字体颜色优化 46 | - 修复TextField中hint为中文时不居中的问题 47 | 48 | ## V0.1.7 `2019-09-23` 49 | 50 | - DarkMode自动跟随系统设置 51 | - App更新UI调整 52 | - 适配Dio3.0版本 53 | - pull_to_refresh更新:加入国际化 54 | 55 | 56 | ## V0.1.6 `2019-09-20` 57 | 58 | - 修复收藏列表进入详情时,页面报错的bug 59 | 60 | ## V0.1.5 `2019-09-19` 61 | 62 | - Flutter SDK更新至**Channel dev, v1.10.3**,修复`我的`页面莫名卡死的问题 63 | - 修改Android端App名称为Fun Android 64 | - 版本更新加入提示 65 | 66 | ## V0.1.4 `2019-09-18` 67 | 68 | - **Android加入版本更新** 69 | - 适配Flutter新版本`Channel dev, v1.10.3` 70 | - 移除修复首页黑屏问题的代码`官方在1.10.1版本已修复` 71 | - 加入LeanCloud API云服务 72 | - 移除之前屏幕适配方案,对NativeView影响过大 73 | - 修复版本更新导致的AppBar中进度条颜色与背景色不明显的问题 74 | - 重构Http使用方式,解耦性更好 75 | - 首页banner高度调整 76 | - Android状态栏透明 77 | 78 | ## 0.1.3 79 | 80 | - 修复各页面里文章**收藏**状态没有同步的问题 81 | - 优化Dropdown弹出动画 82 | - 禁用首页初次加载数据的上拉记载更多功能 83 | - 登录页面输入框可通过回车键切换光标 84 | 85 | 86 | ## 0.1.2 87 | 88 | - 修复积分数值在登录后没有刷新的bug 89 | - 修复进入Splash页面短暂黑屏的bug 90 | - 修复未登录时,点击收藏还可以播放动画的bug 91 | - 默认主题色调整为亮色 92 | 93 | ## 0.1.1 94 | 95 | - 添加积分记录和排行榜功能 96 | - 在设置中添加WebViewPlugin的开关 97 | - 更新收藏动画的实现方式和效果,此刻尽丝滑 98 | - 在详情中移除收藏后,回到收藏列表页面更新 99 | 100 | ## 0.1.0 101 | 102 | - 初始发布 103 | 104 | 105 | -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Language: [English](https://github.com/phoenixsky/fun_android_flutter/blob/master/README-EN.md) | [中文简体](https://github.com/phoenixsky/fun_android_flutter/blob/master/README.md) 4 | 5 | 6 | 7 | ![logo,灵感来自2dimensions是个蓝色的F,自己挺喜欢,就down了下来,后来又翻了好久也没找到作者,如果侵权请联系我](https://upload-images.jianshu.io/upload_images/581515-f3a4b2e4392e63bf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/200) 8 | 9 | > Big F, it's `Fun`, also it means `Flutter`. 10 | 11 | `FunAndroid` is a production project , Provider's best practices with MVVM 12 | 13 | # ScreenShot 14 | 15 | | ![splash.gif](https://upload-images.jianshu.io/upload_images/581515-1e3e9fe19d44adca.gif?imageMogr2/auto-orient/strip) | ![首页空中楼阁](https://upload-images.jianshu.io/upload_images/581515-2f68e3fc18a3161e.gif?imageMogr2/auto-orient/strip) | ![tab概览_1080-50-128.gif](https://upload-images.jianshu.io/upload_images/581515-be91ba09c020f594.gif?imageMogr2/auto-orient/strip) | 16 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | 17 | | ![页面不同状态展示.gif](https://upload-images.jianshu.io/upload_images/581515-81e45c5a72fd6b83.gif?imageMogr2/auto-orient/strip) | ![搜索.gif](https://upload-images.jianshu.io/upload_images/581515-00f7b2f89cf141a1.gif?imageMogr2/auto-orient/strip) | ![收藏-50.gif](https://upload-images.jianshu.io/upload_images/581515-5c5e9b7219100c26.gif?imageMogr2/auto-orient/strip) | 18 | | ![登录页展示.gif](https://upload-images.jianshu.io/upload_images/581515-9d83d6940c9a57ed.gif?imageMogr2/auto-orient/strip) | ![收藏列表到登录.gif](https://upload-images.jianshu.io/upload_images/581515-15084c89cc5a55f2.gif?imageMogr2/auto-orient/strip) | ![主题切换-1080-75-256.gif](https://upload-images.jianshu.io/upload_images/581515-348b013cc8a52621.gif?imageMogr2/auto-orient/strip) | 19 | 20 | # Home Page 21 | 22 | > [https://github.com/phoenixsky/fun_android_flutter](https://github.com/phoenixsky/fun_android_flutter) 23 | 24 | # Download page 25 | 26 | # 下载地址 27 | 28 | > [download page](https://www.pgyer.com/Ki0F) 29 | 30 | ![](https://www.pgyer.com/app/qrcode/Ki0F) 31 | 32 | # Environment : 33 | * Flutter SDK (Channel dev, v1.10.3) 34 | 35 | 36 | # Update 37 | 38 | ## 2019-08-28 39 | 40 | - add WebViewPlugin switcher in Setting Page 41 | - My favourite list can refresh after the unlike in the detail page 42 | 43 | ## 2019-08-26 44 | 45 | - update favourite animation with Hero and Route . (hiding original hero after hero transition.见[pr-37341](https://github.com/flutter/flutter/pull/37341)) 46 | 47 | ![Hero-收藏-25-64.gif](https://upload-images.jianshu.io/upload_images/581515-c95bf682c308bd40.gif?imageMogr2/auto-orient/strip) 48 | 49 | 50 | 51 | # Provider MVVM Best Practices 52 | 53 | - Quickly add a page with pull-down refresh and pull up to load more pages. For example 54 | 1. Model 55 | ![Api](https://upload-images.jianshu.io/upload_images/581515-f60f2fceef71b2cc.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 56 | 2. ViewModel 57 | ![-w494](https://upload-images.jianshu.io/upload_images/581515-3ab778bafeb3b5b7.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 58 | 3. View 59 | ![-w637](https://upload-images.jianshu.io/upload_images/581515-1aa9bd76f0e6f600.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 60 | 61 | > `model.viewState == ViewState.busy` is better way ,but `isBusy` easy to write 62 | 63 | # What can you find here?? 64 | 65 | 1. Provider 66 | 1. When and Where init data 67 | 2. how to be with ViewState(`loading`、`error`、`empty`、`idle`、`unAuthorized`)。 68 | 3. use together with `pull to refresh` 69 | 2. Clear Structure。 70 | 3. Drop setState, Partial Rerefresh with XxxBuilder 71 | 4. App base module 72 | 1. Theme 73 | 2. DarkMode 74 | 3. Switch Font 75 | 4. Skeleton 76 | 5. i18n 77 | 6. Dio with Cookjar,use cookie login 78 | 79 | # To-Do 80 | 81 | 1. homepage second floor can't navigate 82 | 2. Sign in Hero animation shift 83 | 3. Sign out add animation 84 | 85 | # Bug 86 | 87 | 1. webview_flutter](https://pub.dev/packages/webview_flutter) some url can't navigate 88 | 2. [webview_flutter](https://pub.dev/packages/webview_flutter) in `CustomScrollView` can't scroll ,[issue](https://github.com/flutter/flutter/issues/31243#issuecomment-521564216) 。 89 | 3. anti-aliasing when same-colour blocks in SignIn Page。见[issue](https://github.com/flutter/flutter/issues/14288) 。 90 | 91 | 92 | 93 | # Thanks 94 | 95 | 1. [V2Lf](https://github.com/w4mxl/V2LF) ,made me like flutter 96 | 2. `goweii`[WanAndroid](https://github.com/goweii/WanAndroid) 97 | 3. [Tutorials](https://github.com/FilledStacks/flutter-tutorials),Video tutorial on youtube 98 | 4. [pull_to_refresh](https://pub.dev/packages/pull_to_refresh) 99 | 5. ZCOOL Font 100 | 6. [WanAndroid](https://www.wanandroid.com/blog/show/2) provide Api 101 | 102 | # About Me 103 | 104 | - [Github](https://github.com/phoenixsky) 105 | - [Blog](http://blog.phoenixsky.cn/) 106 | - [简书](https://www.jianshu.com/u/145e6297cb26) 107 | - Email: moran.fc@gmail.com 108 | 109 | # License 110 | 111 | Copyright 2019 phoenixsky 112 | 113 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 114 | 115 | http://www.apache.org/licenses/LICENSE-2.0 116 | 117 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 118 | -------------------------------------------------------------------------------- /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 | 28 | android { 29 | compileSdkVersion 29 30 | 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | 35 | defaultConfig { 36 | applicationId "cn.phoenixsky.wan_android" 37 | minSdkVersion 16 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | } 43 | signingConfigs { 44 | release { 45 | // 签名文件并没有开源,可以直接在${project}/android/目录下创建signing文件夹 46 | // 根据`keystoreProperties`自行配置文件 47 | def keystorePropertiesFile = rootProject.file("signing/key.properties") 48 | if (keystorePropertiesFile.exists()) { 49 | def keystoreProperties = new Properties() 50 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 51 | keyAlias keystoreProperties['keyAlias'] 52 | keyPassword keystoreProperties['keyPassword'] 53 | storeFile file(keystoreProperties['storeFile']) 54 | storePassword keystoreProperties['storePassword'] 55 | } 56 | } 57 | } 58 | buildTypes { 59 | // 签名文件并没有上传,可以使用自己的签名文件或者移除一下指令使用默认的签名 60 | release { 61 | signingConfig signingConfigs.release 62 | } 63 | debug { 64 | } 65 | } 66 | 67 | // lintOptions { 68 | // 69 | // checkReleaseBuilds false 70 | // 71 | // abortOnError false 72 | // 73 | // } 74 | 75 | } 76 | 77 | flutter { 78 | source '../..' 79 | } 80 | 81 | dependencies { 82 | testImplementation 'junit:junit:4.12' 83 | androidTestImplementation 'androidx.test:runner:1.1.1' 84 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 85 | } 86 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | #Flutter Wrapper 2 | -keep class io.flutter.app.** { *; } 3 | -keep class io.flutter.plugin.** { *; } 4 | -keep class io.flutter.util.** { *; } 5 | -keep class io.flutter.view.** { *; } 6 | -keep class io.flutter.** { *; } 7 | -keep class io.flutter.plugins.** { *; } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 29 | 33 | 34 | 37 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 55 | 59 | 60 | 61 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /android/app/src/main/java/cn/phoenixsky/wan_android/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.phoenixsky.wan_android; 2 | 3 | import io.flutter.embedding.android.FlutterActivity; 4 | 5 | public class MainActivity extends FlutterActivity { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launch_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-mdpi/launch_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launch_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-xhdpi/launch_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launch_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-xxhdpi/launch_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | //classpath 'com.android.tools.build:gradle:3.2.1' 9 | classpath 'com.android.tools.build:gradle:3.5.1' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | jcenter() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | task clean(type: Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | android.enableR8=true 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | #distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /assets/animations/like.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/animations/like.flr -------------------------------------------------------------------------------- /assets/fonts/ZCOOLKuaiLe-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/fonts/ZCOOLKuaiLe-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/fonts/iconfont.ttf -------------------------------------------------------------------------------- /assets/images/home_second_floor_builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/home_second_floor_builder.png -------------------------------------------------------------------------------- /assets/images/login_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/login_logo.png -------------------------------------------------------------------------------- /assets/images/logo_wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/logo_wechat.png -------------------------------------------------------------------------------- /assets/images/logo_weibo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/logo_weibo.png -------------------------------------------------------------------------------- /assets/images/splash_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/splash_android.png -------------------------------------------------------------------------------- /assets/images/splash_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/splash_bg.png -------------------------------------------------------------------------------- /assets/images/splash_bg_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/splash_bg_dark.png -------------------------------------------------------------------------------- /assets/images/splash_flutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/splash_flutter.png -------------------------------------------------------------------------------- /assets/images/splash_fun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/splash_fun.png -------------------------------------------------------------------------------- /assets/images/user_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/assets/images/user_avatar.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - device_info (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - flutter_webview_plugin (0.0.1): 6 | - Flutter 7 | - FMDB (2.7.5): 8 | - FMDB/standard (= 2.7.5) 9 | - FMDB/standard (2.7.5) 10 | - launch_review (0.0.1): 11 | - Flutter 12 | - open_file (0.0.1): 13 | - Flutter 14 | - package_info (0.0.1): 15 | - Flutter 16 | - path_provider (0.0.1): 17 | - Flutter 18 | - screen (0.0.1): 19 | - Flutter 20 | - share (0.5.2): 21 | - Flutter 22 | - shared_preferences (0.0.1): 23 | - Flutter 24 | - sqflite (0.0.1): 25 | - Flutter 26 | - FMDB (~> 2.7.2) 27 | - url_launcher (0.0.1): 28 | - Flutter 29 | - video_player (0.0.1): 30 | - Flutter 31 | - wakelock (0.0.1): 32 | - Flutter 33 | - webview_flutter (0.0.1): 34 | - Flutter 35 | 36 | DEPENDENCIES: 37 | - device_info (from `.symlinks/plugins/device_info/ios`) 38 | - Flutter (from `Flutter`) 39 | - flutter_webview_plugin (from `.symlinks/plugins/flutter_webview_plugin/ios`) 40 | - launch_review (from `.symlinks/plugins/launch_review/ios`) 41 | - open_file (from `.symlinks/plugins/open_file/ios`) 42 | - package_info (from `.symlinks/plugins/package_info/ios`) 43 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 44 | - screen (from `.symlinks/plugins/screen/ios`) 45 | - share (from `.symlinks/plugins/share/ios`) 46 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) 47 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 48 | - url_launcher (from `.symlinks/plugins/url_launcher/ios`) 49 | - video_player (from `.symlinks/plugins/video_player/ios`) 50 | - wakelock (from `.symlinks/plugins/wakelock/ios`) 51 | - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) 52 | 53 | SPEC REPOS: 54 | trunk: 55 | - FMDB 56 | 57 | EXTERNAL SOURCES: 58 | device_info: 59 | :path: ".symlinks/plugins/device_info/ios" 60 | Flutter: 61 | :path: Flutter 62 | flutter_webview_plugin: 63 | :path: ".symlinks/plugins/flutter_webview_plugin/ios" 64 | launch_review: 65 | :path: ".symlinks/plugins/launch_review/ios" 66 | open_file: 67 | :path: ".symlinks/plugins/open_file/ios" 68 | package_info: 69 | :path: ".symlinks/plugins/package_info/ios" 70 | path_provider: 71 | :path: ".symlinks/plugins/path_provider/ios" 72 | screen: 73 | :path: ".symlinks/plugins/screen/ios" 74 | share: 75 | :path: ".symlinks/plugins/share/ios" 76 | shared_preferences: 77 | :path: ".symlinks/plugins/shared_preferences/ios" 78 | sqflite: 79 | :path: ".symlinks/plugins/sqflite/ios" 80 | url_launcher: 81 | :path: ".symlinks/plugins/url_launcher/ios" 82 | video_player: 83 | :path: ".symlinks/plugins/video_player/ios" 84 | wakelock: 85 | :path: ".symlinks/plugins/wakelock/ios" 86 | webview_flutter: 87 | :path: ".symlinks/plugins/webview_flutter/ios" 88 | 89 | SPEC CHECKSUMS: 90 | device_info: d7d233b645a32c40dfdc212de5cf646ca482f175 91 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 92 | flutter_webview_plugin: ed9e8a6a96baf0c867e90e1bce2673913eeac694 93 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 94 | launch_review: 75d5a956ba8eaa493e9c9d4bf4c05e505e8d5ed0 95 | open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d 96 | package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 97 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 98 | screen: abd91ca7bf3426e1cc3646d27e9b2358d6bf07b0 99 | share: 7d22fe8baedfe93aefd864bf0b73f29711fbb0a3 100 | shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d 101 | sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 102 | url_launcher: 0067ddb8f10d36786672aa0722a21717dba3a298 103 | video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e 104 | wakelock: 0d4a70faf8950410735e3f61fb15d517c8a6efc4 105 | webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 106 | 107 | PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d 108 | 109 | COCOAPODS: 1.8.4 110 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "appicon.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "appicon-1.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "appicon-0.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "appicon-8.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "appicon-7.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "appicon-6.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "appicon-5.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "appicon-4.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "appicon-3.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" : "appicon-2.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/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/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/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/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/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/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/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/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-0.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-2.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-3.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-4.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-5.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-6.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-7.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon-8.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/AppIcon.appiconset/appicon.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "appicon-3.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "appicon.png", 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "scale" : "1x" 18 | }, 19 | { 20 | "idiom" : "universal", 21 | "filename" : "appicon-4.png", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "idiom" : "universal", 26 | "filename" : "appicon-1.png", 27 | "appearances" : [ 28 | { 29 | "appearance" : "luminosity", 30 | "value" : "dark" 31 | } 32 | ], 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "universal", 37 | "filename" : "appicon-5.png", 38 | "scale" : "3x" 39 | }, 40 | { 41 | "idiom" : "universal", 42 | "filename" : "appicon-2.png", 43 | "appearances" : [ 44 | { 45 | "appearance" : "luminosity", 46 | "value" : "dark" 47 | } 48 | ], 49 | "scale" : "3x" 50 | } 51 | ], 52 | "info" : { 53 | "version" : 1, 54 | "author" : "xcode" 55 | } 56 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-1.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-2.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-3.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-4.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon-5.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phoenixsky/fun_android_flutter/1d56e91b79693ef452209395aedfdbb9c0094f34/ios/Runner/Assets.xcassets/LaunchImage.imageset/appicon.png -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Fun Flutter 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Fun Android 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSApplicationQueriesSchemes 26 | 27 | itms 28 | jianshu 29 | juejin 30 | 31 | LSRequiresIPhoneOS 32 | 33 | NSAppTransportSecurity 34 | 35 | NSAllowsArbitraryLoads 36 | 37 | NSAllowsArbitraryLoadsInWebContent 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIMainStoryboardFile 43 | Main 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | UIViewControllerBasedStatusBarAppearance 56 | 57 | io.flutter.embedded_views_preview 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/zh-Hans.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ios/Runner/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/config/net/api.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:dio/native_imp.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:dio/dio.dart'; 6 | import 'package:fun_android/utils/platform_utils.dart'; 7 | 8 | export 'package:dio/dio.dart'; 9 | 10 | // 必须是顶层函数 11 | _parseAndDecode(String response) { 12 | return jsonDecode(response); 13 | } 14 | 15 | parseJson(String text) { 16 | return compute(_parseAndDecode, text); 17 | } 18 | 19 | abstract class BaseHttp extends DioForNative { 20 | BaseHttp() { 21 | /// 初始化 加入app通用处理 22 | (transformer as DefaultTransformer).jsonDecodeCallback = parseJson; 23 | interceptors..add(HeaderInterceptor()); 24 | init(); 25 | } 26 | 27 | void init(); 28 | } 29 | 30 | /// 添加常用Header 31 | class HeaderInterceptor extends InterceptorsWrapper { 32 | @override 33 | onRequest(RequestOptions options) async { 34 | options.connectTimeout = 1000 * 45; 35 | options.receiveTimeout = 1000 * 45; 36 | 37 | var appVersion = await PlatformUtils.getAppVersion(); 38 | var version = Map() 39 | ..addAll({ 40 | 'appVerison': appVersion, 41 | }); 42 | options.headers['version'] = version; 43 | options.headers['platform'] = Platform.operatingSystem; 44 | return options; 45 | } 46 | } 47 | 48 | /// 子类需要重写 49 | abstract class BaseResponseData { 50 | int code = 0; 51 | String message; 52 | dynamic data; 53 | 54 | bool get success; 55 | 56 | BaseResponseData({this.code, this.message, this.data}); 57 | 58 | @override 59 | String toString() { 60 | return 'BaseRespData{code: $code, message: $message, data: $data}'; 61 | } 62 | } 63 | 64 | 65 | /// 接口的code没有返回为true的异常 66 | class NotSuccessException implements Exception { 67 | String message; 68 | 69 | NotSuccessException.fromRespData(BaseResponseData respData) { 70 | message = respData.message; 71 | } 72 | 73 | @override 74 | String toString() { 75 | return 'NotExpectedException{respData: $message}'; 76 | } 77 | } 78 | 79 | /// 用于未登录等权限不够,需要跳转授权页面 80 | class UnAuthorizedException implements Exception { 81 | const UnAuthorizedException(); 82 | 83 | @override 84 | String toString() => 'UnAuthorizedException'; 85 | } 86 | 87 | -------------------------------------------------------------------------------- /lib/config/net/pgyer_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'api.dart'; 5 | 6 | final Http http = Http(); 7 | 8 | class Http extends BaseHttp { 9 | @override 10 | void init() { 11 | options.baseUrl = 'https://www.pgyer.com/apiv2/'; 12 | interceptors.add(PgyerApiInterceptor()); 13 | } 14 | } 15 | 16 | /// App相关 API 17 | class PgyerApiInterceptor extends InterceptorsWrapper { 18 | @override 19 | onRequest(RequestOptions options) async { 20 | options.queryParameters['_api_key'] = '00f25cece8e201753872c268b5832df9'; 21 | options.queryParameters['appKey'] = '0f7026d9c95933c7d0553628605b6ea4'; 22 | debugPrint('---api-request--->url--> ${options.baseUrl}${options.path}' + 23 | ' queryParameters: ${options.queryParameters}'); 24 | return options; 25 | } 26 | 27 | @override 28 | onResponse(Response response) { 29 | ResponseData respData = ResponseData.fromJson(response.data); 30 | if (respData.success) { 31 | response.data = respData.data; 32 | return http.resolve(response); 33 | } else { 34 | throw NotSuccessException.fromRespData(respData); 35 | } 36 | } 37 | } 38 | 39 | class ResponseData extends BaseResponseData { 40 | bool get success => code == 0; 41 | 42 | ResponseData.fromJson(Map json) { 43 | code = json['code']; 44 | message = json['message']; 45 | data = json['data']; 46 | } 47 | } 48 | 49 | /// CheckApp更新接口 50 | class AppUpdateInfo { 51 | String buildBuildVersion; 52 | String forceUpdateVersion; 53 | String forceUpdateVersionNo; 54 | bool needForceUpdate; 55 | String downloadURL; 56 | bool buildHaveNewVersion; 57 | String buildVersionNo; 58 | String buildVersion; 59 | String buildShortcutUrl; 60 | String buildUpdateDescription; 61 | 62 | static AppUpdateInfo fromMap(Map map) { 63 | if (map == null) return null; 64 | AppUpdateInfo pgyerApiBean = AppUpdateInfo(); 65 | pgyerApiBean.buildBuildVersion = map['buildBuildVersion']; 66 | pgyerApiBean.forceUpdateVersion = map['forceUpdateVersion']; 67 | pgyerApiBean.forceUpdateVersionNo = map['forceUpdateVersionNo']; 68 | pgyerApiBean.needForceUpdate = map['needForceUpdate']; 69 | pgyerApiBean.downloadURL = map['downloadURL']; 70 | pgyerApiBean.buildHaveNewVersion = map['buildHaveNewVersion']; 71 | pgyerApiBean.buildVersionNo = map['buildVersionNo']; 72 | pgyerApiBean.buildVersion = map['buildVersion']; 73 | pgyerApiBean.buildShortcutUrl = map['buildShortcutUrl']; 74 | pgyerApiBean.buildUpdateDescription = map['buildUpdateDescription']; 75 | return pgyerApiBean; 76 | } 77 | 78 | Map toJson() => { 79 | "buildBuildVersion": buildBuildVersion, 80 | "forceUpdateVersion": forceUpdateVersion, 81 | "forceUpdateVersionNo": forceUpdateVersionNo, 82 | "needForceUpdate": needForceUpdate, 83 | "downloadURL": downloadURL, 84 | "buildHaveNewVersion": buildHaveNewVersion, 85 | "buildVersionNo": buildVersionNo, 86 | "buildVersion": buildVersion, 87 | "buildShortcutUrl": buildShortcutUrl, 88 | "buildUpdateDescription": buildUpdateDescription, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /lib/config/net/wan_android_api.dart: -------------------------------------------------------------------------------- 1 | import 'package:cookie_jar/cookie_jar.dart'; 2 | import 'package:dio/dio.dart'; 3 | import 'package:dio_cookie_manager/dio_cookie_manager.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'api.dart'; 6 | import '../storage_manager.dart'; 7 | 8 | final Http http = Http(); 9 | 10 | class Http extends BaseHttp { 11 | @override 12 | void init() { 13 | options.baseUrl = 'https://www.wanandroid.com/'; 14 | interceptors 15 | ..add(ApiInterceptor()) 16 | // cookie持久化 异步 17 | ..add(CookieManager( 18 | PersistCookieJar(dir: StorageManager.temporaryDirectory.path))); 19 | } 20 | } 21 | 22 | /// 玩Android API 23 | class ApiInterceptor extends InterceptorsWrapper { 24 | @override 25 | onRequest(RequestOptions options) async { 26 | debugPrint('---api-request--->url--> ${options.baseUrl}${options.path}' + 27 | ' queryParameters: ${options.queryParameters}'); 28 | // debugPrint('---api-request--->data--->${options.data}'); 29 | return options; 30 | } 31 | 32 | @override 33 | onResponse(Response response) { 34 | // debugPrint('---api-response--->resp----->${response.data}'); 35 | ResponseData respData = ResponseData.fromJson(response.data); 36 | if (respData.success) { 37 | response.data = respData.data; 38 | return http.resolve(response); 39 | } else { 40 | if (respData.code == -1001) { 41 | // 如果cookie过期,需要清除本地存储的登录信息 42 | // StorageManager.localStorage.deleteItem(UserModel.keyUser); 43 | throw const UnAuthorizedException(); // 需要登录 44 | } else { 45 | throw NotSuccessException.fromRespData(respData); 46 | } 47 | } 48 | } 49 | } 50 | 51 | class ResponseData extends BaseResponseData { 52 | bool get success => 0 == code; 53 | 54 | ResponseData.fromJson(Map json) { 55 | code = json['errorCode']; 56 | message = json['errorMsg']; 57 | data = json['data']; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/config/provider_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/view_model/favourite_model.dart'; 2 | import 'package:fun_android/view_model/locale_model.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:fun_android/view_model/theme_model.dart'; 5 | import 'package:fun_android/view_model/user_model.dart'; 6 | import 'package:provider/single_child_widget.dart'; 7 | 8 | List providers = [ 9 | ...independentServices, 10 | ...dependentServices, 11 | ...uiConsumableProviders 12 | ]; 13 | 14 | /// 独立的model 15 | List independentServices = [ 16 | ChangeNotifierProvider( 17 | create: (context) => ThemeModel(), 18 | ), 19 | ChangeNotifierProvider( 20 | create: (context) => LocaleModel(), 21 | ), 22 | ChangeNotifierProvider( 23 | create: (context) => GlobalFavouriteStateModel(), 24 | ) 25 | ]; 26 | 27 | /// 需要依赖的model 28 | /// 29 | /// UserModel依赖globalFavouriteStateModel 30 | List dependentServices = [ 31 | ChangeNotifierProxyProvider( 32 | create: null, 33 | update: (context, globalFavouriteStateModel, userModel) => 34 | userModel ?? 35 | UserModel(globalFavouriteStateModel: globalFavouriteStateModel), 36 | ) 37 | ]; 38 | 39 | List uiConsumableProviders = [ 40 | // StreamProvider( 41 | // builder: (context) => Provider.of(context, listen: false).user, 42 | // ) 43 | ]; 44 | -------------------------------------------------------------------------------- /lib/config/resource_mananger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | class ImageHelper { 8 | static const String baseUrl = 'http://www.meetingplus.cn'; 9 | static const String imagePrefix = '$baseUrl/uimg/'; 10 | 11 | static String wrapUrl(String url) { 12 | if (url.startsWith('http')) { 13 | return url; 14 | } else {} 15 | return imagePrefix + url; 16 | } 17 | 18 | static String wrapAssets(String url) { 19 | return "assets/images/" + url; 20 | } 21 | 22 | static Widget placeHolder({double width, double height}) { 23 | return SizedBox( 24 | width: width, 25 | height: height, 26 | child: CupertinoActivityIndicator(radius: min(10.0, width / 3))); 27 | } 28 | 29 | static Widget error({double width, double height, double size}) { 30 | return SizedBox( 31 | width: width, 32 | height: height, 33 | child: Icon( 34 | Icons.error_outline, 35 | size: size, 36 | )); 37 | } 38 | 39 | static String randomUrl( 40 | {int width = 100, int height = 100, Object key = ''}) { 41 | return 'http://placeimg.com/$width/$height/${key.hashCode.toString() + key.toString()}'; 42 | } 43 | } 44 | 45 | class IconFonts { 46 | IconFonts._(); 47 | 48 | /// iconfont:flutter base 49 | static const String fontFamily = 'iconfont'; 50 | 51 | static const IconData pageEmpty = IconData(0xe63c, fontFamily: fontFamily); 52 | static const IconData pageError = IconData(0xe600, fontFamily: fontFamily); 53 | static const IconData pageNetworkError = IconData(0xe678, fontFamily: fontFamily); 54 | static const IconData pageUnAuth = IconData(0xe65f, fontFamily: fontFamily); 55 | } 56 | -------------------------------------------------------------------------------- /lib/config/router_manger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:fun_android/config/storage_manager.dart'; 4 | import 'package:fun_android/model/article.dart'; 5 | import 'package:fun_android/model/tree.dart'; 6 | import 'package:fun_android/ui/page/coin/coin_ranking_list_page.dart'; 7 | import 'package:fun_android/ui/page/coin/coin_record_list_page.dart'; 8 | import 'package:fun_android/ui/page/favourite_list_page.dart'; 9 | import 'package:fun_android/ui/page/article/article_list_page.dart'; 10 | import 'package:fun_android/ui/page/setting_page.dart'; 11 | import 'package:fun_android/ui/page/tab/home_second_floor_page.dart'; 12 | import 'package:fun_android/ui/page/user/login_page.dart'; 13 | import 'package:fun_android/ui/page/splash.dart'; 14 | import 'package:fun_android/ui/page/tab/tab_navigator.dart'; 15 | import 'package:fun_android/ui/page/article/article_detail_page.dart'; 16 | import 'package:fun_android/ui/page/article/article_detail_plugin_page.dart'; 17 | import 'package:fun_android/ui/page/user/register_page.dart'; 18 | import 'package:fun_android/ui/widget/page_route_anim.dart'; 19 | import 'package:fun_android/view_model/setting_model.dart'; 20 | 21 | class RouteName { 22 | static const String splash = 'splash'; 23 | static const String tab = '/'; 24 | static const String homeSecondFloor = 'homeSecondFloor'; 25 | static const String login = 'login'; 26 | static const String register = 'register'; 27 | static const String articleDetail = 'articleDetail'; 28 | static const String structureList = 'structureList'; 29 | static const String favouriteList = 'favouriteList'; 30 | static const String setting = 'setting'; 31 | static const String coinRecordList = 'coinRecordList'; 32 | static const String coinRankingList = 'coinRankingList'; 33 | } 34 | 35 | class MyRouter { 36 | static Route generateRoute(RouteSettings settings) { 37 | switch (settings.name) { 38 | case RouteName.splash: 39 | return NoAnimRouteBuilder(SplashPage()); 40 | case RouteName.tab: 41 | return NoAnimRouteBuilder(TabNavigator()); 42 | case RouteName.homeSecondFloor: 43 | return SlideTopRouteBuilder(MyBlogPage()); 44 | case RouteName.login: 45 | return CupertinoPageRoute( 46 | fullscreenDialog: true, builder: (_) => LoginPage()); 47 | case RouteName.register: 48 | return CupertinoPageRoute(builder: (_) => RegisterPage()); 49 | case RouteName.articleDetail: 50 | var article = settings.arguments as Article; 51 | return CupertinoPageRoute(builder: (_) { 52 | // 根据配置调用页面 53 | return StorageManager.sharedPreferences.getBool(kUseWebViewPlugin) ?? 54 | false 55 | ? ArticleDetailPluginPage( 56 | article: article, 57 | ) 58 | : ArticleDetailPage( 59 | article: article, 60 | ); 61 | }); 62 | case RouteName.structureList: 63 | var list = settings.arguments as List; 64 | Tree tree = list[0] as Tree; 65 | int index = list[1]; 66 | return CupertinoPageRoute( 67 | builder: (_) => ArticleCategoryTabPage(tree, index)); 68 | case RouteName.favouriteList: 69 | return CupertinoPageRoute(builder: (_) => FavouriteListPage()); 70 | case RouteName.setting: 71 | return CupertinoPageRoute(builder: (_) => SettingPage()); 72 | case RouteName.coinRecordList: 73 | return CupertinoPageRoute(builder: (_) => CoinRecordListPage()); 74 | case RouteName.coinRankingList: 75 | return CupertinoPageRoute(builder: (_) => CoinRankingListPage()); 76 | default: 77 | return CupertinoPageRoute( 78 | builder: (_) => Scaffold( 79 | body: Center( 80 | child: Text('No route defined for ${settings.name}'), 81 | ), 82 | )); 83 | } 84 | } 85 | } 86 | 87 | /// Pop路由 88 | class PopRoute extends PopupRoute { 89 | final Duration _duration = Duration(milliseconds: 300); 90 | Widget child; 91 | 92 | PopRoute({@required this.child}); 93 | 94 | @override 95 | Color get barrierColor => null; 96 | 97 | @override 98 | bool get barrierDismissible => true; 99 | 100 | @override 101 | String get barrierLabel => null; 102 | 103 | @override 104 | Widget buildPage(BuildContext context, Animation animation, 105 | Animation secondaryAnimation) { 106 | return child; 107 | } 108 | 109 | @override 110 | Duration get transitionDuration => _duration; 111 | } 112 | -------------------------------------------------------------------------------- /lib/config/storage_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:localstorage/localstorage.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | class StorageManager { 8 | /// app全局配置 eg:theme 9 | static SharedPreferences sharedPreferences; 10 | 11 | /// 临时目录 eg: cookie 12 | static Directory temporaryDirectory; 13 | 14 | 15 | /// 初始化必备操作 eg:user数据 16 | static LocalStorage localStorage; 17 | 18 | /// 必备数据的初始化操作 19 | /// 20 | /// 由于是同步操作会导致阻塞,所以应尽量减少存储容量 21 | static init() async { 22 | // async 异步操作 23 | // sync 同步操作 24 | temporaryDirectory = await getTemporaryDirectory(); 25 | sharedPreferences = await SharedPreferences.getInstance(); 26 | localStorage = LocalStorage('LocalStorage'); 27 | await localStorage.ready; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/config/ui_adapter_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | 4 | import 'package:flutter/gestures.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | import 'dart:ui' as ui; 7 | 8 | import 'package:flutter/widgets.dart'; 9 | /// https://github.com/genius158/FlutterTest 10 | 11 | const double SCREEN_WIDTH = 414; 12 | 13 | class InnerWidgetsFlutterBinding extends WidgetsFlutterBinding { 14 | 15 | static WidgetsBinding ensureInitialized() { 16 | if (WidgetsBinding.instance == null) InnerWidgetsFlutterBinding(); 17 | return WidgetsBinding.instance; 18 | } 19 | 20 | double get adapterRatio { 21 | return ui.window.physicalSize.width / SCREEN_WIDTH; 22 | } 23 | 24 | @override 25 | ViewConfiguration createViewConfiguration() { 26 | 27 | return ViewConfiguration( 28 | size: window.physicalSize / adapterRatio, 29 | devicePixelRatio: adapterRatio, 30 | ); 31 | } 32 | 33 | 34 | /// wrap [GestureBinding _handlePointerDataPacket] 35 | /// replace the [PixelRatio] 36 | 37 | @override 38 | void initInstances() { 39 | super.initInstances(); 40 | ui.window.onPointerDataPacket = _handlePointerDataPacket; 41 | } 42 | 43 | @override 44 | void unlocked() { 45 | super.unlocked(); 46 | _flushPointerEventQueue(); 47 | } 48 | 49 | final Queue _pendingPointerEvents = Queue(); 50 | 51 | void _handlePointerDataPacket(ui.PointerDataPacket packet) { 52 | _pendingPointerEvents.addAll(PointerEventConverter.expand( 53 | packet.data, 54 | // 适配事件的转换比率,采用我们修改的 55 | adapterRatio)); 56 | if (!locked) _flushPointerEventQueue(); 57 | } 58 | 59 | @override 60 | void cancelPointer(int pointer) { 61 | if (_pendingPointerEvents.isEmpty && !locked) 62 | scheduleMicrotask(_flushPointerEventQueue); 63 | _pendingPointerEvents.addFirst(PointerCancelEvent(pointer: pointer)); 64 | } 65 | 66 | void _flushPointerEventQueue() { 67 | assert(!locked); 68 | while (_pendingPointerEvents.isNotEmpty) 69 | _handlePointerEvent(_pendingPointerEvents.removeFirst()); 70 | } 71 | 72 | final Map _hitTests = {}; 73 | 74 | void _handlePointerEvent(PointerEvent event) { 75 | assert(!locked); 76 | HitTestResult result; 77 | if (event is PointerDownEvent) { 78 | assert(!_hitTests.containsKey(event.pointer)); 79 | result = HitTestResult(); 80 | hitTest(result, event.position); 81 | _hitTests[event.pointer] = result; 82 | assert(() { 83 | if (debugPrintHitTestResults) debugPrint('$event: $result'); 84 | return true; 85 | }()); 86 | } else if (event is PointerUpEvent || event is PointerCancelEvent) { 87 | result = _hitTests.remove(event.pointer); 88 | } else if (event.down) { 89 | result = _hitTests[event.pointer]; 90 | } else { 91 | return; // We currently ignore add, remove, and hover move events. 92 | } 93 | if (result != null) dispatchEvent(event, result); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/generated/intl/messages_all.dart: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart 2 | // This is a library that looks up messages for specific locales by 3 | // delegating to the appropriate library. 4 | 5 | // Ignore issues from commonly used lints in this file. 6 | // ignore_for_file:implementation_imports, file_names, unnecessary_new 7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering 8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment 9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases 10 | // ignore_for_file:comment_references 11 | 12 | import 'dart:async'; 13 | 14 | import 'package:intl/intl.dart'; 15 | import 'package:intl/message_lookup_by_library.dart'; 16 | import 'package:intl/src/intl_helpers.dart'; 17 | 18 | import 'messages_en.dart' as messages_en; 19 | import 'messages_zh.dart' as messages_zh; 20 | 21 | typedef Future LibraryLoader(); 22 | Map _deferredLibraries = { 23 | 'en': () => new Future.value(null), 24 | 'zh': () => new Future.value(null), 25 | }; 26 | 27 | MessageLookupByLibrary _findExact(String localeName) { 28 | switch (localeName) { 29 | case 'en': 30 | return messages_en.messages; 31 | case 'zh': 32 | return messages_zh.messages; 33 | default: 34 | return null; 35 | } 36 | } 37 | 38 | /// User programs should call this before using [localeName] for messages. 39 | Future initializeMessages(String localeName) async { 40 | var availableLocale = Intl.verifiedLocale( 41 | localeName, 42 | (locale) => _deferredLibraries[locale] != null, 43 | onFailure: (_) => null); 44 | if (availableLocale == null) { 45 | return new Future.value(false); 46 | } 47 | var lib = _deferredLibraries[availableLocale]; 48 | await (lib == null ? new Future.value(false) : lib()); 49 | initializeInternalMessageLookup(() => new CompositeMessageLookup()); 50 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); 51 | return new Future.value(true); 52 | } 53 | 54 | bool _messagesExistFor(String locale) { 55 | try { 56 | return _findExact(locale) != null; 57 | } catch (e) { 58 | return false; 59 | } 60 | } 61 | 62 | MessageLookupByLibrary _findGeneratedMessagesFor(String locale) { 63 | var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor, 64 | onFailure: (_) => null); 65 | if (actualLocale == null) return null; 66 | return _findExact(actualLocale); 67 | } 68 | -------------------------------------------------------------------------------- /lib/l10n/intl_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "Fun Android", 3 | "actionConfirm": "Confirm", 4 | "actionCancel": "Cancel", 5 | "viewStateMessageError": "Load Failed", 6 | "viewStateMessageNetworkError": "Load Failed,Check network ", 7 | "viewStateMessageEmpty": "Nothing Found", 8 | "viewStateMessageUnAuth": "Not sign in yet", 9 | "viewStateButtonRefresh": "Refresh", 10 | "viewStateButtonRetry": "Retry", 11 | "viewStateButtonLogin": "Sign In", 12 | "refreshTwoLevel": "release to enter second floor", 13 | "retry": "Retry", 14 | "splashSkip": "Skip", 15 | "tabHome": "Home", 16 | "tabProject": "Project", 17 | "tabStructure": "Structure", 18 | "tabUser": "Me", 19 | "setting": "Setting", 20 | "settingLanguage": "Language", 21 | "settingFont": "System Font", 22 | "logout": "Sign Out", 23 | "favourites": "Favorites", 24 | "darkMode": "Dark Mode", 25 | "theme": "Theme", 26 | "about": "About", 27 | "close": "Close", 28 | "feedback": "FeedBack", 29 | "githubIssue": "Can't find mail app,please github issues", 30 | "autoBySystem": "Auto", 31 | "fontKuaiLe": "ZCOOL KuaiLe", 32 | "fieldNotNull": "not empty", 33 | "userName": "Username", 34 | "password": "Password", 35 | "toSignUp": "Sign Up", 36 | "signUp": "Sign Up", 37 | "rePassword": "Confirm Password", 38 | "twoPwdDifferent": "The two passwords differ", 39 | "toSignIn": "Sign In", 40 | "signIn": "Sign In", 41 | "noAccount": "No Account ? ", 42 | "myFavourites": "My favourites", 43 | "signIn3thd": "More", 44 | "searchHot": "Hot", 45 | "searchShake": "Shake", 46 | "searchHistory": "History", 47 | "clear": "Clear", 48 | "refresh": "Refresh", 49 | "unLike": "UnLike", 50 | "Like": "Like", 51 | "share": "Share", 52 | "wechatAccount": "Wechat", 53 | "rate": "Rate", 54 | "needLogin": "Go to Sign In", 55 | "loadFailed": "Load failed,retry later", 56 | "collectionRemove": "Remove", 57 | "article_tag_top": "Top", 58 | "openBrowser": "Open Browser", 59 | "coin": "Coin", 60 | "appUpdateCheckUpdate": "Check Update", 61 | "appUpdateActionUpdate": "Update", 62 | "appUpdateLeastVersion": "Least version now ", 63 | "appUpdateDownloading": "Downloading...", 64 | "appUpdateDownloadFailed": "Download failed", 65 | "appUpdateReDownloadContent": "It has been detected that it has been downloaded, whether it is installed?", 66 | "appUpdateActionDownloadAgain": "Download", 67 | "appUpdateActionInstallApk": "Install", 68 | "appUpdateUpdate": "Version Update", 69 | "appUpdateFoundNewVersion": "New version {version}", 70 | "appUpdateDownloadCanceled": "Download canceled", 71 | "appUpdateDoubleBackTips": "Press back again, cancel download" 72 | } -------------------------------------------------------------------------------- /lib/l10n/intl_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appName": "玩Android", 3 | "actionConfirm": "确认", 4 | "actionCancel": "取消", 5 | "viewStateMessageError": "加载失败", 6 | "viewStateMessageNetworkError": "网络连接异常,请检查网络或稍后重试", 7 | "viewStateMessageEmpty": "空空如也", 8 | "viewStateMessageUnAuth": "未登录", 9 | "viewStateButtonRefresh": "刷新一下", 10 | "viewStateButtonRetry": "重试", 11 | "viewStateButtonLogin": "登录", 12 | "refreshTwoLevel": "欢迎光临,我的空中楼阁", 13 | "retry": "重试", 14 | "splashSkip": "跳过", 15 | "tabHome": "首页", 16 | "tabProject": "项目", 17 | "tabStructure": "体系", 18 | "tabUser": "我的", 19 | "setting": "设置", 20 | "settingLanguage": "多语言", 21 | "settingFont": "字体", 22 | "logout": "退出登录", 23 | "favourites": "收藏", 24 | "darkMode": "黑夜模式", 25 | "theme": "色彩主题", 26 | "about": "关于", 27 | "close": "关闭", 28 | "feedback": "意见反馈", 29 | "githubIssue": "未找到邮件客户端,请前往github,提issue", 30 | "autoBySystem": "跟随系统", 31 | "fontKuaiLe": "快乐字体", 32 | "fieldNotNull": "不能为空", 33 | "userName": "用户名", 34 | "password": "密码", 35 | "signUp": "注册", 36 | "toSignUp": "去注册", 37 | "rePassword": "确认密码", 38 | "twoPwdDifferent": "两次密码不一致", 39 | "toSignIn": "点我登录", 40 | "signIn": "登录", 41 | "noAccount": "还没账号? ", 42 | "myFavourites": "我的收藏", 43 | "signIn3thd": "第三方登录", 44 | "searchHot": "热门搜索", 45 | "searchShake": "换一换", 46 | "searchHistory": "历史搜索", 47 | "clear": "清空", 48 | "refresh": "刷新", 49 | "unLike": "取消收藏", 50 | "Like": "收藏", 51 | "share": "分享", 52 | "wechatAccount": "公众号", 53 | "rate": "评分", 54 | "needLogin": "请先登录", 55 | "loadFailed": "加载失败,请稍后重试", 56 | "collectionRemove": "移除收藏", 57 | "article_tag_top": "置顶", 58 | "openBrowser": "浏览器打开", 59 | "coin": "积分", 60 | "appUpdateCheckUpdate": "检查更新", 61 | "appUpdateActionUpdate": "更新", 62 | "appUpdateLeastVersion": "已是最新版本", 63 | "appUpdateDownloading": "下载中,请稍后...", 64 | "appUpdateDownloadFailed": "下载失败", 65 | "appUpdateReDownloadContent": "检测到本地已下载过该版本,是否直接安装?", 66 | "appUpdateActionDownloadAgain": "重新下载", 67 | "appUpdateActionInstallApk": "直接安装", 68 | "appUpdateUpdate": "版本更新", 69 | "appUpdateFoundNewVersion": "发现新版本{version},是否更新?", 70 | "appUpdateDownloadCanceled": "下载已取消", 71 | "appUpdateDoubleBackTips": "再次点击返回键,取消下载" 72 | } -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter_localizations/flutter_localizations.dart'; 5 | 6 | import 'package:oktoast/oktoast.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 9 | 10 | import 'package:fun_android/config/storage_manager.dart'; 11 | 12 | import 'config/provider_manager.dart'; 13 | import 'config/router_manger.dart'; 14 | import 'generated/l10n.dart'; 15 | import 'view_model/locale_model.dart'; 16 | import 'view_model/theme_model.dart'; 17 | 18 | main() async { 19 | Provider.debugCheckInvalidValueType = null; 20 | WidgetsFlutterBinding.ensureInitialized(); 21 | await StorageManager.init(); 22 | runApp(App()); 23 | // Android状态栏透明 splash为白色,所以调整状态栏文字为黑色 24 | SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( 25 | statusBarColor: Colors.transparent, 26 | statusBarBrightness: Brightness.light)); 27 | } 28 | 29 | class App extends StatelessWidget { 30 | @override 31 | Widget build(BuildContext context) { 32 | return OKToast( 33 | child: MultiProvider( 34 | providers: providers, 35 | child: Consumer2( 36 | builder: (context, themeModel, localeModel, child) { 37 | return RefreshConfiguration( 38 | hideFooterWhenNotFull: true, //列表数据不满一页,不触发加载更多 39 | child: MaterialApp( 40 | debugShowCheckedModeBanner: false, 41 | theme: themeModel.themeData(), 42 | darkTheme: themeModel.themeData(platformDarkMode: true), 43 | locale: localeModel.locale, 44 | localizationsDelegates: const [ 45 | S.delegate, 46 | RefreshLocalizations.delegate, //下拉刷新 47 | GlobalCupertinoLocalizations.delegate, 48 | GlobalMaterialLocalizations.delegate, 49 | GlobalWidgetsLocalizations.delegate 50 | ], 51 | supportedLocales: S.delegate.supportedLocales, 52 | onGenerateRoute: MyRouter.generateRoute, 53 | initialRoute: RouteName.splash, 54 | ), 55 | ); 56 | }))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/model/article.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/utils/string_utils.dart'; 2 | 3 | class Article { 4 | String apkLink; 5 | String author; 6 | /// 2019.10.13 添加分享人,author可能为空 7 | String shareUser; 8 | int chapterId; 9 | String chapterName; 10 | bool collect; 11 | int courseId; 12 | String desc; 13 | String envelopePic; 14 | bool fresh; 15 | int id; 16 | String link; 17 | String niceDate; 18 | String origin; 19 | int originId; 20 | String prefix; 21 | String projectLink; 22 | int publishTime; 23 | int superChapterId; 24 | String superChapterName; 25 | List tags; 26 | String title; 27 | int type; 28 | int userId; 29 | int visible; 30 | int zan; 31 | 32 | 33 | 34 | static Article fromMap(Map map) { 35 | if (map == null) return null; 36 | Article articleBean = Article(); 37 | articleBean.apkLink = map['apkLink']; 38 | articleBean.author = map['author']; 39 | articleBean.shareUser = map['shareUser']; 40 | articleBean.chapterId = map['chapterId']; 41 | // articleBean.chapterName = map['chapterName']; 42 | articleBean.chapterName = StringUtils.urlDecoder(map["chapterName"]); 43 | articleBean.collect = map['collect']; 44 | articleBean.courseId = map['courseId']; 45 | // articleBean.desc = map['desc']; 46 | articleBean.desc = StringUtils.urlDecoder(map["desc"]); 47 | articleBean.envelopePic = map['envelopePic']; 48 | articleBean.fresh = map['fresh']; 49 | articleBean.id = map['id']; 50 | articleBean.link = map['link']; 51 | articleBean.niceDate = map['niceDate']; 52 | articleBean.origin = map['origin']; 53 | articleBean.originId = map['originId']; 54 | articleBean.prefix = map['prefix']; 55 | articleBean.projectLink = map['projectLink']; 56 | articleBean.publishTime = map['publishTime']; 57 | articleBean.superChapterId = map['superChapterId']; 58 | // articleBean.superChapterName = map['superChapterName']; 59 | articleBean.superChapterName = StringUtils.urlDecoder(map["superChapterName"]); 60 | articleBean.tags = List() 61 | ..addAll((map['tags'] as List ?? []).map((o) => TagsBean.fromMap(o))); 62 | articleBean.title = StringUtils.urlDecoder(map["title"]); 63 | articleBean.type = map['type']; 64 | articleBean.userId = map['userId']; 65 | articleBean.visible = map['visible']; 66 | articleBean.zan = map['zan']; 67 | return articleBean; 68 | } 69 | 70 | Map toJson() => { 71 | "apkLink": apkLink, 72 | "author": author, 73 | "shareUser": shareUser, 74 | "chapterId": chapterId, 75 | "chapterName": chapterName, 76 | "collect": collect, 77 | "courseId": courseId, 78 | "desc": desc, 79 | "envelopePic": envelopePic, 80 | "fresh": fresh, 81 | "id": id, 82 | "link": link, 83 | "niceDate": niceDate, 84 | "origin": origin, 85 | "originId": originId, 86 | "prefix": prefix, 87 | "projectLink": projectLink, 88 | "publishTime": publishTime, 89 | "superChapterId": superChapterId, 90 | "superChapterName": superChapterName, 91 | "tags": tags, 92 | "title": title, 93 | "type": type, 94 | "userId": userId, 95 | "visible": visible, 96 | "zan": zan, 97 | }; 98 | } 99 | 100 | class TagsBean { 101 | String name; 102 | String url; 103 | 104 | static TagsBean fromMap(Map map) { 105 | if (map == null) return null; 106 | TagsBean tagsBean = TagsBean(); 107 | tagsBean.name = map['name']; 108 | tagsBean.url = map['url']; 109 | return tagsBean; 110 | } 111 | 112 | Map toJson() => { 113 | "name": name, 114 | "url": url, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /lib/model/banner.dart: -------------------------------------------------------------------------------- 1 | class Banner { 2 | String desc; 3 | int id; 4 | String imagePath; 5 | int isVisible; 6 | int order; 7 | String title; 8 | int type; 9 | String url; 10 | 11 | Banner.fromJsonMap(Map map) 12 | : desc = map["desc"], 13 | id = map["id"], 14 | imagePath = map["imagePath"], 15 | isVisible = map["isVisible"], 16 | order = map["order"], 17 | title = map["title"], 18 | type = map["type"], 19 | url = map["url"]; 20 | 21 | Map toJson() { 22 | final Map data = new Map(); 23 | data['desc'] = desc; 24 | data['id'] = id; 25 | data['imagePath'] = imagePath; 26 | data['isVisible'] = isVisible; 27 | data['order'] = order; 28 | data['title'] = title; 29 | data['type'] = type; 30 | data['url'] = url; 31 | return data; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/model/coin_record.dart: -------------------------------------------------------------------------------- 1 | class CoinRecord { 2 | int coinCount; 3 | int date; 4 | String desc; 5 | int id; 6 | int type; 7 | int userId; 8 | String userName; 9 | 10 | static CoinRecord fromMap(Map map) { 11 | if (map == null) return null; 12 | CoinRecord coinRecordBean = CoinRecord(); 13 | coinRecordBean.coinCount = map['coinCount']; 14 | coinRecordBean.date = map['date']; 15 | coinRecordBean.desc = map['desc']; 16 | coinRecordBean.id = map['id']; 17 | coinRecordBean.type = map['type']; 18 | coinRecordBean.userId = map['userId']; 19 | coinRecordBean.userName = map['userName']; 20 | return coinRecordBean; 21 | } 22 | 23 | Map toJson() => { 24 | "coinCount": coinCount, 25 | "date": date, 26 | "desc": desc, 27 | "id": id, 28 | "type": type, 29 | "userId": userId, 30 | "userName": userName, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /lib/model/navigation_site.dart: -------------------------------------------------------------------------------- 1 | import 'article.dart'; 2 | 3 | class NavigationSite { 4 | 5 | List
articles; 6 | int cid; 7 | String name; 8 | 9 | static NavigationSite fromMap(Map map) { 10 | if (map == null) return null; 11 | NavigationSite naviBean = NavigationSite(); 12 | naviBean.articles = List() 13 | ..addAll((map['articles'] as List ?? []).map((o) => Article.fromMap(o))); 14 | naviBean.cid = map['cid']; 15 | naviBean.name = map['name']; 16 | return naviBean; 17 | } 18 | 19 | Map toJson() => { 20 | "articles": articles, 21 | "cid": cid, 22 | "name": name, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /lib/model/search.dart: -------------------------------------------------------------------------------- 1 | class SearchHotKey { 2 | int id; 3 | String link; 4 | String name; 5 | int order; 6 | int visible; 7 | 8 | static SearchHotKey fromMap(Map map) { 9 | if (map == null) return null; 10 | SearchHotKey searchBean = SearchHotKey(); 11 | searchBean.id = map['id']; 12 | searchBean.link = map['link']; 13 | searchBean.name = map['name']; 14 | searchBean.order = map['order']; 15 | searchBean.visible = map['visible']; 16 | return searchBean; 17 | } 18 | 19 | Map toJson() => { 20 | "id": id, 21 | "link": link, 22 | "name": name, 23 | "order": order, 24 | "visible": visible, 25 | }; 26 | } -------------------------------------------------------------------------------- /lib/model/tree.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/utils/string_utils.dart'; 2 | 3 | class Tree { 4 | List children; 5 | int courseId; 6 | int id; 7 | String name; 8 | int order; 9 | int parentChapterId; 10 | bool userControlSetTop; 11 | int visible; 12 | 13 | Tree.fromJsonMap(Map map) 14 | : children = 15 | List.from(map["children"].map((it) => Tree.fromJsonMap(it))), 16 | courseId = map["courseId"], 17 | id = map["id"], 18 | name = StringUtils.urlDecoder(map["name"]), 19 | order = map["order"], 20 | parentChapterId = map["parentChapterId"], 21 | userControlSetTop = map["userControlSetTop"], 22 | visible = map["visible"]; 23 | 24 | Map toJson() { 25 | final Map data = new Map(); 26 | data['children'] = 27 | children != null ? children.map((v) => v.toJson()).toList() : null; 28 | data['courseId'] = courseId; 29 | data['id'] = id; 30 | data['name'] = name; 31 | data['order'] = order; 32 | data['parentChapterId'] = parentChapterId; 33 | data['userControlSetTop'] = userControlSetTop; 34 | data['visible'] = visible; 35 | return data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/model/user.dart: -------------------------------------------------------------------------------- 1 | 2 | class User { 3 | 4 | bool admin; 5 | List chapterTops; 6 | List collectIds; 7 | String email; 8 | String icon; 9 | int id; 10 | String nickname; 11 | String password; 12 | String token; 13 | int type; 14 | String username; 15 | 16 | User.fromJsonMap(Map map): 17 | admin = map["admin"], 18 | chapterTops = map["chapterTops"], 19 | collectIds = map["collectIds"], 20 | email = map["email"], 21 | icon = map["icon"], 22 | id = map["id"], 23 | nickname = map["nickname"], 24 | password = map["password"], 25 | token = map["token"], 26 | type = map["type"], 27 | username = map["username"]; 28 | 29 | Map toJson() { 30 | final Map data = new Map(); 31 | data['admin'] = admin; 32 | data['chapterTops'] = chapterTops; 33 | data['collectIds'] = collectIds; 34 | data['email'] = email; 35 | data['icon'] = icon; 36 | data['id'] = id; 37 | data['nickname'] = nickname; 38 | data['password'] = password; 39 | data['token'] = token; 40 | data['type'] = type; 41 | data['username'] = username; 42 | return data; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/provider/provider_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | /// Provider封装类 5 | /// 6 | /// 方便数据初始化 7 | class ProviderWidget extends StatefulWidget { 8 | final ValueWidgetBuilder builder; 9 | final T model; 10 | final Widget child; 11 | final Function(T model) onModelReady; 12 | final bool autoDispose; 13 | 14 | ProviderWidget({ 15 | Key key, 16 | @required this.builder, 17 | @required this.model, 18 | this.child, 19 | this.onModelReady, 20 | this.autoDispose: true, 21 | }) : super(key: key); 22 | 23 | _ProviderWidgetState createState() => _ProviderWidgetState(); 24 | } 25 | 26 | class _ProviderWidgetState 27 | extends State> { 28 | T model; 29 | 30 | @override 31 | void initState() { 32 | model = widget.model; 33 | widget.onModelReady?.call(model); 34 | super.initState(); 35 | } 36 | 37 | @override 38 | void dispose() { 39 | if (widget.autoDispose) model.dispose(); 40 | super.dispose(); 41 | } 42 | 43 | @override 44 | Widget build(BuildContext context) { 45 | return ChangeNotifierProvider.value( 46 | value: model, 47 | child: Consumer( 48 | builder: widget.builder, 49 | child: widget.child, 50 | ), 51 | ); 52 | } 53 | } 54 | 55 | class ProviderWidget2 56 | extends StatefulWidget { 57 | final Widget Function(BuildContext context, A model1, B model2, Widget child) 58 | builder; 59 | final A model1; 60 | final B model2; 61 | final Widget child; 62 | final Function(A model1, B model2) onModelReady; 63 | final bool autoDispose; 64 | 65 | ProviderWidget2({ 66 | Key key, 67 | @required this.builder, 68 | @required this.model1, 69 | @required this.model2, 70 | this.child, 71 | this.onModelReady, 72 | this.autoDispose, 73 | }) : super(key: key); 74 | 75 | _ProviderWidgetState2 createState() => _ProviderWidgetState2(); 76 | } 77 | 78 | class _ProviderWidgetState2 79 | extends State> { 80 | A model1; 81 | B model2; 82 | 83 | @override 84 | void initState() { 85 | model1 = widget.model1; 86 | model2 = widget.model2; 87 | widget.onModelReady?.call(model1, model2); 88 | super.initState(); 89 | } 90 | 91 | @override 92 | void dispose() { 93 | if (widget.autoDispose) { 94 | model1.dispose(); 95 | model2.dispose(); 96 | } 97 | super.dispose(); 98 | } 99 | 100 | @override 101 | Widget build(BuildContext context) { 102 | return MultiProvider( 103 | providers: [ 104 | ChangeNotifierProvider.value(value: model1), 105 | ChangeNotifierProvider.value(value: model2), 106 | ], 107 | child: Consumer2( 108 | builder: widget.builder, 109 | child: widget.child, 110 | )); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /lib/provider/provider_widget_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/generated/l10n.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | /// Provider封装类 6 | /// 7 | /// 方便数据初始化 8 | class ProviderWidget extends StatefulWidget { 9 | final ValueWidgetBuilder builder; 10 | final S Function(BuildContext, T) selector; 11 | final T model; 12 | final Widget child; 13 | final Function(T model) onModelReady; 14 | final bool autoDispose; 15 | 16 | ProviderWidget({ 17 | Key key, 18 | @required this.builder, 19 | @required this.model, 20 | this.selector, 21 | this.child, 22 | this.onModelReady, 23 | this.autoDispose, 24 | }) : super(key: key); 25 | 26 | _ProviderWidgetState createState() => _ProviderWidgetState(); 27 | } 28 | 29 | class _ProviderWidgetState 30 | extends State> { 31 | T model; 32 | 33 | @override 34 | void initState() { 35 | model = widget.model; 36 | widget.onModelReady?.call(model); 37 | super.initState(); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | if (widget.autoDispose) model.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return ChangeNotifierProvider.value( 49 | value: model, 50 | child: Selector( 51 | selector: widget.selector, 52 | builder: widget.builder, 53 | child: widget.child, 54 | ), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/provider/view_state.dart: -------------------------------------------------------------------------------- 1 | 2 | /// 页面状态类型 3 | enum ViewState { 4 | idle, 5 | busy, //加载中 6 | empty, //无数据 7 | error, //加载失败 8 | } 9 | 10 | /// 错误类型 11 | enum ViewStateErrorType { 12 | defaultError, 13 | networkTimeOutError, //网络错误 14 | unauthorizedError //为授权(一般为未登录) 15 | } 16 | 17 | class ViewStateError { 18 | ViewStateErrorType _errorType; 19 | String message; 20 | String errorMessage; 21 | 22 | ViewStateError(this._errorType, {this.message, this.errorMessage}) { 23 | _errorType ??= ViewStateErrorType.defaultError; 24 | message ??= errorMessage; 25 | } 26 | 27 | ViewStateErrorType get errorType => _errorType; 28 | 29 | /// 以下变量是为了代码书写方便,加入的get方法.严格意义上讲,并不严谨 30 | get isDefaultError => _errorType == ViewStateErrorType.defaultError; 31 | get isNetworkTimeOut => _errorType == ViewStateErrorType.networkTimeOutError; 32 | get isUnauthorized => _errorType == ViewStateErrorType.unauthorizedError; 33 | 34 | @override 35 | String toString() { 36 | return 'ViewStateError{errorType: $_errorType, message: $message, errorMessage: $errorMessage}'; 37 | } 38 | } 39 | 40 | //enum ConnectivityStatus { WiFi, Cellular, Offline } 41 | -------------------------------------------------------------------------------- /lib/provider/view_state_list_model.dart: -------------------------------------------------------------------------------- 1 | import 'view_state_model.dart'; 2 | 3 | /// 基于 4 | abstract class ViewStateListModel extends ViewStateModel { 5 | /// 页面数据 6 | List list = []; 7 | 8 | /// 第一次进入页面loading skeleton 9 | initData() async { 10 | setBusy(); 11 | await refresh(init: true); 12 | } 13 | 14 | // 下拉刷新 15 | refresh({bool init = false}) async { 16 | try { 17 | List data = await loadData(); 18 | if (data.isEmpty) { 19 | list.clear(); 20 | setEmpty(); 21 | } else { 22 | onCompleted(data); 23 | list.clear(); 24 | list.addAll(data); 25 | setIdle(); 26 | } 27 | } catch (e, s) { 28 | if (init) list.clear(); 29 | setError(e, s); 30 | } 31 | } 32 | 33 | // 加载数据 34 | Future> loadData(); 35 | 36 | onCompleted(List data) {} 37 | } 38 | -------------------------------------------------------------------------------- /lib/provider/view_state_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fun_android/config/net/api.dart'; 6 | import 'package:fun_android/generated/l10n.dart'; 7 | import 'package:oktoast/oktoast.dart'; 8 | 9 | import 'view_state.dart'; 10 | 11 | export 'view_state.dart'; 12 | 13 | class ViewStateModel with ChangeNotifier { 14 | /// 防止页面销毁后,异步任务才完成,导致报错 15 | bool _disposed = false; 16 | 17 | /// 当前的页面状态,默认为busy,可在viewModel的构造方法中指定; 18 | ViewState _viewState; 19 | 20 | /// 根据状态构造 21 | /// 22 | /// 子类可以在构造函数指定需要的页面状态 23 | /// FooModel():super(viewState:ViewState.busy); 24 | ViewStateModel({ViewState viewState}) 25 | : _viewState = viewState ?? ViewState.idle { 26 | // debugPrint('ViewStateModel---constructor--->$runtimeType'); 27 | } 28 | 29 | /// ViewState 30 | ViewState get viewState => _viewState; 31 | 32 | set viewState(ViewState viewState) { 33 | _viewStateError = null; 34 | _viewState = viewState; 35 | notifyListeners(); 36 | } 37 | 38 | /// ViewStateError 39 | ViewStateError _viewStateError; 40 | 41 | ViewStateError get viewStateError => _viewStateError; 42 | 43 | /// 以下变量是为了代码书写方便,加入的get方法.严格意义上讲,并不严谨 44 | /// 45 | /// get 46 | bool get isBusy => viewState == ViewState.busy; 47 | 48 | bool get isIdle => viewState == ViewState.idle; 49 | 50 | bool get isEmpty => viewState == ViewState.empty; 51 | 52 | bool get isError => viewState == ViewState.error; 53 | 54 | /// set 55 | void setIdle() { 56 | viewState = ViewState.idle; 57 | } 58 | 59 | void setBusy() { 60 | viewState = ViewState.busy; 61 | } 62 | 63 | void setEmpty() { 64 | viewState = ViewState.empty; 65 | } 66 | 67 | /// [e]分类Error和Exception两种 68 | void setError(e, stackTrace, {String message}) { 69 | ViewStateErrorType errorType = ViewStateErrorType.defaultError; 70 | 71 | /// 见https://github.com/flutterchina/dio/blob/master/README-ZH.md#dioerrortype 72 | if (e is DioError) { 73 | if (e.type == DioErrorType.CONNECT_TIMEOUT || 74 | e.type == DioErrorType.SEND_TIMEOUT || 75 | e.type == DioErrorType.RECEIVE_TIMEOUT) { 76 | // timeout 77 | errorType = ViewStateErrorType.networkTimeOutError; 78 | message = e.error; 79 | } else if (e.type == DioErrorType.RESPONSE) { 80 | // incorrect status, such as 404, 503... 81 | message = e.error; 82 | } else if (e.type == DioErrorType.CANCEL) { 83 | // to be continue... 84 | message = e.error; 85 | } else { 86 | // dio将原error重新套了一层 87 | e = e.error; 88 | if (e is UnAuthorizedException) { 89 | stackTrace = null; 90 | errorType = ViewStateErrorType.unauthorizedError; 91 | } else if (e is NotSuccessException) { 92 | stackTrace = null; 93 | message = e.message; 94 | } else if (e is SocketException) { 95 | errorType = ViewStateErrorType.networkTimeOutError; 96 | message = e.message; 97 | } else { 98 | message = e.message; 99 | } 100 | } 101 | } 102 | viewState = ViewState.error; 103 | _viewStateError = ViewStateError( 104 | errorType, 105 | message: message, 106 | errorMessage: e.toString(), 107 | ); 108 | printErrorStack(e, stackTrace); 109 | onError(viewStateError); 110 | } 111 | 112 | void onError(ViewStateError viewStateError) {} 113 | 114 | /// 显示错误消息 115 | showErrorMessage(context, {String message}) { 116 | if (viewStateError != null || message != null) { 117 | if (viewStateError.isNetworkTimeOut) { 118 | message ??= S.of(context).viewStateMessageNetworkError; 119 | } else { 120 | message ??= viewStateError.message; 121 | } 122 | Future.microtask(() { 123 | showToast(message, context: context); 124 | }); 125 | } 126 | } 127 | 128 | @override 129 | String toString() { 130 | return 'BaseModel{_viewState: $viewState, _viewStateError: $_viewStateError}'; 131 | } 132 | 133 | @override 134 | void notifyListeners() { 135 | if (!_disposed) { 136 | super.notifyListeners(); 137 | } 138 | } 139 | 140 | @override 141 | void dispose() { 142 | _disposed = true; 143 | // debugPrint('view_state_model dispose -->$runtimeType'); 144 | super.dispose(); 145 | } 146 | } 147 | 148 | /// [e]为错误类型 :可能为 Error , Exception ,String 149 | /// [s]为堆栈信息 150 | printErrorStack(e, s) { 151 | debugPrint(''' 152 | <-----↓↓↓↓↓↓↓↓↓↓-----error-----↓↓↓↓↓↓↓↓↓↓-----> 153 | $e 154 | <-----↑↑↑↑↑↑↑↑↑↑-----error-----↑↑↑↑↑↑↑↑↑↑----->'''); 155 | if (s != null) debugPrint(''' 156 | <-----↓↓↓↓↓↓↓↓↓↓-----trace-----↓↓↓↓↓↓↓↓↓↓-----> 157 | $s 158 | <-----↑↑↑↑↑↑↑↑↑↑-----trace-----↑↑↑↑↑↑↑↑↑↑-----> 159 | '''); 160 | } 161 | -------------------------------------------------------------------------------- /lib/provider/view_state_refresh_list_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:fun_android/generated/l10n.dart'; 4 | import 'package:oktoast/oktoast.dart'; 5 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 6 | 7 | import 'view_state_list_model.dart'; 8 | 9 | /// 基于 10 | abstract class ViewStateRefreshListModel extends ViewStateListModel { 11 | /// 分页第一页页码 12 | static const int pageNumFirst = 0; 13 | 14 | /// 分页条目数量 15 | static const int pageSize = 20; 16 | 17 | RefreshController _refreshController = 18 | RefreshController(initialRefresh: false); 19 | 20 | RefreshController get refreshController => _refreshController; 21 | 22 | /// 当前页码 23 | int _currentPageNum = pageNumFirst; 24 | 25 | /// 下拉刷新 26 | /// 27 | /// [init] 是否是第一次加载 28 | /// true: Error时,需要跳转页面 29 | /// false: Error时,不需要跳转页面,直接给出提示 30 | Future> refresh({bool init = false}) async { 31 | try { 32 | _currentPageNum = pageNumFirst; 33 | var data = await loadData(pageNum: pageNumFirst); 34 | if (data.isEmpty) { 35 | refreshController.refreshCompleted(resetFooterState: true); 36 | list.clear(); 37 | setEmpty(); 38 | } else { 39 | onCompleted(data); 40 | list.clear(); 41 | list.addAll(data); 42 | refreshController.refreshCompleted(); 43 | // 小于分页的数量,禁止上拉加载更多 44 | if (data.length < pageSize) { 45 | refreshController.loadNoData(); 46 | } else { 47 | //防止上次上拉加载更多失败,需要重置状态 48 | refreshController.loadComplete(); 49 | } 50 | setIdle(); 51 | } 52 | return data; 53 | } catch (e, s) { 54 | /// 页面已经加载了数据,如果刷新报错,不应该直接跳转错误页面 55 | /// 而是显示之前的页面数据.给出错误提示 56 | if (init) list.clear(); 57 | refreshController.refreshFailed(); 58 | setError(e, s); 59 | return null; 60 | } 61 | } 62 | 63 | /// 上拉加载更多 64 | Future> loadMore() async { 65 | try { 66 | var data = await loadData(pageNum: ++_currentPageNum); 67 | if (data.isEmpty) { 68 | _currentPageNum--; 69 | refreshController.loadNoData(); 70 | } else { 71 | onCompleted(data); 72 | list.addAll(data); 73 | if (data.length < pageSize) { 74 | refreshController.loadNoData(); 75 | } else { 76 | refreshController.loadComplete(); 77 | } 78 | notifyListeners(); 79 | } 80 | return data; 81 | } catch (e, s) { 82 | _currentPageNum--; 83 | refreshController.loadFailed(); 84 | debugPrint('error--->\n' + e.toString()); 85 | debugPrint('statck--->\n' + s.toString()); 86 | return null; 87 | } 88 | } 89 | 90 | // 加载数据 91 | Future> loadData({int pageNum}); 92 | 93 | @override 94 | void dispose() { 95 | _refreshController.dispose(); 96 | super.dispose(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/service/app_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:fun_android/config/net/pgyer_api.dart'; 3 | 4 | /// App相关接口 5 | class AppRepository { 6 | static Future checkUpdate(String platform, String version) async { 7 | debugPrint('检查更新,当前版本为===>$version'); 8 | var response = await http.post('app/check', queryParameters: { 9 | 'buildVersion': version 10 | }); 11 | var result = AppUpdateInfo.fromMap(response.data); 12 | if(result.buildHaveNewVersion){ 13 | debugPrint('发现新版本===>${result.buildVersion}'); 14 | return result; 15 | } 16 | debugPrint('没有发现新版本'); 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/service/wan_android_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/config/net/wan_android_api.dart'; 2 | import 'package:fun_android/model/article.dart'; 3 | import 'package:fun_android/model/banner.dart'; 4 | import 'package:fun_android/model/coin_record.dart'; 5 | import 'package:fun_android/model/search.dart'; 6 | import 'package:fun_android/model/navigation_site.dart'; 7 | import 'package:fun_android/model/tree.dart'; 8 | import 'package:fun_android/model/user.dart'; 9 | 10 | class WanAndroidRepository { 11 | // 轮播 12 | static Future fetchBanners() async { 13 | var response = await http.get('banner/json'); 14 | return response.data 15 | .map((item) => Banner.fromJsonMap(item)) 16 | .toList(); 17 | } 18 | 19 | // 置顶文章 20 | static Future fetchTopArticles() async { 21 | var response = await http.get('article/top/json'); 22 | return response.data.map
((item) => Article.fromMap(item)).toList(); 23 | } 24 | 25 | // 文章 26 | static Future fetchArticles(int pageNum, {int cid}) async { 27 | await Future.delayed(Duration(seconds: 1)); //增加动效 28 | var response = await http.get('article/list/$pageNum/json', 29 | queryParameters: (cid != null ? {'cid': cid} : null)); 30 | return response.data['datas'] 31 | .map
((item) => Article.fromMap(item)) 32 | .toList(); 33 | } 34 | 35 | // 项目分类 36 | static Future fetchTreeCategories() async { 37 | var response = await http.get('tree/json'); 38 | return response.data.map((item) => Tree.fromJsonMap(item)).toList(); 39 | } 40 | 41 | // 体系分类 42 | static Future fetchProjectCategories() async { 43 | var response = await http.get('project/tree/json'); 44 | return response.data.map((item) => Tree.fromJsonMap(item)).toList(); 45 | } 46 | 47 | // 导航 48 | static Future fetchNavigationSite() async { 49 | var response = await http.get('navi/json'); 50 | return response.data 51 | .map((item) => NavigationSite.fromMap(item)) 52 | .toList(); 53 | } 54 | 55 | // 公众号分类 56 | static Future fetchWechatAccounts() async { 57 | var response = await http.get('wxarticle/chapters/json'); 58 | return response.data.map((item) => Tree.fromJsonMap(item)).toList(); 59 | } 60 | 61 | // 公众号文章 62 | static Future fetchWechatAccountArticles(int pageNum, int id) async { 63 | var response = await http.get('wxarticle/list/$id/$pageNum/json'); 64 | return response.data['datas'] 65 | .map
((item) => Article.fromMap(item)) 66 | .toList(); 67 | } 68 | 69 | // 搜索热门记录 70 | static Future fetchSearchHotKey() async { 71 | var response = await http.get('hotkey/json'); 72 | return response.data 73 | .map((item) => SearchHotKey.fromMap(item)) 74 | .toList(); 75 | } 76 | 77 | // 搜索结果 78 | static Future fetchSearchResult({key = "", int pageNum = 0}) async { 79 | var response = 80 | await http.post('article/query/$pageNum/json', queryParameters: { 81 | 'k': key, 82 | }); 83 | return response.data['datas'] 84 | .map
((item) => Article.fromMap(item)) 85 | .toList(); 86 | } 87 | 88 | /// 登录 89 | /// [Http._init] 添加了拦截器 设置了自动cookie. 90 | static Future login(String username, String password) async { 91 | var response = await http.post('user/login', queryParameters: { 92 | 'username': username, 93 | 'password': password, 94 | }); 95 | return User.fromJsonMap(response.data); 96 | } 97 | 98 | /// 注册 99 | static Future register( 100 | String username, String password, String rePassword) async { 101 | var response = await http.post('user/register', queryParameters: { 102 | 'username': username, 103 | 'password': password, 104 | 'repassword': rePassword, 105 | }); 106 | return User.fromJsonMap(response.data); 107 | } 108 | 109 | /// 登出 110 | static logout() async { 111 | /// 自动移除cookie 112 | await http.get('user/logout/json'); 113 | } 114 | 115 | static testLoginState() async { 116 | await http.get('lg/todo/listnotdo/0/json/1'); 117 | } 118 | 119 | // 收藏列表 120 | static Future fetchCollectList(int pageNum) async { 121 | var response = await http.get('lg/collect/list/$pageNum/json'); 122 | return response.data['datas'] 123 | .map
((item) => Article.fromMap(item)) 124 | .toList(); 125 | } 126 | 127 | // 收藏 128 | static collect(id) async { 129 | await http.post('lg/collect/$id/json'); 130 | } 131 | 132 | // 取消收藏 133 | static unCollect(id) async { 134 | await http.post('lg/uncollect_originId/$id/json'); 135 | } 136 | 137 | // 取消收藏2 138 | static unMyCollect({id, originId}) async { 139 | await http.post('lg/uncollect/$id/json', 140 | queryParameters: {'originId': originId ?? -1}); 141 | } 142 | 143 | // 个人积分 144 | static Future fetchCoin() async { 145 | var response = await http.get('lg/coin/getcount/json'); 146 | return response.data; 147 | } 148 | 149 | // 我的积分记录 150 | static Future fetchCoinRecordList(int pageNum) async { 151 | var response = await http.get('lg/coin/list/$pageNum/json'); 152 | return response.data['datas'] 153 | .map((item) => CoinRecord.fromMap(item)) 154 | .toList(); 155 | } 156 | 157 | // 积分排行榜 158 | /// { 159 | /// "coinCount": 448, 160 | /// "username": "S**24n" 161 | /// }, 162 | static Future fetchRankingList(int pageNum) async { 163 | var response = await http.get('coin/rank/$pageNum/json'); 164 | return response.data['datas']; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/ui/helper/dialog_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fun_android/generated/l10n.dart'; 4 | 5 | class DialogHelper { 6 | static showLoginDialog(context) async { 7 | return await showCupertinoDialog( 8 | context: context, 9 | builder: (context) => CupertinoAlertDialog( 10 | title: Text(S.of(context).needLogin), 11 | actions: [ 12 | CupertinoDialogAction( 13 | onPressed: () { 14 | Navigator.of(context).pop(false); 15 | }, 16 | child: Text( 17 | S.of(context).actionCancel, 18 | ), 19 | ), 20 | CupertinoDialogAction( 21 | onPressed: () async { 22 | Navigator.of(context).pop(true); 23 | }, 24 | child: Text(S.of(context).actionConfirm), 25 | ), 26 | ], 27 | )); 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/ui/helper/favourite_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:fun_android/config/router_manger.dart'; 3 | import 'package:fun_android/model/article.dart'; 4 | import 'package:fun_android/ui/widget/favourite_animation.dart'; 5 | import 'package:fun_android/view_model/favourite_model.dart'; 6 | import 'package:fun_android/view_model/user_model.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | import 'dialog_helper.dart'; 10 | 11 | /// 收藏文章. 12 | /// 如果用户未登录,需要跳转到登录界面 13 | /// 如果执行失败,需要给与提示 14 | /// 15 | /// 由于存在递归操作,所以抽取为方法,而且多处调用 16 | /// 多个页面使用该方法,目前这种方式并不优雅,抽取位置有待商榷 17 | /// 18 | /// 19 | addFavourites(BuildContext context, 20 | {Article article, 21 | FavouriteModel model, 22 | Object tag: 'addFavourite', 23 | bool playAnim: true}) async { 24 | await model.collect(article); 25 | if (model.isError) { 26 | if (model.viewStateError.isUnauthorized) { 27 | if (await DialogHelper.showLoginDialog(context)) { 28 | var success = await Navigator.pushNamed(context, RouteName.login); 29 | if (success ?? false) { 30 | //登录后,判断是否已经收藏 31 | if (!Provider.of(context, listen: false) 32 | .user 33 | .collectIds 34 | .contains(article.id)) { 35 | addFavourites(context, article: article, model: model, tag: tag); 36 | } 37 | } 38 | } 39 | } else { 40 | model.showErrorMessage(context); 41 | } 42 | } else { 43 | if (playAnim) { 44 | ///接口调用成功播放动画 45 | Navigator.push( 46 | context, 47 | HeroDialogRoute( 48 | builder: (_) => FavouriteAnimationWidget( 49 | tag: tag, 50 | add: article.collect, 51 | ))); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/ui/helper/refresh_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fun_android/generated/l10n.dart'; 4 | import 'package:fun_android/ui/page/tab/home_second_floor_page.dart'; 5 | import 'package:fun_android/ui/widget/activity_indicator.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | 8 | /// 首页列表的header 9 | class HomeRefreshHeader extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | var strings = RefreshLocalizations.of(context)?.currentLocalization ?? 13 | EnRefreshString(); 14 | return ClassicHeader( 15 | canTwoLevelText: S.of(context).refreshTwoLevel, 16 | textStyle: TextStyle(color: Colors.white), 17 | outerBuilder: (child) => HomeSecondFloorOuter(child), 18 | twoLevelView: Container(), 19 | height: 70 + MediaQuery.of(context).padding.top / 3, 20 | refreshingIcon: ActivityIndicator(brightness: Brightness.dark), 21 | releaseText: strings.canRefreshText, 22 | ); 23 | } 24 | } 25 | 26 | /// 通用的footer 27 | /// 28 | /// 由于国际化需要context的原因,所以无法在[RefreshConfiguration]配置 29 | class RefresherFooter extends StatelessWidget { 30 | @override 31 | Widget build(BuildContext context) { 32 | return ClassicFooter( 33 | // failedText: S.of(context).loadMoreFailed, 34 | // idleText: S.of(context).loadMoreIdle, 35 | // loadingText: S.of(context).loadMoreLoading, 36 | // noDataText: S.of(context).loadMoreNoData, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/ui/helper/theme_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ThemeHelper { 4 | static InputDecorationTheme inputDecorationTheme(ThemeData theme) { 5 | 6 | var primaryColor = theme.primaryColor; 7 | var dividerColor = theme.dividerColor; 8 | var errorColor = theme.errorColor; 9 | var disabledColor = theme.disabledColor; 10 | 11 | var width = 0.5; 12 | 13 | return InputDecorationTheme( 14 | hintStyle: TextStyle(fontSize: 14), 15 | errorBorder: UnderlineInputBorder( 16 | borderSide: BorderSide(width: width, color: errorColor)), 17 | focusedErrorBorder: UnderlineInputBorder( 18 | borderSide: BorderSide(width: 0.7, color: errorColor)), 19 | focusedBorder: UnderlineInputBorder( 20 | borderSide: BorderSide(width: width, color: primaryColor)), 21 | enabledBorder: UnderlineInputBorder( 22 | borderSide: BorderSide(width: width, color: dividerColor)), 23 | border: UnderlineInputBorder( 24 | borderSide: BorderSide(width: width, color: dividerColor)), 25 | disabledBorder: UnderlineInputBorder( 26 | borderSide: BorderSide(width: width, color: disabledColor)), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/ui/page/article/article_detail_plugin_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 6 | import 'package:fun_android/provider/provider_widget.dart'; 7 | import 'package:fun_android/ui/helper/favourite_helper.dart'; 8 | import 'package:fun_android/model/article.dart'; 9 | import 'package:fun_android/view_model/favourite_model.dart'; 10 | import 'package:fun_android/view_model/user_model.dart'; 11 | import 'package:provider/provider.dart'; 12 | import 'package:share/share.dart'; 13 | import 'package:url_launcher/url_launcher.dart'; 14 | 15 | import 'article_detail_page.dart'; 16 | 17 | class ArticleDetailPluginPage extends StatefulWidget { 18 | final Article article; 19 | 20 | ArticleDetailPluginPage({this.article}); 21 | 22 | @override 23 | _WebViewState createState() => _WebViewState(); 24 | } 25 | 26 | class _WebViewState extends State { 27 | final flutterWebViewPlugin = FlutterWebviewPlugin(); 28 | Completer _finishedCompleter = Completer(); 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | flutterWebViewPlugin.onStateChanged.listen((WebViewStateChanged state) { 34 | debugPrint('onStateChanged: ${state.type} ${state.url}'); 35 | if (!_finishedCompleter.isCompleted && 36 | state.type == WebViewState.finishLoad) { 37 | _finishedCompleter.complete(true); 38 | } 39 | }); 40 | } 41 | 42 | @override 43 | void dispose() { 44 | flutterWebViewPlugin.dispose(); 45 | super.dispose(); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return WebviewScaffold( 51 | url: widget.article.link, 52 | withJavascript: true, 53 | displayZoomControls: true, 54 | withZoom: true, 55 | appBar: AppBar( 56 | title: WebViewTitle( 57 | title: widget.article.title, 58 | future: _finishedCompleter.future, 59 | ), 60 | actions: [ 61 | IconButton( 62 | // tooltip: '用浏览器打开', 63 | icon: Icon(Icons.language), 64 | onPressed: () { 65 | launch(widget.article.link, forceSafariVC: false); 66 | }, 67 | ), 68 | IconButton( 69 | // tooltip: '分享', 70 | icon: Icon(Icons.share), 71 | onPressed: () { 72 | Share.share(widget.article.link, subject: widget.article.title); 73 | }, 74 | ), 75 | ], 76 | ), 77 | bottomNavigationBar: IconTheme( 78 | data: Theme.of(context).iconTheme.copyWith(opacity: 0.5), 79 | child: BottomAppBar( 80 | child: Row( 81 | mainAxisAlignment: MainAxisAlignment.spaceAround, 82 | children: [ 83 | IconButton( 84 | icon: const Icon(Icons.arrow_back_ios), 85 | onPressed: flutterWebViewPlugin.goBack, 86 | ), 87 | IconButton( 88 | icon: const Icon(Icons.arrow_forward_ios), 89 | onPressed: flutterWebViewPlugin.goForward, 90 | ), 91 | IconButton( 92 | icon: const Icon(Icons.autorenew), 93 | onPressed: flutterWebViewPlugin.reload, 94 | ), 95 | ProviderWidget( 96 | model: FavouriteModel( 97 | globalFavouriteModel: Provider.of(context, listen: false)), 98 | builder: (context, model, child) => IconButton( 99 | icon: 100 | Provider.of(context, listen: false).hasUser && 101 | widget.article.collect ?? 102 | true 103 | ? Icon(Icons.favorite, color: Colors.redAccent[100]) 104 | : Icon(Icons.favorite_border), 105 | onPressed: () async { 106 | await addFavourites(context, 107 | article: widget.article, model: model, playAnim: false); 108 | }, 109 | ), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/ui/page/article/article_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:fun_android/ui/helper/refresh_helper.dart'; 4 | import 'package:fun_android/ui/widget/article_skeleton.dart'; 5 | import 'package:fun_android/ui/widget/skeleton.dart'; 6 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 7 | import 'package:fun_android/model/article.dart'; 8 | import 'package:fun_android/model/tree.dart'; 9 | import 'package:fun_android/provider/provider_widget.dart'; 10 | import 'package:fun_android/provider/view_state_widget.dart'; 11 | import 'package:fun_android/ui/widget/article_list_Item.dart'; 12 | import 'package:fun_android/view_model/structure_model.dart'; 13 | 14 | /// 文章列表页面 15 | class ArticleListPage extends StatefulWidget { 16 | /// 目录id 17 | final int cid; 18 | 19 | ArticleListPage(this.cid); 20 | 21 | @override 22 | _ArticleListPageState createState() => _ArticleListPageState(); 23 | } 24 | 25 | class _ArticleListPageState extends State 26 | with AutomaticKeepAliveClientMixin { 27 | @override 28 | bool get wantKeepAlive => true; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | super.build(context); 33 | return ProviderWidget( 34 | model: StructureListModel(widget.cid), 35 | onModelReady: (model) => model.initData(), 36 | builder: (context, model, child) { 37 | if (model.isBusy) { 38 | return SkeletonList( 39 | builder: (context, index) => ArticleSkeletonItem(), 40 | ); 41 | } else if (model.isError && model.list.isEmpty) { 42 | return ViewStateErrorWidget( 43 | error: model.viewStateError, onPressed: model.initData); 44 | } else if (model.isEmpty) { 45 | return ViewStateEmptyWidget(onPressed: model.initData); 46 | } 47 | return SmartRefresher( 48 | controller: model.refreshController, 49 | header: WaterDropHeader(), 50 | footer: RefresherFooter(), 51 | onRefresh: model.refresh, 52 | onLoading: model.loadMore, 53 | enablePullUp: true, 54 | child: ListView.builder( 55 | itemCount: model.list.length, 56 | itemBuilder: (context, index) { 57 | Article item = model.list[index]; 58 | return ArticleItemWidget(item); 59 | })); 60 | }, 61 | ); 62 | } 63 | } 64 | 65 | /// 体系--> 选择相关知识点的详情页 66 | class ArticleCategoryTabPage extends StatelessWidget { 67 | final Tree tree; 68 | final int index; 69 | 70 | ArticleCategoryTabPage(this.tree, this.index); 71 | 72 | @override 73 | Widget build(BuildContext context) { 74 | return DefaultTabController( 75 | length: tree.children.length, 76 | initialIndex: index, 77 | child: Scaffold( 78 | appBar: AppBar( 79 | centerTitle: true, 80 | title: Text(tree.name), 81 | bottom: TabBar( 82 | isScrollable: true, 83 | tabs: List.generate( 84 | tree.children.length, 85 | (index) => Tab( 86 | text: tree.children[index].name, 87 | ))), 88 | ), 89 | body: TabBarView( 90 | children: List.generate(tree.children.length, 91 | (index) => ArticleListPage(tree.children[index].id)), 92 | )), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/ui/page/change_log_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_markdown/flutter_markdown.dart'; 7 | import 'package:fun_android/generated/l10n.dart'; 8 | import 'package:fun_android/provider/view_state_widget.dart'; 9 | import 'package:fun_android/ui/widget/app_update.dart'; 10 | import 'package:fun_android/utils/platform_utils.dart'; 11 | import 'package:package_info/package_info.dart'; 12 | 13 | class ChangeLogPage extends StatefulWidget { 14 | @override 15 | _ChangeLogPageState createState() => _ChangeLogPageState(); 16 | } 17 | 18 | class _ChangeLogPageState extends State { 19 | ValueNotifier versionNotifier; 20 | 21 | @override 22 | void initState() { 23 | versionNotifier = ValueNotifier(''); 24 | PackageInfo.fromPlatform().then((packageInfo) { 25 | versionNotifier.value = 26 | '${packageInfo.version}(${packageInfo.buildNumber})'; 27 | }); 28 | super.initState(); 29 | } 30 | 31 | Widget build(BuildContext context) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: ValueListenableBuilder( 35 | valueListenable: versionNotifier, 36 | builder: (ctx, value, child) => 37 | Text(S.of(context).appUpdateCheckUpdate + ' v$value')), 38 | ), 39 | body: SafeArea( 40 | child: Stack(children: [ 41 | Padding( 42 | padding: const EdgeInsets.only(bottom: 75), 43 | child: ChangeLogView(), 44 | ), 45 | Positioned( 46 | left: 30, 47 | right: 30, 48 | bottom: 8, 49 | child: Platform.isIOS 50 | ? CupertinoButton( 51 | color: Theme.of(context).accentColor, 52 | child: Text(S.of(context).close), 53 | onPressed: () { 54 | Navigator.pop(context); 55 | }) 56 | : AppUpdateButton(), 57 | ) 58 | ]), 59 | ), 60 | ); 61 | } 62 | } 63 | 64 | class ChangeLogView extends StatefulWidget { 65 | @override 66 | _ChangeLogViewState createState() => _ChangeLogViewState(); 67 | } 68 | 69 | class _ChangeLogViewState extends State { 70 | String _changelog; 71 | 72 | @override 73 | void initState() { 74 | rootBundle.loadString("CHANGELOG.md").then((data) { 75 | setState(() { 76 | _changelog = data; 77 | }); 78 | }); 79 | super.initState(); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | if (_changelog == null) { 85 | return ViewStateBusyWidget(); 86 | } 87 | return Markdown(data: _changelog); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/ui/page/coin/coin_ranking_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/provider/provider_widget.dart'; 3 | import 'package:fun_android/provider/view_state_widget.dart'; 4 | import 'package:fun_android/ui/helper/refresh_helper.dart'; 5 | import 'package:fun_android/ui/widget/skeleton.dart'; 6 | import 'package:fun_android/view_model/coin_model.dart'; 7 | import 'package:fun_android/view_model/user_model.dart'; 8 | import 'package:provider/provider.dart'; 9 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 10 | 11 | ///积分排行榜 12 | class CoinRankingListPage extends StatelessWidget { 13 | @override 14 | Widget build(BuildContext context) { 15 | UserModel userModel = Provider.of(context); 16 | String selfName = userModel.user.username.replaceRange(1, 3, '**'); 17 | return Scaffold( 18 | appBar: AppBar( 19 | centerTitle: true, 20 | title: Text('积分排行榜'), 21 | ), 22 | body: ProviderWidget( 23 | model: CoinRankingListModel(), 24 | onModelReady: (model) => model.initData(), 25 | builder: (context, model, child) { 26 | if (model.isBusy) { 27 | return SkeletonList( 28 | length: 11, 29 | builder: (context, index) => CoinRankingListItemSkeleton(), 30 | ); 31 | } else if (model.isError && model.list.isEmpty) { 32 | return ViewStateErrorWidget( 33 | error: model.viewStateError, onPressed: model.initData); 34 | } else if (model.isEmpty) { 35 | return ViewStateEmptyWidget(onPressed: model.initData); 36 | } 37 | return SmartRefresher( 38 | controller: model.refreshController, 39 | header: WaterDropHeader(), 40 | footer: RefresherFooter(), 41 | onRefresh: model.refresh, 42 | onLoading: model.loadMore, 43 | enablePullUp: true, 44 | child: ListView.separated( 45 | itemCount: model.list.length, 46 | separatorBuilder: (context, index) => Divider( 47 | indent: 10, 48 | endIndent: 10, 49 | height: 1, 50 | ), 51 | itemBuilder: (context, index) { 52 | // {"coinCount": 448,"username": "S**24n"}, 53 | Map item = model.list[index]; 54 | String userName = item['username']; 55 | String coinCount = item['coinCount'].toString(); 56 | return ListTile( 57 | dense: true, 58 | contentPadding: 59 | EdgeInsets.symmetric(horizontal: 25, vertical: 10), 60 | onTap: () {}, 61 | leading: Text('${index + 1}'), 62 | title: Text(userName, 63 | style: TextStyle( 64 | fontSize: 16, 65 | color: selfName == userName 66 | ? Colors.amberAccent 67 | : null)), 68 | trailing: Text( 69 | coinCount, 70 | style: TextStyle(color: Theme.of(context).accentColor), 71 | ), 72 | ); 73 | })); 74 | }, 75 | ), 76 | ); 77 | } 78 | } 79 | 80 | class CoinRankingListItemSkeleton extends StatelessWidget { 81 | @override 82 | Widget build(BuildContext context) { 83 | return Container( 84 | decoration: BottomBorderDecoration(), 85 | child: ListTile( 86 | dense: true, 87 | contentPadding: EdgeInsets.symmetric(horizontal: 25, vertical: 10), 88 | title: Row(children: [ 89 | SkeletonBox(width: 20, height: 10), 90 | SizedBox(width: 20), 91 | SkeletonBox(width: 250, height: 10) 92 | ]), 93 | trailing: SkeletonBox(width: 20, height: 10)), 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/ui/page/coin/coin_record_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/config/router_manger.dart'; 3 | import 'package:fun_android/model/coin_record.dart'; 4 | import 'package:fun_android/provider/provider_widget.dart'; 5 | import 'package:fun_android/provider/view_state_widget.dart'; 6 | import 'package:fun_android/ui/helper/refresh_helper.dart'; 7 | import 'package:fun_android/ui/widget/skeleton.dart'; 8 | import 'package:fun_android/view_model/coin_model.dart'; 9 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 10 | 11 | /// 积分记录 12 | class CoinRecordListPage extends StatelessWidget { 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | centerTitle: true, 18 | title: Text('积分明细'), 19 | actions: [ 20 | FlatButton( 21 | child: Text( 22 | '排行榜', 23 | style: TextStyle(color: Colors.white), 24 | ), 25 | onPressed: () { 26 | Navigator.pushNamed(context, RouteName.coinRankingList); 27 | }, 28 | ) 29 | ], 30 | ), 31 | body: ProviderWidget( 32 | model: CoinRecordListModel(), 33 | onModelReady: (model) => model.initData(), 34 | builder: (context, model, child) { 35 | if (model.isBusy) { 36 | return SkeletonList( 37 | length: 11, 38 | builder: (context, index) => CoinRecordItemSkeleton(), 39 | ); 40 | } else if (model.isError && model.list.isEmpty) { 41 | return ViewStateErrorWidget( 42 | error: model.viewStateError, onPressed: model.initData); 43 | } else if (model.isEmpty) { 44 | return ViewStateEmptyWidget(onPressed: model.initData); 45 | } 46 | return SmartRefresher( 47 | controller: model.refreshController, 48 | header: WaterDropHeader(), 49 | footer: RefresherFooter(), 50 | onRefresh: model.refresh, 51 | onLoading: model.loadMore, 52 | enablePullUp: true, 53 | child: ListView.separated( 54 | itemCount: model.list.length, 55 | separatorBuilder: (context, index) => Divider( 56 | indent: 10, 57 | endIndent: 10, 58 | height: 1, 59 | ), 60 | itemBuilder: (context, index) { 61 | // desc": "2019-08-28 10:09:15 签到,积分:10 + 12" 62 | // desc 提出 bug #issues/174 ,积分+10 63 | CoinRecord item = model. list[index]; 64 | String dateTime = 65 | DateTime.fromMillisecondsSinceEpoch(item.date) 66 | .toString() 67 | .substring(0, 19); 68 | String title; 69 | String coin; 70 | if (item.type == 1) { 71 | //签到 72 | title = '签到'; 73 | coin = item.desc.substring(item.desc.indexOf(':') + 1); 74 | } else if (item.type == 99) { 75 | //修复bug 76 | title = item.desc.substring(0,item.desc.indexOf(',')); 77 | coin= item.coinCount.toString(); 78 | }else{ 79 | title ='其他类型'; 80 | coin= item.coinCount.toString(); 81 | } 82 | return ListTile( 83 | contentPadding: 84 | EdgeInsets.symmetric(horizontal: 20, vertical: 5), 85 | onTap: () {}, 86 | title: Text(title), 87 | subtitle: Padding( 88 | padding: EdgeInsets.only(top: 10), 89 | child: Text(dateTime)), 90 | trailing: Text( 91 | coin, 92 | style: TextStyle(color: Theme.of(context).accentColor), 93 | ), 94 | ); 95 | })); 96 | }, 97 | ), 98 | ); 99 | } 100 | } 101 | 102 | class CoinRecordItemSkeleton extends StatelessWidget { 103 | @override 104 | Widget build(BuildContext context) { 105 | return Container( 106 | decoration: BottomBorderDecoration(), 107 | child: ListTile( 108 | contentPadding: EdgeInsets.symmetric(horizontal: 25, vertical: 5), 109 | title: UnconstrainedBox( 110 | alignment: Alignment.centerLeft, 111 | child: SkeletonBox(width: 80, height: 10)), 112 | subtitle: Padding( 113 | padding: const EdgeInsets.only(top: 10.0), 114 | child: SkeletonBox(width: 180, height: 10), 115 | ), 116 | trailing: SkeletonBox(width: 50, height: 10)), 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/ui/page/search/search_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide SearchDelegate; 2 | import 'package:provider/provider.dart'; 3 | import 'package:fun_android/flutter/search.dart'; 4 | import 'package:fun_android/view_model/search_model.dart'; 5 | 6 | import 'search_results.dart'; 7 | import 'search_suggestions.dart'; 8 | 9 | class DefaultSearchDelegate extends SearchDelegate { 10 | SearchHistoryModel _searchHistoryModel = SearchHistoryModel(); 11 | SearchHotKeyModel _searchHotKeyModel = SearchHotKeyModel(); 12 | 13 | @override 14 | ThemeData appBarTheme(BuildContext context) { 15 | var theme = Theme.of(context); 16 | return super.appBarTheme(context).copyWith( 17 | primaryColor: theme.scaffoldBackgroundColor, 18 | primaryColorBrightness: theme.brightness); 19 | } 20 | 21 | @override 22 | List buildActions(BuildContext context) { 23 | return [ 24 | IconButton( 25 | icon: Icon(Icons.clear), 26 | onPressed: () { 27 | if (query.isEmpty) { 28 | close(context, null); 29 | } else { 30 | query = ''; 31 | showSuggestions(context); 32 | } 33 | }, 34 | ), 35 | ]; 36 | } 37 | 38 | @override 39 | Widget buildLeading(BuildContext context) { 40 | return IconButton( 41 | icon: Icon(Icons.arrow_back), 42 | onPressed: () { 43 | close(context, null); 44 | }, 45 | ); 46 | } 47 | 48 | @override 49 | Widget buildResults(BuildContext context) { 50 | // if (query.length < 4) { 51 | // return Column( 52 | // mainAxisAlignment: MainAxisAlignment.center, 53 | // children: [ 54 | // Center( 55 | // child: Text( 56 | // "Search term must be longer than two letters.", 57 | // ), 58 | // ) 59 | // ], 60 | // ); 61 | // } 62 | debugPrint('buildResults-query' + query); 63 | if (query.length > 0) { 64 | return SearchResults( 65 | keyword: query, searchHistoryModel: _searchHistoryModel); 66 | } 67 | return SizedBox.shrink(); 68 | } 69 | 70 | @override 71 | Widget buildSuggestions(BuildContext context) { 72 | return MultiProvider( 73 | providers: [ 74 | ChangeNotifierProvider.value(value: _searchHistoryModel), 75 | ChangeNotifierProvider.value(value: _searchHotKeyModel), 76 | ], 77 | child: SearchSuggestions(delegate: this), 78 | ); 79 | } 80 | 81 | @override 82 | void close(BuildContext context, result) { 83 | _searchHistoryModel.dispose(); 84 | _searchHotKeyModel.dispose(); 85 | super.close(context, result); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/ui/page/search/search_results.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/ui/helper/refresh_helper.dart'; 3 | import 'package:pull_to_refresh/pull_to_refresh.dart'; 4 | import 'package:fun_android/model/article.dart'; 5 | import 'package:fun_android/provider/provider_widget.dart'; 6 | import 'package:fun_android/ui/widget/article_list_Item.dart'; 7 | import 'package:fun_android/provider/view_state_widget.dart'; 8 | import 'package:fun_android/view_model/search_model.dart'; 9 | 10 | class SearchResults extends StatelessWidget { 11 | final String keyword; 12 | final SearchHistoryModel searchHistoryModel; 13 | 14 | SearchResults({this.keyword, this.searchHistoryModel}); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return ProviderWidget( 19 | model: SearchResultModel( 20 | keyword: keyword, searchHistoryModel: searchHistoryModel), 21 | onModelReady: (model) { 22 | model.initData(); 23 | }, 24 | builder: (context, model, child) { 25 | if (model.isBusy) { 26 | return ViewStateBusyWidget(); 27 | } else if (model.isError && model.list.isEmpty) { 28 | return ViewStateErrorWidget( 29 | error: model.viewStateError, onPressed: model.initData); 30 | } else if (model.isEmpty) { 31 | return ViewStateEmptyWidget(onPressed: model.initData); 32 | } 33 | return SmartRefresher( 34 | controller: model.refreshController, 35 | header: WaterDropHeader(), 36 | footer: RefresherFooter(), 37 | onRefresh: model.refresh, 38 | onLoading: model.loadMore, 39 | enablePullUp: true, 40 | child: ListView.builder( 41 | itemCount: model.list.length, 42 | itemBuilder: (context, index) { 43 | Article item = model.list[index]; 44 | return ArticleItemWidget(item); 45 | })); 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ui/page/tab/home_second_floor_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/config/resource_mananger.dart'; 3 | import 'package:webview_flutter/webview_flutter.dart'; 4 | 5 | import 'home_page.dart'; 6 | 7 | class HomeSecondFloorOuter extends StatelessWidget { 8 | final Widget child; 9 | 10 | HomeSecondFloorOuter(this.child); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | height: kHomeRefreshHeight + MediaQuery.of(context).padding.top + 20, 16 | decoration: BoxDecoration( 17 | image: DecorationImage( 18 | image: AssetImage( 19 | ImageHelper.wrapAssets('home_second_floor_builder.png')), 20 | fit: BoxFit.fill, 21 | ), 22 | ), 23 | child: Stack( 24 | children: [ 25 | Align( 26 | alignment: Alignment.center, 27 | child: Text('跌跌撞撞中,依旧热爱这个世界.', 28 | style: Theme.of(context).textTheme.overline.copyWith( 29 | color: Colors.white, 30 | fontSize: 22, 31 | fontWeight: FontWeight.bold, 32 | )), 33 | ), 34 | Align(alignment: Alignment(0, 0.85), child: child), 35 | ], 36 | ), 37 | alignment: Alignment.bottomCenter, 38 | ); 39 | } 40 | } 41 | 42 | class MyBlogPage extends StatefulWidget { 43 | @override 44 | _MyBlogPageState createState() => _MyBlogPageState(); 45 | } 46 | 47 | class _MyBlogPageState extends State { 48 | ValueNotifier notifier = ValueNotifier(false); 49 | 50 | @override 51 | void dispose() { 52 | notifier.dispose(); 53 | super.dispose(); 54 | } 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Scaffold( 59 | floatingActionButton: FloatingActionButton( 60 | heroTag: 'homeFab', 61 | child: Icon(Icons.arrow_downward), 62 | onPressed: () { 63 | Navigator.of(context).pop(); 64 | }, 65 | ), 66 | body: Stack( 67 | children: [ 68 | Positioned( 69 | top: -MediaQuery.of(context).padding.top, 70 | bottom: 0, 71 | left: 0, 72 | right: 0, 73 | child: WebView( 74 | // 初始化加载的url 75 | initialUrl: 'http://blog.phoenixsky.cn', 76 | javascriptMode: JavascriptMode.unrestricted, 77 | onWebViewCreated: (WebViewController controller) {}, 78 | onPageFinished: (String value) { 79 | print('onPageFinished'); 80 | notifier.value = true; 81 | }, 82 | ), 83 | ), 84 | Container(), 85 | ValueListenableBuilder( 86 | valueListenable: notifier, 87 | builder: (context, value, child) => value 88 | ? SizedBox.shrink() 89 | : Center( 90 | child: CircularProgressIndicator(), 91 | )) 92 | ], 93 | )); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/ui/page/tab/tab_navigator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fun_android/generated/l10n.dart'; 4 | import 'package:fun_android/ui/widget/app_update.dart'; 5 | 6 | import 'home_page.dart'; 7 | import 'project_page.dart'; 8 | import 'structure_page.dart'; 9 | import 'user_page.dart'; 10 | import 'wechat_account_page.dart'; 11 | 12 | List pages = [ 13 | HomePage(), 14 | ProjectPage(), 15 | WechatAccountPage(), 16 | StructurePage(), 17 | UserPage() 18 | ]; 19 | 20 | class TabNavigator extends StatefulWidget { 21 | TabNavigator({Key key}) : super(key: key); 22 | 23 | @override 24 | _TabNavigatorState createState() => _TabNavigatorState(); 25 | } 26 | 27 | class _TabNavigatorState extends State { 28 | var _pageController = PageController(); 29 | int _selectedIndex = 0; 30 | DateTime _lastPressed; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Scaffold( 35 | body: WillPopScope( 36 | onWillPop: () async { 37 | if (_lastPressed == null || 38 | DateTime.now().difference(_lastPressed) > Duration(seconds: 1)) { 39 | //两次点击间隔超过1秒则重新计时 40 | _lastPressed = DateTime.now(); 41 | return false; 42 | } 43 | return true; 44 | }, 45 | child: PageView.builder( 46 | itemBuilder: (ctx, index) => pages[index], 47 | itemCount: pages.length, 48 | controller: _pageController, 49 | physics: NeverScrollableScrollPhysics(), 50 | onPageChanged: (index) { 51 | setState(() { 52 | _selectedIndex = index; 53 | }); 54 | }, 55 | ), 56 | ), 57 | bottomNavigationBar: BottomNavigationBar( 58 | type: BottomNavigationBarType.fixed, 59 | items: [ 60 | BottomNavigationBarItem( 61 | icon: Icon(Icons.home), 62 | title: Text(S.of(context).tabHome), 63 | ), 64 | BottomNavigationBarItem( 65 | icon: Icon(Icons.format_list_bulleted), 66 | title: Text(S.of(context).tabProject), 67 | ), 68 | BottomNavigationBarItem( 69 | icon: Icon(Icons.group_work), 70 | title: Text(S.of(context).wechatAccount), 71 | ), 72 | BottomNavigationBarItem( 73 | icon: Icon(Icons.call_split), 74 | title: Text(S.of(context).tabStructure), 75 | ), 76 | BottomNavigationBarItem( 77 | icon: Icon(Icons.insert_emoticon), 78 | title: Text(S.of(context).tabUser), 79 | ), 80 | ], 81 | currentIndex: _selectedIndex, 82 | onTap: (index) { 83 | _pageController.jumpToPage(index); 84 | }, 85 | ), 86 | ); 87 | } 88 | 89 | @override 90 | void initState() { 91 | checkAppUpdate(context); 92 | super.initState(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/ui/page/user/login_field_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fun_android/generated/l10n.dart'; 4 | 5 | /// 登录页面表单字段框封装类 6 | class LoginTextField extends StatefulWidget { 7 | final String label; 8 | final IconData icon; 9 | final bool obscureText; 10 | final TextEditingController controller; 11 | final FormFieldValidator validator; 12 | final FocusNode focusNode; 13 | final TextInputAction textInputAction; 14 | final ValueChanged onFieldSubmitted; 15 | 16 | LoginTextField({ 17 | this.label, 18 | this.icon, 19 | this.controller, 20 | this.obscureText: false, 21 | this.validator, 22 | this.focusNode, 23 | this.textInputAction, 24 | this.onFieldSubmitted, 25 | }); 26 | 27 | @override 28 | _LoginTextFieldState createState() => _LoginTextFieldState(); 29 | } 30 | 31 | class _LoginTextFieldState extends State { 32 | TextEditingController controller; 33 | 34 | /// 默认遮挡密码 35 | ValueNotifier obscureNotifier; 36 | 37 | @override 38 | void initState() { 39 | controller = widget.controller ?? TextEditingController(); 40 | obscureNotifier = ValueNotifier(widget.obscureText); 41 | super.initState(); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | obscureNotifier.dispose(); 47 | // 默认没有传入controller,需要内部释放 48 | if (widget.controller == null) { 49 | controller.dispose(); 50 | } 51 | super.dispose(); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | var theme = Theme.of(context); 57 | return Padding( 58 | padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), 59 | child: ValueListenableBuilder( 60 | valueListenable: obscureNotifier, 61 | builder: (context, value, child) => TextFormField( 62 | controller: controller, 63 | obscureText: value, 64 | validator: (text) { 65 | var validator = widget.validator ?? (_) => null; 66 | return text.trim().length > 0 67 | ? validator(text) 68 | : S.of(context).fieldNotNull; 69 | }, 70 | focusNode: widget.focusNode, 71 | textInputAction: widget.textInputAction, 72 | onFieldSubmitted: widget.onFieldSubmitted, 73 | decoration: InputDecoration( 74 | prefixIcon: Icon(widget.icon, color: theme.accentColor, size: 22), 75 | hintText: widget.label, 76 | hintStyle: TextStyle(fontSize: 16), 77 | suffixIcon: LoginTextFieldSuffixIcon( 78 | controller: controller, 79 | obscureText: widget.obscureText, 80 | obscureNotifier: obscureNotifier, 81 | ), 82 | ), 83 | ), 84 | ), 85 | ); 86 | } 87 | } 88 | 89 | class LoginTextFieldSuffixIcon extends StatelessWidget { 90 | final TextEditingController controller; 91 | 92 | final ValueNotifier obscureNotifier; 93 | 94 | final bool obscureText; 95 | 96 | LoginTextFieldSuffixIcon( 97 | {this.controller, this.obscureNotifier, this.obscureText}); 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | var theme = Theme.of(context); 102 | return Row( 103 | mainAxisSize: MainAxisSize.min, 104 | mainAxisAlignment: MainAxisAlignment.end, 105 | crossAxisAlignment: CrossAxisAlignment.center, 106 | children: [ 107 | Offstage( 108 | offstage: !obscureText, 109 | child: InkWell( 110 | onTap: () { 111 | // debugPrint('onTap'); 112 | obscureNotifier.value = !obscureNotifier.value; 113 | }, 114 | child: ValueListenableBuilder( 115 | valueListenable: obscureNotifier, 116 | builder: (context, value, child) => Icon( 117 | CupertinoIcons.eye, 118 | size: 30, 119 | color: value ? theme.hintColor : theme.accentColor, 120 | ), 121 | ), 122 | ), 123 | ), 124 | LoginTextFieldClearIcon(controller) 125 | ], 126 | ); 127 | } 128 | } 129 | 130 | class LoginTextFieldClearIcon extends StatefulWidget { 131 | final TextEditingController controller; 132 | 133 | LoginTextFieldClearIcon(this.controller); 134 | 135 | @override 136 | _LoginTextFieldClearIconState createState() => 137 | _LoginTextFieldClearIconState(); 138 | } 139 | 140 | class _LoginTextFieldClearIconState extends State { 141 | ValueNotifier notifier; 142 | 143 | @override 144 | void initState() { 145 | notifier = ValueNotifier(widget.controller.text.isEmpty); 146 | widget.controller.addListener(() { 147 | if(mounted) notifier.value = widget.controller.text.isEmpty; 148 | }); 149 | super.initState(); 150 | } 151 | 152 | @override 153 | void dispose() { 154 | notifier.dispose(); 155 | super.dispose(); 156 | } 157 | 158 | @override 159 | Widget build(BuildContext context) { 160 | return ValueListenableBuilder( 161 | valueListenable: notifier, 162 | builder: (context, bool value, child) { 163 | return Offstage( 164 | offstage: value, 165 | child: child, 166 | ); 167 | }, 168 | child: InkWell( 169 | onTap: () { 170 | WidgetsBinding.instance.addPostFrameCallback((_) { 171 | widget.controller.clear(); 172 | }); 173 | }, 174 | child: Icon(CupertinoIcons.clear, 175 | size: 30, color: Theme.of(context).hintColor)), 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/ui/page/user/login_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:fun_android/config/resource_mananger.dart'; 5 | import 'package:fun_android/ui/widget/bottom_clipper.dart'; 6 | import 'package:fun_android/view_model/theme_model.dart'; 7 | 8 | class LoginTopPanel extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return ClipPath( 12 | clipper: BottomClipper(), 13 | child: Container( 14 | height: 220, 15 | color: Theme.of(context).primaryColor, 16 | ), 17 | ); 18 | } 19 | } 20 | 21 | class LoginLogo extends StatelessWidget { 22 | @override 23 | Widget build(BuildContext context) { 24 | var theme = Theme.of(context); 25 | return Consumer( 26 | builder: (context, themeModel, child) { 27 | return InkWell( 28 | onTap: () { 29 | themeModel.switchRandomTheme(); 30 | }, 31 | child: child, 32 | ); 33 | }, 34 | child: Hero( 35 | tag: 'loginLogo', 36 | child: Image.asset( 37 | ImageHelper.wrapAssets('login_logo.png'), 38 | width: 130, 39 | height: 100, 40 | fit: BoxFit.fitWidth, 41 | color: theme.brightness == Brightness.dark 42 | ? theme.accentColor 43 | : Colors.white, 44 | colorBlendMode: BlendMode.srcIn, 45 | ), 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | class LoginFormContainer extends StatelessWidget { 52 | final Widget child; 53 | 54 | LoginFormContainer({this.child}); 55 | 56 | @override 57 | Widget build(BuildContext context) { 58 | return Container( 59 | margin: EdgeInsets.symmetric(horizontal: 30, vertical: 30), 60 | padding: EdgeInsets.symmetric(horizontal: 10, vertical: 15), 61 | decoration: ShapeDecoration( 62 | shape: RoundedRectangleBorder(), 63 | color: Theme.of(context).cardColor, 64 | shadows: [ 65 | BoxShadow( 66 | color: Theme.of(context).primaryColor.withAlpha(20), 67 | offset: Offset(1.0, 1.0), 68 | blurRadius: 10.0, 69 | spreadRadius: 3.0), 70 | ]), 71 | child: child, 72 | ); 73 | } 74 | } 75 | 76 | 77 | /// LoginPage 按钮样式封装 78 | class LoginButtonWidget extends StatelessWidget { 79 | final Widget child; 80 | final VoidCallback onPressed; 81 | 82 | LoginButtonWidget({this.child, this.onPressed}); 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | var color = Theme.of(context).primaryColor.withAlpha(180); 87 | return Padding( 88 | padding: const EdgeInsets.fromLTRB(15, 40, 15, 20), 89 | child: CupertinoButton( 90 | padding: EdgeInsets.all(0), 91 | color: color, 92 | disabledColor: color, 93 | borderRadius: BorderRadius.circular(110), 94 | pressedOpacity: 0.5, 95 | child: child, 96 | onPressed: onPressed, 97 | )); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/ui/widget/activity_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// 由于app不管明暗模式,都是有底色 5 | /// 所以将indicator颜色为亮色 6 | class ActivityIndicator extends StatelessWidget { 7 | final double radius; 8 | final Brightness brightness; 9 | 10 | ActivityIndicator({this.radius, this.brightness}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Theme( 15 | data: ThemeData( 16 | cupertinoOverrideTheme: CupertinoThemeData(brightness: brightness), 17 | ), 18 | child: CupertinoActivityIndicator(radius: radius ?? 10)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/ui/widget/animated_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ScaleAnimatedSwitcher extends StatelessWidget { 4 | final Widget child; 5 | 6 | ScaleAnimatedSwitcher({this.child}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return AnimatedSwitcher( 11 | duration: Duration(milliseconds: 300), 12 | transitionBuilder: (child, animation) => ScaleTransition( 13 | scale: animation, 14 | child: child, 15 | ), 16 | child: child, 17 | ); 18 | } 19 | } 20 | 21 | class EmptyAnimatedSwitcher extends StatelessWidget { 22 | final bool display; 23 | final Widget child; 24 | 25 | EmptyAnimatedSwitcher({this.display: true, this.child}); 26 | 27 | @override 28 | Widget build(BuildContext context) { 29 | return ScaleAnimatedSwitcher(child: display ? child : SizedBox.shrink()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/ui/widget/app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/ui/widget/activity_indicator.dart'; 3 | 4 | /// 由于app不管明暗模式,都是有底色 5 | /// 所以将indicator颜色为亮色 6 | class AppBarIndicator extends StatelessWidget { 7 | final double radius; 8 | 9 | AppBarIndicator({this.radius}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return ActivityIndicator( 14 | brightness: Brightness.dark, 15 | radius: radius, 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ui/widget/article_skeleton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'skeleton.dart'; 4 | 5 | class ArticleSkeletonItem extends StatelessWidget { 6 | final int index; 7 | 8 | ArticleSkeletonItem({this.index: 0}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | var width = MediaQuery.of(context).size.width; 13 | bool isDark = Theme.of(context).brightness == Brightness.dark; 14 | 15 | return Container( 16 | margin: const EdgeInsets.symmetric(horizontal: 20), 17 | padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 0), 18 | decoration: BoxDecoration( 19 | border: Border( 20 | bottom: Divider.createBorderSide(context, 21 | width: 0.7, color: Colors.redAccent))), 22 | child: Column( 23 | mainAxisSize: MainAxisSize.min, 24 | mainAxisAlignment: MainAxisAlignment.start, 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | children: [ 27 | Row( 28 | mainAxisAlignment: MainAxisAlignment.start, 29 | children: [ 30 | Container( 31 | height: 20, 32 | width: 20, 33 | decoration: SkeletonDecoration(isCircle: true, isDark: isDark), 34 | ), 35 | Container( 36 | margin: EdgeInsets.only(left: 10), 37 | height: 5, 38 | width: 100, 39 | decoration: SkeletonDecoration(isDark: isDark), 40 | ), 41 | Expanded(child: SizedBox.shrink()), 42 | Container( 43 | height: 5, 44 | width: 30, 45 | decoration: SkeletonDecoration(isDark: isDark), 46 | ), 47 | ], 48 | ), 49 | SizedBox( 50 | height: 0, 51 | ), 52 | Column( 53 | crossAxisAlignment: CrossAxisAlignment.start, 54 | children: [ 55 | SizedBox( 56 | height: 10, 57 | ), 58 | Container( 59 | height: 6.5, 60 | width: width * 0.7, 61 | decoration: SkeletonDecoration(isDark: isDark), 62 | ), 63 | SizedBox( 64 | height: 10, 65 | ), 66 | Container( 67 | height: 6.5, 68 | width: width * 0.8, 69 | decoration: SkeletonDecoration(isDark: isDark), 70 | ), 71 | SizedBox( 72 | height: 10, 73 | ), 74 | Container( 75 | height: 6.5, 76 | width: width * 0.5, 77 | decoration: SkeletonDecoration(isDark: isDark), 78 | ), 79 | ], 80 | ), 81 | SizedBox( 82 | height: 10, 83 | ), 84 | Row( 85 | children: [ 86 | Container( 87 | margin: EdgeInsets.only(right: 10), 88 | height: 8, 89 | width: 20, 90 | decoration: SkeletonDecoration(isDark: isDark), 91 | ), 92 | Container( 93 | height: 8, 94 | width: 80, 95 | decoration: SkeletonDecoration(isDark: isDark), 96 | ), 97 | Expanded(child: SizedBox.shrink()), 98 | Container( 99 | height: 20, 100 | width: 20, 101 | decoration: SkeletonDecoration(isDark: isDark), 102 | ), 103 | ], 104 | ), 105 | ], 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/ui/widget/article_tag.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ArticleTag extends StatelessWidget { 4 | final String text; 5 | final Color color; 6 | 7 | ArticleTag(this.text, {this.color}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | var themeColor = color ?? Theme.of(context).accentColor; 12 | return Container( 13 | padding: EdgeInsets.symmetric( 14 | horizontal: 3, 15 | vertical: 0.5, 16 | ), 17 | margin: EdgeInsets.only(right: 5), 18 | decoration: BoxDecoration( 19 | border: Border.all(width: 1, color: color ?? themeColor)), 20 | child: Text(text, 21 | style: TextStyle(color: color ?? themeColor, fontSize: 10)), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/ui/widget/banner_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:fun_android/config/resource_mananger.dart'; 5 | 6 | class BannerImage extends StatelessWidget { 7 | final String url; 8 | final BoxFit fit; 9 | 10 | BannerImage(this.url, {this.fit: BoxFit.fill}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return CachedNetworkImage( 15 | imageUrl: ImageHelper.wrapUrl(url), 16 | placeholder: (context, url) => 17 | Center(child: CupertinoActivityIndicator()), 18 | errorWidget: (context, url, error) => Icon(Icons.error), 19 | fit: fit); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/ui/widget/bottom_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BottomClipper extends CustomClipper { 4 | @override 5 | getClip(Size size) { 6 | var path = Path(); 7 | path.lineTo(0, 0); 8 | path.lineTo(0, size.height - 50); 9 | 10 | var p1 = Offset(size.width / 2, size.height); 11 | var p2 = Offset(size.width, size.height - 50); 12 | path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy); 13 | path.lineTo(size.width, size.height - 50); 14 | path.lineTo(size.width, 0); 15 | return path; 16 | } 17 | 18 | @override 19 | bool shouldReclip(CustomClipper oldClipper) { 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/ui/widget/button_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ButtonProgressIndicator extends StatelessWidget { 4 | 5 | final double size; 6 | final Color color; 7 | 8 | ButtonProgressIndicator( 9 | { this.size: 24, this.color: Colors.white}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SizedBox( 14 | width: size, 15 | height: size, 16 | child: CircularProgressIndicator( 17 | strokeWidth: 2, 18 | valueColor: AlwaysStoppedAnimation(color), 19 | )); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/ui/widget/favourite_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flare_flutter/flare_actor.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const kAnimAddFavouritesTag = 'kAnimAddFavouritesTag'; 5 | 6 | class FavouriteAnimationWidget extends StatefulWidget { 7 | /// Hero动画的唯一标识 8 | final Object tag; 9 | 10 | /// true 添加到收藏,false从收藏移除 11 | final bool add; 12 | 13 | FavouriteAnimationWidget({@required this.tag, @required this.add}); 14 | 15 | @override 16 | _FavouriteAnimationWidgetState createState() => 17 | _FavouriteAnimationWidgetState(); 18 | } 19 | 20 | class _FavouriteAnimationWidgetState extends State { 21 | bool playing = false; 22 | 23 | @override 24 | void initState() { 25 | WidgetsBinding.instance.addPostFrameCallback((_) { 26 | setState(() { 27 | playing = true; 28 | }); 29 | }); 30 | super.initState(); 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Hero( 36 | tag: widget.tag, 37 | child: FlareActor( 38 | "assets/animations/like.flr", 39 | alignment: Alignment.center, 40 | fit: BoxFit.contain, 41 | animation: widget.add ? 'like' : 'unLike', 42 | shouldClip: false, 43 | isPaused: !playing, 44 | callback: (name) { 45 | Navigator.pop(context); 46 | playing = false; 47 | }, 48 | ), 49 | ); 50 | } 51 | } 52 | 53 | /// Dialog下使用Hero动画的路由 54 | class HeroDialogRoute extends PageRoute { 55 | HeroDialogRoute({this.builder}) : super(); 56 | 57 | final WidgetBuilder builder; 58 | 59 | @override 60 | bool get opaque => false; 61 | 62 | @override 63 | bool get barrierDismissible => true; 64 | 65 | @override 66 | Duration get transitionDuration => const Duration(milliseconds: 800); 67 | 68 | @override 69 | bool get maintainState => true; 70 | 71 | @override 72 | Color get barrierColor => Colors.black12; 73 | 74 | // @override 75 | // Widget buildTransitions(BuildContext context, Animation animation, 76 | // Animation secondaryAnimation, Widget child) { 77 | // return new FadeTransition( 78 | // opacity: new CurvedAnimation(parent: animation, curve: Curves.easeIn), 79 | // child: child); 80 | // } 81 | 82 | @override 83 | Widget buildPage(BuildContext context, Animation animation, 84 | Animation secondaryAnimation) { 85 | return builder(context); 86 | } 87 | 88 | @override 89 | String get barrierLabel => null; 90 | } 91 | -------------------------------------------------------------------------------- /lib/ui/widget/image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:fun_android/config/resource_mananger.dart'; 4 | 5 | enum ImageType { 6 | normal, 7 | random, //随机 8 | assets, //资源目录 9 | } 10 | 11 | class WrapperImage extends StatelessWidget { 12 | final String url; 13 | final double width; 14 | final double height; 15 | final BoxFit fit; 16 | final ImageType imageType; 17 | 18 | WrapperImage( 19 | {@required this.url, 20 | @required this.width, 21 | @required this.height, 22 | this.imageType: ImageType.normal, 23 | this.fit: BoxFit.cover}); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return CachedNetworkImage( 28 | imageUrl: imageUrl, 29 | width: width, 30 | height: height, 31 | placeholder: (_, __) => 32 | ImageHelper.placeHolder(width: width, height: height), 33 | errorWidget: (_, __, ___) => 34 | ImageHelper.error(width: width, height: height), 35 | fit: fit, 36 | ); 37 | } 38 | 39 | String get imageUrl { 40 | switch (imageType) { 41 | case ImageType.random: 42 | return ImageHelper.randomUrl( 43 | key: url, width: width.toInt(), height: height.toInt()); 44 | case ImageType.assets: 45 | return ImageHelper.wrapAssets(url); 46 | case ImageType.normal: 47 | return url; 48 | } 49 | return url; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/ui/widget/page_route_anim.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class NoAnimRouteBuilder extends PageRouteBuilder { 4 | final Widget page; 5 | 6 | NoAnimRouteBuilder(this.page) 7 | : super( 8 | opaque: false, 9 | pageBuilder: (context, animation, secondaryAnimation) => page, 10 | transitionDuration: Duration(milliseconds: 0), 11 | transitionsBuilder: 12 | (context, animation, secondaryAnimation, child) => child); 13 | } 14 | 15 | class FadeRouteBuilder extends PageRouteBuilder { 16 | final Widget page; 17 | 18 | FadeRouteBuilder(this.page) 19 | : super( 20 | pageBuilder: (context, animation, secondaryAnimation) => page, 21 | transitionDuration: Duration(milliseconds: 500), 22 | transitionsBuilder: (context, animation, secondaryAnimation, 23 | child) => 24 | FadeTransition( 25 | opacity: Tween(begin: 0.1, end: 1.0).animate(CurvedAnimation( 26 | parent: animation, 27 | curve: Curves.fastOutSlowIn, 28 | )), 29 | child: child, 30 | )); 31 | } 32 | 33 | class SlideTopRouteBuilder extends PageRouteBuilder { 34 | final Widget page; 35 | 36 | SlideTopRouteBuilder(this.page) 37 | : super( 38 | pageBuilder: (context, animation, secondaryAnimation) => page, 39 | transitionDuration: Duration(milliseconds: 800), 40 | transitionsBuilder: 41 | (context, animation, secondaryAnimation, child) => 42 | SlideTransition( 43 | position: Tween( 44 | begin: Offset(0.0, -1.0), end: Offset(0.0, 0.0)) 45 | .animate(CurvedAnimation( 46 | parent: animation, curve: Curves.fastOutSlowIn)), 47 | child: child, 48 | )); 49 | } 50 | 51 | class SizeRoute extends PageRouteBuilder { 52 | final Widget page; 53 | 54 | SizeRoute(this.page) 55 | : super( 56 | pageBuilder: (context, animation, secondaryAnimation) => page, 57 | transitionDuration: Duration(milliseconds: 300), 58 | transitionsBuilder: (context, animation, secondaryAnimation, child) => 59 | // Align( 60 | // child: SizeTransition(child: child, sizeFactor: animation), 61 | // ), 62 | ScaleTransition( 63 | scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( 64 | parent: animation, curve: Curves.fastOutSlowIn)), 65 | child: child, 66 | ), 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /lib/ui/widget/skeleton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class SkeletonBox extends StatelessWidget { 5 | final double width; 6 | final double height; 7 | final bool isCircle; 8 | 9 | SkeletonBox( 10 | {@required this.width, @required this.height, this.isCircle: false}); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | bool isDark = Theme.of(context).brightness == Brightness.dark; 15 | Divider.createBorderSide(context, width: 0.7); 16 | return Container( 17 | width: width, 18 | height: height, 19 | decoration: SkeletonDecoration(isCircle: isCircle, isDark: isDark), 20 | ); 21 | } 22 | } 23 | 24 | /// 骨架屏 元素背景 ->形状及颜色 25 | class SkeletonDecoration extends BoxDecoration { 26 | SkeletonDecoration({ 27 | isCircle: false, 28 | isDark: false, 29 | }) : super( 30 | color: !isDark ? Colors.grey[350] : Colors.grey[700], 31 | shape: isCircle ? BoxShape.circle : BoxShape.rectangle, 32 | ); 33 | } 34 | 35 | /// 骨架屏 元素背景 ->形状及颜色 36 | class BottomBorderDecoration extends BoxDecoration { 37 | BottomBorderDecoration() 38 | : super(border: Border(bottom: BorderSide(width: 0.3))); 39 | } 40 | 41 | /// 骨架屏 42 | class SkeletonList extends StatelessWidget { 43 | final EdgeInsetsGeometry padding; 44 | final int length; 45 | final IndexedWidgetBuilder builder; 46 | 47 | SkeletonList( 48 | {this.length: 6, //一般屏幕长度够用 49 | this.padding = const EdgeInsets.all(7), 50 | @required this.builder}); 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | var theme = Theme.of(context); 55 | bool isDark = theme.brightness == Brightness.dark; 56 | 57 | // var highlightColor = isDark 58 | // ? Colors.grey[500] 59 | // : Color.alphaBlend(theme.accentColor.withAlpha(20), Colors.grey[100]); 60 | 61 | return SingleChildScrollView( 62 | physics: NeverScrollableScrollPhysics(), 63 | child: Shimmer.fromColors( 64 | period: Duration(milliseconds: 1200), 65 | baseColor: isDark ? Colors.grey[700] : Colors.grey[350], 66 | highlightColor: isDark ? Colors.grey[500] : Colors.grey[200], 67 | child: Padding( 68 | padding: padding, 69 | child: Column( 70 | children: 71 | List.generate(length, (index) => builder(context, index)), 72 | ))), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/ui/widget/third_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/generated/l10n.dart'; 3 | import 'package:oktoast/oktoast.dart'; 4 | import 'package:fun_android/config/resource_mananger.dart'; 5 | 6 | class ThirdLogin extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | var theme = Theme.of(context); 10 | return Column( 11 | children: [ 12 | Row( 13 | mainAxisAlignment: MainAxisAlignment.center, 14 | children: [ 15 | Container( 16 | color: theme.hintColor.withAlpha(50), 17 | height: 0.6, 18 | width: 60, 19 | ), 20 | Padding( 21 | padding: const EdgeInsets.symmetric(horizontal: 20), 22 | child: Text(S.of(context).signIn3thd, 23 | style: TextStyle(color: theme.hintColor)), 24 | ), 25 | Container( 26 | color: theme.hintColor.withAlpha(50), 27 | height: 0.6, 28 | width: 60, 29 | ), 30 | ], 31 | ), 32 | Padding( 33 | padding: const EdgeInsets.only(top: 20), 34 | child: Row( 35 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 36 | children: [ 37 | GestureDetector( 38 | onTap: () { 39 | showToast('蓄势待发,敬请期待'); 40 | }, 41 | child: Image.asset( 42 | ImageHelper.wrapAssets('logo_wechat.png'), 43 | width: 40, 44 | height: 40, 45 | ), 46 | ), 47 | GestureDetector( 48 | onTap: () { 49 | showToast('蓄势待发,敬请期待'); 50 | }, 51 | child: Image.asset( 52 | ImageHelper.wrapAssets('logo_weibo.png'), 53 | width: 40, 54 | height: 40, 55 | ), 56 | ) 57 | ], 58 | ), 59 | ) 60 | ], 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/utils/animation_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class AnimationUtils{ 4 | 5 | static Widget scaleTransitionBuilder(Widget child, Animation animation) { 6 | return ScaleTransition( 7 | scale: animation, 8 | child: child, 9 | ); 10 | } 11 | } -------------------------------------------------------------------------------- /lib/utils/platform_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | 4 | import 'package:device_info/device_info.dart'; 5 | import 'package:package_info/package_info.dart'; 6 | 7 | export 'dart:io'; 8 | 9 | /// 是否是生产环境 10 | const bool inProduction = const bool.fromEnvironment("dart.vm.product"); 11 | 12 | class PlatformUtils { 13 | 14 | static Future getAppPackageInfo() { 15 | return PackageInfo.fromPlatform(); 16 | } 17 | 18 | static Future getAppVersion() async { 19 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 20 | return packageInfo.version; 21 | } 22 | static Future getBuildNum() async { 23 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 24 | return packageInfo.buildNumber; 25 | } 26 | 27 | static Future getDeviceInfo() async { 28 | DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); 29 | if (Platform.isAndroid) { 30 | return await deviceInfo.androidInfo; 31 | } else if (Platform.isIOS) { 32 | return await deviceInfo.iosInfo; 33 | } else { 34 | return null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/utils/status_bar_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | class StatusBarUtils { 6 | 7 | /// 根据主题色彩控制状态栏字体颜色 8 | /// 通过AnnotatedRegion实现 9 | static systemUiOverlayStyle(BuildContext context) { 10 | return Theme.of(context).brightness == Brightness.light 11 | ? SystemUiOverlayStyle.dark 12 | : SystemUiOverlayStyle.light; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/string_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:crypto/crypto.dart'; 3 | import 'package:html_unescape/html_unescape.dart'; 4 | 5 | class StringUtils { 6 | static String toMD5(String data) { 7 | var content = new Utf8Encoder().convert(data); 8 | var digest = md5.convert(content); 9 | return digest.toString(); 10 | } 11 | 12 | static String urlDecoder(String data) { 13 | return data == null ? null : HtmlUnescape().convert(data); 14 | } 15 | 16 | static String removeHtmlLabel(String data) { 17 | return data?.replaceAll(RegExp('<[^>]+>'), ''); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/utils/third_app_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:url_launcher/url_launcher.dart'; 2 | 3 | class ThirdAppUtils { 4 | static Future canOpenApp(url) async { 5 | Uri uri = Uri.parse(url); 6 | var scheme; 7 | switch (uri.host) { 8 | case 'www.jianshu.com': //简书 9 | scheme = 'jianshu://${uri.pathSegments.join("/")}'; 10 | break; 11 | case 'juejin.im': //掘金 12 | /// 原始链接:https://juejin.im/post/5d66565cf265da03e71b0672 13 | /// App链接:juejin://post/5d66565cf265da03e71b0672 14 | scheme = 'juejin://${uri.pathSegments.join("/")}'; 15 | break; 16 | default: 17 | break; 18 | } 19 | if (await canLaunch(scheme)) { 20 | return scheme; 21 | } else { 22 | throw 'Could not launch $url'; 23 | } 24 | } 25 | 26 | static openAppByUrl(url) async { 27 | await launch(url); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/view_model/app_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:fun_android/config/net/pgyer_api.dart'; 5 | import 'package:fun_android/provider/view_state_model.dart'; 6 | import 'package:fun_android/service/app_repository.dart'; 7 | import 'package:fun_android/utils/platform_utils.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | 10 | const kAppFirstEntry = 'kAppFirstEntry'; 11 | 12 | // 主要用于app启动相关 13 | class AppModel with ChangeNotifier { 14 | bool isFirst = false; 15 | 16 | loadIsFirstEntry() async { 17 | var sharedPreferences = await SharedPreferences.getInstance(); 18 | isFirst = sharedPreferences.getBool(kAppFirstEntry); 19 | notifyListeners(); 20 | } 21 | } 22 | 23 | class AppUpdateModel extends ViewStateModel { 24 | Future checkUpdate() async { 25 | AppUpdateInfo appUpdateInfo; 26 | setBusy(); 27 | try { 28 | var appVersion = await PlatformUtils.getAppVersion(); 29 | appUpdateInfo = 30 | await AppRepository.checkUpdate(Platform.operatingSystem, appVersion); 31 | setIdle(); 32 | } catch (e, s) { 33 | setError(e,s); 34 | } 35 | return appUpdateInfo; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/view_model/coin_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/model/coin_record.dart'; 2 | import 'package:fun_android/provider/view_state_model.dart'; 3 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 4 | import 'package:fun_android/service/wan_android_repository.dart'; 5 | 6 | /// 个人积分 7 | class CoinModel extends ViewStateModel { 8 | int coin = 0; 9 | 10 | initData() async { 11 | setBusy(); 12 | try { 13 | coin = await WanAndroidRepository.fetchCoin(); 14 | setIdle(); 15 | } catch (e, s) { 16 | setError(e,s); 17 | } 18 | } 19 | } 20 | 21 | /// 个人积分 22 | class CoinRecordListModel extends ViewStateRefreshListModel { 23 | @override 24 | Future> loadData({int pageNum}) async { 25 | return await WanAndroidRepository.fetchCoinRecordList(pageNum); 26 | } 27 | } 28 | 29 | /// 积分排行榜 30 | class CoinRankingListModel extends ViewStateRefreshListModel { 31 | @override 32 | Future loadData({int pageNum}) async { 33 | return await WanAndroidRepository.fetchRankingList(pageNum); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/view_model/favourite_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/model/article.dart'; 3 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 4 | import 'package:fun_android/provider/view_state_model.dart'; 5 | import 'package:fun_android/service/wan_android_repository.dart'; 6 | 7 | import 'login_model.dart'; 8 | 9 | /// 我的收藏列表 10 | class FavouriteListModel extends ViewStateRefreshListModel
{ 11 | LoginModel loginModel; 12 | 13 | FavouriteListModel({this.loginModel}); 14 | 15 | @override 16 | void onError(ViewStateError viewStateError) { 17 | super.onError(viewStateError); 18 | if (viewStateError.isUnauthorized) { 19 | loginModel.logout(); 20 | } 21 | } 22 | 23 | 24 | @override 25 | Future> loadData({int pageNum}) async { 26 | return await WanAndroidRepository.fetchCollectList(pageNum); 27 | } 28 | } 29 | 30 | /// 收藏/取消收藏 31 | class FavouriteModel extends ViewStateModel { 32 | GlobalFavouriteStateModel globalFavouriteModel; 33 | 34 | FavouriteModel({@required this.globalFavouriteModel}); 35 | 36 | collect(Article article) async { 37 | setBusy(); 38 | try { 39 | // article.collect 字段为null,代表是从我的收藏页面进入的 需要调用特殊的取消接口 40 | if (article.collect == null) { 41 | await WanAndroidRepository.unMyCollect( 42 | id: article.id, originId: article.originId); 43 | globalFavouriteModel.removeFavourite(article.originId); 44 | } else { 45 | if (article.collect) { 46 | await WanAndroidRepository.unCollect(article.id); 47 | globalFavouriteModel.removeFavourite(article.id); 48 | } else { 49 | await WanAndroidRepository.collect(article.id); 50 | globalFavouriteModel.addFavourite(article.id); 51 | } 52 | } 53 | article.collect = !(article.collect ?? true); 54 | setIdle(); 55 | } catch (e, s) { 56 | setError(e, s); 57 | } 58 | } 59 | } 60 | 61 | /// 全局维护状态是否收藏 62 | /// 63 | class GlobalFavouriteStateModel extends ChangeNotifier { 64 | /// 将页面列表项中所有的收藏状态操作结果存储到集合中. 65 | /// 66 | /// [key]为articleId,[value]为bool类型,代表是否收藏 67 | /// 68 | /// 设置static的目的是,列表更新时,刷新该map中的值 69 | static final Map _map = Map(); 70 | 71 | /// 列表数据刷新后,同步刷新该map数据 72 | /// 73 | /// 在其他终端(如PC端)收藏/取消收藏后,会导致两边状态不一致. 74 | /// 列表页面刷新后,应该将新的收藏状态同步更新到map 75 | static refresh(List
list) { 76 | list.forEach((article) { 77 | if (_map.containsKey(article.id)) { 78 | _map[article.id] = article.collect; 79 | } 80 | }); 81 | } 82 | 83 | addFavourite(int id) { 84 | _map[id] = true; 85 | notifyListeners(); 86 | } 87 | 88 | removeFavourite(int id) { 89 | _map[id] = false; 90 | notifyListeners(); 91 | } 92 | 93 | /// 用于切换用户后,将该用户所有收藏的文章,对应的状态置为true 94 | replaceAll(List ids) { 95 | _map.clear(); 96 | ids.forEach((id) => _map[id] = true); 97 | notifyListeners(); 98 | } 99 | 100 | contains(id) { 101 | return _map.containsKey(id); 102 | } 103 | 104 | operator [](int id) { 105 | return _map[id]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/view_model/home_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/model/article.dart'; 2 | import 'package:fun_android/model/banner.dart'; 3 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 4 | import 'package:fun_android/service/wan_android_repository.dart'; 5 | 6 | import 'favourite_model.dart'; 7 | 8 | class HomeModel extends ViewStateRefreshListModel { 9 | List _banners; 10 | List
_topArticles; 11 | 12 | List get banners => _banners; 13 | 14 | List
get topArticles => _topArticles; 15 | 16 | @override 17 | Future loadData({int pageNum}) async { 18 | List futures = []; 19 | if (pageNum == ViewStateRefreshListModel.pageNumFirst) { 20 | futures.add(WanAndroidRepository.fetchBanners()); 21 | futures.add(WanAndroidRepository.fetchTopArticles()); 22 | } 23 | futures.add(WanAndroidRepository.fetchArticles(pageNum)); 24 | 25 | var result = await Future.wait(futures); 26 | if (pageNum == ViewStateRefreshListModel.pageNumFirst) { 27 | _banners = result[0]; 28 | _topArticles = result[1]; 29 | return result[2]; 30 | } else { 31 | return result[0]; 32 | } 33 | } 34 | 35 | @override 36 | onCompleted(List data) { 37 | GlobalFavouriteStateModel.refresh(data); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/view_model/locale_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:fun_android/config/storage_manager.dart'; 3 | import 'package:fun_android/generated/l10n.dart'; 4 | 5 | class LocaleModel extends ChangeNotifier { 6 | // static const localeNameList = ['auto', '中文', 'English']; 7 | static const localeValueList = ['', 'zh-CN', 'en']; 8 | 9 | // 10 | static const kLocaleIndex = 'kLocaleIndex'; 11 | 12 | int _localeIndex; 13 | 14 | int get localeIndex => _localeIndex; 15 | 16 | Locale get locale { 17 | if (_localeIndex > 0) { 18 | var value = localeValueList[_localeIndex].split("-"); 19 | return Locale(value[0], value.length == 2 ? value[1] : ''); 20 | } 21 | // 跟随系统 22 | return null; 23 | } 24 | 25 | LocaleModel() { 26 | _localeIndex = StorageManager.sharedPreferences.getInt(kLocaleIndex) ?? 0; 27 | } 28 | 29 | switchLocale(int index) { 30 | _localeIndex = index; 31 | notifyListeners(); 32 | StorageManager.sharedPreferences.setInt(kLocaleIndex, index); 33 | } 34 | 35 | static String localeName(index, context) { 36 | switch (index) { 37 | case 0: 38 | return S.of(context).autoBySystem; 39 | case 1: 40 | return '中文'; 41 | case 2: 42 | return 'English'; 43 | default: 44 | return ''; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/view_model/login_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/config/storage_manager.dart'; 2 | import 'package:fun_android/provider/view_state_model.dart'; 3 | import 'package:fun_android/service/wan_android_repository.dart'; 4 | 5 | import 'user_model.dart'; 6 | 7 | const String kLoginName = 'kLoginName'; 8 | 9 | class LoginModel extends ViewStateModel { 10 | final UserModel userModel; 11 | 12 | LoginModel(this.userModel) : assert(userModel != null); 13 | 14 | String getLoginName() { 15 | return StorageManager.sharedPreferences.getString(kLoginName); 16 | } 17 | 18 | Future login(loginName, password) async { 19 | setBusy(); 20 | try { 21 | var user = await WanAndroidRepository.login(loginName, password); 22 | userModel.saveUser(user); 23 | StorageManager.sharedPreferences 24 | .setString(kLoginName, userModel.user.username); 25 | setIdle(); 26 | return true; 27 | } catch (e, s) { 28 | setError(e,s); 29 | return false; 30 | } 31 | } 32 | 33 | Future logout() async { 34 | if (!userModel.hasUser) { 35 | //防止递归 36 | return false; 37 | } 38 | setBusy(); 39 | try { 40 | await WanAndroidRepository.logout(); 41 | userModel.clearUser(); 42 | setIdle(); 43 | return true; 44 | } catch (e, s) { 45 | setError(e,s); 46 | return false; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/view_model/project_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/model/article.dart'; 2 | import 'package:fun_android/model/tree.dart'; 3 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 4 | import 'package:fun_android/provider/view_state_list_model.dart'; 5 | import 'package:fun_android/service/wan_android_repository.dart'; 6 | 7 | import 'favourite_model.dart'; 8 | 9 | class ProjectCategoryModel extends ViewStateListModel { 10 | @override 11 | Future> loadData() async { 12 | return await WanAndroidRepository.fetchProjectCategories(); 13 | } 14 | } 15 | 16 | class ProjectListModel extends ViewStateRefreshListModel
{ 17 | @override 18 | Future> loadData({int pageNum}) async { 19 | return await WanAndroidRepository.fetchArticles(pageNum, cid: 294); 20 | } 21 | @override 22 | onCompleted(List data) { 23 | GlobalFavouriteStateModel.refresh(data); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/view_model/register_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/provider/view_state_model.dart'; 2 | import 'package:fun_android/service/wan_android_repository.dart'; 3 | 4 | class RegisterModel extends ViewStateModel { 5 | 6 | Future singUp(loginName, password, rePassword) async { 7 | setBusy(); 8 | try { 9 | await WanAndroidRepository.register(loginName, password, rePassword); 10 | setIdle(); 11 | return true; 12 | } catch (e, s) { 13 | setError(e,s); 14 | return false; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/view_model/scroll_controller_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TapToTopModel with ChangeNotifier { 4 | ScrollController _scrollController; 5 | 6 | double _height; 7 | 8 | bool _showTopBtn = false; 9 | 10 | ScrollController get scrollController => _scrollController; 11 | 12 | bool get showTopBtn => _showTopBtn; 13 | 14 | TapToTopModel(this._scrollController, {double height: 200}) { 15 | _height = height; 16 | } 17 | 18 | init() { 19 | _scrollController.addListener(() { 20 | if (_scrollController.offset > _height && !_showTopBtn) { 21 | _showTopBtn = true; 22 | notifyListeners(); 23 | } else if (_scrollController.offset < _height && _showTopBtn) { 24 | _showTopBtn = false; 25 | notifyListeners(); 26 | } 27 | }); 28 | } 29 | 30 | scrollToTop() { 31 | _scrollController.animateTo(0, 32 | duration: Duration(milliseconds: 300), curve: Curves.easeOutCubic); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/view_model/search_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:collection/collection.dart'; 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:localstorage/localstorage.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:fun_android/model/search.dart'; 8 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 9 | import 'package:fun_android/provider/view_state_list_model.dart'; 10 | import 'package:fun_android/service/wan_android_repository.dart'; 11 | 12 | const String kLocalStorageSearch = 'kLocalStorageSearch'; 13 | const String kSearchHotList = 'kSearchHotList'; 14 | const String kSearchHistory = 'kSearchHistory'; 15 | 16 | class SearchHotKeyModel extends ViewStateListModel { 17 | @override 18 | Future loadData() async { 19 | LocalStorage localStorage = LocalStorage(kLocalStorageSearch); 20 | // localStorage.deleteItem(keySearchHotList);//测试没有缓存 21 | await localStorage.ready; 22 | List localList = (localStorage.getItem(kSearchHotList) ?? []).map((item) { 23 | return SearchHotKey.fromMap(item); 24 | }).toList(); 25 | 26 | if (localList.isEmpty) { 27 | //缓存为空,需要同步加载网络数据 28 | List netList = await WanAndroidRepository.fetchSearchHotKey(); 29 | localStorage.setItem(kSearchHotList, netList); 30 | return netList; 31 | } else { 32 | // localList.removeRange(0, 3);//测试缓存与网络数据不一致 33 | WanAndroidRepository.fetchSearchHotKey().then((netList) { 34 | netList = netList ?? []; 35 | if (!ListEquality().equals(netList, localList)) { 36 | list = netList; 37 | localStorage.setItem(kSearchHotList, list); 38 | setIdle(); 39 | } 40 | }); 41 | return localList; 42 | } 43 | } 44 | 45 | shuffle(){ 46 | list.shuffle(); 47 | notifyListeners(); 48 | } 49 | 50 | } 51 | 52 | class SearchHistoryModel extends ViewStateListModel { 53 | clearHistory() async { 54 | debugPrint('clearHistory'); 55 | var sharedPreferences = await SharedPreferences.getInstance(); 56 | sharedPreferences.remove(kSearchHistory); 57 | list.clear(); 58 | setEmpty(); 59 | } 60 | 61 | addHistory(String keyword) async { 62 | var sharedPreferences = await SharedPreferences.getInstance(); 63 | var histories = sharedPreferences.getStringList(kSearchHistory) ?? []; 64 | histories 65 | ..remove(keyword) 66 | ..insert(0, keyword); 67 | await sharedPreferences.setStringList(kSearchHistory, histories); 68 | notifyListeners(); 69 | } 70 | 71 | @override 72 | Future> loadData() async { 73 | var sharedPreferences = await SharedPreferences.getInstance(); 74 | return sharedPreferences.getStringList(kSearchHistory) ?? []; 75 | } 76 | } 77 | 78 | class SearchResultModel extends ViewStateRefreshListModel { 79 | final String keyword; 80 | final SearchHistoryModel searchHistoryModel; 81 | 82 | SearchResultModel({this.keyword, this.searchHistoryModel}); 83 | 84 | @override 85 | Future loadData({int pageNum}) async { 86 | if (keyword.isEmpty) return []; 87 | searchHistoryModel.addHistory(keyword); 88 | return await WanAndroidRepository.fetchSearchResult( 89 | key: keyword, pageNum: pageNum); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/view_model/setting_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:fun_android/config/storage_manager.dart'; 3 | 4 | /// 使用原生WebView 5 | const String kUseWebViewPlugin = 'kUseWebViewPlugin'; 6 | 7 | class UseWebViewPluginModel extends ChangeNotifier { 8 | get value => 9 | StorageManager.sharedPreferences.getBool(kUseWebViewPlugin) ?? false; 10 | 11 | switchValue(){ 12 | StorageManager.sharedPreferences 13 | .setBool(kUseWebViewPlugin, !value); 14 | notifyListeners(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lib/view_model/structure_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/provider/view_state_list_model.dart'; 2 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 3 | import 'package:fun_android/service/wan_android_repository.dart'; 4 | 5 | import 'favourite_model.dart'; 6 | 7 | class StructureCategoryModel extends ViewStateListModel { 8 | @override 9 | Future loadData() async { 10 | return await WanAndroidRepository.fetchTreeCategories(); 11 | } 12 | } 13 | 14 | class StructureListModel extends ViewStateRefreshListModel { 15 | final int cid; 16 | 17 | StructureListModel(this.cid); 18 | 19 | @override 20 | Future loadData({int pageNum}) async { 21 | return await WanAndroidRepository.fetchArticles(pageNum, cid: cid); 22 | } 23 | 24 | @override 25 | onCompleted(List data) { 26 | GlobalFavouriteStateModel.refresh(data); 27 | } 28 | } 29 | 30 | /// 网址导航 31 | class NavigationSiteModel extends ViewStateListModel { 32 | @override 33 | Future loadData() async { 34 | return await WanAndroidRepository.fetchNavigationSite(); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /lib/view_model/theme_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:fun_android/generated/l10n.dart'; 6 | import 'package:fun_android/ui/helper/theme_helper.dart'; 7 | import 'package:fun_android/config/storage_manager.dart'; 8 | 9 | //const Color(0xFF5394FF), 10 | 11 | class ThemeModel with ChangeNotifier { 12 | static const kThemeColorIndex = 'kThemeColorIndex'; 13 | static const kThemeUserDarkMode = 'kThemeUserDarkMode'; 14 | static const kFontIndex = 'kFontIndex'; 15 | 16 | static const fontValueList = ['system', 'kuaile']; 17 | 18 | /// 用户选择的明暗模式 19 | bool _userDarkMode; 20 | 21 | /// 当前主题颜色 22 | MaterialColor _themeColor; 23 | 24 | /// 当前字体索引 25 | int _fontIndex; 26 | 27 | ThemeModel() { 28 | /// 用户选择的明暗模式 29 | _userDarkMode = 30 | StorageManager.sharedPreferences.getBool(kThemeUserDarkMode) ?? false; 31 | 32 | /// 获取主题色 33 | _themeColor = Colors.primaries[ 34 | StorageManager.sharedPreferences.getInt(kThemeColorIndex) ?? 5]; 35 | 36 | /// 获取字体 37 | _fontIndex = StorageManager.sharedPreferences.getInt(kFontIndex) ?? 0; 38 | } 39 | 40 | int get fontIndex => _fontIndex; 41 | 42 | /// 切换指定色彩 43 | /// 44 | /// 没有传[brightness]就不改变brightness,color同理 45 | void switchTheme({bool userDarkMode, MaterialColor color}) { 46 | _userDarkMode = userDarkMode ?? _userDarkMode; 47 | _themeColor = color ?? _themeColor; 48 | notifyListeners(); 49 | saveTheme2Storage(_userDarkMode, _themeColor); 50 | } 51 | 52 | /// 随机一个主题色彩 53 | /// 54 | /// 可以指定明暗模式,不指定则保持不变 55 | void switchRandomTheme({Brightness brightness}) { 56 | int colorIndex = Random().nextInt(Colors.primaries.length - 1); 57 | switchTheme( 58 | userDarkMode: Random().nextBool(), 59 | color: Colors.primaries[colorIndex], 60 | ); 61 | } 62 | 63 | /// 切换字体 64 | switchFont(int index) { 65 | _fontIndex = index; 66 | switchTheme(); 67 | saveFontIndex(index); 68 | } 69 | 70 | /// 根据主题 明暗 和 颜色 生成对应的主题 71 | /// [dark]系统的Dark Mode 72 | themeData({bool platformDarkMode: false}) { 73 | var isDark = platformDarkMode || _userDarkMode; 74 | Brightness brightness = isDark ? Brightness.dark : Brightness.light; 75 | 76 | var themeColor = _themeColor; 77 | var accentColor = isDark ? themeColor[700] : _themeColor; 78 | var themeData = ThemeData( 79 | brightness: brightness, 80 | // 主题颜色属于亮色系还是属于暗色系(eg:dark时,AppBarTitle文字及状态栏文字的颜色为白色,反之为黑色) 81 | // 这里设置为dark目的是,不管App是明or暗,都将appBar的字体颜色的默认值设为白色. 82 | // 再AnnotatedRegion的方式,调整响应的状态栏颜色 83 | primaryColorBrightness: Brightness.dark, 84 | accentColorBrightness: Brightness.dark, 85 | primarySwatch: themeColor, 86 | accentColor: accentColor, 87 | fontFamily: fontValueList[fontIndex]); 88 | 89 | themeData = themeData.copyWith( 90 | brightness: brightness, 91 | accentColor: accentColor, 92 | cupertinoOverrideTheme: CupertinoThemeData( 93 | primaryColor: themeColor, 94 | brightness: brightness, 95 | ), 96 | 97 | appBarTheme: themeData.appBarTheme.copyWith(elevation: 0), 98 | splashColor: themeColor.withAlpha(50), 99 | hintColor: themeData.hintColor.withAlpha(90), 100 | errorColor: Colors.red, 101 | cursorColor: accentColor, 102 | textTheme: themeData.textTheme.copyWith( 103 | /// 解决中文hint不居中的问题 https://github.com/flutter/flutter/issues/40248 104 | subhead: themeData.textTheme.subhead 105 | .copyWith(textBaseline: TextBaseline.alphabetic)), 106 | textSelectionColor: accentColor.withAlpha(60), 107 | textSelectionHandleColor: accentColor.withAlpha(60), 108 | toggleableActiveColor: accentColor, 109 | chipTheme: themeData.chipTheme.copyWith( 110 | pressElevation: 0, 111 | padding: EdgeInsets.symmetric(horizontal: 10), 112 | labelStyle: themeData.textTheme.caption, 113 | backgroundColor: themeData.chipTheme.backgroundColor.withOpacity(0.1), 114 | ), 115 | // textTheme: CupertinoTextThemeData(brightness: Brightness.light) 116 | inputDecorationTheme: ThemeHelper.inputDecorationTheme(themeData), 117 | ); 118 | return themeData; 119 | } 120 | 121 | /// 数据持久化到shared preferences 122 | saveTheme2Storage(bool userDarkMode, MaterialColor themeColor) async { 123 | var index = Colors.primaries.indexOf(themeColor); 124 | await Future.wait([ 125 | StorageManager.sharedPreferences 126 | .setBool(kThemeUserDarkMode, userDarkMode), 127 | StorageManager.sharedPreferences.setInt(kThemeColorIndex, index) 128 | ]); 129 | } 130 | 131 | /// 根据索引获取字体名称,这里牵涉到国际化 132 | static String fontName(index, context) { 133 | switch (index) { 134 | case 0: 135 | return S.of(context).autoBySystem; 136 | case 1: 137 | return S.of(context).fontKuaiLe; 138 | default: 139 | return ''; 140 | } 141 | } 142 | 143 | /// 字体选择持久化 144 | static saveFontIndex(int index) async { 145 | await StorageManager.sharedPreferences.setInt(kFontIndex, index); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /lib/view_model/user_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:fun_android/config/storage_manager.dart'; 3 | import 'package:fun_android/model/user.dart'; 4 | 5 | import 'favourite_model.dart'; 6 | 7 | class UserModel extends ChangeNotifier { 8 | static const String kUser = 'kUser'; 9 | 10 | final GlobalFavouriteStateModel globalFavouriteStateModel; 11 | 12 | User _user; 13 | 14 | User get user => _user; 15 | 16 | bool get hasUser => user != null; 17 | 18 | UserModel({@required this.globalFavouriteStateModel}) { 19 | var userMap = StorageManager.localStorage.getItem(kUser); 20 | _user = userMap != null ? User.fromJsonMap(userMap) : null; 21 | } 22 | 23 | saveUser(User user) { 24 | _user = user; 25 | notifyListeners(); 26 | globalFavouriteStateModel.replaceAll(_user.collectIds); 27 | StorageManager.localStorage.setItem(kUser, user); 28 | } 29 | 30 | /// 清除持久化的用户数据 31 | clearUser() { 32 | _user = null; 33 | notifyListeners(); 34 | StorageManager.localStorage.deleteItem(kUser); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/view_model/wechat_account_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:fun_android/model/article.dart'; 2 | import 'package:fun_android/model/tree.dart'; 3 | import 'package:fun_android/provider/view_state_refresh_list_model.dart'; 4 | import 'package:fun_android/provider/view_state_list_model.dart'; 5 | import 'package:fun_android/service/wan_android_repository.dart'; 6 | 7 | import 'favourite_model.dart'; 8 | 9 | /// 微信公众号 10 | class WechatAccountCategoryModel extends ViewStateListModel { 11 | @override 12 | Future> loadData() async { 13 | return await WanAndroidRepository.fetchWechatAccounts(); 14 | } 15 | } 16 | 17 | /// 微信公众号文章 18 | class WechatArticleListModel extends ViewStateRefreshListModel
{ 19 | /// 公众号id 20 | final int id; 21 | 22 | WechatArticleListModel(this.id); 23 | 24 | @override 25 | Future> loadData({int pageNum}) async { 26 | return await WanAndroidRepository.fetchWechatAccountArticles(pageNum, id); 27 | } 28 | 29 | @override 30 | onCompleted(List data) { 31 | GlobalFavouriteStateModel.refresh(data); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: fun_android 2 | description: Fun or Flutter for Android 3 | 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: ">=2.8.0 <3.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | flutter_localizations: 13 | sdk: flutter 14 | cupertino_icons: ^1.0.0 15 | 16 | # State 17 | provider: ^4.3.2+2 18 | 19 | # Base Component 20 | # WebView iOS添加键值对 o.flutter.embedded_views_preview = YES 21 | webview_flutter: ^1.0.7 22 | flutter_webview_plugin: ^0.3.11 23 | device_info: ^0.4.2+7 24 | package_info: ^0.4.3 25 | quiver: ^2.1.3 26 | # connectivity: ^0.4.3+6 27 | 28 | # Data 29 | # json_annotation: ^2.4.0 30 | dio: ^3.0.10 31 | dio_cookie_manager: ^1.0.0 32 | cookie_jar: ^1.0.1 33 | shared_preferences: ^2.0.5 34 | # 本地json对象存储 35 | localstorage: ^4.0.0+1 36 | 37 | # Html Encoder/Decoder 38 | html_unescape: ^1.0.2 39 | # 解析html标签 40 | flutter_html: ^1.3.0 41 | 42 | # 判断list是否相等 43 | collection: ^1.14.13 44 | # crypto: ^2.0.6 45 | 46 | # UI 47 | oktoast: ^2.3.2 48 | pull_to_refresh: ^1.6.1 49 | flutter_swiper: ^1.1.6 50 | # cached_network_image: ^1.1.1 51 | cached_network_image: ^2.3.1 52 | # 类iOS侧滑菜单 53 | flutter_slidable: ^0.5.7 54 | # 谁用谁闪亮 55 | shimmer: ^1.1.1 56 | # 识别图片的主要颜色 57 | # palette_generator: ^0.2.0 58 | # 更新展示支持mark_down 59 | flutter_markdown: ^0.4.4 60 | flare_flutter: ^2.0.6 61 | 62 | # third_app 63 | # 启动第三方app 64 | url_launcher: ^5.1.6 65 | share: 0.6.2+1 #分享 66 | launch_review: ^2.0.0 67 | open_file: ^3.0.1 68 | 69 | # android权限弹窗 70 | # permission_handler: ^3.2.2 71 | 72 | # 上报 目前问题比较多 73 | # flutter_bugly: 0.2.7 74 | 75 | dev_dependencies: 76 | flutter_test: 77 | sdk: flutter 78 | 79 | # build_runner: ^1.6.2 80 | # json_serializable: ^3.1.0 81 | 82 | flutter: 83 | uses-material-design: true 84 | fonts: 85 | - family: iconfont 86 | fonts: 87 | - asset: assets/fonts/iconfont.ttf 88 | - family: kuaile # 站酷 快乐字体 https://fonts.google.com/?selection.family=Noto+Sans+SC&subset=chinese-simplified 89 | fonts: 90 | - asset: assets/fonts/ZCOOLKuaiLe-Regular.ttf 91 | assets: 92 | - assets/animations/ 93 | - assets/images/ 94 | - CHANGELOG.md 95 | 96 | flutter_intl: 97 | enabled: true 98 | -------------------------------------------------------------------------------- /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 | void main() { 11 | // testWidgets('Counter increments smoke test', (WidgetTester tester) async { 12 | // // Build our app and trigger a frame. 13 | // await tester.pumpWidget(App()); 14 | // 15 | // // Verify that our counter starts at 0. 16 | // expect(find.text('0'), findsOneWidget); 17 | // expect(find.text('1'), findsNothing); 18 | // 19 | // // Tap the '+' icon and trigger a frame. 20 | // await tester.tap(find.byIcon(Icons.add)); 21 | // await tester.pump(); 22 | // 23 | // // Verify that our counter has incremented. 24 | // expect(find.text('0'), findsNothing); 25 | // expect(find.text('1'), findsOneWidget); 26 | // }); 27 | 28 | test(('copy array'), () { 29 | var a = [1000000000, 2, 3]; 30 | var b = [...a]; 31 | a[0] = 111; 32 | print(a); 33 | print(b); 34 | 35 | var aa = ['我还没改名1', '我还没改名2','我还没改名3']; 36 | var bb = [...aa]; 37 | aa[0] = '我已经改名啦1'; 38 | print(aa); 39 | print(bb); 40 | 41 | // var aa = [Person('11'),Person('22'),Person('33')]; 42 | 43 | var aaa = [ 44 | Person('111'), 45 | Person('222'), 46 | Person('222') 47 | ]; 48 | var bbb = [...aaa]; 49 | aaa[0].name = '我要改名了'; 50 | print(aaa); 51 | print(bbb); 52 | }); 53 | } 54 | 55 | class Person { 56 | String name; 57 | 58 | Person(this.name); 59 | 60 | @override 61 | String toString() { 62 | return 'Person{name: $name}'; 63 | } 64 | } 65 | --------------------------------------------------------------------------------