├── .fvm ├── flutter_sdk └── fvm_config.json ├── .github └── workflows │ └── publish_release.yml ├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── xycz │ │ │ │ └── cnblogs │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── 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 │ │ │ ├── playstore-icon.png │ │ │ ├── values-en │ │ │ └── strings.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ ├── values-zh │ │ │ └── strings.xml │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── images │ └── logo.png ├── locales │ ├── en.json │ └── zh.json ├── lotties │ ├── empty.json │ ├── error.json │ └── loadding.json └── templates │ ├── blog │ ├── blog.html │ ├── dark.css │ ├── knowledge.html │ └── light.css │ ├── js │ ├── common.js │ └── highlight.js │ └── news │ ├── dark.css │ ├── light.css │ └── news.html ├── document └── new_version.json ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.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-83.5x83.5@2x.png │ │ ├── icon-1024.png │ │ ├── icon-20-ipad.png │ │ ├── icon-20@2x-ipad.png │ │ ├── icon-20@2x.png │ │ ├── icon-20@3x.png │ │ ├── icon-29-ipad.png │ │ ├── icon-29.png │ │ ├── icon-29@2x-ipad.png │ │ ├── icon-29@2x.png │ │ ├── icon-29@3x.png │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ └── icon-83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ ├── en.lproj │ └── InfoPlist.strings │ └── zh-Hans.lproj │ ├── InfoPlist.strings │ └── Main.strings ├── lib ├── app │ ├── app_error.dart │ ├── app_style.dart │ ├── controller │ │ ├── app_settings_controller.dart │ │ ├── base_controller.dart │ │ └── base_webview_controller.dart │ ├── event_bus.dart │ ├── log.dart │ └── utils.dart ├── generated │ └── locales.g.dart ├── main.dart ├── models │ ├── blogs │ │ ├── blog_comment_item_model.dart │ │ ├── blog_content_model.dart │ │ ├── blog_list_item_model.dart │ │ ├── blog_list_item_v2_model.dart │ │ ├── knowledge_list_item_model.dart │ │ └── user_blog_info_model.dart │ ├── news │ │ ├── news_comment_item_model.dart │ │ └── news_list_item_model.dart │ ├── oauth │ │ ├── token_model.dart │ │ └── user_token_model.dart │ ├── questions │ │ ├── answer_comment_list_item_model.dart │ │ ├── answer_list_item_model.dart │ │ └── question_list_item_model.dart │ ├── search │ │ └── search_item_model.dart │ ├── statuses │ │ ├── statuses_comment_item_model.dart │ │ └── statuses_list_item_model.dart │ ├── user │ │ ├── bookmark_list_item_model.dart │ │ └── user_info_model.dart │ └── version_model.dart ├── modules │ ├── blogs │ │ ├── comment │ │ │ ├── blog_comment_controller.dart │ │ │ └── blog_comment_page.dart │ │ ├── content │ │ │ ├── blog_content_controller.dart │ │ │ └── blog_content_page.dart │ │ ├── home │ │ │ ├── blogs_home_controller.dart │ │ │ ├── blogs_home_page.dart │ │ │ ├── blogs_list_controller.dart │ │ │ ├── blogs_list_view.dart │ │ │ └── knowledge │ │ │ │ ├── blogs_knowledge_controller.dart │ │ │ │ └── blogs_knowledge_view.dart │ │ └── knowledge_content │ │ │ ├── knowledge_content_controller.dart │ │ │ └── knowledge_content_page.dart │ ├── indexed │ │ ├── indexed_controller.dart │ │ └── indexed_page.dart │ ├── news │ │ ├── comment │ │ │ ├── news_comment_controller.dart │ │ │ └── news_comment_page.dart │ │ ├── content │ │ │ ├── news_content_controller.dart │ │ │ └── news_content_page.dart │ │ └── home │ │ │ ├── news_home_controller.dart │ │ │ ├── news_home_page.dart │ │ │ ├── news_list_controller.dart │ │ │ └── news_list_view.dart │ ├── other │ │ ├── debug_log_page.dart │ │ └── web_view │ │ │ ├── web_view_controller.dart │ │ │ └── web_view_page.dart │ ├── questions │ │ ├── comment │ │ │ ├── answer_comment_controller.dart │ │ │ └── answer_comment_page.dart │ │ ├── detail │ │ │ ├── question_detail_controller.dart │ │ │ └── question_detail_page.dart │ │ └── home │ │ │ ├── questions_home_controller.dart │ │ │ ├── questions_home_page.dart │ │ │ ├── questions_list_controller.dart │ │ │ └── questions_list_view.dart │ ├── search │ │ ├── search_controller.dart │ │ ├── search_list_view.dart │ │ ├── search_list_view_controlelr.dart │ │ └── search_page.dart │ ├── statuses │ │ ├── detail │ │ │ ├── statuses_detail_controller.dart │ │ │ └── statuses_detail_page.dart │ │ └── home │ │ │ ├── statuses_home_controller.dart │ │ │ ├── statuses_home_page.dart │ │ │ ├── statuses_list_controller.dart │ │ │ └── statuses_list_view.dart │ └── user │ │ ├── blogs │ │ ├── user_blogs_controller.dart │ │ └── user_blogs_page.dart │ │ ├── bookmark │ │ ├── bookmark_controller.dart │ │ └── bookmark_page.dart │ │ ├── home │ │ ├── user_home_controller.dart │ │ └── user_home_page.dart │ │ └── login │ │ ├── login_controller.dart │ │ └── login_page.dart ├── requests │ ├── base │ │ ├── api.dart │ │ ├── app_log_interceptor.dart │ │ ├── http_client.dart │ │ └── oauth_interceptor.dart │ ├── blogs_request.dart │ ├── common_request.dart │ ├── news_request.dart │ ├── oauth_request.dart │ ├── questions_request.dart │ ├── search_request.dart │ ├── statuses_request.dart │ └── user_request.dart ├── routes │ ├── app_navigation.dart │ ├── app_pages.dart │ └── route_path.dart ├── services │ ├── api_service.dart │ ├── local_storage_service.dart │ └── user_service.dart └── widgets │ ├── custom_html.dart │ ├── items │ ├── answer_comment_item_widget.dart │ ├── blog_comment_item_widget.dart │ ├── blog_item_widget.dart │ ├── knowledge_item_widget.dart │ ├── news_comment_item_widget.dart │ ├── news_item_widget.dart │ ├── question_item_widget.dart │ ├── statuses_comment_item_widget.dart │ └── statuses_item_widget.dart │ ├── keep_alive_wrapper.dart │ ├── net_image.dart │ ├── number_step_dialog.dart │ ├── page_list_view.dart │ ├── rectangular_indicator.dart │ ├── status │ ├── app_empty_widget.dart │ ├── app_error_widget.dart │ ├── app_loadding_widget.dart │ └── app_not_login_widget.dart │ └── statuses_content.dart ├── pubspec.yaml ├── screenshot ├── screenshot_dark.jpg └── screenshot_light.jpg └── test └── widget_test.dart /.fvm/flutter_sdk: -------------------------------------------------------------------------------- 1 | C:/Users/DW-SVN-SERVER/fvm/versions/3.13.9 -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.13.9", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 博客园Flutter 2 | 3 | 使用Flutter编写的博客园客户端,支持iOS及Android。 4 | 5 | 基于[博客园开放API](https://api.cnblogs.com/help)开发;受限于API,部分功能可能并不完善。 6 | 7 | ![浅色模式](/screenshot/screenshot_light.jpg) 8 | 9 | ![深色模式](/screenshot/screenshot_dark.jpg) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | compileSdkVersion 33 36 | ndkVersion flutter.ndkVersion 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = '1.8' 45 | } 46 | 47 | sourceSets { 48 | main.java.srcDirs += 'src/main/kotlin' 49 | } 50 | 51 | defaultConfig { 52 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 53 | applicationId "com.xycz.cnblogs" 54 | // You can update the following values to match your application needs. 55 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 56 | minSdkVersion 21 57 | targetSdkVersion flutter.targetSdkVersion 58 | versionCode flutterVersionCode.toInteger() 59 | versionName flutterVersionName 60 | } 61 | signingConfigs { 62 | release { 63 | keyAlias keystoreProperties['keyAlias'] 64 | keyPassword keystoreProperties['keyPassword'] 65 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 66 | storePassword keystoreProperties['storePassword'] 67 | } 68 | } 69 | buildTypes { 70 | debug { 71 | signingConfig signingConfigs.release 72 | } 73 | profile { 74 | signingConfig signingConfigs.release 75 | } 76 | release { 77 | signingConfig signingConfigs.release 78 | } 79 | } 80 | } 81 | 82 | flutter { 83 | source '../..' 84 | } 85 | 86 | dependencies { 87 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 88 | } 89 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 13 | 21 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /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/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/playstore-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/android/app/src/main/res/playstore-icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Cnblogs 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /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/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/assets/images/logo.png -------------------------------------------------------------------------------- /assets/templates/blog/blog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 15 | 16 | 17 | 18 |
19 |

@title

20 | 21 | avatar 22 |
23 |
@username
24 |
@puttime
25 |
26 |
27 |
28 | 29 |
30 | @content 31 |
32 |
33 |

@stat

34 |

@puttime

35 |
36 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /assets/templates/blog/knowledge.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 24 | 25 | 26 | 27 |
28 |

@title

29 |

30 | @username 31 | @puttime 32 |

33 |
34 | 35 |
36 | @content 37 |
38 |
39 |

@stat

40 |

@puttime

41 |
42 | 43 | 46 | 47 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /assets/templates/news/news.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 |
19 |

@title

20 |

21 | @puttime 22 |

23 |
24 | 25 |
26 | @content 27 |
28 |
29 |

@stat

30 |

@puttime

31 |
32 | 33 | 36 | 37 | -------------------------------------------------------------------------------- /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/.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | #platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | target.build_configurations.each do |config| 41 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 42 | '$(inherited)', 43 | 44 | ## dart: PermissionGroup.calendar 45 | # 'PERMISSION_EVENTS=1', 46 | 47 | ## dart: PermissionGroup.reminders 48 | # 'PERMISSION_REMINDERS=1', 49 | 50 | ## dart: PermissionGroup.contacts 51 | # 'PERMISSION_CONTACTS=1', 52 | 53 | ## dart: PermissionGroup.camera 54 | 'PERMISSION_CAMERA=1', 55 | 56 | ## dart: PermissionGroup.microphone 57 | # 'PERMISSION_MICROPHONE=1', 58 | 59 | ## dart: PermissionGroup.speech 60 | # 'PERMISSION_SPEECH_RECOGNIZER=1', 61 | 62 | ## dart: PermissionGroup.photos 63 | 'PERMISSION_PHOTOS=1', 64 | 65 | ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 66 | 'PERMISSION_LOCATION=1', 67 | 68 | ## dart: PermissionGroup.notification 69 | 'PERMISSION_NOTIFICATIONS=1', 70 | 71 | ## dart: PermissionGroup.mediaLibrary 72 | # 'PERMISSION_MEDIA_LIBRARY=1', 73 | 74 | ## dart: PermissionGroup.sensors 75 | # 'PERMISSION_SENSORS=1', 76 | 77 | ## dart: PermissionGroup.bluetooth 78 | # 'PERMISSION_BLUETOOTH=1', 79 | 80 | ## dart: PermissionGroup.appTrackingTransparency 81 | # 'PERMISSION_APP_TRACKING_TRANSPARENCY=1', 82 | 83 | ## dart: PermissionGroup.criticalAlerts 84 | # 'PERMISSION_CRITICAL_ALERTS=1' 85 | ] 86 | 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.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.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.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 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x-ipad.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/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/4d75b3d94c7613af07266254255fc6d957e397a2/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /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/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/zh-Hans.lproj/Main.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /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/app/controller/base_webview_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 4 | import 'package:flutter_cnblogs/app/log.dart'; 5 | import 'package:flutter_cnblogs/app/utils.dart'; 6 | import 'package:flutter_cnblogs/routes/app_navigation.dart'; 7 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 8 | 9 | class BaseWebViewController extends BaseController { 10 | final UniqueKey webViewkey = UniqueKey(); 11 | late InAppWebViewController? webViewController; 12 | final InAppWebViewGroupOptions webViewGroupOptions = InAppWebViewGroupOptions( 13 | crossPlatform: InAppWebViewOptions( 14 | transparentBackground: true, 15 | useShouldOverrideUrlLoading: true, 16 | ), 17 | ); 18 | void onWebViewCreated(InAppWebViewController controller) { 19 | webViewController = controller; 20 | webViewController?.addJavaScriptHandler( 21 | handlerName: 'showImage', 22 | callback: (args) { 23 | openImageViewer( 24 | args[0].toString(), 25 | (args[1] as List).map((e) => e as String).toList(), 26 | ); 27 | }, 28 | ); 29 | webViewController?.addJavaScriptHandler( 30 | handlerName: 'showAuthor', 31 | callback: (args) { 32 | openUserBlogs(); 33 | }, 34 | ); 35 | } 36 | 37 | final RegExp blogRegExp1 = RegExp(r'cnblogs.com/(.*?)/p/(.*?).html'); 38 | final RegExp blogRegExp2 = 39 | RegExp(r'cnblogs.com/(.*?)/archive/\d+/\d+/\d+/(.*?).html'); 40 | Future shouldOverrideUrlLoading( 41 | InAppWebViewController controller, NavigationAction action) async { 42 | var uri = action.request.url!; 43 | var url = uri.toString(); 44 | if (url.startsWith("about:blank")) { 45 | return NavigationActionPolicy.ALLOW; 46 | } 47 | if (blogRegExp1.hasMatch(url)) { 48 | AppNavigator.toBlogContent(url: url); 49 | return NavigationActionPolicy.CANCEL; 50 | } 51 | 52 | var match2 = blogRegExp2.firstMatch(url); 53 | if (match2 != null) { 54 | String blogApp = match2.group(1)!; 55 | String blogPostId = match2.group(2)!; 56 | AppNavigator.toBlogContent( 57 | url: "http://www.cnblogs.com/$blogApp/p/$blogPostId.html"); 58 | return NavigationActionPolicy.CANCEL; 59 | } 60 | 61 | Log.i(uri.toString()); 62 | //使用WebView打开 63 | // launchUrl(uri, mode: LaunchMode.inAppWebView); 64 | AppNavigator.toWebView(url); 65 | return NavigationActionPolicy.CANCEL; 66 | } 67 | 68 | void openImageViewer(String img, List allImgs) { 69 | Utils.showImageViewer(allImgs.indexOf(img), allImgs); 70 | } 71 | 72 | Future getHighlightScript() async { 73 | return await rootBundle.loadString('assets/templates/js/highlight.js'); 74 | } 75 | 76 | Future getCommonScript() async { 77 | return await rootBundle.loadString('assets/templates/js/common.js'); 78 | } 79 | 80 | void openUserBlogs() {} 81 | } 82 | -------------------------------------------------------------------------------- /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/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/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/models/blogs/blog_content_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 BlogContentModel { 11 | BlogContentModel({ 12 | required this.id, 13 | required this.title, 14 | required this.url, 15 | required this.description, 16 | required this.viewCount, 17 | required this.diggCount, 18 | required this.commentCount, 19 | required this.author, 20 | required this.avatar, 21 | required this.blogId, 22 | required this.blogApp, 23 | required this.dateAdded, 24 | required this.dateUpdated, 25 | required this.body, 26 | this.tags, 27 | this.categories, 28 | }); 29 | 30 | factory BlogContentModel.fromJson(Map json) { 31 | final List? tags = json['tags'] is List ? [] : null; 32 | if (tags != null) { 33 | for (final dynamic item in json['tags']!) { 34 | if (item != null) { 35 | tags.add(asT(item)!); 36 | } 37 | } 38 | } 39 | 40 | final List? categories = 41 | json['categories'] is List ? [] : null; 42 | if (categories != null) { 43 | for (final dynamic item in json['categories']!) { 44 | if (item != null) { 45 | categories.add(asT(item)!); 46 | } 47 | } 48 | } 49 | return BlogContentModel( 50 | id: asT(json['id'])!, 51 | title: asT(json['title']) ?? "", 52 | url: asT(json['url']) ?? "", 53 | description: asT(json['description']) ?? "", 54 | viewCount: asT(json['viewCount'])!, 55 | diggCount: asT(json['diggCount'])!, 56 | commentCount: asT(json['commentCount'])!, 57 | author: asT(json['author']) ?? "", 58 | avatar: asT(json['avatar']) ?? "", 59 | blogId: asT(json['blogId'])!, 60 | blogApp: asT(json['blogApp']) ?? "", 61 | dateAdded: asT(json['dateAdded']) ?? "", 62 | dateUpdated: asT(json['dateUpdated']) ?? "", 63 | body: asT(json['body']) ?? "", 64 | tags: tags, 65 | categories: categories, 66 | ); 67 | } 68 | 69 | int id; 70 | String title; 71 | String url; 72 | String description; 73 | int viewCount; 74 | int diggCount; 75 | int commentCount; 76 | String author; 77 | String avatar; 78 | int blogId; 79 | String blogApp; 80 | String dateAdded; 81 | String dateUpdated; 82 | String body; 83 | List? tags; 84 | List? categories; 85 | DateTime get postDateTime => DateTime.parse(dateUpdated); 86 | 87 | @override 88 | String toString() { 89 | return jsonEncode(this); 90 | } 91 | 92 | Map toJson() => { 93 | 'id': id, 94 | 'title': title, 95 | 'url': url, 96 | 'description': description, 97 | 'viewCount': viewCount, 98 | 'diggCount': diggCount, 99 | 'commentCount': commentCount, 100 | 'author': author, 101 | 'avatar': avatar, 102 | 'blogId': blogId, 103 | 'blogApp': blogApp, 104 | 'dateAdded': dateAdded, 105 | 'dateUpdated': dateUpdated, 106 | 'body': body, 107 | 'tags': tags, 108 | 'categories': categories, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /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/models/blogs/blog_list_item_v2_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 BlogListItemV2Model { 11 | BlogListItemV2Model({ 12 | required this.id, 13 | required this.title, 14 | required this.url, 15 | required this.description, 16 | required this.viewCount, 17 | required this.diggCount, 18 | required this.commentCount, 19 | required this.author, 20 | required this.blogId, 21 | required this.blogUrl, 22 | required this.avatar, 23 | required this.postType, 24 | required this.postConfig, 25 | required this.dateAdded, 26 | required this.dateUpdated, 27 | this.entryName, 28 | }); 29 | 30 | factory BlogListItemV2Model.fromJson(Map json) => 31 | BlogListItemV2Model( 32 | id: asT(json['id'])!, 33 | title: asT(json['title'])!, 34 | url: asT(json['url'])!, 35 | description: asT(json['description'])!, 36 | viewCount: asT(json['viewCount'])!, 37 | diggCount: asT(json['diggCount'])!, 38 | commentCount: asT(json['commentCount'])!, 39 | author: asT(json['author'])!, 40 | blogId: asT(json['blogId'])!, 41 | blogUrl: asT(json['blogUrl'])!, 42 | avatar: asT(json['avatar']) ?? "", 43 | postType: asT(json['postType'])!, 44 | postConfig: asT(json['postConfig'])!, 45 | dateAdded: asT(json['dateAdded'])!, 46 | dateUpdated: asT(json['dateUpdated'])!, 47 | entryName: asT(json['entryName']), 48 | ); 49 | 50 | int id; 51 | String title; 52 | String url; 53 | String description; 54 | int viewCount; 55 | int diggCount; 56 | int commentCount; 57 | String author; 58 | int blogId; 59 | String blogUrl; 60 | String avatar; 61 | int postType; 62 | int postConfig; 63 | String dateAdded; 64 | String dateUpdated; 65 | String? entryName; 66 | 67 | @override 68 | String toString() { 69 | return jsonEncode(this); 70 | } 71 | 72 | Map toJson() => { 73 | 'id': id, 74 | 'title': title, 75 | 'url': url, 76 | 'description': description, 77 | 'viewCount': viewCount, 78 | 'diggCount': diggCount, 79 | 'commentCount': commentCount, 80 | 'author': author, 81 | 'blogId': blogId, 82 | 'blogUrl': blogUrl, 83 | 'avatar': avatar, 84 | 'postType': postType, 85 | 'postConfig': postConfig, 86 | 'dateAdded': dateAdded, 87 | 'dateUpdated': dateUpdated, 88 | 'entryName': entryName, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /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/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/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(json['ParentComment']), 42 | ); 43 | 44 | int commentID; 45 | int contentID; 46 | String commentContent; 47 | String userGuid; 48 | int userId; 49 | String userName; 50 | String faceUrl; 51 | int floor; 52 | String dateAdded; 53 | int agreeCount; 54 | int antiCount; 55 | int parentCommentID; 56 | Object? parentComment; 57 | DateTime? get postDateTime => DateTime.tryParse(dateAdded); 58 | 59 | @override 60 | String toString() { 61 | return jsonEncode(this); 62 | } 63 | 64 | Map toJson() => { 65 | 'CommentID': commentID, 66 | 'ContentID': contentID, 67 | 'CommentContent': commentContent, 68 | 'UserGuid': userGuid, 69 | 'UserId': userId, 70 | 'UserName': userName, 71 | 'FaceUrl': faceUrl, 72 | 'Floor': floor, 73 | 'DateAdded': dateAdded, 74 | 'AgreeCount': agreeCount, 75 | 'AntiCount': antiCount, 76 | 'ParentCommentID': parentCommentID, 77 | 'ParentComment': parentComment, 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /lib/modules/blogs/comment/blog_comment_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_error.dart'; 3 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 4 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 5 | import 'package:flutter_cnblogs/models/blogs/blog_comment_item_model.dart'; 6 | import 'package:flutter_cnblogs/services/user_service.dart'; 7 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 8 | import 'package:get/get.dart'; 9 | 10 | import '../../../requests/blogs_request.dart'; 11 | 12 | class BlogCommentController extends BasePageController { 13 | final String blogApp; 14 | final int postId; 15 | BlogCommentController({ 16 | required this.blogApp, 17 | required this.postId, 18 | }); 19 | final BlogsRequest blogsRequest = BlogsRequest(); 20 | 21 | @override 22 | Future> getData(int page, int pageSize) async { 23 | return await blogsRequest.getBlogComment( 24 | pageIndex: page, 25 | pageSize: pageSize, 26 | blogApp: blogApp, 27 | postId: postId, 28 | ); 29 | } 30 | 31 | @override 32 | void handleError(Object exception, {bool showPageError = false}) { 33 | super.handleError(AppError("博客园API只支持显示2022以后的评论"), 34 | showPageError: showPageError); 35 | } 36 | 37 | void showAddCommentDialog() async { 38 | if (!UserService.instance.logined.value && 39 | !await UserService.instance.login()) { 40 | return; 41 | } 42 | TextEditingController controller = TextEditingController(); 43 | var result = await Get.dialog( 44 | AlertDialog( 45 | title: Text(LocaleKeys.add_comment_title.tr), 46 | content: Column( 47 | mainAxisSize: MainAxisSize.min, 48 | children: [ 49 | TextField( 50 | controller: controller, 51 | maxLines: 3, 52 | minLines: 3, 53 | decoration: InputDecoration( 54 | border: const OutlineInputBorder(), 55 | hintText: LocaleKeys.add_comment_tip.tr, 56 | ), 57 | ), 58 | ], 59 | ), 60 | actions: [ 61 | TextButton( 62 | onPressed: () { 63 | Get.back(result: false); 64 | }, 65 | child: Text(LocaleKeys.dialog_cancel.tr), 66 | ), 67 | TextButton( 68 | onPressed: () { 69 | Get.back(result: true); 70 | }, 71 | child: Text(LocaleKeys.dialog_confirm.tr), 72 | ), 73 | ], 74 | ), 75 | ); 76 | if (!(result ?? false)) { 77 | return; 78 | } 79 | if (controller.text.isEmpty) { 80 | return; 81 | } 82 | sendComment(controller.text); 83 | } 84 | 85 | void sendComment(String text) async { 86 | try { 87 | SmartDialog.showLoading(msg: ''); 88 | await blogsRequest.postBlogComment( 89 | blogApp: blogApp, 90 | body: text, 91 | postId: postId, 92 | ); 93 | refreshData(); 94 | } catch (e) { 95 | SmartDialog.showToast(e.toString()); 96 | } finally { 97 | SmartDialog.dismiss(status: SmartStatus.loading); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /lib/modules/blogs/knowledge_content/knowledge_content_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'package:flutter_cnblogs/app/controller/base_webview_controller.dart'; 4 | import 'package:flutter_cnblogs/app/utils.dart'; 5 | import 'package:flutter_cnblogs/models/blogs/knowledge_list_item_model.dart'; 6 | import 'package:flutter_cnblogs/requests/blogs_request.dart'; 7 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 8 | import 'package:get/get.dart'; 9 | import 'package:share_plus/share_plus.dart'; 10 | import 'package:url_launcher/url_launcher_string.dart'; 11 | 12 | class KnowledgeContentController extends BaseWebViewController { 13 | final BlogsRequest request = BlogsRequest(); 14 | final KnowledgeListItemModel item; 15 | KnowledgeContentController(this.item); 16 | 17 | @override 18 | void onWebViewCreated(InAppWebViewController controller) { 19 | super.onWebViewCreated(controller); 20 | loadData(); 21 | } 22 | 23 | Future loadData() async { 24 | try { 25 | pageError.value = false; 26 | pageLoadding.value = true; 27 | var content = await request.getKnowledgeContent(id: item.id); 28 | var highlightScript = await getHighlightScript(); 29 | var commonScript = await getCommonScript(); 30 | 31 | var style = await loadStyle(); 32 | var htmlTemplate = await loadTemplate(); 33 | htmlTemplate = htmlTemplate.replaceAll("@highlightJs", highlightScript); 34 | htmlTemplate = htmlTemplate.replaceAll("@commonJs", commonScript); 35 | 36 | htmlTemplate = htmlTemplate.replaceAll("@style", style); 37 | 38 | htmlTemplate = htmlTemplate.replaceAll("@title", item.title); 39 | htmlTemplate = htmlTemplate.replaceAll("@username", item.author); 40 | htmlTemplate = htmlTemplate.replaceAll( 41 | "@puttime", Utils.dateFormatWithYear.format(item.postDateTime)); 42 | htmlTemplate = htmlTemplate.replaceAll( 43 | "@stat", "${item.viewcount}浏览    ${item.diggcount}推荐"); 44 | htmlTemplate = htmlTemplate.replaceAll("@content", content); 45 | webViewController?.loadData( 46 | data: htmlTemplate, 47 | ); 48 | } catch (e) { 49 | pageError.value = true; 50 | 51 | errorMsg.value = e.toString(); 52 | } finally { 53 | pageLoadding.value = false; 54 | } 55 | } 56 | 57 | Future loadTemplate() async { 58 | return await rootBundle.loadString('assets/templates/blog/knowledge.html'); 59 | } 60 | 61 | Future loadStyle() async { 62 | return await rootBundle.loadString(Get.isDarkMode 63 | ? 'assets/templates/blog/dark.css' 64 | : 'assets/templates/blog/light.css'); 65 | } 66 | 67 | void share() { 68 | Share.share('《${item.title}》\r\nhttps://kb.cnblogs.com/page/${item.id}/'); 69 | } 70 | 71 | void openBrowser() { 72 | launchUrlString( 73 | 'https://kb.cnblogs.com/page/${item.id}/', 74 | mode: LaunchMode.externalApplication, 75 | ); 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_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/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 | -------------------------------------------------------------------------------- /lib/modules/news/comment/news_comment_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_error.dart'; 3 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 4 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 5 | import 'package:flutter_cnblogs/models/news/news_comment_item_model.dart'; 6 | import 'package:flutter_cnblogs/requests/news_request.dart'; 7 | import 'package:flutter_cnblogs/services/user_service.dart'; 8 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 9 | import 'package:get/get.dart'; 10 | 11 | class NewsCommentController extends BasePageController { 12 | final int newsId; 13 | NewsCommentController({ 14 | required this.newsId, 15 | }); 16 | final NewsRequest newsRequest = NewsRequest(); 17 | 18 | @override 19 | Future> getData(int page, int pageSize) async { 20 | return await newsRequest.getNewsComment( 21 | pageIndex: page, 22 | pageSize: pageSize, 23 | newsId: newsId, 24 | ); 25 | } 26 | 27 | @override 28 | void handleError(Object exception, {bool showPageError = false}) { 29 | super.handleError(AppError("博客园API只支持显示2022以后的评论"), 30 | showPageError: showPageError); 31 | } 32 | 33 | void showAddCommentDialog() async { 34 | if (!UserService.instance.logined.value && 35 | !await UserService.instance.login()) { 36 | return; 37 | } 38 | TextEditingController controller = TextEditingController(); 39 | var result = await Get.dialog( 40 | AlertDialog( 41 | title: Text(LocaleKeys.add_comment_title.tr), 42 | content: Column( 43 | mainAxisSize: MainAxisSize.min, 44 | children: [ 45 | TextField( 46 | controller: controller, 47 | maxLines: 3, 48 | minLines: 3, 49 | decoration: InputDecoration( 50 | border: const OutlineInputBorder(), 51 | hintText: LocaleKeys.add_comment_tip.tr, 52 | ), 53 | ), 54 | ], 55 | ), 56 | actions: [ 57 | TextButton( 58 | onPressed: () { 59 | Get.back(result: false); 60 | }, 61 | child: Text(LocaleKeys.dialog_cancel.tr), 62 | ), 63 | TextButton( 64 | onPressed: () { 65 | Get.back(result: true); 66 | }, 67 | child: Text(LocaleKeys.dialog_confirm.tr), 68 | ), 69 | ], 70 | ), 71 | ); 72 | if (!(result ?? false)) { 73 | return; 74 | } 75 | if (controller.text.isEmpty) { 76 | return; 77 | } 78 | sendComment(controller.text); 79 | } 80 | 81 | void sendComment(String text) async { 82 | try { 83 | SmartDialog.showLoading(msg: ''); 84 | await newsRequest.postNewsComment( 85 | body: text, 86 | newsId: newsId, 87 | ); 88 | refreshData(); 89 | } catch (e) { 90 | SmartDialog.showToast(e.toString()); 91 | } finally { 92 | SmartDialog.dismiss(status: SmartStatus.loading); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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/news/content/news_content_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | import 'package:flutter_cnblogs/app/controller/base_webview_controller.dart'; 4 | import 'package:flutter_cnblogs/app/utils.dart'; 5 | import 'package:flutter_cnblogs/models/news/news_list_item_model.dart'; 6 | import 'package:flutter_cnblogs/requests/news_request.dart'; 7 | import 'package:flutter_cnblogs/routes/app_navigation.dart'; 8 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 9 | import 'package:get/get.dart'; 10 | import 'package:share_plus/share_plus.dart'; 11 | import 'package:url_launcher/url_launcher_string.dart'; 12 | 13 | class NewsContentController extends BaseWebViewController { 14 | final NewsRequest request = NewsRequest(); 15 | final NewsListItemModel item; 16 | 17 | NewsContentController(this.item) { 18 | commentCount.value = item.commentCount; 19 | } 20 | 21 | @override 22 | void onWebViewCreated(InAppWebViewController controller) { 23 | super.onWebViewCreated(controller); 24 | loadData(); 25 | } 26 | 27 | var commentCount = 0.obs; 28 | Future loadData() async { 29 | try { 30 | pageError.value = false; 31 | pageLoadding.value = true; 32 | var content = await request.getNewsContent(id: item.id); 33 | 34 | var commonScript = await getCommonScript(); 35 | var style = await loadStyle(); 36 | var htmlTemplate = await loadTemplate(); 37 | htmlTemplate = htmlTemplate.replaceAll("@commonJs", commonScript); 38 | htmlTemplate = htmlTemplate.replaceAll("@style", style); 39 | 40 | htmlTemplate = htmlTemplate.replaceAll("@title", item.title); 41 | htmlTemplate = htmlTemplate.replaceAll( 42 | "@puttime", Utils.dateFormatWithYear.format(item.postDateTime)); 43 | htmlTemplate = htmlTemplate.replaceAll("@content", content); 44 | htmlTemplate = htmlTemplate.replaceAll("@stat", 45 | "${item.viewCount}浏览    ${item.diggCount}推荐    ${item.commentCount}评论"); 46 | webViewController?.loadData( 47 | data: htmlTemplate, 48 | ); 49 | } catch (e) { 50 | pageError.value = true; 51 | 52 | errorMsg.value = e.toString(); 53 | } finally { 54 | pageLoadding.value = false; 55 | } 56 | } 57 | 58 | Future loadTemplate() async { 59 | return await rootBundle.loadString('assets/templates/news/news.html'); 60 | } 61 | 62 | Future loadStyle() async { 63 | return await rootBundle.loadString(Get.isDarkMode 64 | ? 'assets/templates/news/dark.css' 65 | : 'assets/templates/news/light.css'); 66 | } 67 | 68 | void share() { 69 | Share.share('《${item.title}》\r\nhttps://news.cnblogs.com/n/${item.id}/'); 70 | } 71 | 72 | void openBrowser() { 73 | launchUrlString( 74 | 'https://news.cnblogs.com/n/${item.id}/', 75 | mode: LaunchMode.externalApplication, 76 | ); 77 | } 78 | 79 | void openComment() { 80 | AppNavigator.toNewsComment( 81 | newsId: item.id, 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/questions/comment/answer_comment_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 3 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 4 | import 'package:flutter_cnblogs/models/questions/answer_comment_list_item_model.dart'; 5 | import 'package:flutter_cnblogs/requests/questions_request.dart'; 6 | import 'package:flutter_cnblogs/services/user_service.dart'; 7 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 8 | import 'package:get/get.dart'; 9 | 10 | class AnswerCommentController 11 | extends BasePageController { 12 | final int answerId; 13 | final int questionId; 14 | AnswerCommentController({ 15 | required this.answerId, 16 | required this.questionId, 17 | }); 18 | final QuestionsRequest request = QuestionsRequest(); 19 | 20 | @override 21 | Future> getData( 22 | int page, int pageSize) async { 23 | if (page > 1) { 24 | return []; 25 | } 26 | return await request.getAnswerComments(answerId: answerId); 27 | } 28 | 29 | void showAddCommentDialog() async { 30 | if (!UserService.instance.logined.value && 31 | !await UserService.instance.login()) { 32 | return; 33 | } 34 | TextEditingController controller = TextEditingController(); 35 | var result = await Get.dialog( 36 | AlertDialog( 37 | title: Text(LocaleKeys.add_comment_title.tr), 38 | content: Column( 39 | mainAxisSize: MainAxisSize.min, 40 | children: [ 41 | TextField( 42 | controller: controller, 43 | maxLines: 3, 44 | minLines: 3, 45 | decoration: InputDecoration( 46 | border: const OutlineInputBorder(), 47 | hintText: LocaleKeys.add_comment_tip.tr, 48 | ), 49 | ), 50 | ], 51 | ), 52 | actions: [ 53 | TextButton( 54 | onPressed: () { 55 | Get.back(result: false); 56 | }, 57 | child: Text(LocaleKeys.dialog_cancel.tr), 58 | ), 59 | TextButton( 60 | onPressed: () { 61 | Get.back(result: true); 62 | }, 63 | child: Text(LocaleKeys.dialog_confirm.tr), 64 | ), 65 | ], 66 | ), 67 | ); 68 | if (!(result ?? false)) { 69 | return; 70 | } 71 | if (controller.text.isEmpty) { 72 | return; 73 | } 74 | sendComment(controller.text); 75 | } 76 | 77 | void sendComment(String text) async { 78 | try { 79 | SmartDialog.showLoading(msg: ''); 80 | await request.postAnswerComment( 81 | answerId: answerId, 82 | body: text, 83 | questionId: questionId, 84 | ); 85 | refreshData(); 86 | } catch (e) { 87 | SmartDialog.showToast(e.toString()); 88 | } finally { 89 | SmartDialog.dismiss(status: SmartStatus.loading); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/statuses/home/statuses_list_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 2 | import 'package:flutter_cnblogs/app/utils.dart'; 3 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 4 | import 'package:flutter_cnblogs/models/statuses/statuses_list_item_model.dart'; 5 | import 'package:flutter_cnblogs/requests/statuses_request.dart'; 6 | import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; 7 | import 'package:get/get.dart'; 8 | 9 | class StatusesListController extends BasePageController { 10 | final String title; 11 | StatusesListController(this.title); 12 | final StatusesRequest statusesRequest = StatusesRequest(); 13 | 14 | @override 15 | Future> getData(int page, int pageSize) async { 16 | if (title == LocaleKeys.statuses_all) { 17 | return await statusesRequest.getStatuses( 18 | type: 'all', 19 | withUserAuth: false, 20 | pageIndex: page, 21 | ); 22 | } else if (title == LocaleKeys.statuses_recentcomment) { 23 | return await statusesRequest.getStatuses( 24 | type: 'recentcomment', 25 | withUserAuth: true, 26 | pageIndex: page, 27 | ); 28 | } else if (title == LocaleKeys.statuses_comment) { 29 | return await statusesRequest.getStatuses( 30 | type: 'comment', 31 | withUserAuth: true, 32 | pageIndex: page, 33 | ); 34 | } else if (title == LocaleKeys.statuses_following) { 35 | return await statusesRequest.getStatuses( 36 | type: 'following', 37 | withUserAuth: true, 38 | pageIndex: page, 39 | ); 40 | } else if (title == LocaleKeys.statuses_mention) { 41 | return await statusesRequest.getStatuses( 42 | type: 'mention', 43 | withUserAuth: true, 44 | pageIndex: page, 45 | ); 46 | } else if (title == LocaleKeys.statuses_my) { 47 | return await statusesRequest.getStatuses( 48 | type: 'my', 49 | withUserAuth: true, 50 | pageIndex: page, 51 | ); 52 | } else if (title == LocaleKeys.statuses_mycomment) { 53 | return await statusesRequest.getStatuses( 54 | type: 'mycomment', 55 | withUserAuth: true, 56 | pageIndex: page, 57 | ); 58 | } 59 | return []; 60 | } 61 | 62 | @override 63 | void onLogin() { 64 | if (title != LocaleKeys.statuses_all) { 65 | refreshData(); 66 | } 67 | 68 | super.onLogin(); 69 | } 70 | 71 | @override 72 | void onLogout() { 73 | if (title != LocaleKeys.statuses_all) { 74 | refreshData(); 75 | } 76 | super.onLogout(); 77 | } 78 | 79 | void delete(int id) async { 80 | try { 81 | var result = await Utils.showAlertDialog( 82 | LocaleKeys.statuses_detail_delete_tip.tr, 83 | title: LocaleKeys.statuses_detail_delete.tr, 84 | ); 85 | if (!result) { 86 | return; 87 | } 88 | SmartDialog.showLoading(msg: ''); 89 | await statusesRequest.deleteStatuses( 90 | statusId: id, 91 | ); 92 | refreshData(); 93 | } catch (e) { 94 | SmartDialog.showToast(e.toString()); 95 | } finally { 96 | SmartDialog.dismiss(status: SmartStatus.loading); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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_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/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/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 | -------------------------------------------------------------------------------- /lib/modules/user/bookmark/bookmark_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_style.dart'; 3 | import 'package:flutter_cnblogs/app/utils.dart'; 4 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 5 | import 'package:flutter_cnblogs/modules/user/bookmark/bookmark_controller.dart'; 6 | import 'package:flutter_cnblogs/widgets/page_list_view.dart'; 7 | import 'package:get/get.dart'; 8 | 9 | class BookmarkPage extends GetView { 10 | const BookmarkPage({Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Scaffold( 15 | appBar: AppBar( 16 | title: Text( 17 | LocaleKeys.bookmark_title.tr, 18 | ), 19 | ), 20 | body: PageListView( 21 | firstRefresh: true, 22 | pageController: controller, 23 | padding: AppStyle.edgeInsetsA12, 24 | separatorBuilder: (context, index) => AppStyle.vGap12, 25 | itemBuilder: (_, i) { 26 | var item = controller.list[i]; 27 | return Dismissible( 28 | key: Key('key${item.wzLinkId}'), 29 | background: Container( 30 | color: Colors.red, 31 | child: const ListTile( 32 | trailing: Icon( 33 | Icons.delete, 34 | color: Colors.white, 35 | ), 36 | ), 37 | ), 38 | direction: DismissDirection.endToStart, 39 | confirmDismiss: (direction) async { 40 | var result = await Utils.showAlertDialog( 41 | LocaleKeys.bookmark_del_msg.tr, 42 | title: LocaleKeys.bookmark_del.tr, 43 | ); 44 | return result; 45 | }, 46 | onDismissed: ((direction) { 47 | controller.delete(item); 48 | }), 49 | child: InkWell( 50 | onTap: () => controller.openUrl(item), 51 | child: Container( 52 | decoration: BoxDecoration( 53 | borderRadius: AppStyle.radius8, 54 | color: Theme.of(context).cardColor, 55 | ), 56 | padding: AppStyle.edgeInsetsA12, 57 | child: Column( 58 | crossAxisAlignment: CrossAxisAlignment.stretch, 59 | children: [ 60 | Text( 61 | item.title, 62 | style: const TextStyle(fontSize: 16), 63 | ), 64 | AppStyle.vGap8, 65 | Text( 66 | LocaleKeys.bookmark_addtime.trParams( 67 | { 68 | "time": Utils.parseTime(item.addDateTime), 69 | }, 70 | ), 71 | style: const TextStyle(fontSize: 14, color: Colors.grey), 72 | ), 73 | ], 74 | ), 75 | ), 76 | ), 77 | ); 78 | }, 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /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/modules/user/login/login_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 3 | import 'package:flutter_cnblogs/app/log.dart'; 4 | import 'package:flutter_cnblogs/requests/base/api.dart'; 5 | import 'package:flutter_cnblogs/requests/oauth_request.dart'; 6 | import 'package:flutter_cnblogs/services/user_service.dart'; 7 | import 'package:flutter_inappwebview/flutter_inappwebview.dart'; 8 | import 'package:get/get.dart'; 9 | 10 | class LoginController extends BaseController { 11 | final UniqueKey webViewkey = UniqueKey(); 12 | final OAuthRequest request = OAuthRequest(); 13 | late InAppWebViewController? webViewController; 14 | final InAppWebViewGroupOptions webViewGroupOptions = InAppWebViewGroupOptions( 15 | crossPlatform: InAppWebViewOptions( 16 | transparentBackground: true, 17 | useShouldOverrideUrlLoading: true, 18 | clearCache: true, 19 | ), 20 | ); 21 | void onWebViewCreated(InAppWebViewController controller) async { 22 | webViewController = controller; 23 | pageLoadding.value = true; 24 | 25 | goLogin(); 26 | } 27 | 28 | void goLogin() { 29 | var uri = Uri( 30 | scheme: "https", 31 | host: "oauth.cnblogs.com", 32 | path: "connect/authorize", 33 | queryParameters: { 34 | "client_id": Api.kClientID, 35 | "scope": "openid profile CnBlogsApi offline_access", 36 | "response_type": "code id_token", 37 | "redirect_uri": "https://oauth.cnblogs.com/auth/callback", 38 | "state": DateTime.now().millisecondsSinceEpoch.toString(), 39 | "nonce": DateTime.now().millisecondsSinceEpoch.toString(), 40 | }, 41 | ); 42 | webViewController?.loadUrl(urlRequest: URLRequest(url: uri)); 43 | } 44 | 45 | void refreshWeb() { 46 | webViewController?.reload(); 47 | } 48 | 49 | void onLoadStart(InAppWebViewController controller, Uri? uri) { 50 | pageLoadding.value = true; 51 | pageError.value = false; 52 | } 53 | 54 | void onLoadStop(InAppWebViewController controller, Uri? uri) async { 55 | pageLoadding.value = false; 56 | } 57 | 58 | void onLoadError( 59 | InAppWebViewController controller, Uri? uri, int code, String e) { 60 | pageLoadding.value = false; 61 | pageError.value = true; 62 | errorMsg.value = "$code $e"; 63 | } 64 | 65 | Future shouldOverrideUrlLoading( 66 | InAppWebViewController controller, NavigationAction action) async { 67 | var uri = action.request.url!; 68 | var url = uri.toString(); 69 | if (url.contains('oauth.cnblogs.com/auth/callback')) { 70 | var code = RegExp(r"code=(.*?)&").firstMatch(url)?.group(1) ?? ""; 71 | Log.i(code); 72 | getToken(code); 73 | return NavigationActionPolicy.CANCEL; 74 | } 75 | 76 | Log.i(url); 77 | return NavigationActionPolicy.ALLOW; 78 | } 79 | 80 | void getToken(String code) async { 81 | try { 82 | pageLoadding.value = true; 83 | var userToken = await request.getUserToken(code); 84 | UserService.instance.setAuthInfo(userToken); 85 | Get.back(result: true); 86 | } catch (e) { 87 | Log.logPrint(e); 88 | } finally { 89 | pageLoadding.value = false; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/requests/user_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_cnblogs/models/user/bookmark_list_item_model.dart'; 2 | import 'package:flutter_cnblogs/models/user/user_info_model.dart'; 3 | import 'package:flutter_cnblogs/requests/base/http_client.dart'; 4 | 5 | class UserRequest { 6 | Future getUserInfo() async { 7 | var result = await HttpClient.instance.get( 8 | '/api/users', 9 | withUserAuth: true, 10 | ); 11 | 12 | return UserInfoModel.fromJson(result); 13 | } 14 | 15 | /// 分页获取收藏列表 16 | /// - https://api.cnblogs.com/api/Bookmarks?pageIndex={pageIndex}&pageSize={pageSize} 17 | Future> getBookmarks( 18 | {required int pageIndex, int pageSize = 20}) async { 19 | List ls = []; 20 | var result = await HttpClient.instance.get( 21 | '/api/Bookmarks', 22 | queryParameters: { 23 | 'pageIndex': pageIndex, 24 | 'pageSize': pageSize, 25 | }, 26 | withUserAuth: true, 27 | ); 28 | for (var item in result) { 29 | ls.add(BookmarkListItemModel.fromJson(item)); 30 | } 31 | return ls; 32 | } 33 | 34 | /// 根据id删除收藏 35 | /// - https://api.cnblogs.com/api/bookmarks/{id} 36 | Future deleteBookmark(int id) async { 37 | await HttpClient.instance.delete( 38 | '/api/bookmarks/$id', 39 | data: {}, 40 | withUserAuth: true, 41 | ); 42 | return true; 43 | } 44 | 45 | /// 根据url删除收藏 46 | /// - https://api.cnblogs.com/api/Bookmarks?url={url} 47 | Future deleteBookmarkByUrl(String url) async { 48 | await HttpClient.instance.delete( 49 | '/api/Bookmarks', 50 | queryParameters: { 51 | 'url': Uri.encodeComponent(url), 52 | }, 53 | data: {}, 54 | withUserAuth: true, 55 | ); 56 | return true; 57 | } 58 | 59 | /// 根据URL检查收藏是否已存在 60 | /// - https://api.cnblogs.com/api/Bookmarks?url={url} 61 | Future checkBookmark(String url) async { 62 | return await HttpClient.instance.head( 63 | '/api/Bookmarks', 64 | queryParameters: { 65 | "url": Uri.encodeComponent(url), 66 | }, 67 | withUserAuth: true, 68 | ); 69 | } 70 | 71 | /// 添加收藏 72 | /// - https://api.cnblogs.com/api/Bookmarks 73 | Future addBookmark(String title, String url) async { 74 | await HttpClient.instance.post( 75 | '/api/Bookmarks', 76 | data: { 77 | "Title": title, 78 | "LinkUrl": url, 79 | "Summary": "", 80 | "Tags": [], 81 | }, 82 | withUserAuth: true, 83 | ); 84 | return true; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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/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/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/services/user_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_cnblogs/app/event_bus.dart'; 2 | import 'package:flutter_cnblogs/app/log.dart'; 3 | import 'package:flutter_cnblogs/models/user/user_info_model.dart'; 4 | import 'package:flutter_cnblogs/models/oauth/user_token_model.dart'; 5 | import 'package:flutter_cnblogs/requests/oauth_request.dart'; 6 | import 'package:flutter_cnblogs/requests/user_request.dart'; 7 | import 'package:flutter_cnblogs/routes/route_path.dart'; 8 | import 'package:flutter_cnblogs/services/local_storage_service.dart'; 9 | import 'package:get/get.dart'; 10 | 11 | class UserService extends GetxService { 12 | static UserService get instance => Get.find(); 13 | final LocalStorageService storage = Get.find(); 14 | final OAuthRequest authRequest = OAuthRequest(); 15 | final UserRequest userRequest = UserRequest(); 16 | Rx userProfile = Rx(null); 17 | int userId = 0; 18 | String token = ""; 19 | int expires = 0; 20 | var logined = false.obs; 21 | 22 | void init() { 23 | userId = storage.getValue(LocalStorageService.kUserID, 0); 24 | token = storage.getValue(LocalStorageService.kUserAccessToken, ''); 25 | if (token.isEmpty) { 26 | return; 27 | } 28 | 29 | expires = storage.getValue(LocalStorageService.kUserAccessTokenExpiresTime, 30 | DateTime.now().millisecondsSinceEpoch); 31 | logined.value = true; 32 | } 33 | 34 | void setAuthInfo(UserTokenModel info) { 35 | token = info.accessToken; 36 | expires = DateTime.now() 37 | .add(Duration(seconds: info.expiresIn)) 38 | .millisecondsSinceEpoch; 39 | storage.setValue(LocalStorageService.kUserAccessToken, token); 40 | storage.setValue(LocalStorageService.kUserAccessTokenExpiresTime, expires); 41 | storage.setValue(LocalStorageService.kUserRefreshToken, info.refreshToken); 42 | logined.value = true; 43 | EventBus.instance.emit(EventBus.kLogined, 0); 44 | refreshProfile(); 45 | } 46 | 47 | Future login() async { 48 | return (await Get.toNamed(RoutePath.kUserLogin)) ?? false; 49 | } 50 | 51 | Future refreshToken() async { 52 | try { 53 | if (!logined.value) { 54 | return await login(); 55 | } 56 | 57 | UserTokenModel tokenModel = await authRequest.refreshUserToken( 58 | storage.getValue(LocalStorageService.kUserRefreshToken, ''), 59 | ); 60 | setAuthInfo(tokenModel); 61 | 62 | return true; 63 | } catch (e) { 64 | Log.logPrint(e); 65 | return false; 66 | } 67 | } 68 | 69 | void logout() { 70 | userProfile.value = null; 71 | storage.removeValue(LocalStorageService.kUserID); 72 | storage.removeValue(LocalStorageService.kUserAccessToken); 73 | storage.removeValue(LocalStorageService.kUserAccessTokenExpiresTime); 74 | storage.removeValue(LocalStorageService.kUserRefreshToken); 75 | logined.value = false; 76 | EventBus.instance.emit(EventBus.kLogouted, 0); 77 | } 78 | 79 | /// 刷新个人资料 80 | Future refreshProfile() async { 81 | try { 82 | if (!logined.value) { 83 | return; 84 | } 85 | userProfile.value = await userRequest.getUserInfo(); 86 | userId = userProfile.value?.spaceUserId ?? 0; 87 | storage.setValue(LocalStorageService.kUserID, userId); 88 | } catch (e) { 89 | Log.logPrint(e); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /lib/widgets/custom_html.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_style.dart'; 3 | import 'package:flutter_cnblogs/app/utils.dart'; 4 | import 'package:flutter_cnblogs/routes/app_navigation.dart'; 5 | import 'package:flutter_cnblogs/widgets/net_image.dart'; 6 | import 'package:flutter_html/flutter_html.dart'; 7 | 8 | class CustomHtml extends StatelessWidget { 9 | final String content; 10 | const CustomHtml({required this.content, Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Html( 15 | data: content, 16 | shrinkWrap: true, 17 | style: { 18 | "body": Style( 19 | padding: HtmlPaddings.zero, 20 | margin: Margins.zero, 21 | ), 22 | "p": Style( 23 | lineHeight: LineHeight.em(1.2), 24 | ), 25 | }, 26 | extensions: [ 27 | TagExtension( 28 | tagsToExtend: {"img"}, 29 | builder: (extensionContext) { 30 | return Padding( 31 | padding: AppStyle.edgeInsetsV8, 32 | child: GestureDetector( 33 | onTap: () { 34 | Utils.showImageViewer( 35 | 0, 36 | [ 37 | extensionContext.attributes["src"].toString(), 38 | ], 39 | ); 40 | }, 41 | child: NetImage( 42 | extensionContext.attributes["src"].toString(), 43 | borderRadius: 4, 44 | ), 45 | ), 46 | ); 47 | }, 48 | ), 49 | TagExtension( 50 | tagsToExtend: {"pre"}, 51 | builder: (extensionContext) { 52 | return Container( 53 | decoration: BoxDecoration( 54 | color: Colors.grey.withOpacity(.1), 55 | borderRadius: AppStyle.radius4, 56 | border: Border.all(color: Colors.grey.withOpacity(.2)), 57 | ), 58 | width: double.infinity, 59 | padding: AppStyle.edgeInsetsA8, 60 | child: SingleChildScrollView( 61 | scrollDirection: Axis.horizontal, 62 | child: Text( 63 | extensionContext.element!.text, 64 | softWrap: false, 65 | style: const TextStyle(fontSize: 12), 66 | ), 67 | ), 68 | ); 69 | }, 70 | ), 71 | ], 72 | onLinkTap: (url, attributes, element) async { 73 | if (url != null) { 74 | await AppNavigator.toWebView(url); 75 | } 76 | }, 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/widgets/items/answer_comment_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_style.dart'; 3 | import 'package:flutter_cnblogs/app/utils.dart'; 4 | import 'package:flutter_cnblogs/models/questions/answer_comment_list_item_model.dart'; 5 | import 'package:flutter_cnblogs/widgets/net_image.dart'; 6 | import 'package:remixicon/remixicon.dart'; 7 | 8 | class AnswerCommentItemWidget extends StatelessWidget { 9 | final AnswerCommentListItemModel item; 10 | const AnswerCommentItemWidget(this.item, {Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | decoration: BoxDecoration( 16 | borderRadius: AppStyle.radius4, 17 | color: Theme.of(context).cardColor, 18 | ), 19 | padding: AppStyle.edgeInsetsA12, 20 | child: Column( 21 | crossAxisAlignment: CrossAxisAlignment.start, 22 | children: [ 23 | buildAuthor(), 24 | AppStyle.vGap12, 25 | Text( 26 | item.content, 27 | ), 28 | AppStyle.vGap12, 29 | Row( 30 | mainAxisAlignment: MainAxisAlignment.end, 31 | children: [ 32 | AppStyle.hGap16, 33 | const Icon( 34 | Remix.thumb_up_line, 35 | size: 16, 36 | color: Colors.grey, 37 | ), 38 | AppStyle.hGap4, 39 | Text( 40 | item.diggCount.toString(), 41 | style: const TextStyle(fontSize: 14, color: Colors.grey), 42 | ), 43 | ], 44 | ), 45 | ], 46 | ), 47 | ); 48 | } 49 | 50 | Widget buildAuthor() { 51 | return InkWell( 52 | onTap: () {}, 53 | borderRadius: AppStyle.radius4, 54 | child: Row( 55 | children: [ 56 | item.postUserInfo.iconName.isEmpty 57 | ? Container( 58 | width: 40, 59 | height: 40, 60 | decoration: BoxDecoration( 61 | borderRadius: AppStyle.radius24, 62 | border: Border.all( 63 | color: Colors.grey.withOpacity(.2), 64 | ), 65 | ), 66 | child: const Icon( 67 | Icons.account_circle, 68 | color: Colors.grey, 69 | ), 70 | ) 71 | : NetImage( 72 | item.postUserInfo.iconName, 73 | borderRadius: 24, 74 | width: 40, 75 | height: 40, 76 | ), 77 | AppStyle.hGap8, 78 | Expanded( 79 | child: Column( 80 | crossAxisAlignment: CrossAxisAlignment.start, 81 | children: [ 82 | Text( 83 | item.postUserName, 84 | style: const TextStyle(fontSize: 14), 85 | ), 86 | Text( 87 | Utils.parseTime(item.addDateTime), 88 | style: const TextStyle(fontSize: 12, color: Colors.grey), 89 | ), 90 | ], 91 | ), 92 | ), 93 | ], 94 | ), 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/widgets/items/blog_comment_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_style.dart'; 3 | import 'package:flutter_cnblogs/app/utils.dart'; 4 | import 'package:flutter_cnblogs/models/blogs/blog_comment_item_model.dart'; 5 | import 'package:flutter_cnblogs/widgets/custom_html.dart'; 6 | import 'package:flutter_cnblogs/widgets/net_image.dart'; 7 | 8 | class BlogCommentItemWidget extends StatelessWidget { 9 | final BlogCommentItemModel item; 10 | const BlogCommentItemWidget(this.item, {Key? key}) : super(key: key); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | decoration: BoxDecoration( 16 | borderRadius: AppStyle.radius4, 17 | color: Theme.of(context).cardColor, 18 | ), 19 | padding: AppStyle.edgeInsetsA12, 20 | child: Column( 21 | crossAxisAlignment: CrossAxisAlignment.start, 22 | children: [ 23 | buildAuthor(), 24 | AppStyle.vGap12, 25 | CustomHtml( 26 | content: item.body, 27 | ), 28 | ], 29 | ), 30 | ); 31 | } 32 | 33 | Widget buildAuthor() { 34 | return InkWell( 35 | onTap: () {}, 36 | borderRadius: AppStyle.radius4, 37 | child: Row( 38 | children: [ 39 | item.faceUrl.isEmpty 40 | ? Container( 41 | width: 40, 42 | height: 40, 43 | decoration: BoxDecoration( 44 | borderRadius: AppStyle.radius24, 45 | border: Border.all( 46 | color: Colors.grey.withOpacity(.2), 47 | ), 48 | ), 49 | child: const Icon( 50 | Icons.account_circle, 51 | color: Colors.grey, 52 | ), 53 | ) 54 | : NetImage( 55 | item.faceUrl, 56 | borderRadius: 24, 57 | width: 40, 58 | height: 40, 59 | ), 60 | AppStyle.hGap8, 61 | Expanded( 62 | child: Column( 63 | crossAxisAlignment: CrossAxisAlignment.start, 64 | children: [ 65 | Text( 66 | item.author, 67 | style: const TextStyle(fontSize: 14), 68 | ), 69 | Text( 70 | Utils.parseTime(item.postDateTime), 71 | style: const TextStyle(fontSize: 12, color: Colors.grey), 72 | ), 73 | ], 74 | ), 75 | ), 76 | Text( 77 | "#${item.floor}", 78 | style: const TextStyle(fontSize: 12, color: Colors.grey), 79 | ) 80 | ], 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/widgets/items/knowledge_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_style.dart'; 3 | import 'package:flutter_cnblogs/app/utils.dart'; 4 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 5 | import 'package:flutter_cnblogs/models/blogs/knowledge_list_item_model.dart'; 6 | import 'package:flutter_cnblogs/routes/app_navigation.dart'; 7 | import 'package:get/get.dart'; 8 | import 'package:remixicon/remixicon.dart'; 9 | 10 | class KnowledgeItemWidget extends StatelessWidget { 11 | final KnowledgeListItemModel item; 12 | 13 | const KnowledgeItemWidget(this.item, {Key? key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | margin: AppStyle.edgeInsetsB8, 19 | child: Material( 20 | color: Theme.of(context).cardColor, 21 | borderRadius: AppStyle.radius4, 22 | child: InkWell( 23 | onTap: () { 24 | AppNavigator.toKnowledgeContent(item); 25 | }, 26 | borderRadius: AppStyle.radius4, 27 | child: Container( 28 | decoration: BoxDecoration( 29 | borderRadius: AppStyle.radius4, 30 | ), 31 | padding: AppStyle.edgeInsetsA12, 32 | child: Column( 33 | crossAxisAlignment: CrossAxisAlignment.start, 34 | children: [ 35 | Text( 36 | item.title, 37 | style: const TextStyle(fontSize: 16), 38 | ), 39 | AppStyle.vGap8, 40 | Text( 41 | LocaleKeys.blogs_home_posttime.trParams({ 42 | "time": Utils.parseTime(item.postDateTime), 43 | }), 44 | style: const TextStyle(fontSize: 12, color: Colors.grey), 45 | ), 46 | AppStyle.vGap8, 47 | Text( 48 | item.summary, 49 | maxLines: 2, 50 | overflow: TextOverflow.ellipsis, 51 | style: const TextStyle(fontSize: 14, color: Colors.grey), 52 | ), 53 | AppStyle.vGap8, 54 | Row( 55 | children: [ 56 | Text( 57 | item.author, 58 | style: const TextStyle(fontSize: 12), 59 | ), 60 | const Expanded(child: SizedBox()), 61 | buildStat(), 62 | ], 63 | ), 64 | ], 65 | ), 66 | ), 67 | ), 68 | ), 69 | ); 70 | } 71 | 72 | Widget buildStat() { 73 | return Row( 74 | mainAxisSize: MainAxisSize.min, 75 | children: [ 76 | const Icon( 77 | Remix.eye_line, 78 | size: 16, 79 | color: Colors.grey, 80 | ), 81 | AppStyle.hGap4, 82 | Text( 83 | item.viewcount.toString(), 84 | style: const TextStyle(fontSize: 12, color: Colors.grey), 85 | ), 86 | AppStyle.hGap16, 87 | const Icon( 88 | Remix.thumb_up_line, 89 | size: 16, 90 | color: Colors.grey, 91 | ), 92 | AppStyle.hGap4, 93 | Text( 94 | item.diggcount.toString(), 95 | style: const TextStyle(fontSize: 12, color: Colors.grey), 96 | ), 97 | ], 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/widgets/items/statuses_comment_item_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_cnblogs/app/app_style.dart'; 3 | import 'package:flutter_cnblogs/app/utils.dart'; 4 | import 'package:flutter_cnblogs/generated/locales.g.dart'; 5 | import 'package:flutter_cnblogs/models/statuses/statuses_comment_item_model.dart'; 6 | import 'package:flutter_cnblogs/services/user_service.dart'; 7 | import 'package:flutter_cnblogs/widgets/net_image.dart'; 8 | import 'package:flutter_cnblogs/widgets/statuses_content.dart'; 9 | import 'package:get/get.dart'; 10 | 11 | class StatusesCommentItemWidget extends StatelessWidget { 12 | final StatusesCommentItemModel item; 13 | final Function()? onReply; 14 | final Function()? onDelete; 15 | const StatusesCommentItemWidget(this.item, 16 | {this.onReply, this.onDelete, Key? key}) 17 | : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Container( 22 | margin: AppStyle.edgeInsetsB12, 23 | child: Container( 24 | decoration: BoxDecoration( 25 | borderRadius: AppStyle.radius4, 26 | color: Theme.of(context).cardColor, 27 | ), 28 | padding: AppStyle.edgeInsetsA12, 29 | child: Column( 30 | crossAxisAlignment: CrossAxisAlignment.start, 31 | children: [ 32 | buildAuthor(), 33 | AppStyle.vGap4, 34 | StatusesContent( 35 | content: item.content, 36 | ), 37 | AppStyle.vGap4, 38 | buildStat(), 39 | ], 40 | ), 41 | ), 42 | ); 43 | } 44 | 45 | Widget buildAuthor() { 46 | return Row( 47 | mainAxisSize: MainAxisSize.min, 48 | children: [ 49 | NetImage( 50 | item.userIconUrl, 51 | borderRadius: 24, 52 | width: 24, 53 | height: 24, 54 | ), 55 | AppStyle.hGap8, 56 | Column( 57 | crossAxisAlignment: CrossAxisAlignment.start, 58 | children: [ 59 | Text( 60 | item.userDisplayName, 61 | style: const TextStyle(fontSize: 14), 62 | ), 63 | ], 64 | ) 65 | ], 66 | ); 67 | } 68 | 69 | Widget buildStat() { 70 | return Row( 71 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 72 | children: [ 73 | Text( 74 | Utils.parseTime(item.postDateTime), 75 | style: const TextStyle(fontSize: 12, color: Colors.grey), 76 | ), 77 | const Expanded(child: Center()), 78 | Visibility( 79 | visible: item.userId == UserService.instance.userId, 80 | child: InkWell( 81 | onTap: onDelete, 82 | child: Text( 83 | LocaleKeys.statuses_detail_delete.tr, 84 | style: const TextStyle(fontSize: 14, color: Colors.grey), 85 | ), 86 | ), 87 | ), 88 | AppStyle.hGap12, 89 | InkWell( 90 | onTap: onReply, 91 | child: Text( 92 | LocaleKeys.statuses_detail_reply.tr, 93 | style: const TextStyle(fontSize: 14, color: Colors.grey), 94 | ), 95 | ), 96 | ], 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/widgets/page_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:easy_refresh/easy_refresh.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_cnblogs/app/controller/base_controller.dart'; 4 | import 'package:flutter_cnblogs/widgets/status/app_empty_widget.dart'; 5 | import 'package:flutter_cnblogs/widgets/status/app_error_widget.dart'; 6 | import 'package:flutter_cnblogs/widgets/status/app_loadding_widget.dart'; 7 | import 'package:flutter_cnblogs/widgets/status/app_not_login_widget.dart'; 8 | import 'package:get/get.dart'; 9 | 10 | typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index); 11 | 12 | class PageListView extends StatelessWidget { 13 | final BasePageController pageController; 14 | final IndexedWidgetBuilder itemBuilder; 15 | final IndexedWidgetBuilder? separatorBuilder; 16 | final EdgeInsets? padding; 17 | final bool firstRefresh; 18 | final Function()? onLoginSuccess; 19 | final bool showPageLoadding; 20 | const PageListView({ 21 | required this.itemBuilder, 22 | required this.pageController, 23 | this.padding, 24 | this.firstRefresh = false, 25 | this.showPageLoadding = false, 26 | this.separatorBuilder, 27 | this.onLoginSuccess, 28 | Key? key, 29 | }) : super(key: key); 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return Obx( 34 | () => Stack( 35 | children: [ 36 | EasyRefresh( 37 | header: const MaterialHeader(), 38 | footer: const MaterialFooter( 39 | infiniteOffset: 100, 40 | clamping: false, 41 | ), 42 | controller: pageController.easyRefreshController, 43 | refreshOnStart: firstRefresh, 44 | onLoad: pageController.loadData, 45 | onRefresh: pageController.refreshData, 46 | child: ListView.separated( 47 | padding: padding, 48 | itemCount: pageController.list.length, 49 | itemBuilder: itemBuilder, 50 | controller: pageController.scrollController, 51 | separatorBuilder: 52 | separatorBuilder ?? (context, i) => const SizedBox(), 53 | ), 54 | ), 55 | Offstage( 56 | offstage: !pageController.pageEmpty.value, 57 | child: AppEmptyWidget( 58 | onRefresh: () => pageController.refreshData(), 59 | ), 60 | ), 61 | Offstage( 62 | offstage: !(showPageLoadding && pageController.pageLoadding.value), 63 | child: const AppLoaddingWidget(), 64 | ), 65 | Offstage( 66 | offstage: !pageController.pageError.value, 67 | child: AppErrorWidget( 68 | errorMsg: pageController.errorMsg.value, 69 | onRefresh: () => pageController.refreshData(), 70 | ), 71 | ), 72 | Offstage( 73 | offstage: !pageController.notLogin.value, 74 | child: AppNotLoginWidget( 75 | onLoginSuccess: onLoginSuccess, 76 | ), 77 | ), 78 | ], 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /screenshot/screenshot_dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/screenshot/screenshot_dark.jpg -------------------------------------------------------------------------------- /screenshot/screenshot_light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoyaocz/flutter_cnblogs/4d75b3d94c7613af07266254255fc6d957e397a2/screenshot/screenshot_light.jpg -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------