├── ios
├── Runner
│ ├── zh-Hans.lproj
│ │ ├── Main.strings
│ │ └── InfoPlist.strings
│ ├── Runner-Bridging-Header.h
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── icon-29.png
│ │ │ ├── icon-40.png
│ │ │ ├── icon-76.png
│ │ │ ├── icon-1024.png
│ │ │ ├── icon-20@2x.png
│ │ │ ├── icon-20@3x.png
│ │ │ ├── icon-29@2x.png
│ │ │ ├── icon-29@3x.png
│ │ │ ├── icon-40@2x.png
│ │ │ ├── icon-40@3x.png
│ │ │ ├── icon-60@2x.png
│ │ │ ├── icon-60@3x.png
│ │ │ ├── icon-76@2x.png
│ │ │ ├── icon-20-ipad.png
│ │ │ ├── icon-29-ipad.png
│ │ │ ├── icon-83.5@2x.png
│ │ │ ├── icon-20@2x-ipad.png
│ │ │ ├── icon-29@2x-ipad.png
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ └── Icon-App-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ ├── README.md
│ │ │ └── Contents.json
│ ├── en.lproj
│ │ └── InfoPlist.strings
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── Main.storyboard
│ │ └── LaunchScreen.storyboard
│ └── Info.plist
├── Flutter
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ └── AppFrameworkInfo.plist
├── Runner.xcodeproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── IDEWorkspaceChecks.plist
├── .gitignore
└── Podfile
├── .fvm
├── flutter_sdk
└── fvm_config.json
├── assets
├── images
│ └── logo.png
└── templates
│ ├── news
│ └── news.html
│ ├── blog
│ ├── blog.html
│ └── knowledge.html
│ └── js
│ └── common.js
├── android
├── app
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── values-en
│ │ │ │ │ └── strings.xml
│ │ │ │ ├── values-zh
│ │ │ │ │ └── strings.xml
│ │ │ │ ├── values
│ │ │ │ │ ├── strings.xml
│ │ │ │ │ └── styles.xml
│ │ │ │ ├── playstore-icon.png
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-ldpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── xml
│ │ │ │ │ └── network_security_config.xml
│ │ │ │ ├── drawable
│ │ │ │ │ └── launch_background.xml
│ │ │ │ ├── drawable-v21
│ │ │ │ │ └── launch_background.xml
│ │ │ │ └── values-night
│ │ │ │ │ └── styles.xml
│ │ │ ├── kotlin
│ │ │ │ └── com
│ │ │ │ │ └── xycz
│ │ │ │ │ └── cnblogs
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── debug
│ │ │ └── AndroidManifest.xml
│ │ └── profile
│ │ │ └── AndroidManifest.xml
│ └── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
└── build.gradle
├── screenshot
├── screenshot_dark.jpg
└── screenshot_light.jpg
├── document
└── new_version.json
├── lib
├── requests
│ ├── base
│ │ ├── api.dart
│ │ ├── oauth_interceptor.dart
│ │ └── app_log_interceptor.dart
│ ├── common_request.dart
│ ├── search_request.dart
│ ├── oauth_request.dart
│ └── user_request.dart
├── modules
│ ├── blogs
│ │ ├── home
│ │ │ ├── knowledge
│ │ │ │ ├── blogs_knowledge_controller.dart
│ │ │ │ └── blogs_knowledge_view.dart
│ │ │ ├── blogs_list_view.dart
│ │ │ ├── blogs_list_controller.dart
│ │ │ ├── blogs_home_page.dart
│ │ │ └── blogs_home_controller.dart
│ │ ├── comment
│ │ │ ├── blog_comment_page.dart
│ │ │ └── blog_comment_controller.dart
│ │ └── knowledge_content
│ │ │ ├── knowledge_content_page.dart
│ │ │ └── knowledge_content_controller.dart
│ ├── news
│ │ ├── home
│ │ │ ├── news_list_controller.dart
│ │ │ ├── news_list_view.dart
│ │ │ ├── news_home_page.dart
│ │ │ └── news_home_controller.dart
│ │ ├── comment
│ │ │ ├── news_comment_page.dart
│ │ │ └── news_comment_controller.dart
│ │ └── content
│ │ │ └── news_content_controller.dart
│ ├── questions
│ │ ├── detail
│ │ │ └── question_detail_controller.dart
│ │ ├── home
│ │ │ ├── questions_list_view.dart
│ │ │ ├── questions_home_page.dart
│ │ │ ├── questions_list_controller.dart
│ │ │ └── questions_home_controller.dart
│ │ └── comment
│ │ │ ├── answer_comment_page.dart
│ │ │ └── answer_comment_controller.dart
│ ├── user
│ │ ├── blogs
│ │ │ ├── user_blogs_controller.dart
│ │ │ └── user_blogs_page.dart
│ │ ├── bookmark
│ │ │ ├── bookmark_controller.dart
│ │ │ └── bookmark_page.dart
│ │ ├── login
│ │ │ ├── login_page.dart
│ │ │ └── login_controller.dart
│ │ └── home
│ │ │ └── user_home_controller.dart
│ ├── statuses
│ │ └── home
│ │ │ ├── statuses_list_view.dart
│ │ │ ├── statuses_home_page.dart
│ │ │ └── statuses_list_controller.dart
│ ├── other
│ │ ├── web_view
│ │ │ └── web_view_controller.dart
│ │ └── debug_log_page.dart
│ ├── search
│ │ └── search_list_view_controlelr.dart
│ └── indexed
│ │ ├── indexed_controller.dart
│ │ └── indexed_page.dart
├── widgets
│ ├── status
│ │ ├── app_loadding_widget.dart
│ │ ├── app_empty_widget.dart
│ │ ├── app_not_login_widget.dart
│ │ └── app_error_widget.dart
│ ├── keep_alive_wrapper.dart
│ ├── net_image.dart
│ ├── custom_html.dart
│ ├── items
│ │ ├── blog_comment_item_widget.dart
│ │ ├── answer_comment_item_widget.dart
│ │ ├── statuses_comment_item_widget.dart
│ │ └── knowledge_item_widget.dart
│ └── page_list_view.dart
├── models
│ ├── oauth
│ │ ├── token_model.dart
│ │ └── user_token_model.dart
│ ├── version_model.dart
│ ├── blogs
│ │ ├── user_blog_info_model.dart
│ │ ├── knowledge_list_item_model.dart
│ │ ├── blog_comment_item_model.dart
│ │ ├── blog_list_item_model.dart
│ │ ├── blog_list_item_v2_model.dart
│ │ └── blog_content_model.dart
│ ├── news
│ │ ├── news_list_item_model.dart
│ │ └── news_comment_item_model.dart
│ ├── user
│ │ ├── bookmark_list_item_model.dart
│ │ └── user_info_model.dart
│ ├── statuses
│ │ ├── statuses_comment_item_model.dart
│ │ └── statuses_list_item_model.dart
│ └── search
│ │ └── search_item_model.dart
├── app
│ ├── event_bus.dart
│ ├── app_error.dart
│ ├── log.dart
│ └── controller
│ │ └── base_webview_controller.dart
├── routes
│ └── route_path.dart
└── services
│ ├── api_service.dart
│ ├── local_storage_service.dart
│ └── user_service.dart
├── .gitignore
├── LICENSE
├── test
└── widget_test.dart
├── README.md
├── analysis_options.yaml
├── pubspec.yaml
└── .metadata
/ios/Runner/zh-Hans.lproj/Main.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.fvm/flutter_sdk:
--------------------------------------------------------------------------------
1 | C:/Users/DW-SVN-SERVER/fvm/versions/3.13.9
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/.fvm/fvm_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "flutterSdkVersion": "3.13.9",
3 | "flavors": {}
4 | }
--------------------------------------------------------------------------------
/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/assets/images/logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Cnblogs
3 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-zh/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 博客园
3 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Cnblogs
3 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/screenshot/screenshot_dark.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/screenshot/screenshot_dark.jpg
--------------------------------------------------------------------------------
/screenshot/screenshot_light.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/screenshot/screenshot_light.jpg
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/android/app/src/main/res/playstore-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/playstore-icon.png
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/xycz/cnblogs/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.xycz.cnblogs
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/document/new_version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.8",
3 | "version_num": 10008,
4 | "version_desc": "1. 支持查看新闻评论\n2. 修复APP标题显示错误 #4",
5 | "download_url": "https://github.com/xiaoyaocz/flutter_cnblogs/releases"
6 | }
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
6 |
--------------------------------------------------------------------------------
/ios/Runner/zh-Hans.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /*
2 | InfoPlist.strings
3 | Runner
4 |
5 | Created by xiaoyaocz on 2022/12/12.
6 |
7 | */
8 | "CFBundleDisplayName" = "博客园";
9 | "NSCameraUsageDescription" = "需要使用摄像头扫描二维码";
10 | "NSPhotoLibraryUsageDescription" = "保存图片至你的相册";
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | /*
2 | InfoPlist.strings
3 | Runner
4 |
5 | Created by xiaoyaocz on 2022/12/12.
6 |
7 | */
8 | "CFBundleDisplayName" = "Cnblogs";
9 | "NSCameraUsageDescription" = "This app needs camera access to scan QR codes";
10 | "NSPhotoLibraryUsageDescription" = "Save pictures to your album";
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/ios/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.
--------------------------------------------------------------------------------
/lib/requests/base/api.dart:
--------------------------------------------------------------------------------
1 | class Api {
2 | /// 服务器地址
3 | static const String kBaseUrl = "https://api.cnblogs.com";
4 |
5 | /// 服务器地址
6 | static const String kOAuthBaseUrl = "https://oauth.cnblogs.com";
7 |
8 | // ClientID、ClientSecret请自行前往:https://api.cnblogs.com/help 申请
9 |
10 | /// 客户端ID
11 | static String kClientID = "";
12 |
13 | /// 客户端密钥
14 | static String kClientSecret = "";
15 | }
16 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
4 | def properties = new Properties()
5 |
6 | assert localPropertiesFile.exists()
7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
8 |
9 | def flutterSdkPath = properties.getProperty("flutter.sdk")
10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
12 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/modules/blogs/home/knowledge/blogs_knowledge_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/models/blogs/knowledge_list_item_model.dart';
3 | import 'package:flutter_cnblogs/requests/blogs_request.dart';
4 |
5 | class BlogsKnowledgeController
6 | extends BasePageController {
7 | final BlogsRequest blogsRequest = BlogsRequest();
8 |
9 | @override
10 | Future> getData(int page, int pageSize) async {
11 | return await blogsRequest.getKbArticles(pageIndex: page);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/widgets/status/app_loadding_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:lottie/lottie.dart';
4 |
5 | class AppLoaddingWidget extends StatelessWidget {
6 | const AppLoaddingWidget({Key? key}) : super(key: key);
7 |
8 | @override
9 | Widget build(BuildContext context) {
10 | return Center(
11 | child: Padding(
12 | padding: AppStyle.edgeInsetsA12,
13 | child: LottieBuilder.asset(
14 | 'assets/lotties/loadding.json',
15 | width: 200,
16 | ),
17 | ),
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/requests/common_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:dio/dio.dart';
2 | import 'package:flutter_cnblogs/models/version_model.dart';
3 |
4 | /// 通用的请求
5 | class CommonRequest {
6 | /// 检查更新
7 | Future checkUpdate() async {
8 | var result = await Dio().get(
9 | "https://cdn.jsdelivr.net/gh/xiaoyaocz/flutter_cnblogs@master/document/new_version.json",
10 | queryParameters: {
11 | "ts": DateTime.now().millisecondsSinceEpoch,
12 | },
13 | options: Options(
14 | responseType: ResponseType.json,
15 | ),
16 | );
17 | return VersionModel.fromJson(result.data);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/widgets/keep_alive_wrapper.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class KeepAliveWrapper extends StatefulWidget {
4 | final Widget child;
5 |
6 | const KeepAliveWrapper({Key? key, required this.child}) : super(key: key);
7 |
8 | @override
9 | State createState() => _KeepAliveWrapperState();
10 | }
11 |
12 | class _KeepAliveWrapperState extends State
13 | with AutomaticKeepAliveClientMixin {
14 | @override
15 | Widget build(BuildContext context) {
16 | super.build(context);
17 | return widget.child;
18 | }
19 |
20 | @override
21 | bool get wantKeepAlive => true;
22 | }
23 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | **/dgph
2 | *.mode1v3
3 | *.mode2v3
4 | *.moved-aside
5 | *.pbxuser
6 | *.perspectivev3
7 | **/*sync/
8 | .sconsign.dblite
9 | .tags*
10 | **/.vagrant/
11 | **/DerivedData/
12 | Icon?
13 | **/Pods/
14 | **/.symlinks/
15 | profile
16 | xcuserdata
17 | **/.generated/
18 | Flutter/App.framework
19 | Flutter/Flutter.framework
20 | Flutter/Flutter.podspec
21 | Flutter/Generated.xcconfig
22 | Flutter/ephemeral/
23 | Flutter/app.flx
24 | Flutter/app.zip
25 | Flutter/flutter_assets/
26 | Flutter/flutter_export_environment.sh
27 | ServiceDefinitions.json
28 | Runner/GeneratedPluginRegistrant.*
29 |
30 | # Exceptions to above rules.
31 | !default.mode1v3
32 | !default.mode2v3
33 | !default.pbxuser
34 | !default.perspectivev3
35 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.7.10'
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 |
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:7.2.0'
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 | }
12 | }
13 |
14 | allprojects {
15 | repositories {
16 | google()
17 | mavenCentral()
18 | }
19 | }
20 |
21 | rootProject.buildDir = '../build'
22 | subprojects {
23 | project.buildDir = "${rootProject.buildDir}/${project.name}"
24 | }
25 | subprojects {
26 | project.evaluationDependsOn(':app')
27 | }
28 |
29 | tasks.register("clean", Delete) {
30 | delete rootProject.buildDir
31 | }
32 |
--------------------------------------------------------------------------------
/assets/templates/news/news.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
15 |
16 |
17 |
18 |
24 |
25 |
26 | @content
27 |
28 |
29 |
@stat
30 |
@puttime
31 |
32 |
33 |
36 |
37 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 11.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .packages
31 | .pub-cache/
32 | .pub/
33 | /build/
34 |
35 | # Symbolication related
36 | app.*.symbols
37 |
38 | # Obfuscation related
39 | app.*.map.json
40 |
41 | # Android Studio will place build artifacts here
42 | /android/app/debug
43 | /android/app/profile
44 | /android/app/release
45 |
46 | *.env
47 |
48 | pubspec.lock
--------------------------------------------------------------------------------
/assets/templates/blog/blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
15 |
16 |
17 |
18 |
28 |
29 |
30 | @content
31 |
32 |
33 |
@stat
34 |
@puttime
35 |
36 |
37 |
40 |
41 |
--------------------------------------------------------------------------------
/lib/modules/news/home/news_list_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/models/news/news_list_item_model.dart';
4 | import 'package:flutter_cnblogs/requests/news_request.dart';
5 |
6 | class NewsListController extends BasePageController {
7 | final String title;
8 | NewsListController(this.title);
9 | final NewsRequest newsRequest = NewsRequest();
10 |
11 | @override
12 | Future> getData(int page, int pageSize) async {
13 | if (title == LocaleKeys.news_home_hot) {
14 | return await newsRequest.getHot(pageIndex: page);
15 | } else if (title == LocaleKeys.news_home_hot_week) {
16 | return await newsRequest.getHotWeek(pageIndex: page);
17 | } else if (title == LocaleKeys.news_home_recommended) {
18 | return await newsRequest.getRecommended(pageIndex: page);
19 | } else {
20 | return await newsRequest.getNew(pageIndex: page);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/models/oauth/token_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class TokenModel {
11 | TokenModel({
12 | required this.accessToken,
13 | required this.expiresIn,
14 | required this.tokenType,
15 | required this.scope,
16 | });
17 |
18 | factory TokenModel.fromJson(Map json) => TokenModel(
19 | accessToken: asT(json['access_token'])!,
20 | expiresIn: asT(json['expires_in'])!,
21 | tokenType: asT(json['token_type'])!,
22 | scope: asT(json['scope'])!,
23 | );
24 |
25 | String accessToken;
26 | int expiresIn;
27 | String tokenType;
28 | String scope;
29 |
30 | @override
31 | String toString() {
32 | return jsonEncode(this);
33 | }
34 |
35 | Map toJson() => {
36 | 'access_token': accessToken,
37 | 'expires_in': expiresIn,
38 | 'token_type': tokenType,
39 | 'scope': scope,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/lib/modules/blogs/home/knowledge/blogs_knowledge_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/blogs/home/knowledge/blogs_knowledge_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/keep_alive_wrapper.dart';
5 | import 'package:flutter_cnblogs/widgets/items/knowledge_item_widget.dart';
6 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
7 | import 'package:get/get.dart';
8 |
9 | class BlogsKnowledgeView extends GetView {
10 | const BlogsKnowledgeView({Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return KeepAliveWrapper(
15 | child: PageListView(
16 | pageController: controller,
17 | padding: AppStyle.edgeInsetsV12,
18 | firstRefresh: true,
19 | itemBuilder: (_, i) {
20 | var item = controller.list[i];
21 | return KnowledgeItemWidget(
22 | item,
23 | );
24 | },
25 | ),
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/assets/templates/blog/knowledge.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
24 |
25 |
26 |
27 |
34 |
35 |
36 | @content
37 |
38 |
39 |
@stat
40 |
@puttime
41 |
42 |
43 |
46 |
47 |
--------------------------------------------------------------------------------
/lib/models/version_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class VersionModel {
11 | VersionModel({
12 | required this.version,
13 | required this.versionNum,
14 | required this.versionDesc,
15 | required this.downloadUrl,
16 | });
17 |
18 | factory VersionModel.fromJson(Map json) => VersionModel(
19 | version: asT(json['version'])!,
20 | versionNum: asT(json['version_num'])!,
21 | versionDesc: asT(json['version_desc'])!,
22 | downloadUrl: asT(json['download_url'])!,
23 | );
24 |
25 | String version;
26 | int versionNum;
27 | String versionDesc;
28 | String downloadUrl;
29 |
30 | @override
31 | String toString() {
32 | return jsonEncode(this);
33 | }
34 |
35 | Map toJson() => {
36 | 'version': version,
37 | 'version_num': versionNum,
38 | 'version_desc': versionDesc,
39 | 'download_url': downloadUrl,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 xiaoyaocz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/lib/modules/questions/detail/question_detail_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/models/questions/answer_list_item_model.dart';
3 | import 'package:flutter_cnblogs/models/questions/question_list_item_model.dart';
4 | import 'package:flutter_cnblogs/requests/questions_request.dart';
5 | import 'package:get/get.dart';
6 |
7 | class QuestionDetailController extends BaseController {
8 | final int questionId;
9 | QuestionDetailController(this.questionId);
10 | final QuestionsRequest request = QuestionsRequest();
11 | Rx detail = Rx(null);
12 | RxList answers = RxList();
13 | @override
14 | void onInit() {
15 | loadData();
16 | super.onInit();
17 | }
18 |
19 | void loadData() async {
20 | detail.value = await request.getQuestionById(questionId: questionId);
21 | var answerList = await request.getQuestionAnswers(questionId: questionId);
22 | answerList.sort((b, a) => a.sort.compareTo(b.sort));
23 | answers.value = answerList;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/modules/blogs/home/blogs_list_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/blogs/home/blogs_list_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/items/blog_item_widget.dart';
5 | import 'package:flutter_cnblogs/widgets/keep_alive_wrapper.dart';
6 |
7 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
8 | import 'package:get/get.dart';
9 |
10 | class BlogsListView extends StatelessWidget {
11 | final String tag;
12 | const BlogsListView(this.tag, {Key? key}) : super(key: key);
13 | BlogsListController get controller => Get.find(tag: tag);
14 | @override
15 | Widget build(BuildContext context) {
16 | return KeepAliveWrapper(
17 | child: PageListView(
18 | pageController: controller,
19 | padding: AppStyle.edgeInsetsA4,
20 | firstRefresh: true,
21 | itemBuilder: (_, i) {
22 | var item = controller.list[i];
23 | return BlogItemWidget(
24 | item,
25 | );
26 | },
27 | ),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/app/event_bus.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter_cnblogs/app/log.dart';
4 |
5 | /// 全局事件
6 | class EventBus {
7 | /// 登录
8 | static const String kLogined = "Logined";
9 |
10 | /// 注销登录
11 | static const String kLogouted = "Logouted";
12 |
13 | /// 点击了底部导航
14 | static const String kBottomNavigationBarClicked =
15 | "BottomNavigationBarClicked";
16 |
17 | static EventBus? _instance;
18 |
19 | static EventBus get instance {
20 | _instance ??= EventBus();
21 | return _instance!;
22 | }
23 |
24 | final Map _streams = {};
25 |
26 | /// 触发事件
27 | void emit(String name, T data) {
28 | if (!_streams.containsKey(name)) {
29 | _streams.addAll({name: StreamController.broadcast()});
30 | }
31 | Log.d("Emit Event:$name\r\n$data");
32 |
33 | _streams[name]!.add(data);
34 | }
35 |
36 | /// 监听事件
37 | StreamSubscription listen(String name, Function(dynamic)? onData) {
38 | if (!_streams.containsKey(name)) {
39 | _streams.addAll({name: StreamController.broadcast()});
40 | }
41 | return _streams[name]!.stream.listen(onData);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/modules/user/blogs/user_blogs_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/app/log.dart';
3 | import 'package:flutter_cnblogs/models/blogs/blog_list_item_model.dart';
4 | import 'package:flutter_cnblogs/requests/blogs_request.dart';
5 | import 'package:get/get.dart';
6 |
7 | class UserBlogsController extends BasePageController {
8 | final String blogApp;
9 | UserBlogsController(this.blogApp);
10 | final BlogsRequest blogsRequest = BlogsRequest();
11 |
12 | var name = "".obs;
13 | var subtitle = "".obs;
14 | @override
15 | void onInit() {
16 | loadBlogInfo();
17 | super.onInit();
18 | }
19 |
20 | void loadBlogInfo() async {
21 | try {
22 | var result = await blogsRequest.getUserBlogsInfo(blogApp);
23 | name.value = result.title;
24 | subtitle.value = result.subtitle;
25 | } catch (e) {
26 | Log.logPrint(e);
27 | }
28 | }
29 |
30 | @override
31 | Future> getData(int page, int pageSize) async {
32 | return await blogsRequest.getUserBlogs(blogApp: blogApp, pageIndex: page);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/modules/questions/home/questions_list_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/questions/home/questions_list_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/keep_alive_wrapper.dart';
5 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
6 | import 'package:flutter_cnblogs/widgets/items/question_item_widget.dart';
7 | import 'package:get/get.dart';
8 |
9 | class QuestionsListView extends StatelessWidget {
10 | final String tag;
11 | const QuestionsListView(this.tag, {Key? key}) : super(key: key);
12 | QuestionsListController get controller =>
13 | Get.find(tag: tag);
14 | @override
15 | Widget build(BuildContext context) {
16 | return KeepAliveWrapper(
17 | child: PageListView(
18 | pageController: controller,
19 | padding: AppStyle.edgeInsetsA4,
20 | firstRefresh: true,
21 | itemBuilder: (_, i) {
22 | var item = controller.list[i];
23 | return QuestionItemWidget(
24 | item,
25 | );
26 | },
27 | ),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/modules/user/bookmark/bookmark_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/models/user/bookmark_list_item_model.dart';
3 | import 'package:flutter_cnblogs/requests/user_request.dart';
4 | import 'package:flutter_cnblogs/routes/app_navigation.dart';
5 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
6 |
7 | class BookmarkController extends BasePageController {
8 | final UserRequest userRequest = UserRequest();
9 |
10 | @override
11 | Future> getData(int page, int pageSize) async {
12 | return await userRequest.getBookmarks(pageIndex: page);
13 | }
14 |
15 | @override
16 | void onLogin() {
17 | easyRefreshController.callRefresh();
18 | super.onLogin();
19 | }
20 |
21 | void openUrl(BookmarkListItemModel item) {
22 | AppNavigator.toWebView(item.linkUrl);
23 | }
24 |
25 | void delete(BookmarkListItemModel item) async {
26 | try {
27 | await userRequest.deleteBookmark(item.wzLinkId);
28 | list.remove(item);
29 | } catch (e) {
30 | SmartDialog.showToast(e.toString());
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | // This is a basic Flutter widget test.
2 | //
3 | // To perform an interaction with a widget in your test, use the WidgetTester
4 | // utility in the flutter_test package. For example, you can send tap and scroll
5 | // gestures. You can also use WidgetTester to find child widgets in the widget
6 | // tree, read text, and verify that the values of widget properties are correct.
7 |
8 | import 'package:flutter/material.dart';
9 | import 'package:flutter_test/flutter_test.dart';
10 |
11 | import 'package:flutter_cnblogs/main.dart';
12 |
13 | void main() {
14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async {
15 | // Build our app and trigger a frame.
16 | await tester.pumpWidget(const MyApp());
17 |
18 | // Verify that our counter starts at 0.
19 | expect(find.text('0'), findsOneWidget);
20 | expect(find.text('1'), findsNothing);
21 |
22 | // Tap the '+' icon and trigger a frame.
23 | await tester.tap(find.byIcon(Icons.add));
24 | await tester.pump();
25 |
26 | // Verify that our counter has incremented.
27 | expect(find.text('0'), findsNothing);
28 | expect(find.text('1'), findsOneWidget);
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/lib/modules/blogs/home/blogs_list_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/models/blogs/blog_list_item_model.dart';
4 | import 'package:flutter_cnblogs/requests/blogs_request.dart';
5 |
6 | class BlogsListController extends BasePageController {
7 | final String title;
8 | BlogsListController(this.title);
9 | final BlogsRequest blogsRequest = BlogsRequest();
10 |
11 | @override
12 | Future> getData(int page, int pageSize) async {
13 | if (title == LocaleKeys.blogs_home_new) {
14 | return await blogsRequest.getSitehome(pageIndex: page);
15 | } else if (title == LocaleKeys.blogs_home_mostliked) {
16 | return await blogsRequest.getMostliked(pageIndex: page);
17 | } else if (title == LocaleKeys.blogs_home_picked) {
18 | return await blogsRequest.getPicked(pageIndex: page);
19 | } else if (title == LocaleKeys.blogs_home_mostread) {
20 | return await blogsRequest.getMostRead(pageIndex: page);
21 | } else {
22 | return await blogsRequest.getSitehome(pageIndex: page);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/routes/route_path.dart:
--------------------------------------------------------------------------------
1 | /// 路由路径
2 | class RoutePath {
3 | /// 首页
4 | static const kIndex = "/index";
5 |
6 | /// 登录
7 | static const kUserLogin = "/user/login";
8 |
9 | /// 博客内容
10 | static const kBlogContent = "/blogs/content";
11 |
12 | /// 博文评论
13 | static const kBlogComment = "/blogs/comment";
14 |
15 | /// 知识库内容
16 | static const kKnowledgeContent = "/blogs/knowledge/content";
17 |
18 | /// 新闻内容
19 | static const kNewsContent = "/news/content";
20 |
21 | /// 用户博客
22 | static const kUserBlog = "/user/blog";
23 |
24 | /// 我的收藏
25 | static const kUserBookmark = "/user/bookmark";
26 |
27 | /// WebView
28 | static const kWebView = "/other/webview";
29 |
30 | /// 闪存详情与评论
31 | static const kStatusesDetail = "/statuses/detail";
32 |
33 | /// 发布闪存详情
34 | static const kStatusesAdd = "/statuses/add";
35 |
36 | /// 闪存标签
37 | static const kStatusesTag = "/statuses/tag";
38 |
39 | /// 搜索
40 | static const kSearch = "/search";
41 |
42 | /// 博问详情
43 | static const kQuestionDetail = "/question/detail";
44 |
45 | /// 回答评论
46 | static const kAnswerComment = "/question/answer/comment";
47 |
48 | /// 新闻评论
49 | static const kNewsComment = "/news/comment";
50 | }
51 |
--------------------------------------------------------------------------------
/lib/requests/search_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/models/search/search_item_model.dart';
2 | import 'package:flutter_cnblogs/requests/base/http_client.dart';
3 |
4 | class SearchRequest {
5 | /// 搜索
6 | /// - https://api.cnblogs.com/api/ZzkDocuments/{category}?keyWords={keyWords}&pageIndex={pageIndex}&startDate={startDate}&endDate={endDate}&viewTimesAtLeast={viewTimesAtLeast}
7 | Future> search({
8 | required String category,
9 | required String keyword,
10 | required int pageIndex,
11 | String startDate = '',
12 | String endDate = '',
13 | int viewTimesAtLeast = 0,
14 | int pageSize = 20,
15 | }) async {
16 | List ls = [];
17 | var result = await HttpClient.instance.get(
18 | '/api/ZzkDocuments/$category',
19 | queryParameters: {
20 | 'keyWords': keyword,
21 | 'startDate': startDate,
22 | 'endDate': endDate,
23 | 'viewTimesAtLeast': viewTimesAtLeast,
24 | 'pageIndex': pageIndex,
25 | 'pageSize': pageSize,
26 | },
27 | withApiAuth: true,
28 | );
29 | for (var item in result) {
30 | ls.add(SearchItemModel.fromJson(item));
31 | }
32 | return ls;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/modules/statuses/home/statuses_list_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/statuses/home/statuses_list_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/keep_alive_wrapper.dart';
5 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
6 | import 'package:flutter_cnblogs/widgets/items/statuses_item_widget.dart';
7 | import 'package:get/get.dart';
8 |
9 | class StatusesListView extends StatelessWidget {
10 | final String tag;
11 | const StatusesListView(this.tag, {Key? key}) : super(key: key);
12 | StatusesListController get controller =>
13 | Get.find(tag: tag);
14 | @override
15 | Widget build(BuildContext context) {
16 | return KeepAliveWrapper(
17 | child: PageListView(
18 | pageController: controller,
19 | padding: AppStyle.edgeInsetsA8,
20 | firstRefresh: true,
21 | itemBuilder: (_, i) {
22 | var item = controller.list[i];
23 | return StatusesItemWidget(
24 | item,
25 | onDelete: () => controller.delete(item.id),
26 | );
27 | },
28 | ),
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/modules/user/blogs/user_blogs_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/user/blogs/user_blogs_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/items/blog_item_widget.dart';
5 |
6 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
7 | import 'package:get/get.dart';
8 |
9 | class UserBlogsPage extends StatelessWidget {
10 | final String blogApp;
11 | const UserBlogsPage(this.blogApp, {Key? key}) : super(key: key);
12 | UserBlogsController get controller => Get.put(
13 | UserBlogsController(blogApp),
14 | tag: blogApp,
15 | );
16 | @override
17 | Widget build(BuildContext context) {
18 | return Scaffold(
19 | appBar: AppBar(
20 | title: Obx(
21 | () => Text(controller.name.value),
22 | ),
23 | ),
24 | body: PageListView(
25 | pageController: controller,
26 | padding: AppStyle.edgeInsetsV8,
27 | firstRefresh: true,
28 | itemBuilder: (_, i) {
29 | var item = controller.list[i];
30 | return BlogItemWidget(
31 | item,
32 | );
33 | },
34 | ),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/models/oauth/user_token_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class UserTokenModel {
11 | UserTokenModel({
12 | required this.idToken,
13 | required this.accessToken,
14 | required this.expiresIn,
15 | required this.tokenType,
16 | required this.refreshToken,
17 | });
18 |
19 | factory UserTokenModel.fromJson(Map json) => UserTokenModel(
20 | idToken: asT(json['id_token'])!,
21 | accessToken: asT(json['access_token'])!,
22 | expiresIn: asT(json['expires_in'])!,
23 | tokenType: asT(json['token_type'])!,
24 | refreshToken: asT(json['refresh_token'])!,
25 | );
26 |
27 | String idToken;
28 | String accessToken;
29 | int expiresIn;
30 | String tokenType;
31 | String refreshToken;
32 |
33 | @override
34 | String toString() {
35 | return jsonEncode(this);
36 | }
37 |
38 | Map toJson() => {
39 | 'id_token': idToken,
40 | 'access_token': accessToken,
41 | 'expires_in': expiresIn,
42 | 'token_type': tokenType,
43 | 'refresh_token': refreshToken,
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 博客园Flutter
2 |
3 | 使用Flutter编写的博客园客户端,支持iOS及Android。
4 |
5 | 基于[博客园开放API](https://api.cnblogs.com/help)开发;受限于API,部分功能可能并不完善。
6 |
7 | 
8 |
9 | 
10 |
11 | ## 安装
12 |
13 | - Android:
14 | 1. 前往[Releases](https://github.com/xiaoyaocz/flutter_cnblogs/releases/latest)下载app-release.apk安装即可
15 | - iOS:
16 | 1. 前往[Releases](https://github.com/xiaoyaocz/flutter_cnblogs/releases/latest)下载ios_no_sign.ipa
17 | 2. 使用Sideloadly、iTools等工具自行签名安装
18 |
19 | ## 开发
20 |
21 | ## 环境
22 |
23 | Flutter:3.13.9
24 |
25 | ### 说明
26 |
27 | 开发前请先[申请博客园API KEY](https://oauth.cnblogs.com/),在根目录创建`.env`文件并写入以下内容
28 |
29 | ```
30 | CLIENT_ID=【申请的CLIENT_ID】
31 | CLIENT_SECRET=【申请的CLIENT_SECRET】
32 | ```
33 |
34 | ### 框架
35 |
36 | - `GetX` 状态管理、路由管理、国际化
37 | - `Dio` 网络请求
38 | - `Hive` 数据存储
39 |
40 | ### 目录结构
41 |
42 | - `app` 一些通用的类及样式
43 | - `services` 提供数据存储等服务
44 | - `requests` 请求的封装
45 | - `generated` 生成的国际化文件,使用 `get generate locales`生成
46 | - `modules` 模块,每个会有两个文件,view及controller
47 | - `widgets` 自定义的小组件
48 | - `routes` 路由定义
49 | - `models` 实体类
50 |
51 | ## TODO
52 |
53 | - 博客
54 | - [ ] 检查博文收藏状态(API似乎有问题)
55 | - 博问
56 | - [ ] 删除回答
57 | - [ ] 修改回答
58 | - [ ] 提问
59 | - [ ] 回答
60 | - [ ] 删除评论
61 | - [ ] 更新评论
62 |
63 |
--------------------------------------------------------------------------------
/lib/widgets/status/app_empty_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 | import 'package:lottie/lottie.dart';
5 | import 'package:get/get.dart';
6 |
7 | class AppEmptyWidget extends StatelessWidget {
8 | final Function()? onRefresh;
9 | const AppEmptyWidget({this.onRefresh, Key? key}) : super(key: key);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Center(
14 | child: GestureDetector(
15 | onTap: () {
16 | onRefresh?.call();
17 | },
18 | child: Padding(
19 | padding: AppStyle.edgeInsetsA12,
20 | child: Column(
21 | mainAxisSize: MainAxisSize.min,
22 | children: [
23 | LottieBuilder.asset(
24 | 'assets/lotties/empty.json',
25 | width: 200,
26 | height: 200,
27 | repeat: false,
28 | ),
29 | Text(
30 | LocaleKeys.state_empty.tr,
31 | textAlign: TextAlign.center,
32 | style: const TextStyle(fontSize: 12, color: Colors.grey),
33 | ),
34 | ],
35 | ),
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/modules/news/home/news_list_view.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/news/home/news_list_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/keep_alive_wrapper.dart';
5 | import 'package:flutter_cnblogs/widgets/items/news_item_widget.dart';
6 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
7 | import 'package:get/get.dart';
8 |
9 | class NewsListView extends StatelessWidget {
10 | final String tag;
11 | const NewsListView(this.tag, {Key? key}) : super(key: key);
12 | NewsListController get controller => Get.find(tag: tag);
13 | @override
14 | Widget build(BuildContext context) {
15 | return KeepAliveWrapper(
16 | child: PageListView(
17 | pageController: controller,
18 | padding: AppStyle.edgeInsetsA4,
19 | firstRefresh: true,
20 | itemBuilder: (_, i) {
21 | var item = controller.list[i];
22 | return NewsItemWidget(
23 | item,
24 | );
25 | },
26 | separatorBuilder: ((context, index) => Divider(
27 | height: 12,
28 | color: Colors.grey.withOpacity(.2),
29 | endIndent: 8,
30 | indent: 8,
31 | )),
32 | ),
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/app/app_error.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/generated/locales.g.dart';
2 | import 'package:get/get.dart';
3 |
4 | class AppError extends Error {
5 | /// 错误码
6 | final int code;
7 |
8 | /// 错误信息
9 | final String message;
10 |
11 | /// 是否是Http请求错误
12 | final bool isHttpError;
13 |
14 | final bool notLogin;
15 |
16 | AppError(
17 | this.message, {
18 | this.code = 0,
19 | this.isHttpError = false,
20 | this.notLogin = false,
21 | });
22 | @override
23 | String toString() {
24 | if (isHttpError && message.isEmpty) {
25 | return statusCodeToString(code);
26 | }
27 |
28 | return message;
29 | }
30 |
31 | String statusCodeToString(int statusCode) {
32 | switch (statusCode) {
33 | case 400:
34 | return LocaleKeys.network_status_400.tr;
35 | case 401:
36 | return LocaleKeys.network_status_401.tr;
37 | case 403:
38 | return LocaleKeys.network_status_403.tr;
39 | case 404:
40 | return LocaleKeys.network_status_404.tr;
41 | case 500:
42 | return LocaleKeys.network_status_500.tr;
43 | case 502:
44 | return LocaleKeys.network_status_502.tr;
45 | case 503:
46 | return LocaleKeys.network_status_503.tr;
47 | default:
48 | return "${LocaleKeys.network_status_request_error.tr}($statusCode)";
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/models/blogs/user_blog_info_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class UserBlogInfoModel {
11 | UserBlogInfoModel({
12 | required this.blogId,
13 | required this.title,
14 | required this.subtitle,
15 | required this.postCount,
16 | required this.pageSize,
17 | required this.enableScript,
18 | });
19 |
20 | factory UserBlogInfoModel.fromJson(Map json) =>
21 | UserBlogInfoModel(
22 | blogId: asT(json['blogId'])!,
23 | title: asT(json['title'])!,
24 | subtitle: asT(json['subtitle'])!,
25 | postCount: asT(json['postCount'])!,
26 | pageSize: asT(json['pageSize'])!,
27 | enableScript: asT(json['enableScript'])!,
28 | );
29 |
30 | int blogId;
31 | String title;
32 | String subtitle;
33 | int postCount;
34 | int pageSize;
35 | bool enableScript;
36 |
37 | @override
38 | String toString() {
39 | return jsonEncode(this);
40 | }
41 |
42 | Map toJson() => {
43 | 'blogId': blogId,
44 | 'title': title,
45 | 'subtitle': subtitle,
46 | 'postCount': postCount,
47 | 'pageSize': pageSize,
48 | 'enableScript': enableScript,
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/lib/modules/news/comment/news_comment_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 | import 'package:flutter_cnblogs/modules/news/comment/news_comment_controller.dart';
5 | import 'package:flutter_cnblogs/widgets/items/news_comment_item_widget.dart';
6 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
7 | import 'package:get/get.dart';
8 |
9 | class NewsCommentPage extends GetView {
10 | const NewsCommentPage({Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Scaffold(
15 | appBar: AppBar(
16 | title: Text(LocaleKeys.blog_comment_title.tr),
17 | ),
18 | body: PageListView(
19 | pageController: controller,
20 | padding: AppStyle.edgeInsetsA12,
21 | firstRefresh: true,
22 | separatorBuilder: ((context, index) {
23 | return AppStyle.vGap12;
24 | }),
25 | itemBuilder: (_, i) {
26 | var item = controller.list[i];
27 | return NewsCommentItemWidget(item);
28 | },
29 | ),
30 | // floatingActionButton: FloatingActionButton(
31 | // onPressed: controller.showAddCommentDialog,
32 | // child: const Icon(Icons.add),
33 | // ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/modules/blogs/comment/blog_comment_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 | import 'package:flutter_cnblogs/modules/blogs/comment/blog_comment_controller.dart';
5 | import 'package:flutter_cnblogs/widgets/items/blog_comment_item_widget.dart';
6 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
7 | import 'package:get/get.dart';
8 |
9 | class BlogCommentPage extends GetView {
10 | const BlogCommentPage({Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Scaffold(
15 | appBar: AppBar(
16 | title: Text(LocaleKeys.blog_comment_title.tr),
17 | ),
18 | body: PageListView(
19 | pageController: controller,
20 | padding: AppStyle.edgeInsetsA12,
21 | firstRefresh: true,
22 | separatorBuilder: ((context, index) {
23 | return AppStyle.vGap12;
24 | }),
25 | itemBuilder: (_, i) {
26 | var item = controller.list[i];
27 | return BlogCommentItemWidget(item);
28 | },
29 | ),
30 | // floatingActionButton: FloatingActionButton(
31 | // onPressed: controller.showAddCommentDialog,
32 | // child: const Icon(Icons.add),
33 | // ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/services/api_service.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/log.dart';
2 | import 'package:flutter_cnblogs/models/oauth/token_model.dart';
3 | import 'package:flutter_cnblogs/requests/oauth_request.dart';
4 | import 'package:flutter_cnblogs/services/local_storage_service.dart';
5 | import 'package:get/get.dart';
6 |
7 | class ApiService extends GetxService {
8 | static ApiService get instance => Get.find();
9 | final LocalStorageService storage = Get.find();
10 | final OAuthRequest oAuthRequest = OAuthRequest();
11 | String token = '';
12 | int expires = 0;
13 |
14 | void init() {
15 | token = storage.getValue(LocalStorageService.kAccessToken, "");
16 | expires = storage.getValue(LocalStorageService.kAccessTokenExpiresTime,
17 | DateTime.now().millisecondsSinceEpoch);
18 | }
19 |
20 | Future getToken() async {
21 | try {
22 | TokenModel tokenModel = await oAuthRequest.getToken();
23 | token = tokenModel.accessToken;
24 | expires = DateTime.now()
25 | .add(Duration(seconds: tokenModel.expiresIn))
26 | .millisecondsSinceEpoch;
27 | storage.setValue(LocalStorageService.kAccessToken, token);
28 | storage.setValue(LocalStorageService.kAccessTokenExpiresTime, expires);
29 |
30 | return true;
31 | } catch (e) {
32 | Log.logPrint(e);
33 | return false;
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/modules/questions/comment/answer_comment_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 |
5 | import 'package:flutter_cnblogs/modules/questions/comment/answer_comment_controller.dart';
6 | import 'package:flutter_cnblogs/widgets/items/answer_comment_item_widget.dart';
7 | import 'package:flutter_cnblogs/widgets/page_list_view.dart';
8 | import 'package:get/get.dart';
9 |
10 | class AnswerCommentPage extends GetView {
11 | const AnswerCommentPage({Key? key}) : super(key: key);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Scaffold(
16 | appBar: AppBar(
17 | title: Text(LocaleKeys.blog_comment_title.tr),
18 | ),
19 | body: PageListView(
20 | pageController: controller,
21 | padding: AppStyle.edgeInsetsA12,
22 | firstRefresh: true,
23 | separatorBuilder: ((context, index) {
24 | return AppStyle.vGap12;
25 | }),
26 | itemBuilder: (_, i) {
27 | var item = controller.list[i];
28 | return AnswerCommentItemWidget(item);
29 | },
30 | ),
31 | floatingActionButton: FloatingActionButton(
32 | onPressed: controller.showAddCommentDialog,
33 | child: const Icon(Icons.add),
34 | ),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/widgets/status/app_not_login_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 | import 'package:flutter_cnblogs/routes/route_path.dart';
5 | import 'package:get/get.dart';
6 |
7 | class AppNotLoginWidget extends StatelessWidget {
8 | final Function()? onLoginSuccess;
9 |
10 | const AppNotLoginWidget({this.onLoginSuccess, Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Center(
15 | child: Padding(
16 | padding: AppStyle.edgeInsetsA12,
17 | child: Column(
18 | mainAxisSize: MainAxisSize.min,
19 | children: [
20 | Text(
21 | LocaleKeys.state_not_login.tr,
22 | textAlign: TextAlign.center,
23 | style: const TextStyle(fontSize: 14, color: Colors.grey),
24 | ),
25 | AppStyle.vGap12,
26 | ElevatedButton(
27 | onPressed: () async {
28 | var result = await Get.toNamed(RoutePath.kUserLogin);
29 | if (result != null && result) {
30 | onLoginSuccess?.call();
31 | }
32 | },
33 | child: Text(LocaleKeys.login_title.tr),
34 | ),
35 | ],
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/widgets/status/app_error_widget.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 | import 'package:lottie/lottie.dart';
5 | import 'package:get/get.dart';
6 |
7 | class AppErrorWidget extends StatelessWidget {
8 | final Function()? onRefresh;
9 | final String errorMsg;
10 | const AppErrorWidget({this.errorMsg = "", this.onRefresh, Key? key})
11 | : super(key: key);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Center(
16 | child: GestureDetector(
17 | onTap: () {
18 | onRefresh?.call();
19 | },
20 | child: Padding(
21 | padding: AppStyle.edgeInsetsA12,
22 | child: Column(
23 | mainAxisSize: MainAxisSize.min,
24 | children: [
25 | LottieBuilder.asset(
26 | 'assets/lotties/error.json',
27 | width: 260,
28 | repeat: false,
29 | ),
30 | Text(
31 | LocaleKeys.state_error.trParams({
32 | "msg": errorMsg,
33 | }),
34 | textAlign: TextAlign.center,
35 | style: const TextStyle(fontSize: 12, color: Colors.grey),
36 | ),
37 | ],
38 | ),
39 | ),
40 | ),
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/assets/templates/js/common.js:
--------------------------------------------------------------------------------
1 | window.onload = function () {
2 | initImageClickEvent();
3 | initHighlightCode();
4 | }
5 |
6 | // 初始化代码高亮
7 | function initHighlightCode() {
8 | try {
9 | //转换其他的高亮代码至hljs
10 | var elementList = document.getElementsByClassName("cnblogs_Highlighter");
11 | for (let i = 0; i < elementList.length; i++) {
12 | const element = elementList[i];
13 | var code = element.getElementsByTagName("pre")[0].innerHTML;
14 | element.getElementsByTagName("pre")[0].innerHTML = '' + code + '';
15 | }
16 | } catch (e) {
17 | console.log("无法转换cnblogs_Highlighter:" + e)
18 | }
19 | hljs.highlightAll();
20 | }
21 |
22 | var allImgs = [];
23 | // 添加图片点击事件
24 | function initImageClickEvent() {
25 | allImgs = [];
26 | var elementList = document.getElementsByClassName("content")[0].getElementsByTagName("img");
27 | for (let i = 0; i < elementList.length; i++) {
28 | const element = elementList[i];
29 | //跳过计数器
30 | if (element.src.indexOf("counter.cnblogs.com") != -1) {
31 | continue;
32 | }
33 | allImgs.push(element.src);
34 | element.onclick = function (e) {
35 | openImage(e.target.src)
36 | }
37 | }
38 | }
39 | // 打开作者主页
40 | function openAuthor() {
41 | window.flutter_inappwebview.callHandler('showAuthor');
42 | }
43 | // 打开图片浏览
44 | function openImage(src) {
45 | window.flutter_inappwebview.callHandler('showImage', src, allImgs);
46 | }
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/lib/models/blogs/knowledge_list_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class KnowledgeListItemModel {
11 | KnowledgeListItemModel({
12 | required this.id,
13 | required this.title,
14 | required this.summary,
15 | required this.author,
16 | required this.viewcount,
17 | required this.diggcount,
18 | required this.dateadded,
19 | });
20 |
21 | factory KnowledgeListItemModel.fromJson(Map json) =>
22 | KnowledgeListItemModel(
23 | id: asT(json['Id'])!,
24 | title: asT(json['Title'])!,
25 | summary: asT(json['Summary'])!,
26 | author: asT(json['Author']) ?? "",
27 | viewcount: asT(json['ViewCount'])!,
28 | diggcount: asT(json['DiggCount'])!,
29 | dateadded: asT(json['DateAdded'])!,
30 | );
31 |
32 | int id;
33 | String title;
34 | String summary;
35 | String author;
36 | int viewcount;
37 | int diggcount;
38 | String dateadded;
39 | DateTime get postDateTime => DateTime.parse(dateadded);
40 |
41 | @override
42 | String toString() {
43 | return jsonEncode(this);
44 | }
45 |
46 | Map toJson() => {
47 | 'Id': id,
48 | 'Title': title,
49 | 'Summary': summary,
50 | 'Author': author,
51 | 'ViewCount': viewcount,
52 | 'DiggCount': diggcount,
53 | 'DateAdded': dateadded,
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/lib/models/blogs/blog_comment_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class BlogCommentItemModel {
11 | BlogCommentItemModel({
12 | required this.id,
13 | required this.body,
14 | required this.author,
15 | required this.authorUrl,
16 | required this.faceUrl,
17 | required this.floor,
18 | required this.dateAdded,
19 | });
20 |
21 | factory BlogCommentItemModel.fromJson(Map json) =>
22 | BlogCommentItemModel(
23 | id: asT(json['Id']) ?? 0,
24 | body: asT(json['Body']) ?? "",
25 | author: asT(json['Author']) ?? "",
26 | authorUrl: asT(json['AuthorUrl']) ?? "",
27 | faceUrl: asT(json['FaceUrl']) ?? "",
28 | floor: asT(json['Floor']) ?? 0,
29 | dateAdded: asT(json['DateAdded']) ?? "",
30 | );
31 |
32 | int id;
33 | String body;
34 | String author;
35 | String authorUrl;
36 | String faceUrl;
37 | int floor;
38 | String dateAdded;
39 | DateTime? get postDateTime => DateTime.tryParse(dateAdded);
40 |
41 | @override
42 | String toString() {
43 | return jsonEncode(this);
44 | }
45 |
46 | Map toJson() => {
47 | 'Id': id,
48 | 'Body': body,
49 | 'Author': author,
50 | 'AuthorUrl': authorUrl,
51 | 'FaceUrl': faceUrl,
52 | 'Floor': floor,
53 | 'DateAdded': dateAdded,
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/lib/modules/other/web_view/web_view_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
3 | import 'package:flutter_inappwebview/flutter_inappwebview.dart';
4 | import 'package:get/get.dart';
5 |
6 | class AppWebViewController extends BaseController {
7 | final String url;
8 | AppWebViewController(this.url);
9 |
10 | var title = "".obs;
11 | final UniqueKey webViewkey = UniqueKey();
12 | late InAppWebViewController? webViewController;
13 | final InAppWebViewGroupOptions webViewGroupOptions = InAppWebViewGroupOptions(
14 | crossPlatform: InAppWebViewOptions(
15 | transparentBackground: true,
16 | useShouldOverrideUrlLoading: true,
17 | ),
18 | );
19 | void onWebViewCreated(InAppWebViewController controller) {
20 | webViewController = controller;
21 | pageLoadding.value = true;
22 | }
23 |
24 | void refreshWeb() {
25 | webViewController?.reload();
26 | }
27 |
28 | void onTitleChanged(InAppWebViewController controller, String? e) {
29 | title.value = e ?? "";
30 | }
31 |
32 | void onLoadStart(InAppWebViewController controller, Uri? uri) {
33 | pageLoadding.value = true;
34 | pageError.value = false;
35 | }
36 |
37 | void onLoadStop(InAppWebViewController controller, Uri? uri) async {
38 | pageLoadding.value = false;
39 | }
40 |
41 | void onLoadError(
42 | InAppWebViewController controller, Uri? uri, int code, String e) {
43 | pageLoadding.value = false;
44 | pageError.value = true;
45 | errorMsg.value = "$code $e";
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/modules/search/search_list_view_controlelr.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/models/search/search_item_model.dart';
4 | import 'package:flutter_cnblogs/requests/search_request.dart';
5 |
6 | class SearchListController extends BasePageController {
7 | final String tag;
8 | SearchListController(this.tag);
9 | final SearchRequest newsRequest = SearchRequest();
10 |
11 | String keyword = "";
12 | String startDate = "";
13 | String endDate = "";
14 | int view = 0;
15 |
16 | @override
17 | void onInit() {
18 | pageEmpty.value = true;
19 | super.onInit();
20 | }
21 |
22 | @override
23 | Future> getData(int page, int pageSize) async {
24 | if (keyword.isEmpty) {
25 | return [];
26 | }
27 | var category = "";
28 | switch (tag) {
29 | case LocaleKeys.search_type_blog:
30 | category = "blog";
31 | break;
32 | case LocaleKeys.search_type_news:
33 | category = "news";
34 | break;
35 | case LocaleKeys.search_type_question:
36 | category = "question";
37 | break;
38 | case LocaleKeys.search_type_kb:
39 | category = "kb";
40 | break;
41 | default:
42 | }
43 | return await newsRequest.search(
44 | category: category,
45 | keyword: keyword,
46 | startDate: startDate,
47 | endDate: endDate,
48 | viewTimesAtLeast: view,
49 | pageIndex: page,
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/requests/base/oauth_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'package:dio/dio.dart';
2 | import 'package:flutter_cnblogs/services/api_service.dart';
3 | import 'package:flutter_cnblogs/services/user_service.dart';
4 |
5 | class OAuthInterceptor extends Interceptor {
6 | @override
7 | void onRequest(
8 | RequestOptions options, RequestInterceptorHandler handler) async {
9 | if (options.extra["withApiAuth"]) {
10 | var token = "";
11 | if (UserService.instance.logined.value) {
12 | token = await getUserToken();
13 | } else {
14 | token = await getApiToken();
15 | }
16 | options.headers.addAll({"Authorization": "Bearer $token"});
17 | } else if (options.extra["withUserAuth"]) {
18 | var token = await getUserToken();
19 | options.headers.addAll({"Authorization": "Bearer $token"});
20 | }
21 | super.onRequest(options, handler);
22 | }
23 |
24 | Future getApiToken() async {
25 | //读取Token,判断Token是否过期
26 | //Token过期则出现读取
27 | if (ApiService.instance.token.isEmpty ||
28 | ApiService.instance.expires <= DateTime.now().millisecondsSinceEpoch) {
29 | //重新请求
30 | await ApiService.instance.getToken();
31 | }
32 | return ApiService.instance.token;
33 | }
34 |
35 | Future getUserToken() async {
36 | //读取Token,判断Token是否过期
37 | //Token过期则出现读取
38 | if (UserService.instance.token.isEmpty ||
39 | UserService.instance.expires <= DateTime.now().millisecondsSinceEpoch) {
40 | //重新请求
41 | await UserService.instance.refreshToken();
42 | }
43 | return UserService.instance.token;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/modules/news/home/news_home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/news/home/news_home_controller.dart';
4 | import 'package:flutter_cnblogs/modules/news/home/news_list_view.dart';
5 | import 'package:get/get.dart';
6 |
7 | class NewsHomePage extends GetView {
8 | const NewsHomePage({Key? key}) : super(key: key);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Scaffold(
13 | appBar: AppBar(
14 | titleSpacing: 8,
15 | title: Container(
16 | alignment: Alignment.centerLeft,
17 | child: TabBar(
18 | controller: controller.tabController,
19 | tabs: controller.tabs
20 | .map(
21 | (e) => Tab(
22 | text: e.tr,
23 | ),
24 | )
25 | .toList(),
26 | labelPadding: AppStyle.edgeInsetsH20,
27 | isScrollable: true,
28 | indicatorSize: TabBarIndicatorSize.tab,
29 | ),
30 | ),
31 | actions: [
32 | IconButton(
33 | onPressed: controller.toSearch,
34 | icon: const Icon(Icons.search),
35 | )
36 | ],
37 | ),
38 | body: TabBarView(
39 | controller: controller.tabController,
40 | children: controller.tabs
41 | .map(
42 | (e) => NewsListView(
43 | e,
44 | ),
45 | )
46 | .toList(),
47 | ),
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/modules/statuses/home/statuses_home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/modules/statuses/home/statuses_home_controller.dart';
4 | import 'package:flutter_cnblogs/modules/statuses/home/statuses_list_view.dart';
5 | import 'package:get/get.dart';
6 |
7 | class StatusesHomePage extends GetView {
8 | const StatusesHomePage({Key? key}) : super(key: key);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Scaffold(
13 | appBar: AppBar(
14 | titleSpacing: 8,
15 | title: Container(
16 | alignment: Alignment.centerLeft,
17 | child: TabBar(
18 | controller: controller.tabController,
19 | tabs: controller.tabs
20 | .map(
21 | (e) => Tab(
22 | text: e.tr,
23 | ),
24 | )
25 | .toList(),
26 | labelPadding: AppStyle.edgeInsetsH20,
27 | isScrollable: true,
28 | indicatorSize: TabBarIndicatorSize.tab,
29 | ),
30 | ),
31 | ),
32 | body: TabBarView(
33 | controller: controller.tabController,
34 | children: controller.tabs
35 | .map(
36 | (e) => StatusesListView(
37 | e,
38 | ),
39 | )
40 | .toList(),
41 | ),
42 | floatingActionButton: FloatingActionButton(
43 | onPressed: controller.addDialog,
44 | child: const Icon(Icons.add),
45 | ),
46 | );
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/modules/questions/home/questions_home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 |
4 | import 'package:flutter_cnblogs/modules/questions/home/questions_home_controller.dart';
5 | import 'package:flutter_cnblogs/modules/questions/home/questions_list_view.dart';
6 | import 'package:get/get.dart';
7 |
8 | class QuestionsHomePage extends GetView {
9 | const QuestionsHomePage({Key? key}) : super(key: key);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Scaffold(
14 | appBar: AppBar(
15 | titleSpacing: 8,
16 | title: Container(
17 | alignment: Alignment.centerLeft,
18 | child: TabBar(
19 | controller: controller.tabController,
20 | tabs: controller.tabs
21 | .map(
22 | (e) => Tab(
23 | text: e.tr,
24 | ),
25 | )
26 | .toList(),
27 | labelPadding: AppStyle.edgeInsetsH20,
28 | isScrollable: true,
29 | indicatorSize: TabBarIndicatorSize.tab,
30 | ),
31 | ),
32 | actions: [
33 | IconButton(
34 | onPressed: controller.toSearch,
35 | icon: const Icon(Icons.search),
36 | )
37 | ],
38 | ),
39 | body: TabBarView(
40 | controller: controller.tabController,
41 | children: controller.tabs
42 | .map(
43 | (e) => QuestionsListView(
44 | e,
45 | ),
46 | )
47 | .toList(),
48 | ),
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/modules/questions/home/questions_list_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/models/questions/question_list_item_model.dart';
4 | import 'package:flutter_cnblogs/requests/questions_request.dart';
5 | import 'package:flutter_cnblogs/services/user_service.dart';
6 |
7 | class QuestionsListController
8 | extends BasePageController {
9 | final String title;
10 | QuestionsListController(this.title);
11 | final QuestionsRequest questionsRequest = QuestionsRequest();
12 |
13 | @override
14 | Future> getData(int page, int pageSize) async {
15 | if (title == LocaleKeys.questions_home_unsolved) {
16 | return await questionsRequest.getQuestions(
17 | type: "unsolved",
18 | pageIndex: page,
19 | );
20 | } else if (title == LocaleKeys.questions_home_highscore) {
21 | return await questionsRequest.getQuestions(
22 | type: "highscore",
23 | pageIndex: page,
24 | );
25 | } else if (title == LocaleKeys.questions_home_solved) {
26 | return await questionsRequest.getQuestions(
27 | type: "solved",
28 | pageIndex: page,
29 | );
30 | } else if (title == LocaleKeys.questions_home_noanswer) {
31 | return await questionsRequest.getQuestions(
32 | type: "noanswer",
33 | pageIndex: page,
34 | );
35 | } else {
36 | return await questionsRequest.getQuestions(
37 | type: "myquestion",
38 | pageIndex: page,
39 | spaceUserId: UserService.instance.userId,
40 | );
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/widgets/net_image.dart:
--------------------------------------------------------------------------------
1 | import 'package:extended_image/extended_image.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class NetImage extends StatelessWidget {
5 | final String picUrl;
6 | final double? width;
7 | final double? height;
8 | final BoxFit? fit;
9 | final double borderRadius;
10 | const NetImage(this.picUrl,
11 | {this.width,
12 | this.height,
13 | this.fit = BoxFit.cover,
14 | this.borderRadius = 0,
15 | Key? key})
16 | : super(key: key);
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | if (picUrl.isEmpty) {
21 | return SizedBox(
22 | width: width,
23 | height: height,
24 | );
25 | }
26 | var pic = picUrl;
27 | if (pic.startsWith("//")) {
28 | pic = 'https:$pic';
29 | }
30 | return ClipRRect(
31 | borderRadius: BorderRadius.circular(borderRadius),
32 | child: ExtendedImage.network(
33 | pic,
34 | fit: fit,
35 | height: height,
36 | width: width,
37 | shape: BoxShape.rectangle,
38 | borderRadius: BorderRadius.circular(borderRadius),
39 | loadStateChanged: (e) {
40 | if (e.extendedImageLoadState == LoadState.loading) {
41 | return const Icon(
42 | Icons.image,
43 | color: Colors.grey,
44 | size: 24,
45 | );
46 | }
47 | if (e.extendedImageLoadState == LoadState.failed) {
48 | return const Icon(
49 | Icons.broken_image,
50 | color: Colors.grey,
51 | size: 24,
52 | );
53 | }
54 | return null;
55 | },
56 | ),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flutter_cnblogs
2 | version: 1.0.8+10008
3 | description: "博客园Flutter客户端"
4 | environment:
5 | sdk: '>=2.18.2 <3.0.0'
6 |
7 | dependencies:
8 | # 图标
9 | cupertino_icons: ^1.0.2
10 | remixicon: ^1.0.0 #Remix图标
11 |
12 | # 框架、工具
13 | get: ^4.6.6 #状态管理、路由管理、国际化
14 | dio: ^5.3.2 #网络请求
15 | hive: 2.2.3 #持久化存储
16 | hive_flutter: 1.1.0 #持久化存储
17 | logger: ^2.0.2 #日志
18 | intl: ^0.18.1 #国际化
19 | flutter_dotenv: ^5.1.0 #ENV
20 | xpath_selector: ^3.0.2 #XPATH
21 | xpath_selector_html_parser: ^3.0.1 #XPATH
22 |
23 | #Widget
24 | flutter_staggered_grid_view: ^0.7.0 #瀑布流/GridView
25 | easy_refresh: ^3.3.2+1 #下拉刷新、上拉加载
26 | extended_image: ^8.1.1 #拓展Image,支持缓存
27 | flutter_smart_dialog: ^4.9.4 #各种弹窗 Toast/Dialog/Popup
28 | lottie: ^2.6.0 #lottie动画
29 | flutter_html: ^3.0.0-beta.2 #HTML转widget
30 | photo_view: ^0.14.0 #图片浏览
31 | sticky_headers: ^0.3.0+2 #吸顶
32 | flutter_markdown: ^0.6.17+3 #Markdown
33 |
34 | #系统交互
35 | package_info_plus: ^4.1.0 #包信息
36 | url_launcher: ^6.1.14 #打开链接
37 | share_plus: ^7.1.0 #分享
38 | path_provider: ^2.1.1 #常用路径
39 | cross_file: ^0.3.3+5 #跨平台文件
40 | flutter_inappwebview: ^5.7.2+3 #WebView
41 | permission_handler: ^11.0.0 #权限处理
42 | image_gallery_saver: ^2.0.3 #图片保存到相册
43 |
44 | flutter:
45 | sdk: flutter
46 | flutter_localizations:
47 | sdk: flutter
48 |
49 |
50 | dev_dependencies:
51 | flutter_lints: ^2.0.0
52 | flutter_test:
53 | sdk: flutter
54 |
55 | flutter:
56 | uses-material-design: true
57 | assets:
58 | - .env
59 | - assets/images/
60 | - assets/lotties/
61 | - assets/templates/
62 | - assets/templates/js/
63 | - assets/templates/blog/
64 | - assets/templates/news/
65 |
66 |
--------------------------------------------------------------------------------
/lib/requests/base/app_log_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'package:dio/dio.dart';
2 | import 'package:flutter_cnblogs/app/log.dart';
3 |
4 | class AppLogInterceptor extends Interceptor {
5 | @override
6 | void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
7 | options.extra["ts"] = DateTime.now().millisecondsSinceEpoch;
8 | super.onRequest(options, handler);
9 | }
10 |
11 | @override
12 | void onError(DioException err, ErrorInterceptorHandler handler) {
13 | var time =
14 | DateTime.now().millisecondsSinceEpoch - err.requestOptions.extra["ts"];
15 | Log.e('''【HTTP请求错误-${err.type}】 耗时:${time}ms
16 | ${err.message}
17 |
18 | Request Method:${err.requestOptions.method}
19 | Response Code:${err.response?.statusCode}
20 | Request URL:${err.requestOptions.uri}
21 | Request Query:${err.requestOptions.queryParameters}
22 | Request Data:${err.requestOptions.data}
23 | Request Headers:${err.requestOptions.headers}
24 | Response Headers:${err.response?.headers.map}
25 | Response Data:${err.response?.data}''', err.stackTrace);
26 |
27 | super.onError(err, handler);
28 | }
29 |
30 | @override
31 | void onResponse(Response response, ResponseInterceptorHandler handler) {
32 | var time = DateTime.now().millisecondsSinceEpoch -
33 | response.requestOptions.extra["ts"];
34 | Log.i(
35 | '''【HTTP请求响应】 耗时:${time}ms
36 | Request Method:${response.requestOptions.method}
37 | Request Code:${response.statusCode}
38 | Request URL:${response.requestOptions.uri}
39 | Request Query:${response.requestOptions.queryParameters}
40 | Request Data:${response.requestOptions.data}
41 | Request Headers:${response.requestOptions.headers}
42 | Response Headers:${response.headers.map}
43 | Response Data:${response.data}''',
44 | );
45 | super.onResponse(response, handler);
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/modules/news/home/news_home_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
5 | import 'package:flutter_cnblogs/app/event_bus.dart';
6 | import 'package:flutter_cnblogs/generated/locales.g.dart';
7 | import 'package:flutter_cnblogs/modules/news/home/news_list_controller.dart';
8 | import 'package:flutter_cnblogs/routes/app_navigation.dart';
9 |
10 | import 'package:get/get.dart';
11 |
12 | class NewsHomeController extends GetxController
13 | with GetSingleTickerProviderStateMixin {
14 | late TabController tabController;
15 | NewsHomeController() {
16 | tabController = TabController(length: tabs.length, vsync: this);
17 | }
18 | final tabs = [
19 | LocaleKeys.news_home_new,
20 | LocaleKeys.news_home_recommended,
21 | LocaleKeys.news_home_hot,
22 | LocaleKeys.news_home_hot_week,
23 | ];
24 | StreamSubscription? streamSubscription;
25 | @override
26 | void onInit() {
27 | streamSubscription = EventBus.instance.listen(
28 | EventBus.kBottomNavigationBarClicked,
29 | (index) {
30 | if (index == 1) {
31 | refreshOrScrollTop();
32 | }
33 | },
34 | );
35 | for (var tag in tabs) {
36 | Get.put(NewsListController(tag), tag: tag);
37 | }
38 |
39 | super.onInit();
40 | }
41 |
42 | void refreshOrScrollTop() {
43 | var tabIndex = tabController.index;
44 | BasePageController controller =
45 | Get.find(tag: tabs[tabIndex]);
46 |
47 | controller.scrollToTopOrRefresh();
48 | }
49 |
50 | void toSearch() {
51 | AppNavigator.toSearch(SearchType.news);
52 | }
53 |
54 | @override
55 | void onClose() {
56 | streamSubscription?.cancel();
57 | super.onClose();
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.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.
5 |
6 | version:
7 | revision: d9111f64021372856901a1fd5bfbc386cade3318
8 | channel: stable
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
17 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
18 | - platform: android
19 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
20 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
21 | - platform: ios
22 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
23 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
24 | - platform: linux
25 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
26 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
27 | - platform: macos
28 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
29 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
30 | - platform: web
31 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
32 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
33 | - platform: windows
34 | create_revision: d9111f64021372856901a1fd5bfbc386cade3318
35 | base_revision: d9111f64021372856901a1fd5bfbc386cade3318
36 |
37 | # User provided section
38 |
39 | # List of Local paths (relative to this file) that should be
40 | # ignored by the migrate tool.
41 | #
42 | # Files that are not part of the templates will be ignored by default.
43 | unmanaged_files:
44 | - 'lib/main.dart'
45 | - 'ios/Runner.xcodeproj/project.pbxproj'
46 |
--------------------------------------------------------------------------------
/lib/models/news/news_list_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class NewsListItemModel {
11 | NewsListItemModel({
12 | required this.id,
13 | required this.title,
14 | required this.summary,
15 | required this.topicId,
16 | this.topicIcon,
17 | required this.viewCount,
18 | required this.commentCount,
19 | required this.diggCount,
20 | required this.dateAdded,
21 | });
22 |
23 | factory NewsListItemModel.fromJson(Map json) =>
24 | NewsListItemModel(
25 | id: asT(json['Id'])!,
26 | title: asT(json['Title'])!,
27 | summary: asT(json['Summary'])!,
28 | topicId: asT(json['TopicId'])!,
29 | topicIcon: asT(json['TopicIcon']),
30 | viewCount: asT(json['ViewCount'])!,
31 | commentCount: asT(json['CommentCount'])!,
32 | diggCount: asT(json['DiggCount'])!,
33 | dateAdded: asT(json['DateAdded'])!,
34 | );
35 |
36 | int id;
37 | String title;
38 | String summary;
39 | int topicId;
40 | String? topicIcon;
41 | int viewCount;
42 | int commentCount;
43 | int diggCount;
44 | String dateAdded;
45 | DateTime get postDateTime => DateTime.parse(dateAdded);
46 |
47 | @override
48 | String toString() {
49 | return jsonEncode(this);
50 | }
51 |
52 | Map toJson() => {
53 | 'Id': id,
54 | 'Title': title,
55 | 'Summary': summary,
56 | 'TopicId': topicId,
57 | 'TopicIcon': topicIcon,
58 | 'ViewCount': viewCount,
59 | 'CommentCount': commentCount,
60 | 'DiggCount': diggCount,
61 | 'DateAdded': dateAdded,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/lib/models/user/bookmark_list_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class BookmarkListItemModel {
11 | BookmarkListItemModel({
12 | required this.wzLinkId,
13 | required this.title,
14 | required this.linkUrl,
15 | required this.summary,
16 | required this.tags,
17 | required this.dateAdded,
18 | required this.fromCNBlogs,
19 | });
20 |
21 | factory BookmarkListItemModel.fromJson(Map json) {
22 | final List? tags = json['Tags'] is List ? [] : null;
23 | if (tags != null) {
24 | for (final dynamic item in json['Tags']!) {
25 | if (item != null) {
26 | tags.add(asT(item)!);
27 | }
28 | }
29 | }
30 | return BookmarkListItemModel(
31 | wzLinkId: asT(json['WzLinkId'])!,
32 | title: asT(json['Title'])!,
33 | linkUrl: asT(json['LinkUrl'])!,
34 | summary: asT(json['Summary']) ?? "",
35 | tags: tags!,
36 | dateAdded: asT(json['DateAdded'])!,
37 | fromCNBlogs: asT(json['FromCNBlogs'])!,
38 | );
39 | }
40 |
41 | int wzLinkId;
42 | String title;
43 | String linkUrl;
44 | String summary;
45 | List tags;
46 | String dateAdded;
47 | DateTime get addDateTime => DateTime.parse(dateAdded);
48 | bool fromCNBlogs;
49 |
50 | @override
51 | String toString() {
52 | return jsonEncode(this);
53 | }
54 |
55 | Map toJson() => {
56 | 'WzLinkId': wzLinkId,
57 | 'Title': title,
58 | 'LinkUrl': linkUrl,
59 | 'Summary': summary,
60 | 'Tags': tags,
61 | 'DateAdded': dateAdded,
62 | 'FromCNBlogs': fromCNBlogs,
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/lib/models/user/user_info_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class UserInfoModel {
11 | UserInfoModel({
12 | required this.userId,
13 | required this.spaceUserId,
14 | this.blogId,
15 | required this.displayName,
16 | required this.face,
17 | required this.avatar,
18 | required this.seniority,
19 | this.blogApp,
20 | required this.followerCount,
21 | required this.followingCount,
22 | });
23 |
24 | factory UserInfoModel.fromJson(Map json) => UserInfoModel(
25 | userId: asT(json['UserId'])!,
26 | spaceUserId: asT(json['SpaceUserID'])!,
27 | blogId: asT(json['BlogId']),
28 | displayName: asT(json['DisplayName'])!,
29 | face: asT(json['Face'])!,
30 | avatar: asT(json['Avatar'])!,
31 | seniority: asT(json['Seniority'])!,
32 | blogApp: asT(json['BlogApp']),
33 | followingCount: asT(json['FollowingCount']) ?? 0,
34 | followerCount: asT(json['FollowerCount']) ?? 0,
35 | );
36 |
37 | String userId;
38 | int spaceUserId;
39 | int? blogId;
40 | String displayName;
41 | String face;
42 | String avatar;
43 | String seniority;
44 | String? blogApp;
45 | int followingCount;
46 | int followerCount;
47 |
48 | @override
49 | String toString() {
50 | return jsonEncode(this);
51 | }
52 |
53 | Map toJson() => {
54 | 'UserId': userId,
55 | 'SpaceUserId': spaceUserId,
56 | 'BlogId': blogId,
57 | 'DisplayName': displayName,
58 | 'Face': face,
59 | 'Avatar': avatar,
60 | 'Seniority': seniority,
61 | 'BlogApp': blogApp,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/lib/models/statuses/statuses_comment_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class StatusesCommentItemModel {
11 | StatusesCommentItemModel({
12 | required this.id,
13 | required this.content,
14 | required this.dateAdded,
15 | required this.statusId,
16 | required this.userAlias,
17 | required this.userDisplayName,
18 | required this.userIconUrl,
19 | required this.userId,
20 | required this.userGuid,
21 | });
22 |
23 | factory StatusesCommentItemModel.fromJson(Map json) =>
24 | StatusesCommentItemModel(
25 | id: asT(json['Id'])!,
26 | content: asT(json['Content'])!,
27 | dateAdded: asT(json['DateAdded'])!,
28 | statusId: asT(json['StatusId'])!,
29 | userAlias: asT(json['UserAlias'])!,
30 | userDisplayName: asT(json['UserDisplayName'])!,
31 | userIconUrl: asT(json['UserIconUrl'])!,
32 | userId: asT(json['UserId'])!,
33 | userGuid: asT(json['UserGuid'])!,
34 | );
35 |
36 | int id;
37 | String content;
38 | String dateAdded;
39 | int statusId;
40 | String userAlias;
41 | String userDisplayName;
42 | String userIconUrl;
43 | int userId;
44 | String userGuid;
45 | DateTime get postDateTime => DateTime.parse(dateAdded);
46 |
47 | @override
48 | String toString() {
49 | return jsonEncode(this);
50 | }
51 |
52 | Map toJson() => {
53 | 'Id': id,
54 | 'Content': content,
55 | 'DateAdded': dateAdded,
56 | 'StatusId': statusId,
57 | 'UserAlias': userAlias,
58 | 'UserDisplayName': userDisplayName,
59 | 'UserIconUrl': userIconUrl,
60 | 'UserId': userId,
61 | 'UserGuid': userGuid,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/lib/modules/questions/home/questions_home_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
5 | import 'package:flutter_cnblogs/app/event_bus.dart';
6 | import 'package:flutter_cnblogs/generated/locales.g.dart';
7 | import 'package:flutter_cnblogs/modules/questions/home/questions_list_controller.dart';
8 | import 'package:flutter_cnblogs/routes/app_navigation.dart';
9 |
10 | import 'package:get/get.dart';
11 |
12 | class QuestionsHomeController extends GetxController
13 | with GetSingleTickerProviderStateMixin {
14 | late TabController tabController;
15 | QuestionsHomeController() {
16 | tabController = TabController(length: tabs.length, vsync: this);
17 | }
18 | final tabs = [
19 | LocaleKeys.questions_home_unsolved,
20 | LocaleKeys.questions_home_highscore,
21 | LocaleKeys.questions_home_noanswer,
22 | LocaleKeys.questions_home_solved,
23 | LocaleKeys.questions_home_myquestion,
24 | ];
25 | StreamSubscription? streamSubscription;
26 | @override
27 | void onInit() {
28 | streamSubscription = EventBus.instance.listen(
29 | EventBus.kBottomNavigationBarClicked,
30 | (index) {
31 | if (index == 3) {
32 | refreshOrScrollTop();
33 | }
34 | },
35 | );
36 | for (var tag in tabs) {
37 | Get.put(QuestionsListController(tag), tag: tag);
38 | }
39 |
40 | super.onInit();
41 | }
42 |
43 | void refreshOrScrollTop() {
44 | var tabIndex = tabController.index;
45 | BasePageController controller =
46 | Get.find(tag: tabs[tabIndex]);
47 |
48 | controller.scrollToTopOrRefresh();
49 | }
50 |
51 | void toSearch() {
52 | AppNavigator.toSearch(SearchType.question);
53 | }
54 |
55 | @override
56 | void onClose() {
57 | streamSubscription?.cancel();
58 | super.onClose();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/modules/blogs/home/blogs_home_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/app_style.dart';
3 | import 'package:flutter_cnblogs/generated/locales.g.dart';
4 | import 'package:flutter_cnblogs/modules/blogs/home/blogs_home_controller.dart';
5 | import 'package:flutter_cnblogs/modules/blogs/home/blogs_list_view.dart';
6 | import 'package:flutter_cnblogs/modules/blogs/home/knowledge/blogs_knowledge_view.dart';
7 | import 'package:get/get.dart';
8 |
9 | class BlogsHomePage extends GetView {
10 | const BlogsHomePage({Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Scaffold(
15 | appBar: AppBar(
16 | titleSpacing: 8,
17 | title: Container(
18 | alignment: Alignment.centerLeft,
19 | child: TabBar(
20 | controller: controller.tabController,
21 | padding: EdgeInsets.zero,
22 | tabs: controller.tabs
23 | .map(
24 | (e) => Tab(
25 | text: e.tr,
26 | ),
27 | )
28 | .toList(),
29 | labelPadding: AppStyle.edgeInsetsH20,
30 | isScrollable: true,
31 | indicatorSize: TabBarIndicatorSize.tab,
32 | ),
33 | ),
34 | actions: [
35 | IconButton(
36 | onPressed: controller.toSearch,
37 | icon: const Icon(Icons.search),
38 | )
39 | ],
40 | ),
41 | body: TabBarView(
42 | controller: controller.tabController,
43 | children: controller.tabs
44 | .map(
45 | (e) => e == LocaleKeys.blogs_home_knowledge
46 | ? const BlogsKnowledgeView()
47 | : BlogsListView(
48 | e,
49 | ),
50 | )
51 | .toList(),
52 | ),
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Cnblogs
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | flutter_cnblogs
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(FLUTTER_BUILD_NAME)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(FLUTTER_BUILD_NUMBER)
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 | CADisableMinimumFrameDurationOnPhone
47 |
48 | UIApplicationSupportsIndirectInputEvents
49 |
50 | NSAppTransportSecurity
51 |
52 | NSAllowsArbitraryLoads
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/lib/modules/indexed/indexed_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/widgets.dart';
2 | import 'package:flutter_cnblogs/app/event_bus.dart';
3 | import 'package:flutter_cnblogs/app/utils.dart';
4 | import 'package:flutter_cnblogs/modules/blogs/home/blogs_home_page.dart';
5 | import 'package:flutter_cnblogs/modules/news/home/news_home_controller.dart';
6 | import 'package:flutter_cnblogs/modules/news/home/news_home_page.dart';
7 | import 'package:flutter_cnblogs/modules/questions/home/questions_home_controller.dart';
8 | import 'package:flutter_cnblogs/modules/questions/home/questions_home_page.dart';
9 | import 'package:flutter_cnblogs/modules/statuses/home/statuses_home_controller.dart';
10 | import 'package:flutter_cnblogs/modules/statuses/home/statuses_home_page.dart';
11 | import 'package:flutter_cnblogs/modules/user/home/user_home_page.dart';
12 | import 'package:get/get.dart';
13 |
14 | class IndexedController extends GetxController {
15 | var index = 0.obs;
16 | RxList pages = RxList([
17 | const BlogsHomePage(),
18 | const SizedBox(),
19 | const SizedBox(),
20 | const SizedBox(),
21 | const SizedBox(),
22 | ]);
23 |
24 | void setIndex(i) {
25 | if (pages[i] is SizedBox) {
26 | switch (i) {
27 | case 1:
28 | Get.put(NewsHomeController());
29 | pages[i] = const NewsHomePage();
30 | break;
31 | case 2:
32 | Get.put(StatusesHomeController());
33 | pages[i] = const StatusesHomePage();
34 | break;
35 | case 3:
36 | Get.put(QuestionsHomeController());
37 | pages[i] = const QuestionsHomePage();
38 | break;
39 | case 4:
40 | pages[i] = const UserHomePage();
41 | break;
42 | default:
43 | }
44 | }
45 | if (index.value == i) {
46 | EventBus.instance.emit(EventBus.kBottomNavigationBarClicked, i);
47 | }
48 | index.value = i;
49 | }
50 |
51 | @override
52 | void onInit() {
53 | Utils.checkUpdate();
54 | super.onInit();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/modules/other/debug_log_page.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_cnblogs/app/app_style.dart';
5 | import 'package:flutter_cnblogs/app/log.dart';
6 | import 'package:get/get.dart';
7 | import 'package:path_provider/path_provider.dart';
8 | import 'package:share_plus/share_plus.dart';
9 |
10 | class DebugLogPage extends StatelessWidget {
11 | const DebugLogPage({Key? key}) : super(key: key);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Scaffold(
16 | appBar: AppBar(
17 | title: const Text("Log"),
18 | actions: [
19 | IconButton(
20 | onPressed: () async {
21 | var msg = Log.debugLogs
22 | .map((x) => "${x.datetime}\r\n${x.content}")
23 | .join('\r\n\r\n');
24 | var dir = await getApplicationDocumentsDirectory();
25 | var logFile = File(
26 | '${dir.path}/${DateTime.now().millisecondsSinceEpoch}.log');
27 | await logFile.writeAsString(msg);
28 | Share.shareXFiles([XFile(logFile.path)]);
29 | },
30 | icon: const Icon(Icons.save),
31 | ),
32 | IconButton(
33 | onPressed: () {
34 | Log.debugLogs.clear();
35 | },
36 | icon: const Icon(Icons.clear_all),
37 | ),
38 | ],
39 | ),
40 | body: Obx(
41 | () => ListView.separated(
42 | itemCount: Log.debugLogs.length,
43 | separatorBuilder: (_, i) => const Divider(),
44 | padding: AppStyle.edgeInsetsA12,
45 | itemBuilder: (_, i) {
46 | var item = Log.debugLogs[i];
47 | return SelectableText(
48 | "${item.datetime.toString()}\r\n${item.content}",
49 | style: TextStyle(
50 | color: item.color,
51 | fontSize: 12,
52 | ),
53 | );
54 | },
55 | ),
56 | ),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/services/local_storage_service.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:get/get.dart';
4 | import 'package:hive/hive.dart';
5 | import 'package:flutter_cnblogs/app/log.dart';
6 |
7 | class LocalStorageService extends GetxService {
8 | static LocalStorageService get instance => Get.find();
9 |
10 | /// 显示模式
11 | /// * [0] 跟随系统
12 | /// * [1] 浅色模式
13 | /// * [2] 深色模式
14 | static const String kThemeMode = "ThemeMode";
15 |
16 | /// 语言
17 | /// * [zh] 简体中文
18 | /// * [en] 英文
19 | static const String kLanguage = "Language";
20 |
21 | /// DEBUG模式
22 | static const String kDebugModeKey = "DebugMode";
23 |
24 | /// ACCESS_TOKEN
25 | static const String kAccessToken = "AccessToken";
26 |
27 | /// ACCESS_TOKEN过期时间
28 | static const String kAccessTokenExpiresTime = "AccessTokenExpiresTime";
29 |
30 | /// ACCESS_TOKEN
31 | static const String kUserAccessToken = "UserAccessToken";
32 |
33 | /// REFRESH_TOKEN
34 | static const String kUserRefreshToken = "UserRefreshToken";
35 |
36 | /// ACCESS_TOKEN过期时间
37 | static const String kUserAccessTokenExpiresTime =
38 | "UserAccessTokenExpiresTime";
39 |
40 | /// UserID
41 | static const String kUserID = "UserID";
42 |
43 | late Box settingsBox;
44 | Future init() async {
45 | settingsBox = await Hive.openBox(
46 | "LocalStorage",
47 | //加密存储信息,密钥需要32位
48 | encryptionCipher: HiveAesCipher(
49 | utf8.encode(r"ASMCd6hy$n!!@DrU6tc^7@hEBLLWHr0r"),
50 | ),
51 | );
52 | }
53 |
54 | T getValue(dynamic key, T defaultValue) {
55 | var value = settingsBox.get(key, defaultValue: defaultValue) as T;
56 | Log.d("Get LocalStorage:$key\r\n$value");
57 | return value;
58 | }
59 |
60 | Future setValue(dynamic key, T value) async {
61 | Log.d("Set LocalStorage:$key\r\n$value");
62 | return await settingsBox.put(key, value);
63 | }
64 |
65 | Future removeValue(dynamic key) async {
66 | Log.d("Remove LocalStorage:$key");
67 | return await settingsBox.delete(key);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/models/search/search_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class SearchItemModel {
11 | SearchItemModel({
12 | this.title,
13 | this.content,
14 | this.userName,
15 | this.userAlias,
16 | required this.publishTime,
17 | required this.voteTimes,
18 | required this.viewTimes,
19 | required this.commentTimes,
20 | this.uri,
21 | this.id,
22 | this.avatar,
23 | });
24 |
25 | factory SearchItemModel.fromJson(Map json) =>
26 | SearchItemModel(
27 | title: asT(json['Title']) ?? "",
28 | content: asT(json['Content']) ?? "",
29 | userName: asT(json['UserName']) ?? "",
30 | userAlias: asT(json['UserAlias']) ?? "",
31 | publishTime: asT(json['PublishTime'])!,
32 | voteTimes: asT(json['VoteTimes'])!,
33 | viewTimes: asT(json['ViewTimes'])!,
34 | commentTimes: asT(json['CommentTimes'])!,
35 | uri: asT(json['Uri']) ?? "",
36 | id: asT(json['Id']) ?? "",
37 | avatar: asT(json['Avatar']) ?? "",
38 | );
39 |
40 | String? title;
41 | String? content;
42 | String? userName;
43 | String? userAlias;
44 | String publishTime;
45 | int voteTimes;
46 | int viewTimes;
47 | int commentTimes;
48 | String? uri;
49 | String? id;
50 | String? avatar;
51 | DateTime get postDateTime => DateTime.parse(publishTime);
52 |
53 | @override
54 | String toString() {
55 | return jsonEncode(this);
56 | }
57 |
58 | Map toJson() => {
59 | 'Title': title,
60 | 'Content': content,
61 | 'UserName': userName,
62 | 'UserAlias': userAlias,
63 | 'PublishTime': publishTime,
64 | 'VoteTimes': voteTimes,
65 | 'ViewTimes': viewTimes,
66 | 'CommentTimes': commentTimes,
67 | 'Uri': uri,
68 | 'Id': id,
69 | 'Avatar': avatar,
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/lib/app/log.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:get/get.dart';
4 | import 'package:logger/logger.dart';
5 |
6 | class Log {
7 | static RxList debugLogs = [].obs;
8 |
9 | static void addDebugLog(String content, Color? color) {
10 | if (kReleaseMode) {
11 | return;
12 | }
13 | if (content.contains("请求响应")) {
14 | content = content.split("\n").join('\n💡 ');
15 | }
16 | try {
17 | debugLogs.insert(0, DebugLogModel(DateTime.now(), content, color: color));
18 | } catch (e) {
19 | if (kDebugMode) {
20 | print(e);
21 | }
22 | }
23 | }
24 |
25 | static Logger logger = Logger(
26 | printer: PrettyPrinter(
27 | methodCount: 0,
28 | errorMethodCount: 8,
29 | lineLength: 120,
30 | colors: true,
31 | printEmojis: true,
32 | printTime: false,
33 | ),
34 | );
35 |
36 | static void d(String message) {
37 | addDebugLog(message, Colors.orange);
38 | logger.d("${DateTime.now().toString()}\n$message");
39 | }
40 |
41 | static void i(String message) {
42 | addDebugLog(message, Colors.blue);
43 | logger.i("${DateTime.now().toString()}\n$message");
44 | }
45 |
46 | static void e(String message, StackTrace stackTrace) {
47 | addDebugLog('$message\r\n\r\n$stackTrace', Colors.red);
48 | logger.e("${DateTime.now().toString()}\n$message", stackTrace: stackTrace);
49 | }
50 |
51 | static void w(String message) {
52 | addDebugLog(message, Colors.pink);
53 | logger.w("${DateTime.now().toString()}\n$message");
54 | }
55 |
56 | static void logPrint(dynamic obj) {
57 | addDebugLog(obj.toString(), Colors.red);
58 | if (obj is Error) {
59 | Log.e(obj.toString(), obj.stackTrace ?? StackTrace.current);
60 | } else if (kDebugMode) {
61 | print(obj);
62 | }
63 | }
64 | }
65 |
66 | class DebugLogModel {
67 | final String content;
68 | final DateTime datetime;
69 | final Color? color;
70 | DebugLogModel(this.datetime, this.content, {this.color});
71 | }
72 |
--------------------------------------------------------------------------------
/lib/requests/oauth_request.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cnblogs/models/oauth/token_model.dart';
2 | import 'package:flutter_cnblogs/models/oauth/user_token_model.dart';
3 | import 'package:flutter_cnblogs/requests/base/api.dart';
4 | import 'package:flutter_cnblogs/requests/base/http_client.dart';
5 |
6 | class OAuthRequest {
7 | /// Client_Credentials授权
8 | Future getToken() async {
9 | var result = await HttpClient.instance.post(
10 | '/token',
11 | data: {
12 | "client_id": Api.kClientID,
13 | "client_secret": Api.kClientSecret,
14 | "grant_type": "client_credentials",
15 | },
16 | formUrlEncoded: true,
17 | isRetry: true,
18 | );
19 | TokenModel tokenModel = TokenModel.fromJson(result);
20 | return tokenModel;
21 | }
22 |
23 | /// Authorization_Code授权
24 | Future getUserToken(String code) async {
25 | var result = await HttpClient.instance.post(
26 | '/connect/token',
27 | baseUrl: Api.kOAuthBaseUrl,
28 | data: {
29 | "client_id": Api.kClientID,
30 | "client_secret": Api.kClientSecret,
31 | "grant_type": "authorization_code",
32 | "code": code,
33 | "redirect_uri": "https://oauth.cnblogs.com/auth/callback",
34 | },
35 | formUrlEncoded: true,
36 | isRetry: true,
37 | );
38 | UserTokenModel tokenModel = UserTokenModel.fromJson(result);
39 | return tokenModel;
40 | }
41 |
42 | /// 刷新Token
43 | Future refreshUserToken(String refreshToken) async {
44 | var result = await HttpClient.instance.post(
45 | '/connect/token',
46 | baseUrl: Api.kOAuthBaseUrl,
47 | data: {
48 | "client_id": Api.kClientID,
49 | "client_secret": Api.kClientSecret,
50 | "grant_type": "refresh_token",
51 | "refresh_token": refreshToken,
52 | "redirect_uri": "https://oauth.cnblogs.com/auth/callback",
53 | },
54 | formUrlEncoded: true,
55 | isRetry: true,
56 | );
57 | UserTokenModel tokenModel = UserTokenModel.fromJson(result);
58 | return tokenModel;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/models/blogs/blog_list_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class BlogListItemModel {
11 | BlogListItemModel({
12 | required this.id,
13 | required this.title,
14 | required this.url,
15 | required this.description,
16 | required this.author,
17 | required this.blogapp,
18 | required this.avatar,
19 | required this.postdate,
20 | required this.viewcount,
21 | required this.commentcount,
22 | required this.diggcount,
23 | });
24 |
25 | factory BlogListItemModel.fromJson(Map json) =>
26 | BlogListItemModel(
27 | id: asT(json['Id']) ?? 0,
28 | title: asT(json['Title']) ?? "",
29 | url: asT(json['Url']) ?? "",
30 | description: asT(json['Description']) ?? "",
31 | author: asT(json['Author']) ?? "",
32 | blogapp: asT(json['BlogApp']) ?? "",
33 | avatar: asT(json['Avatar']) ?? "",
34 | postdate: asT(json['PostDate']) ?? "",
35 | viewcount: asT(json['ViewCount']) ?? 0,
36 | commentcount: asT(json['CommentCount']) ?? 0,
37 | diggcount: asT(json['DiggCount']) ?? 0,
38 | );
39 |
40 | int id;
41 | String title;
42 | String url;
43 | String description;
44 | String author;
45 | String blogapp;
46 | String avatar;
47 | String postdate;
48 | DateTime get postDateTime => DateTime.parse(postdate);
49 | int viewcount;
50 | int commentcount;
51 | int diggcount;
52 |
53 | @override
54 | String toString() {
55 | return jsonEncode(this);
56 | }
57 |
58 | Map toJson() => {
59 | 'Id': id,
60 | 'Title': title,
61 | 'Url': url,
62 | 'Description': description,
63 | 'Author': author,
64 | 'BlogApp': blogapp,
65 | 'Avatar': avatar,
66 | 'PostDate': postdate,
67 | 'ViewCount': viewcount,
68 | 'CommentCount': commentcount,
69 | 'DiggCount': diggcount,
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/lib/modules/user/login/login_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/modules/user/login/login_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/status/app_error_widget.dart';
5 | import 'package:flutter_inappwebview/flutter_inappwebview.dart';
6 | import 'package:get/get.dart';
7 |
8 | class LoginPage extends GetView {
9 | const LoginPage({Key? key}) : super(key: key);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Scaffold(
14 | appBar: AppBar(
15 | title: Text(LocaleKeys.login_title.tr),
16 | ),
17 | body: Stack(
18 | children: [
19 | Obx(
20 | () => Offstage(
21 | offstage: controller.pageError.value,
22 | child: InAppWebView(
23 | key: controller.webViewkey,
24 | initialOptions: controller.webViewGroupOptions,
25 | onWebViewCreated: controller.onWebViewCreated,
26 | onLoadStart: controller.onLoadStart,
27 | onLoadError: controller.onLoadError,
28 | onLoadStop: controller.onLoadStop,
29 | shouldOverrideUrlLoading: controller.shouldOverrideUrlLoading,
30 | ),
31 | ),
32 | ),
33 | Obx(
34 | () => Offstage(
35 | offstage: !controller.pageError.value,
36 | child: AppErrorWidget(
37 | errorMsg: controller.errorMsg.value,
38 | onRefresh: () => controller.refreshWeb(),
39 | ),
40 | ),
41 | ),
42 | Positioned.fill(
43 | top: 0,
44 | left: 0,
45 | child: Obx(
46 | () => Offstage(
47 | offstage: !controller.pageLoadding.value,
48 | child: Container(
49 | alignment: Alignment.topLeft,
50 | child: const LinearProgressIndicator(),
51 | ),
52 | ),
53 | ),
54 | ),
55 | ],
56 | ),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
13 |
21 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/lib/modules/blogs/home/blogs_home_controller.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_cnblogs/app/controller/base_controller.dart';
5 | import 'package:flutter_cnblogs/app/event_bus.dart';
6 | import 'package:flutter_cnblogs/generated/locales.g.dart';
7 | import 'package:flutter_cnblogs/modules/blogs/home/blogs_list_controller.dart';
8 | import 'package:flutter_cnblogs/modules/blogs/home/knowledge/blogs_knowledge_controller.dart';
9 |
10 | import 'package:flutter_cnblogs/requests/blogs_request.dart';
11 | import 'package:flutter_cnblogs/routes/app_navigation.dart';
12 | import 'package:get/get.dart';
13 |
14 | class BlogsHomeController extends GetxController
15 | with GetSingleTickerProviderStateMixin {
16 | final BlogsRequest blogsRequest = BlogsRequest();
17 | late TabController tabController;
18 | BlogsHomeController() {
19 | tabController = TabController(length: tabs.length, vsync: this);
20 | }
21 |
22 | final tabs = [
23 | LocaleKeys.blogs_home_new,
24 | LocaleKeys.blogs_home_mostread,
25 | LocaleKeys.blogs_home_mostliked,
26 | LocaleKeys.blogs_home_picked,
27 | LocaleKeys.blogs_home_knowledge,
28 | ];
29 |
30 | StreamSubscription? streamSubscription;
31 |
32 | @override
33 | void onInit() {
34 | streamSubscription = EventBus.instance.listen(
35 | EventBus.kBottomNavigationBarClicked,
36 | (index) {
37 | if (index == 0) {
38 | refreshOrScrollTop();
39 | }
40 | },
41 | );
42 | for (var tag in tabs) {
43 | if (tag == LocaleKeys.blogs_home_knowledge) {
44 | Get.put(BlogsKnowledgeController());
45 | } else {
46 | Get.put(BlogsListController(tag), tag: tag);
47 | }
48 | }
49 |
50 | super.onInit();
51 | }
52 |
53 | void refreshOrScrollTop() {
54 | var tabIndex = tabController.index;
55 | BasePageController controller;
56 | if (tabIndex == 4) {
57 | controller = Get.find();
58 | } else {
59 | controller = Get.find(tag: tabs[tabIndex]);
60 | }
61 | controller.scrollToTopOrRefresh();
62 | }
63 |
64 | void toSearch() {
65 | AppNavigator.toSearch(SearchType.blog);
66 | }
67 |
68 | @override
69 | void onClose() {
70 | streamSubscription?.cancel();
71 | super.onClose();
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/modules/user/home/user_home_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/app/controller/app_settings_controller.dart';
3 | import 'package:flutter_cnblogs/app/utils.dart';
4 | import 'package:flutter_cnblogs/generated/locales.g.dart';
5 | import 'package:flutter_cnblogs/routes/app_navigation.dart';
6 | import 'package:flutter_cnblogs/routes/route_path.dart';
7 | import 'package:flutter_cnblogs/services/user_service.dart';
8 | import 'package:get/get.dart';
9 |
10 | class UserHomeController extends GetxController {
11 | final AppSettingsController settingController =
12 | Get.find();
13 | @override
14 | void onInit() {
15 | UserService.instance.refreshProfile();
16 | super.onInit();
17 | }
18 |
19 | /// 登录
20 | void login() {
21 | UserService.instance.login();
22 | }
23 |
24 | /// 退出登录
25 | void logout() async {
26 | var result = await Utils.showAlertDialog(
27 | LocaleKeys.user_home_logout_msg.tr,
28 | title: LocaleKeys.user_home_logout.tr,
29 | );
30 | if (result) {
31 | UserService.instance.logout();
32 | }
33 | }
34 |
35 | /// 我的博客
36 | void myBlog() async {
37 | if (!UserService.instance.logined.value &&
38 | !(await UserService.instance.login())) {
39 | return;
40 | }
41 | var blogApp = UserService.instance.userProfile.value?.blogApp ?? "";
42 | if (blogApp.isNotEmpty) {
43 | AppNavigator.toUserBlog(blogApp);
44 | }
45 | }
46 |
47 | /// 我的收藏
48 | void myBookmark() {
49 | Get.toNamed(RoutePath.kUserBookmark);
50 | }
51 |
52 | /// 主题设置
53 | void setTheme() {
54 | settingController.changeTheme();
55 | }
56 |
57 | /// 语言设置
58 | void setLanguage() {
59 | settingController.changeLanguage();
60 | }
61 |
62 | /// 关于我们
63 | void about() {
64 | Get.dialog(AboutDialog(
65 | applicationIcon: Image.asset(
66 | 'assets/images/logo.png',
67 | width: 48,
68 | height: 48,
69 | ),
70 | applicationName: LocaleKeys.app_name.tr,
71 | applicationVersion: LocaleKeys.app_slogan.tr,
72 | applicationLegalese: LocaleKeys.user_home_about_msg.trParams({
73 | "version": Utils.packageInfo.version,
74 | }),
75 | ));
76 | }
77 |
78 | /// 检查更新
79 | void checkUpdate() {
80 | Utils.checkUpdate(showMsg: true);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/models/statuses/statuses_list_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:math';
3 |
4 | T? asT(dynamic value) {
5 | if (value is T) {
6 | return value;
7 | }
8 | return null;
9 | }
10 |
11 | class StatusesListItemModel {
12 | StatusesListItemModel({
13 | required this.id,
14 | required this.content,
15 | required this.isPrivate,
16 | required this.isLucky,
17 | required this.commentCount,
18 | required this.dateAdded,
19 | required this.userAlias,
20 | required this.userDisplayName,
21 | required this.userIconUrl,
22 | required this.userId,
23 | required this.userGuid,
24 | this.luckyIndex = 0,
25 | });
26 |
27 | factory StatusesListItemModel.fromJson(Map json) =>
28 | StatusesListItemModel(
29 | id: asT(json['Id']) ?? 0,
30 | content: asT(json['Content']) ?? "",
31 | isPrivate: asT(json['IsPrivate']) ?? false,
32 | isLucky: asT(json['IsLucky']) ?? false,
33 | commentCount: asT(json['CommentCount']) ?? 0,
34 | dateAdded: asT(json['DateAdded']) ?? "",
35 | userAlias: asT(json['UserAlias']) ?? "",
36 | userDisplayName: asT(json['UserDisplayName']) ?? "",
37 | userIconUrl: asT(json['UserIconUrl']) ?? "",
38 | userId: asT(json['UserId']) ?? 0,
39 | userGuid: asT(json['UserGuid']) ?? "",
40 |
41 | // API没有返回是啥星星,只能随机加一个了
42 | luckyIndex: Random().nextInt(10),
43 | );
44 |
45 | int id;
46 | String content;
47 | bool isPrivate;
48 | bool isLucky;
49 | int commentCount;
50 | String dateAdded;
51 | String userAlias;
52 | String userDisplayName;
53 | String userIconUrl;
54 | int userId;
55 | String userGuid;
56 | DateTime get postDateTime => DateTime.parse(dateAdded);
57 | int luckyIndex = 0;
58 |
59 | @override
60 | String toString() {
61 | return jsonEncode(this);
62 | }
63 |
64 | Map toJson() => {
65 | 'Id': id,
66 | 'Content': content,
67 | 'IsPrivate': isPrivate,
68 | 'IsLucky': isLucky,
69 | 'CommentCount': commentCount,
70 | 'DateAdded': dateAdded,
71 | 'UserAlias': userAlias,
72 | 'UserDisplayName': userDisplayName,
73 | 'UserIconUrl': userIconUrl,
74 | 'UserId': userId,
75 | 'UserGuid': userGuid,
76 | };
77 | }
78 |
--------------------------------------------------------------------------------
/lib/modules/blogs/knowledge_content/knowledge_content_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/modules/blogs/knowledge_content/knowledge_content_controller.dart';
4 | import 'package:flutter_cnblogs/widgets/status/app_error_widget.dart';
5 | import 'package:flutter_cnblogs/widgets/status/app_loadding_widget.dart';
6 | import 'package:flutter_inappwebview/flutter_inappwebview.dart';
7 | import 'package:get/get.dart';
8 |
9 | class KnowledgeContentPage extends GetView {
10 | const KnowledgeContentPage({Key? key}) : super(key: key);
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Scaffold(
15 | appBar: AppBar(
16 | title: Text(controller.item.title),
17 | centerTitle: false,
18 | actions: [
19 | PopupMenuButton(
20 | itemBuilder: ((context) => [
21 | PopupMenuItem(
22 | onTap: controller.openBrowser,
23 | child: Text(LocaleKeys.blog_content_browser.tr),
24 | ),
25 | PopupMenuItem(
26 | onTap: controller.share,
27 | child: Text(LocaleKeys.blog_content_share.tr),
28 | ),
29 | ]),
30 | child: const SizedBox(
31 | width: 48,
32 | height: 48,
33 | child: Icon(Icons.more_vert),
34 | ),
35 | ),
36 | ],
37 | ),
38 | body: Stack(
39 | children: [
40 | InAppWebView(
41 | key: controller.webViewkey,
42 | initialOptions: controller.webViewGroupOptions,
43 | onWebViewCreated: controller.onWebViewCreated,
44 | shouldOverrideUrlLoading: controller.shouldOverrideUrlLoading,
45 | ),
46 | Obx(
47 | () => Offstage(
48 | offstage: !controller.pageLoadding.value,
49 | child: const AppLoaddingWidget(),
50 | ),
51 | ),
52 | Obx(
53 | () => Offstage(
54 | offstage: !controller.pageError.value,
55 | child: AppErrorWidget(
56 | errorMsg: controller.errorMsg.value,
57 | onRefresh: () => controller.loadData(),
58 | ),
59 | ),
60 | ),
61 | ],
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/modules/indexed/indexed_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_cnblogs/generated/locales.g.dart';
3 | import 'package:flutter_cnblogs/modules/indexed/indexed_controller.dart';
4 | import 'package:get/get.dart';
5 | import 'package:remixicon/remixicon.dart';
6 |
7 | class IndexedPage extends GetView {
8 | const IndexedPage({Key? key}) : super(key: key);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Scaffold(
13 | body: Obx(
14 | () => IndexedStack(
15 | index: controller.index.value,
16 | children: controller.pages,
17 | ),
18 | ),
19 | bottomNavigationBar: Obx(
20 | () => BottomNavigationBar(
21 | currentIndex: controller.index.value,
22 | onTap: controller.setIndex,
23 | selectedFontSize: 12,
24 | unselectedFontSize: 12,
25 | iconSize: 24,
26 | type: BottomNavigationBarType.fixed,
27 | showSelectedLabels: true,
28 | showUnselectedLabels: false,
29 | elevation: 4,
30 | items: [
31 | //博客
32 | BottomNavigationBarItem(
33 | icon: const Icon(Remix.home_smile_line),
34 | activeIcon: const Icon(Remix.home_smile_fill),
35 | label: LocaleKeys.indexed_blogs.tr,
36 | ),
37 | //新闻
38 | BottomNavigationBarItem(
39 | icon: const Icon(Remix.article_line),
40 | activeIcon: const Icon(Remix.article_fill),
41 | label: LocaleKeys.indexed_news.tr,
42 | ),
43 | //闪存
44 | BottomNavigationBarItem(
45 | icon: const Icon(Remix.star_smile_line),
46 | activeIcon: const Icon(Remix.star_smile_fill),
47 | label: LocaleKeys.indexed_statuses.tr,
48 | ),
49 | //博问
50 | BottomNavigationBarItem(
51 | icon: const Icon(Remix.question_line),
52 | activeIcon: const Icon(Remix.question_fill),
53 | label: LocaleKeys.indexed_questions.tr,
54 | ),
55 | //用户
56 | BottomNavigationBarItem(
57 | icon: const Icon(Remix.user_smile_line),
58 | activeIcon: const Icon(Remix.user_smile_fill),
59 | label: LocaleKeys.indexed_user.tr,
60 | ),
61 | ],
62 | ),
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/models/news/news_comment_item_model.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | T? asT(dynamic value) {
4 | if (value is T) {
5 | return value;
6 | }
7 | return null;
8 | }
9 |
10 | class NewsCommentItemModel {
11 | NewsCommentItemModel({
12 | required this.commentID,
13 | required this.contentID,
14 | required this.commentContent,
15 | required this.userGuid,
16 | required this.userId,
17 | required this.userName,
18 | required this.faceUrl,
19 | required this.floor,
20 | required this.dateAdded,
21 | required this.agreeCount,
22 | required this.antiCount,
23 | required this.parentCommentID,
24 | this.parentComment,
25 | });
26 |
27 | factory NewsCommentItemModel.fromJson(Map json) =>
28 | NewsCommentItemModel(
29 | commentID: asT(json['CommentID']) ?? 0,
30 | contentID: asT(json['ContentID']) ?? 0,
31 | commentContent: asT(json['CommentContent']) ?? "",
32 | userGuid: asT(json['UserGuid']) ?? "",
33 | userId: asT(json['UserId']) ?? 0,
34 | userName: asT(json['UserName']) ?? "",
35 | faceUrl: asT(json['FaceUrl']) ?? "",
36 | floor: asT(json['Floor']) ?? 0,
37 | dateAdded: asT(json['DateAdded']) ?? "",
38 | agreeCount: asT(json['AgreeCount']) ?? 0,
39 | antiCount: asT(json['AntiCount']) ?? 0,
40 | parentCommentID: asT(json['ParentCommentID']) ?? 0,
41 | parentComment: asT