├── .gitignore ├── .metadata ├── LICENSE ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── lwlizhe │ │ │ │ └── flutter_novel │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── demo └── android │ └── app-release.apk ├── img ├── icon_tab_bookshelf_n.png ├── icon_tab_bookshelf_p.png ├── icon_tab_home_n.png ├── icon_tab_home_p.png ├── icon_tab_me_n.png ├── icon_tab_me_p.png └── reader │ └── icon_me_vip.png ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── 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 │ └── 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 ├── lib ├── app │ ├── api │ │ ├── api_home.dart │ │ ├── api_novel.dart │ │ └── constance │ │ │ └── const_request_url.dart │ ├── constant │ │ └── custom_color.dart │ ├── main │ │ └── main_page_view.dart │ ├── novel │ │ ├── entity │ │ │ ├── entity_novel_book_chapter.dart │ │ │ ├── entity_novel_book_chapter.g.dart │ │ │ ├── entity_novel_book_key_word_search.dart │ │ │ ├── entity_novel_book_key_word_search.g.dart │ │ │ ├── entity_novel_book_recommend.dart │ │ │ ├── entity_novel_book_recommend.g.dart │ │ │ ├── entity_novel_book_review.dart │ │ │ ├── entity_novel_book_review.g.dart │ │ │ ├── entity_novel_book_source.dart │ │ │ ├── entity_novel_book_source.g.dart │ │ │ ├── entity_novel_chapter_info.dart │ │ │ ├── entity_novel_detail.dart │ │ │ ├── entity_novel_detail.g.dart │ │ │ ├── entity_novel_info.dart │ │ │ ├── entity_novel_short_comment.dart │ │ │ └── entity_novel_short_comment.g.dart │ │ ├── helper │ │ │ ├── helper_cache.dart │ │ │ ├── helper_db.dart │ │ │ └── helper_sp.dart │ │ ├── model │ │ │ ├── model_novel_cache.dart │ │ │ └── zssq │ │ │ │ ├── model_book_db.dart │ │ │ │ └── model_book_net.dart │ │ ├── view │ │ │ ├── novel_about.dart │ │ │ ├── novel_book_find.dart │ │ │ ├── novel_book_intro.dart │ │ │ ├── novel_book_leader_board.dart │ │ │ ├── novel_book_menu.dart │ │ │ ├── novel_book_reader.dart │ │ │ ├── novel_book_search.dart │ │ │ ├── novel_book_search_result.dart │ │ │ ├── novel_book_shelf.dart │ │ │ └── widget │ │ │ │ ├── novel_book_intro_appbar_header_view.dart │ │ │ │ ├── novel_book_intro_book_review_view.dart │ │ │ │ ├── novel_book_intro_bottom_menu_view.dart │ │ │ │ ├── novel_book_intro_copyright_notice_view.dart │ │ │ │ ├── novel_book_intro_header_tag_view.dart │ │ │ │ ├── novel_book_intro_recommend_view.dart │ │ │ │ └── novel_book_intro_short_comment_view.dart │ │ ├── view_model │ │ │ ├── view_model_novel_intro.dart │ │ │ ├── view_model_novel_reader.dart │ │ │ ├── view_model_novel_search.dart │ │ │ └── view_model_novel_shelf.dart │ │ └── widget │ │ │ └── reader │ │ │ ├── cache │ │ │ ├── novel_config_manager.dart │ │ │ └── novel_content_cache_manager.dart │ │ │ ├── content │ │ │ ├── helper │ │ │ │ ├── animation │ │ │ │ │ ├── animation_page_base.dart │ │ │ │ │ ├── animation_page_cover.dart │ │ │ │ │ ├── animation_page_simulation_turn.dart │ │ │ │ │ ├── animation_page_slide.dart │ │ │ │ │ └── controller_animation_with_listener_number.dart │ │ │ │ ├── helper_reader_animation.dart │ │ │ │ ├── helper_reader_content.dart │ │ │ │ └── manager_reader_page.dart │ │ │ ├── widget_reader_content.dart │ │ │ └── widget_reader_painter.dart │ │ │ ├── manager │ │ │ └── manager_reader_progress.dart │ │ │ ├── menu │ │ │ ├── manager_menu_widget.dart │ │ │ ├── widget_reader_bottom_menu.dart │ │ │ ├── widget_reader_catalog_menu.dart │ │ │ ├── widget_reader_setting_menu.dart │ │ │ └── widget_reader_top_menu.dart │ │ │ ├── model │ │ │ ├── model_reader_config.dart │ │ │ └── model_reader_content.dart │ │ │ └── widget │ │ │ ├── widget_novel_reader_error.dart │ │ │ └── widget_novel_reader_loadding.dart │ ├── provider_setup.dart │ ├── router │ │ └── manager_router.dart │ └── widget │ │ ├── scrollable_positioned_list │ │ ├── element_registry.dart │ │ ├── item_positions_listener.dart │ │ ├── item_positions_notifier.dart │ │ ├── positioned_list.dart │ │ ├── post_mount_callback.dart │ │ └── scrollable_positioned_list.dart │ │ ├── widget_expand_text_view.dart │ │ └── widget_tag_view.dart ├── base │ ├── constant │ │ └── constant_text_style.dart │ ├── db │ │ └── manager_db.dart │ ├── http │ │ └── manager_net_request.dart │ ├── router │ │ └── base_router_manager.dart │ ├── sp │ │ └── manager_sp.dart │ ├── structure │ │ ├── base_model.dart │ │ ├── base_view.dart │ │ ├── base_view_model.dart │ │ └── provider │ │ │ ├── app_provider.dart │ │ │ ├── base_provider.dart │ │ │ ├── config_provider.dart │ │ │ └── state_provider.dart │ ├── util │ │ ├── utils_color.dart │ │ ├── utils_navigator.dart │ │ ├── utils_screen.dart │ │ ├── utils_time.dart │ │ └── utils_toast.dart │ └── widget │ │ ├── base_list_item_holder.dart │ │ ├── base_list_item_holder_builder.dart │ │ └── view_common_loading.dart └── main.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Web related 33 | lib/generated_plugin_registrant.dart 34 | 35 | # Exceptions to above rules. 36 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 37 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 1946fc4da0f80c522d7e3ae7d4f7309908ed86f2 8 | channel: unknown 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, lwlizhe 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![]( https://visitor-badge.glitch.me/badge?page_id=<80E38FFA26DAD77BC2F8A56307EDE15B>) 2 | 3 | ## 前言 4 | 5 | 项目重构ing,在掘金不定时更新进度; 6 | 7 | [我的掘金账号](https://juejin.cn/user/2735240658304893) 8 | 9 | 现在重构开发分支是:dev_2.0分支; 10 | 11 | flutter版本是2.8.1 12 | 13 | 14 | ## 特别感谢 15 | 16 | [flutter_app](https://github.com/shichunlei/flutter_app)(追书神器的接口以及介绍页来自于这个项目) 17 | 18 | [BookPage](https://github.com/AnliaLee/BookPage)(阅读页的实现思路参考自这个项目) 19 | 20 | ## 免责声明 21 | 22 | 本项目仅用于研究学习,请勿用于商业,否则后果与本人无关。 23 | 24 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /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 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.lwlizhe.flutter_novel" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 9 | 10 | 11 | 15 | 22 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/lwlizhe/flutter_novel/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.lwlizhe.flutter_novel 2 | 3 | import android.os.Bundle 4 | import io.flutter.app.FlutterActivity 5 | import io.flutter.plugins.GeneratedPluginRegistrant 6 | 7 | class MainActivity: FlutterActivity() { 8 | override fun onCreate(savedInstanceState: Bundle?) { 9 | super.onCreate(savedInstanceState) 10 | GeneratedPluginRegistrant.registerWith(this) 11 | } 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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 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 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /demo/android/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/demo/android/app-release.apk -------------------------------------------------------------------------------- /img/icon_tab_bookshelf_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/icon_tab_bookshelf_n.png -------------------------------------------------------------------------------- /img/icon_tab_bookshelf_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/icon_tab_bookshelf_p.png -------------------------------------------------------------------------------- /img/icon_tab_home_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/icon_tab_home_n.png -------------------------------------------------------------------------------- /img/icon_tab_home_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/icon_tab_home_p.png -------------------------------------------------------------------------------- /img/icon_tab_me_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/icon_tab_me_n.png -------------------------------------------------------------------------------- /img/icon_tab_me_p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/icon_tab_me_p.png -------------------------------------------------------------------------------- /img/reader/icon_me_vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/img/reader/icon_me_vip.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Generated.xcconfig 20 | Flutter/app.flx 21 | Flutter/app.zip 22 | Flutter/flutter_assets/ 23 | Flutter/flutter_export_environment.sh 24 | ServiceDefinitions.json 25 | Runner/GeneratedPluginRegistrant.* 26 | 27 | # Exceptions to above rules. 28 | !default.mode1v3 29 | !default.mode2v3 30 | !default.pbxuser 31 | !default.perspectivev3 32 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.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/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/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 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_novel 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /lib/app/api/api_home.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/lib/app/api/api_home.dart -------------------------------------------------------------------------------- /lib/app/api/constance/const_request_url.dart: -------------------------------------------------------------------------------- 1 | class RequestApi{ 2 | 3 | static const String DMZJ_REFERER_URL="http://images.dmzj.com/"; 4 | 5 | static const String BASE_URL="https://v3api.dmzj.com/novel/"; 6 | 7 | static const String HOME_RECOMMEND=BASE_URL+"recommend.json"; 8 | } -------------------------------------------------------------------------------- /lib/app/constant/custom_color.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomColor { 4 | static Color primary = Color(0xFfff9a6a); 5 | static Color secondary = Color(0xFfff9a6a); 6 | static Color red = Color(0xFFFF2B45); 7 | static Color orange = Color(0xFFF67264); 8 | static Color white = Color(0xFFFFFFFF); 9 | static Color paper = Color(0xFFF5F5F5); 10 | static Color lightGray = Color(0xFFEEEEEE); 11 | static Color darkGray = Color(0xFF333333); 12 | static Color mediumGray = Color(0xFF666666); 13 | static Color gray = Color(0xFF888888); 14 | static Color blackA99 = Color(0x99000000); 15 | static Color blue = Color(0xFF3688FF); 16 | static Color golden = Color(0xff8B7961); 17 | static Color comicBg = Color.fromARGB(255, 245, 245, 238); 18 | } 19 | -------------------------------------------------------------------------------- /lib/app/main/main_page_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide NestedScrollView; 2 | import 'package:flutter_novel/app/novel/view/novel_about.dart'; 3 | import 'package:flutter_novel/app/novel/view/novel_book_find.dart'; 4 | import 'package:flutter_novel/app/novel/view/novel_book_shelf.dart'; 5 | import 'package:flutter_novel/app/router/manager_router.dart'; 6 | import 'package:flutter_novel/base/structure/base_view.dart'; 7 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 8 | import 'package:flutter_novel/base/util/utils_toast.dart'; 9 | 10 | class MainPageView extends BaseStatefulView { 11 | @override 12 | BaseStatefulViewState, BaseViewModel> 13 | buildState() { 14 | return MainPageViewState(); 15 | } 16 | } 17 | 18 | class MainPageViewState 19 | extends BaseStatefulViewState 20 | with SingleTickerProviderStateMixin { 21 | DateTime _lastClickTime; 22 | 23 | TabController primaryTC; 24 | 25 | @override 26 | void initData() { 27 | primaryTC = TabController(length: 3, vsync: this); 28 | 29 | } 30 | 31 | @override 32 | Widget buildView(BuildContext context, BaseViewModel viewModel) { 33 | 34 | return Scaffold( 35 | appBar: AppBar( 36 | title: Text("Flutter Novel"), 37 | bottom: TabBar( 38 | tabs: [ 39 | Tab( 40 | text: "书库", 41 | ), 42 | Tab( 43 | text: "发现", 44 | ), 45 | Tab( 46 | text: "关于", 47 | ) 48 | ], 49 | controller: primaryTC, 50 | ), 51 | actions: [ 52 | Padding( 53 | child: IconButton(icon:Icon(Icons.search),onPressed: (){ 54 | APPRouter.instance.route(APPRouterRequestOption( 55 | APPRouter.ROUTER_NAME_NOVEL_SEARCH, context)); 56 | },), 57 | padding: EdgeInsets.fromLTRB(10, 5, 10, 5), 58 | ), 59 | Padding( 60 | child: Icon(Icons.menu), 61 | padding: EdgeInsets.fromLTRB(10, 5, 10, 5), 62 | ) 63 | ], 64 | ), 65 | backgroundColor: Colors.grey[100], 66 | body: WillPopScope( 67 | child: Container( 68 | child: TabBarView( 69 | children: [ 70 | NovelBookShelfView(), 71 | NovelBookFindView(), 72 | NovelAbout(), 73 | ], 74 | controller: primaryTC, 75 | ), 76 | ), 77 | onWillPop: () async { 78 | if (_lastClickTime == null || 79 | DateTime.now().difference(_lastClickTime) > 80 | Duration(seconds: 1)) { 81 | //两次点击间隔超过1秒则重新计时 82 | _lastClickTime = DateTime.now(); 83 | ToastUtils.showToast("再次点击退出应用"); 84 | return false; 85 | } 86 | return true; 87 | }), 88 | ); 89 | } 90 | 91 | @override 92 | void loadData(BuildContext context, BaseViewModel viewModel) {} 93 | 94 | @override 95 | BaseViewModel buildViewModel(BuildContext context) { 96 | return null; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_chapter.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_book_chapter.g.dart'; 4 | 5 | 6 | @JsonSerializable() 7 | class NovelBookChapter extends Object { 8 | 9 | @JsonKey(name: '_id') 10 | String id; 11 | 12 | @JsonKey(name: 'name') 13 | String name; 14 | 15 | @JsonKey(name: 'source') 16 | String source; 17 | 18 | @JsonKey(name: 'book') 19 | String book; 20 | 21 | @JsonKey(name: 'link') 22 | String link; 23 | 24 | @JsonKey(name: 'chapters') 25 | List chapters; 26 | 27 | @JsonKey(name: 'updated') 28 | String updated; 29 | 30 | @JsonKey(name: 'host') 31 | String host; 32 | 33 | NovelBookChapter(this.id,this.name,this.source,this.book,this.link,this.chapters,this.updated,this.host,); 34 | 35 | factory NovelBookChapter.fromJson(Map srcJson) => _$NovelBookChapterFromJson(srcJson); 36 | 37 | Map toJson() => _$NovelBookChapterToJson(this); 38 | 39 | } 40 | 41 | 42 | @JsonSerializable() 43 | class Chapters extends Object { 44 | 45 | @JsonKey(name: '_id') 46 | String bookId; 47 | 48 | @JsonKey(name: 'title') 49 | String title; 50 | 51 | @JsonKey(name: 'link') 52 | String link; 53 | 54 | @JsonKey(name: 'id') 55 | String id; 56 | 57 | @JsonKey(name: 'time') 58 | int time; 59 | 60 | @JsonKey(name: 'chapterCover') 61 | String chapterCover; 62 | 63 | @JsonKey(name: 'totalpage') 64 | int totalpage; 65 | 66 | @JsonKey(name: 'partsize') 67 | int partsize; 68 | 69 | @JsonKey(name: 'order') 70 | int order; 71 | 72 | @JsonKey(name: 'currency') 73 | int currency; 74 | 75 | @JsonKey(name: 'unreadble') 76 | bool unreadble; 77 | 78 | @JsonKey(name: 'isVip') 79 | bool isVip; 80 | 81 | String novelId; 82 | 83 | Chapters(this.bookId,this.title,this.link,this.id,this.time,this.chapterCover,this.totalpage,this.partsize,this.order,this.currency,this.unreadble,this.isVip,); 84 | 85 | factory Chapters.fromJson(Map srcJson) => _$ChaptersFromJson(srcJson); 86 | 87 | Map toJson() => _$ChaptersToJson(this); 88 | 89 | } 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_chapter.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_book_chapter.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelBookChapter _$NovelBookChapterFromJson(Map json) { 10 | return NovelBookChapter( 11 | json['_id'] as String, 12 | json['name'] as String, 13 | json['source'] as String, 14 | json['book'] as String, 15 | json['link'] as String, 16 | (json['chapters'] as List) 17 | ?.map((e) => 18 | e == null ? null : Chapters.fromJson(e as Map)) 19 | ?.toList(), 20 | json['updated'] as String, 21 | json['host'] as String, 22 | ); 23 | } 24 | 25 | Map _$NovelBookChapterToJson(NovelBookChapter instance) => 26 | { 27 | '_id': instance.id, 28 | 'name': instance.name, 29 | 'source': instance.source, 30 | 'book': instance.book, 31 | 'link': instance.link, 32 | 'chapters': instance.chapters, 33 | 'updated': instance.updated, 34 | 'host': instance.host, 35 | }; 36 | 37 | Chapters _$ChaptersFromJson(Map json) { 38 | return Chapters( 39 | json['_id'] as String, 40 | json['title'] as String, 41 | json['link'] as String, 42 | json['id'] as String, 43 | json['time'] as int, 44 | json['chapterCover'] as String, 45 | json['totalpage'] as int, 46 | json['partsize'] as int, 47 | json['order'] as int, 48 | json['currency'] as int, 49 | json['unreadble'] as bool, 50 | json['isVip'] as bool, 51 | ); 52 | } 53 | 54 | Map _$ChaptersToJson(Chapters instance) => { 55 | '_id': instance.bookId, 56 | 'title': instance.title, 57 | 'link': instance.link, 58 | 'id': instance.id, 59 | 'time': instance.time, 60 | 'chapterCover': instance.chapterCover, 61 | 'totalpage': instance.totalpage, 62 | 'partsize': instance.partsize, 63 | 'order': instance.order, 64 | 'currency': instance.currency, 65 | 'unreadble': instance.unreadble, 66 | 'isVip': instance.isVip, 67 | }; 68 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_key_word_search.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_book_key_word_search.g.dart'; 4 | 5 | @JsonSerializable() 6 | class NovelKeyWordSearch extends Object { 7 | 8 | @JsonKey(name: 'books') 9 | List books; 10 | 11 | @JsonKey(name: 'total') 12 | int total; 13 | 14 | @JsonKey(name: 'ok') 15 | bool ok; 16 | 17 | NovelKeyWordSearch(this.books,this.total,this.ok,); 18 | 19 | factory NovelKeyWordSearch.fromJson(Map srcJson) => _$NovelKeyWordSearchFromJson(srcJson); 20 | 21 | Map toJson() => _$NovelKeyWordSearchToJson(this); 22 | 23 | } 24 | 25 | 26 | @JsonSerializable() 27 | class Books extends Object { 28 | 29 | @JsonKey(name: '_id') 30 | String id; 31 | 32 | @JsonKey(name: 'hasCp') 33 | bool hasCp; 34 | 35 | @JsonKey(name: 'title') 36 | String title; 37 | 38 | @JsonKey(name: 'aliases') 39 | String aliases; 40 | 41 | @JsonKey(name: 'cat') 42 | String cat; 43 | 44 | @JsonKey(name: 'author') 45 | String author; 46 | 47 | @JsonKey(name: 'site') 48 | String site; 49 | 50 | @JsonKey(name: 'cover') 51 | String cover; 52 | 53 | @JsonKey(name: 'shortIntro') 54 | String shortIntro; 55 | 56 | @JsonKey(name: 'lastChapter') 57 | String lastChapter; 58 | 59 | @JsonKey(name: 'retentionRatio') 60 | double retentionRatio; 61 | 62 | @JsonKey(name: 'banned') 63 | int banned; 64 | 65 | @JsonKey(name: 'allowMonthly') 66 | bool allowMonthly; 67 | 68 | @JsonKey(name: 'latelyFollower') 69 | int latelyFollower; 70 | 71 | @JsonKey(name: 'wordCount') 72 | int wordCount; 73 | 74 | @JsonKey(name: 'contentType') 75 | String contentType; 76 | 77 | @JsonKey(name: 'superscript') 78 | String superscript; 79 | 80 | @JsonKey(name: 'sizetype') 81 | int sizetype; 82 | 83 | @JsonKey(name: 'highlight') 84 | Highlight highlight; 85 | 86 | Books(this.id,this.hasCp,this.title,this.aliases,this.cat,this.author,this.site,this.cover,this.shortIntro,this.lastChapter,this.retentionRatio,this.banned,this.allowMonthly,this.latelyFollower,this.wordCount,this.contentType,this.superscript,this.sizetype,this.highlight,); 87 | 88 | factory Books.fromJson(Map srcJson) => _$BooksFromJson(srcJson); 89 | 90 | Map toJson() => _$BooksToJson(this); 91 | 92 | } 93 | 94 | 95 | @JsonSerializable() 96 | class Highlight extends Object { 97 | 98 | @JsonKey(name: 'title') 99 | List title; 100 | 101 | Highlight(this.title,); 102 | 103 | factory Highlight.fromJson(Map srcJson) => _$HighlightFromJson(srcJson); 104 | 105 | Map toJson() => _$HighlightToJson(this); 106 | 107 | } 108 | 109 | 110 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_key_word_search.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_book_key_word_search.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelKeyWordSearch _$NovelKeyWordSearchFromJson(Map json) { 10 | return NovelKeyWordSearch( 11 | (json['books'] as List) 12 | ?.map( 13 | (e) => e == null ? null : Books.fromJson(e as Map)) 14 | ?.toList(), 15 | json['total'] as int, 16 | json['ok'] as bool, 17 | ); 18 | } 19 | 20 | Map _$NovelKeyWordSearchToJson(NovelKeyWordSearch instance) => 21 | { 22 | 'books': instance.books, 23 | 'total': instance.total, 24 | 'ok': instance.ok, 25 | }; 26 | 27 | Books _$BooksFromJson(Map json) { 28 | return Books( 29 | json['_id'] as String, 30 | json['hasCp'] as bool, 31 | json['title'] as String, 32 | json['aliases'] as String, 33 | json['cat'] as String, 34 | json['author'] as String, 35 | json['site'] as String, 36 | json['cover'] as String, 37 | json['shortIntro'] as String, 38 | json['lastChapter'] as String, 39 | (json['retentionRatio'] as num)?.toDouble(), 40 | json['banned'] as int, 41 | json['allowMonthly'] as bool, 42 | json['latelyFollower'] as int, 43 | json['wordCount'] as int, 44 | json['contentType'] as String, 45 | json['superscript'] as String, 46 | json['sizetype'] as int, 47 | json['highlight'] == null 48 | ? null 49 | : Highlight.fromJson(json['highlight'] as Map), 50 | ); 51 | } 52 | 53 | Map _$BooksToJson(Books instance) => { 54 | '_id': instance.id, 55 | 'hasCp': instance.hasCp, 56 | 'title': instance.title, 57 | 'aliases': instance.aliases, 58 | 'cat': instance.cat, 59 | 'author': instance.author, 60 | 'site': instance.site, 61 | 'cover': instance.cover, 62 | 'shortIntro': instance.shortIntro, 63 | 'lastChapter': instance.lastChapter, 64 | 'retentionRatio': instance.retentionRatio, 65 | 'banned': instance.banned, 66 | 'allowMonthly': instance.allowMonthly, 67 | 'latelyFollower': instance.latelyFollower, 68 | 'wordCount': instance.wordCount, 69 | 'contentType': instance.contentType, 70 | 'superscript': instance.superscript, 71 | 'sizetype': instance.sizetype, 72 | 'highlight': instance.highlight, 73 | }; 74 | 75 | Highlight _$HighlightFromJson(Map json) { 76 | return Highlight( 77 | (json['title'] as List)?.map((e) => e as String)?.toList(), 78 | ); 79 | } 80 | 81 | Map _$HighlightToJson(Highlight instance) => { 82 | 'title': instance.title, 83 | }; 84 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_recommend.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_book_recommend.g.dart'; 4 | 5 | @JsonSerializable() 6 | class NovelBookRecommend extends Object { 7 | 8 | @JsonKey(name: 'books') 9 | List books; 10 | 11 | @JsonKey(name: 'ok') 12 | bool ok; 13 | 14 | NovelBookRecommend(this.books,this.ok,); 15 | 16 | factory NovelBookRecommend.fromJson(Map srcJson) => _$NovelBookRecommendFromJson(srcJson); 17 | 18 | Map toJson() => _$NovelBookRecommendToJson(this); 19 | 20 | } 21 | 22 | 23 | @JsonSerializable() 24 | class Books extends Object { 25 | 26 | @JsonKey(name: '_id') 27 | String id; 28 | 29 | @JsonKey(name: 'title') 30 | String title; 31 | 32 | @JsonKey(name: 'author') 33 | String author; 34 | 35 | @JsonKey(name: 'site') 36 | String site; 37 | 38 | @JsonKey(name: 'cover') 39 | String cover; 40 | 41 | @JsonKey(name: 'shortIntro') 42 | String shortIntro; 43 | 44 | @JsonKey(name: 'lastChapter') 45 | String lastChapter; 46 | 47 | @JsonKey(name: 'retentionRatio') 48 | double retentionRatio; 49 | 50 | @JsonKey(name: 'latelyFollower') 51 | int latelyFollower; 52 | 53 | @JsonKey(name: 'majorCate') 54 | String majorCate; 55 | 56 | @JsonKey(name: 'minorCate') 57 | String minorCate; 58 | 59 | @JsonKey(name: 'allowMonthly') 60 | bool allowMonthly; 61 | 62 | @JsonKey(name: 'isSerial') 63 | bool isSerial; 64 | 65 | @JsonKey(name: 'contentType') 66 | String contentType; 67 | 68 | @JsonKey(name: 'allowFree') 69 | bool allowFree; 70 | 71 | @JsonKey(name: 'otherReadRatio') 72 | double otherReadRatio; 73 | 74 | Books(this.id,this.title,this.author,this.site,this.cover,this.shortIntro,this.lastChapter,this.retentionRatio,this.latelyFollower,this.majorCate,this.minorCate,this.allowMonthly,this.isSerial,this.contentType,this.allowFree,this.otherReadRatio,); 75 | 76 | factory Books.fromJson(Map srcJson) => _$BooksFromJson(srcJson); 77 | 78 | Map toJson() => _$BooksToJson(this); 79 | 80 | } 81 | 82 | 83 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_recommend.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_book_recommend.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelBookRecommend _$NovelBookRecommendFromJson(Map json) { 10 | return NovelBookRecommend( 11 | (json['books'] as List) 12 | ?.map( 13 | (e) => e == null ? null : Books.fromJson(e as Map)) 14 | ?.toList(), 15 | json['ok'] as bool, 16 | ); 17 | } 18 | 19 | Map _$NovelBookRecommendToJson(NovelBookRecommend instance) => 20 | { 21 | 'books': instance.books, 22 | 'ok': instance.ok, 23 | }; 24 | 25 | Books _$BooksFromJson(Map json) { 26 | return Books( 27 | json['_id'] as String, 28 | json['title'] as String, 29 | json['author'] as String, 30 | json['site'] as String, 31 | json['cover'] as String, 32 | json['shortIntro'] as String, 33 | json['lastChapter'] as String, 34 | (json['retentionRatio'] as num)?.toDouble(), 35 | json['latelyFollower'] as int, 36 | json['majorCate'] as String, 37 | json['minorCate'] as String, 38 | json['allowMonthly'] as bool, 39 | json['isSerial'] as bool, 40 | json['contentType'] as String, 41 | json['allowFree'] as bool, 42 | (json['otherReadRatio'] as num)?.toDouble(), 43 | ); 44 | } 45 | 46 | Map _$BooksToJson(Books instance) => { 47 | '_id': instance.id, 48 | 'title': instance.title, 49 | 'author': instance.author, 50 | 'site': instance.site, 51 | 'cover': instance.cover, 52 | 'shortIntro': instance.shortIntro, 53 | 'lastChapter': instance.lastChapter, 54 | 'retentionRatio': instance.retentionRatio, 55 | 'latelyFollower': instance.latelyFollower, 56 | 'majorCate': instance.majorCate, 57 | 'minorCate': instance.minorCate, 58 | 'allowMonthly': instance.allowMonthly, 59 | 'isSerial': instance.isSerial, 60 | 'contentType': instance.contentType, 61 | 'allowFree': instance.allowFree, 62 | 'otherReadRatio': instance.otherReadRatio, 63 | }; 64 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_review.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_book_review.g.dart'; 4 | 5 | 6 | @JsonSerializable() 7 | class NovelBookReview extends Object { 8 | 9 | @JsonKey(name: 'total') 10 | int total; 11 | 12 | @JsonKey(name: 'today') 13 | int today; 14 | 15 | @JsonKey(name: 'reviews') 16 | List reviews; 17 | 18 | @JsonKey(name: 'ok') 19 | bool ok; 20 | 21 | NovelBookReview(this.total,this.today,this.reviews,this.ok,); 22 | 23 | factory NovelBookReview.fromJson(Map srcJson) => _$NovelBookReviewFromJson(srcJson); 24 | 25 | Map toJson() => _$NovelBookReviewToJson(this); 26 | 27 | } 28 | 29 | 30 | @JsonSerializable() 31 | class Reviews extends Object { 32 | 33 | @JsonKey(name: '_id') 34 | String id; 35 | 36 | @JsonKey(name: 'rating') 37 | int rating; 38 | 39 | @JsonKey(name: 'author') 40 | Author author; 41 | 42 | @JsonKey(name: 'helpful') 43 | Helpful helpful; 44 | 45 | @JsonKey(name: 'likeCount') 46 | int likeCount; 47 | 48 | @JsonKey(name: 'state') 49 | String state; 50 | 51 | @JsonKey(name: 'updated') 52 | String updated; 53 | 54 | @JsonKey(name: 'created') 55 | String created; 56 | 57 | @JsonKey(name: 'commentCount') 58 | int commentCount; 59 | 60 | @JsonKey(name: 'content') 61 | String content; 62 | 63 | @JsonKey(name: 'title') 64 | String title; 65 | 66 | Reviews(this.id,this.rating,this.author,this.helpful,this.likeCount,this.state,this.updated,this.created,this.commentCount,this.content,this.title,); 67 | 68 | factory Reviews.fromJson(Map srcJson) => _$ReviewsFromJson(srcJson); 69 | 70 | Map toJson() => _$ReviewsToJson(this); 71 | 72 | } 73 | 74 | 75 | @JsonSerializable() 76 | class Author extends Object { 77 | 78 | @JsonKey(name: '_id') 79 | String id; 80 | 81 | @JsonKey(name: 'avatar') 82 | String avatar; 83 | 84 | @JsonKey(name: 'nickname') 85 | String nickname; 86 | 87 | @JsonKey(name: 'activityAvatar') 88 | String activityAvatar; 89 | 90 | @JsonKey(name: 'type') 91 | String type; 92 | 93 | @JsonKey(name: 'lv') 94 | int lv; 95 | 96 | @JsonKey(name: 'gender') 97 | String gender; 98 | 99 | Author(this.id,this.avatar,this.nickname,this.activityAvatar,this.type,this.lv,this.gender,); 100 | 101 | factory Author.fromJson(Map srcJson) => _$AuthorFromJson(srcJson); 102 | 103 | Map toJson() => _$AuthorToJson(this); 104 | 105 | } 106 | 107 | 108 | @JsonSerializable() 109 | class Helpful extends Object { 110 | 111 | @JsonKey(name: 'total') 112 | int total; 113 | 114 | @JsonKey(name: 'yes') 115 | int yes; 116 | 117 | @JsonKey(name: 'no') 118 | int no; 119 | 120 | Helpful(this.total,this.yes,this.no,); 121 | 122 | factory Helpful.fromJson(Map srcJson) => _$HelpfulFromJson(srcJson); 123 | 124 | Map toJson() => _$HelpfulToJson(this); 125 | 126 | } 127 | 128 | 129 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_review.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_book_review.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelBookReview _$NovelBookReviewFromJson(Map json) { 10 | return NovelBookReview( 11 | json['total'] as int, 12 | json['today'] as int, 13 | (json['reviews'] as List) 14 | ?.map((e) => 15 | e == null ? null : Reviews.fromJson(e as Map)) 16 | ?.toList(), 17 | json['ok'] as bool, 18 | ); 19 | } 20 | 21 | Map _$NovelBookReviewToJson(NovelBookReview instance) => 22 | { 23 | 'total': instance.total, 24 | 'today': instance.today, 25 | 'reviews': instance.reviews, 26 | 'ok': instance.ok, 27 | }; 28 | 29 | Reviews _$ReviewsFromJson(Map json) { 30 | return Reviews( 31 | json['_id'] as String, 32 | json['rating'] as int, 33 | json['author'] == null 34 | ? null 35 | : Author.fromJson(json['author'] as Map), 36 | json['helpful'] == null 37 | ? null 38 | : Helpful.fromJson(json['helpful'] as Map), 39 | json['likeCount'] as int, 40 | json['state'] as String, 41 | json['updated'] as String, 42 | json['created'] as String, 43 | json['commentCount'] as int, 44 | json['content'] as String, 45 | json['title'] as String, 46 | ); 47 | } 48 | 49 | Map _$ReviewsToJson(Reviews instance) => { 50 | '_id': instance.id, 51 | 'rating': instance.rating, 52 | 'author': instance.author, 53 | 'helpful': instance.helpful, 54 | 'likeCount': instance.likeCount, 55 | 'state': instance.state, 56 | 'updated': instance.updated, 57 | 'created': instance.created, 58 | 'commentCount': instance.commentCount, 59 | 'content': instance.content, 60 | 'title': instance.title, 61 | }; 62 | 63 | Author _$AuthorFromJson(Map json) { 64 | return Author( 65 | json['_id'] as String, 66 | json['avatar'] as String, 67 | json['nickname'] as String, 68 | json['activityAvatar'] as String, 69 | json['type'] as String, 70 | json['lv'] as int, 71 | json['gender'] as String, 72 | ); 73 | } 74 | 75 | Map _$AuthorToJson(Author instance) => { 76 | '_id': instance.id, 77 | 'avatar': instance.avatar, 78 | 'nickname': instance.nickname, 79 | 'activityAvatar': instance.activityAvatar, 80 | 'type': instance.type, 81 | 'lv': instance.lv, 82 | 'gender': instance.gender, 83 | }; 84 | 85 | Helpful _$HelpfulFromJson(Map json) { 86 | return Helpful( 87 | json['total'] as int, 88 | json['yes'] as int, 89 | json['no'] as int, 90 | ); 91 | } 92 | 93 | Map _$HelpfulToJson(Helpful instance) => { 94 | 'total': instance.total, 95 | 'yes': instance.yes, 96 | 'no': instance.no, 97 | }; 98 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_book_source.g.dart'; 4 | 5 | 6 | List getNovelBookSourceList(List list){ 7 | List result = []; 8 | list.forEach((item){ 9 | result.add(NovelBookSource.fromJson(item)); 10 | }); 11 | return result; 12 | } 13 | @JsonSerializable() 14 | class NovelBookSource extends Object { 15 | 16 | @JsonKey(name: '_id') 17 | String id; 18 | 19 | @JsonKey(name: 'isCharge') 20 | bool isCharge; 21 | 22 | @JsonKey(name: 'name') 23 | String name; 24 | 25 | @JsonKey(name: 'lastChapter') 26 | String lastChapter; 27 | 28 | @JsonKey(name: 'updated') 29 | String updated; 30 | 31 | @JsonKey(name: 'source') 32 | String source; 33 | 34 | @JsonKey(name: 'link') 35 | String link; 36 | 37 | @JsonKey(name: 'starting') 38 | bool starting; 39 | 40 | @JsonKey(name: 'chaptersCount') 41 | int chaptersCount; 42 | 43 | @JsonKey(name: 'host') 44 | String host; 45 | 46 | NovelBookSource(this.id,this.isCharge,this.name,this.lastChapter,this.updated,this.source,this.link,this.starting,this.chaptersCount,this.host,); 47 | 48 | factory NovelBookSource.fromJson(Map srcJson) => _$NovelBookSourceFromJson(srcJson); 49 | 50 | Map toJson() => _$NovelBookSourceToJson(this); 51 | 52 | } 53 | 54 | 55 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_book_source.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_book_source.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelBookSource _$NovelBookSourceFromJson(Map json) { 10 | return NovelBookSource( 11 | json['_id'] as String, 12 | json['isCharge'] as bool, 13 | json['name'] as String, 14 | json['lastChapter'] as String, 15 | json['updated'] as String, 16 | json['source'] as String, 17 | json['link'] as String, 18 | json['starting'] as bool, 19 | json['chaptersCount'] as int, 20 | json['host'] as String, 21 | ); 22 | } 23 | 24 | Map _$NovelBookSourceToJson(NovelBookSource instance) => 25 | { 26 | '_id': instance.id, 27 | 'isCharge': instance.isCharge, 28 | 'name': instance.name, 29 | 'lastChapter': instance.lastChapter, 30 | 'updated': instance.updated, 31 | 'source': instance.source, 32 | 'link': instance.link, 33 | 'starting': instance.starting, 34 | 'chaptersCount': instance.chaptersCount, 35 | 'host': instance.host, 36 | }; 37 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_chapter_info.dart: -------------------------------------------------------------------------------- 1 | class NovelChapterInfo{ 2 | 3 | String title; 4 | 5 | int chapterId; 6 | int volumeId; 7 | int bookId; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_detail.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_detail.g.dart'; 4 | 5 | @JsonSerializable() 6 | class NovelDetailInfo extends Object { 7 | 8 | @JsonKey(name: '_id') 9 | String id; 10 | 11 | @JsonKey(name: 'title') 12 | String title; 13 | 14 | @JsonKey(name: 'author') 15 | String author; 16 | 17 | @JsonKey(name: 'majorCate') 18 | String majorCate; 19 | 20 | @JsonKey(name: 'cover') 21 | String cover; 22 | 23 | @JsonKey(name: 'longIntro') 24 | String longIntro; 25 | 26 | @JsonKey(name: 'starRatingCount') 27 | int starRatingCount; 28 | 29 | @JsonKey(name: 'starRatings') 30 | List starRatings; 31 | 32 | @JsonKey(name: 'isMakeMoneyLimit') 33 | bool isMakeMoneyLimit; 34 | 35 | @JsonKey(name: 'contentLevel') 36 | int contentLevel; 37 | 38 | @JsonKey(name: 'isFineBook') 39 | bool isFineBook; 40 | 41 | @JsonKey(name: 'safelevel') 42 | int safelevel; 43 | 44 | @JsonKey(name: 'allowFree') 45 | bool allowFree; 46 | 47 | @JsonKey(name: 'originalAuthor') 48 | String originalAuthor; 49 | 50 | @JsonKey(name: 'anchors') 51 | List anchors; 52 | 53 | @JsonKey(name: 'authorDesc') 54 | String authorDesc; 55 | 56 | @JsonKey(name: 'rating') 57 | Rating rating; 58 | 59 | @JsonKey(name: 'hasCopyright') 60 | bool hasCopyright; 61 | 62 | @JsonKey(name: 'buytype') 63 | int buytype; 64 | 65 | @JsonKey(name: 'sizetype') 66 | int sizetype; 67 | 68 | @JsonKey(name: 'superscript') 69 | String superscript; 70 | 71 | @JsonKey(name: 'currency') 72 | int currency; 73 | 74 | @JsonKey(name: 'contentType') 75 | String contentType; 76 | 77 | @JsonKey(name: '_le') 78 | bool le; 79 | 80 | @JsonKey(name: 'allowMonthly') 81 | bool allowMonthly; 82 | 83 | @JsonKey(name: 'allowVoucher') 84 | bool allowVoucher; 85 | 86 | @JsonKey(name: 'allowBeanVoucher') 87 | bool allowBeanVoucher; 88 | 89 | @JsonKey(name: 'hasCp') 90 | bool hasCp; 91 | 92 | @JsonKey(name: 'banned') 93 | int banned; 94 | 95 | @JsonKey(name: 'postCount') 96 | int postCount; 97 | 98 | @JsonKey(name: 'totalFollower') 99 | int totalFollower; 100 | 101 | @JsonKey(name: 'latelyFollower') 102 | int latelyFollower; 103 | 104 | @JsonKey(name: 'followerCount') 105 | int followerCount; 106 | 107 | @JsonKey(name: 'wordCount') 108 | int wordCount; 109 | 110 | @JsonKey(name: 'serializeWordCount') 111 | int serializeWordCount; 112 | 113 | @JsonKey(name: 'retentionRatio') 114 | String retentionRatio; 115 | 116 | @JsonKey(name: 'updated') 117 | String updated; 118 | 119 | @JsonKey(name: 'isSerial') 120 | bool isSerial; 121 | 122 | @JsonKey(name: 'chaptersCount') 123 | int chaptersCount; 124 | 125 | @JsonKey(name: 'lastChapter') 126 | String lastChapter; 127 | 128 | @JsonKey(name: 'gender') 129 | List gender; 130 | 131 | @JsonKey(name: 'tags') 132 | List tags; 133 | 134 | @JsonKey(name: 'advertRead') 135 | bool advertRead; 136 | 137 | @JsonKey(name: 'donate') 138 | bool donate; 139 | 140 | @JsonKey(name: 'copyright') 141 | String copyright; 142 | 143 | @JsonKey(name: '_gg') 144 | bool gg; 145 | 146 | @JsonKey(name: 'isForbidForFreeApp') 147 | bool isForbidForFreeApp; 148 | 149 | @JsonKey(name: 'isAllowNetSearch') 150 | bool isAllowNetSearch; 151 | 152 | @JsonKey(name: 'limit') 153 | bool limit; 154 | 155 | @JsonKey(name: 'copyrightInfo') 156 | String copyrightInfo; 157 | 158 | @JsonKey(name: 'copyrightDesc') 159 | String copyrightDesc; 160 | 161 | NovelDetailInfo(this.id,this.title,this.author,this.majorCate,this.cover,this.longIntro,this.starRatingCount,this.starRatings,this.isMakeMoneyLimit,this.contentLevel,this.isFineBook,this.safelevel,this.allowFree,this.originalAuthor,this.anchors,this.authorDesc,this.rating,this.hasCopyright,this.buytype,this.sizetype,this.superscript,this.currency,this.contentType,this.le,this.allowMonthly,this.allowVoucher,this.allowBeanVoucher,this.hasCp,this.banned,this.postCount,this.totalFollower,this.latelyFollower,this.followerCount,this.wordCount,this.serializeWordCount,this.retentionRatio,this.updated,this.isSerial,this.chaptersCount,this.lastChapter,this.gender,this.tags,this.advertRead,this.donate,this.copyright,this.gg,this.isForbidForFreeApp,this.isAllowNetSearch,this.limit,this.copyrightInfo,this.copyrightDesc,); 162 | 163 | factory NovelDetailInfo.fromJson(Map srcJson) => _$NovelDetailInfoFromJson(srcJson); 164 | 165 | Map toJson() => _$NovelDetailInfoToJson(this); 166 | 167 | } 168 | 169 | 170 | @JsonSerializable() 171 | class StarRatings extends Object { 172 | 173 | @JsonKey(name: 'count') 174 | int count; 175 | 176 | @JsonKey(name: 'star') 177 | int star; 178 | 179 | StarRatings(this.count,this.star,); 180 | 181 | factory StarRatings.fromJson(Map srcJson) => _$StarRatingsFromJson(srcJson); 182 | 183 | Map toJson() => _$StarRatingsToJson(this); 184 | 185 | } 186 | 187 | 188 | @JsonSerializable() 189 | class Rating extends Object { 190 | 191 | @JsonKey(name: 'score') 192 | double score; 193 | 194 | @JsonKey(name: 'count') 195 | int count; 196 | 197 | @JsonKey(name: 'tip') 198 | String tip; 199 | 200 | @JsonKey(name: 'isEffect') 201 | bool isEffect; 202 | 203 | Rating(this.score,this.count,this.tip,this.isEffect,); 204 | 205 | factory Rating.fromJson(Map srcJson) => _$RatingFromJson(srcJson); 206 | 207 | Map toJson() => _$RatingToJson(this); 208 | 209 | } 210 | 211 | 212 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_detail.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_detail.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelDetailInfo _$NovelDetailInfoFromJson(Map json) { 10 | return NovelDetailInfo( 11 | json['_id'] as String, 12 | json['title'] as String, 13 | json['author'] as String, 14 | json['majorCate'] as String, 15 | json['cover'] as String, 16 | json['longIntro'] as String, 17 | json['starRatingCount'] as int, 18 | (json['starRatings'] as List) 19 | ?.map((e) => 20 | e == null ? null : StarRatings.fromJson(e as Map)) 21 | ?.toList(), 22 | json['isMakeMoneyLimit'] as bool, 23 | json['contentLevel'] as int, 24 | json['isFineBook'] as bool, 25 | json['safelevel'] as int, 26 | json['allowFree'] as bool, 27 | json['originalAuthor'] as String, 28 | json['anchors'] as List, 29 | json['authorDesc'] as String, 30 | json['rating'] == null 31 | ? null 32 | : Rating.fromJson(json['rating'] as Map), 33 | json['hasCopyright'] as bool, 34 | json['buytype'] as int, 35 | json['sizetype'] as int, 36 | json['superscript'] as String, 37 | json['currency'] as int, 38 | json['contentType'] as String, 39 | json['_le'] as bool, 40 | json['allowMonthly'] as bool, 41 | json['allowVoucher'] as bool, 42 | json['allowBeanVoucher'] as bool, 43 | json['hasCp'] as bool, 44 | json['banned'] as int, 45 | json['postCount'] as int, 46 | json['totalFollower'] as int, 47 | json['latelyFollower'] as int, 48 | json['followerCount'] as int, 49 | json['wordCount'] as int, 50 | json['serializeWordCount'] as int, 51 | json['retentionRatio'] as String, 52 | json['updated'] as String, 53 | json['isSerial'] as bool, 54 | json['chaptersCount'] as int, 55 | json['lastChapter'] as String, 56 | json['gender'] as List, 57 | json['tags'] as List, 58 | json['advertRead'] as bool, 59 | json['donate'] as bool, 60 | json['copyright'] as String, 61 | json['_gg'] as bool, 62 | json['isForbidForFreeApp'] as bool, 63 | json['isAllowNetSearch'] as bool, 64 | json['limit'] as bool, 65 | json['copyrightInfo'] as String, 66 | json['copyrightDesc'] as String, 67 | ); 68 | } 69 | 70 | Map _$NovelDetailInfoToJson(NovelDetailInfo instance) => 71 | { 72 | '_id': instance.id, 73 | 'title': instance.title, 74 | 'author': instance.author, 75 | 'majorCate': instance.majorCate, 76 | 'cover': instance.cover, 77 | 'longIntro': instance.longIntro, 78 | 'starRatingCount': instance.starRatingCount, 79 | 'starRatings': instance.starRatings, 80 | 'isMakeMoneyLimit': instance.isMakeMoneyLimit, 81 | 'contentLevel': instance.contentLevel, 82 | 'isFineBook': instance.isFineBook, 83 | 'safelevel': instance.safelevel, 84 | 'allowFree': instance.allowFree, 85 | 'originalAuthor': instance.originalAuthor, 86 | 'anchors': instance.anchors, 87 | 'authorDesc': instance.authorDesc, 88 | 'rating': instance.rating, 89 | 'hasCopyright': instance.hasCopyright, 90 | 'buytype': instance.buytype, 91 | 'sizetype': instance.sizetype, 92 | 'superscript': instance.superscript, 93 | 'currency': instance.currency, 94 | 'contentType': instance.contentType, 95 | '_le': instance.le, 96 | 'allowMonthly': instance.allowMonthly, 97 | 'allowVoucher': instance.allowVoucher, 98 | 'allowBeanVoucher': instance.allowBeanVoucher, 99 | 'hasCp': instance.hasCp, 100 | 'banned': instance.banned, 101 | 'postCount': instance.postCount, 102 | 'totalFollower': instance.totalFollower, 103 | 'latelyFollower': instance.latelyFollower, 104 | 'followerCount': instance.followerCount, 105 | 'wordCount': instance.wordCount, 106 | 'serializeWordCount': instance.serializeWordCount, 107 | 'retentionRatio': instance.retentionRatio, 108 | 'updated': instance.updated, 109 | 'isSerial': instance.isSerial, 110 | 'chaptersCount': instance.chaptersCount, 111 | 'lastChapter': instance.lastChapter, 112 | 'gender': instance.gender, 113 | 'tags': instance.tags, 114 | 'advertRead': instance.advertRead, 115 | 'donate': instance.donate, 116 | 'copyright': instance.copyright, 117 | '_gg': instance.gg, 118 | 'isForbidForFreeApp': instance.isForbidForFreeApp, 119 | 'isAllowNetSearch': instance.isAllowNetSearch, 120 | 'limit': instance.limit, 121 | 'copyrightInfo': instance.copyrightInfo, 122 | 'copyrightDesc': instance.copyrightDesc, 123 | }; 124 | 125 | StarRatings _$StarRatingsFromJson(Map json) { 126 | return StarRatings( 127 | json['count'] as int, 128 | json['star'] as int, 129 | ); 130 | } 131 | 132 | Map _$StarRatingsToJson(StarRatings instance) => 133 | { 134 | 'count': instance.count, 135 | 'star': instance.star, 136 | }; 137 | 138 | Rating _$RatingFromJson(Map json) { 139 | return Rating( 140 | (json['score'] as num)?.toDouble(), 141 | json['count'] as int, 142 | json['tip'] as String, 143 | json['isEffect'] as bool, 144 | ); 145 | } 146 | 147 | Map _$RatingToJson(Rating instance) => { 148 | 'score': instance.score, 149 | 'count': instance.count, 150 | 'tip': instance.tip, 151 | 'isEffect': instance.isEffect, 152 | }; 153 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_info.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter_novel/app/novel/helper/helper_db.dart'; 5 | 6 | class NovelBookInfo{ 7 | String bookId; 8 | String cover; 9 | String title; 10 | 11 | int currentPageIndex = 0; 12 | int currentChapterIndex = 0; 13 | int currentVolumeIndex = 0; 14 | 15 | Map toDBMap() => { 16 | DBHelper.COLUMN_BOOK_ID: bookId, 17 | DBHelper.COLUMN_IMAGE: cover, 18 | DBHelper.COLUMN_TITLE: title, 19 | DBHelper.COLUMN_CHAPTER_INDEX: currentChapterIndex, 20 | DBHelper.COLUMN_VOLUME_INDEX: currentVolumeIndex, 21 | DBHelper.COLUMN_PAGE_INDEX: currentPageIndex, 22 | }; 23 | 24 | static NovelBookInfo fromDBMap(Map dbMap) { 25 | if (dbMap == null) return null; 26 | NovelBookInfo bookInfo = NovelBookInfo(); 27 | bookInfo.bookId = dbMap[DBHelper.COLUMN_BOOK_ID]; 28 | bookInfo.cover = dbMap[DBHelper.COLUMN_IMAGE]; 29 | bookInfo.title = dbMap[DBHelper.COLUMN_TITLE]; 30 | bookInfo.currentPageIndex = dbMap[DBHelper.COLUMN_PAGE_INDEX]; 31 | bookInfo.currentChapterIndex = dbMap[DBHelper.COLUMN_CHAPTER_INDEX]; 32 | bookInfo.currentVolumeIndex = dbMap[DBHelper.COLUMN_VOLUME_INDEX]; 33 | return bookInfo; 34 | } 35 | } 36 | 37 | class NovelConfigInfo{ 38 | 39 | int currentAnimationMode; 40 | Color currentCanvasBgColor=Color(0xfffff2cc); 41 | 42 | int fontSize = 20; 43 | int lineHeight = 30; 44 | int paragraphSpacing = 10; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_short_comment.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'entity_novel_short_comment.g.dart'; 4 | 5 | @JsonSerializable() 6 | class NovelShortComment extends Object { 7 | 8 | @JsonKey(name: 'today') 9 | int today; 10 | 11 | @JsonKey(name: 'docs') 12 | List docs; 13 | 14 | @JsonKey(name: 'ok') 15 | bool ok; 16 | 17 | NovelShortComment(this.today,this.docs,this.ok,); 18 | 19 | factory NovelShortComment.fromJson(Map srcJson) => _$NovelShortCommentFromJson(srcJson); 20 | 21 | Map toJson() => _$NovelShortCommentToJson(this); 22 | 23 | } 24 | 25 | 26 | @JsonSerializable() 27 | class Docs extends Object { 28 | 29 | @JsonKey(name: '_id') 30 | String id; 31 | 32 | @JsonKey(name: 'rating') 33 | int rating; 34 | 35 | @JsonKey(name: 'type') 36 | String type; 37 | 38 | @JsonKey(name: 'author') 39 | Author author; 40 | 41 | @JsonKey(name: 'book') 42 | Book book; 43 | 44 | @JsonKey(name: 'likeCount') 45 | int likeCount; 46 | 47 | @JsonKey(name: 'priority') 48 | double priority; 49 | 50 | @JsonKey(name: 'block') 51 | String block; 52 | 53 | @JsonKey(name: 'state') 54 | String state; 55 | 56 | @JsonKey(name: 'updated') 57 | String updated; 58 | 59 | @JsonKey(name: 'created') 60 | String created; 61 | 62 | @JsonKey(name: 'content') 63 | String content; 64 | 65 | Docs(this.id,this.rating,this.type,this.author,this.book,this.likeCount,this.priority,this.block,this.state,this.updated,this.created,this.content,); 66 | 67 | factory Docs.fromJson(Map srcJson) => _$DocsFromJson(srcJson); 68 | 69 | Map toJson() => _$DocsToJson(this); 70 | 71 | } 72 | 73 | 74 | @JsonSerializable() 75 | class Author extends Object { 76 | 77 | @JsonKey(name: '_id') 78 | String id; 79 | 80 | @JsonKey(name: 'avatar') 81 | String avatar; 82 | 83 | @JsonKey(name: 'nickname') 84 | String nickname; 85 | 86 | @JsonKey(name: 'activityAvatar') 87 | String activityAvatar; 88 | 89 | @JsonKey(name: 'type') 90 | String type; 91 | 92 | @JsonKey(name: 'lv') 93 | int lv; 94 | 95 | @JsonKey(name: 'gender') 96 | String gender; 97 | 98 | Author(this.id,this.avatar,this.nickname,this.activityAvatar,this.type,this.lv,this.gender,); 99 | 100 | factory Author.fromJson(Map srcJson) => _$AuthorFromJson(srcJson); 101 | 102 | Map toJson() => _$AuthorToJson(this); 103 | 104 | } 105 | 106 | 107 | @JsonSerializable() 108 | class Book extends Object { 109 | 110 | @JsonKey(name: '_id') 111 | String id; 112 | 113 | @JsonKey(name: 'title') 114 | String title; 115 | 116 | @JsonKey(name: 'cover') 117 | String cover; 118 | 119 | Book(this.id,this.title,this.cover,); 120 | 121 | factory Book.fromJson(Map srcJson) => _$BookFromJson(srcJson); 122 | 123 | Map toJson() => _$BookToJson(this); 124 | 125 | } 126 | 127 | 128 | -------------------------------------------------------------------------------- /lib/app/novel/entity/entity_novel_short_comment.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'entity_novel_short_comment.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | NovelShortComment _$NovelShortCommentFromJson(Map json) { 10 | return NovelShortComment( 11 | json['today'] as int, 12 | (json['docs'] as List) 13 | ?.map( 14 | (e) => e == null ? null : Docs.fromJson(e as Map)) 15 | ?.toList(), 16 | json['ok'] as bool, 17 | ); 18 | } 19 | 20 | Map _$NovelShortCommentToJson(NovelShortComment instance) => 21 | { 22 | 'today': instance.today, 23 | 'docs': instance.docs, 24 | 'ok': instance.ok, 25 | }; 26 | 27 | Docs _$DocsFromJson(Map json) { 28 | return Docs( 29 | json['_id'] as String, 30 | json['rating'] as int, 31 | json['type'] as String, 32 | json['author'] == null 33 | ? null 34 | : Author.fromJson(json['author'] as Map), 35 | json['book'] == null 36 | ? null 37 | : Book.fromJson(json['book'] as Map), 38 | json['likeCount'] as int, 39 | (json['priority'] as num)?.toDouble(), 40 | json['block'] as String, 41 | json['state'] as String, 42 | json['updated'] as String, 43 | json['created'] as String, 44 | json['content'] as String, 45 | ); 46 | } 47 | 48 | Map _$DocsToJson(Docs instance) => { 49 | '_id': instance.id, 50 | 'rating': instance.rating, 51 | 'type': instance.type, 52 | 'author': instance.author, 53 | 'book': instance.book, 54 | 'likeCount': instance.likeCount, 55 | 'priority': instance.priority, 56 | 'block': instance.block, 57 | 'state': instance.state, 58 | 'updated': instance.updated, 59 | 'created': instance.created, 60 | 'content': instance.content, 61 | }; 62 | 63 | Author _$AuthorFromJson(Map json) { 64 | return Author( 65 | json['_id'] as String, 66 | json['avatar'] as String, 67 | json['nickname'] as String, 68 | json['activityAvatar'] as String, 69 | json['type'] as String, 70 | json['lv'] as int, 71 | json['gender'] as String, 72 | ); 73 | } 74 | 75 | Map _$AuthorToJson(Author instance) => { 76 | '_id': instance.id, 77 | 'avatar': instance.avatar, 78 | 'nickname': instance.nickname, 79 | 'activityAvatar': instance.activityAvatar, 80 | 'type': instance.type, 81 | 'lv': instance.lv, 82 | 'gender': instance.gender, 83 | }; 84 | 85 | Book _$BookFromJson(Map json) { 86 | return Book( 87 | json['_id'] as String, 88 | json['title'] as String, 89 | json['cover'] as String, 90 | ); 91 | } 92 | 93 | Map _$BookToJson(Book instance) => { 94 | '_id': instance.id, 95 | 'title': instance.title, 96 | 'cover': instance.cover, 97 | }; 98 | -------------------------------------------------------------------------------- /lib/app/novel/helper/helper_cache.dart: -------------------------------------------------------------------------------- 1 | class CacheHelper{ 2 | 3 | } -------------------------------------------------------------------------------- /lib/app/novel/helper/helper_db.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_novel/app/novel/entity/entity_novel_info.dart'; 2 | import 'package:flutter_novel/base/db/manager_db.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | class DBHelper extends BaseDBProvider { 6 | /// DataBase table name 7 | static const String TABLE_NAME = "novel_bookshelf"; 8 | 9 | static const String COLUMN_ID = "_id"; 10 | static const String COLUMN_BOOK_ID = "bookId"; 11 | static const String COLUMN_TITLE = "title"; 12 | static const String COLUMN_CHAPTER_INDEX = "chaptersIndex"; 13 | static const String COLUMN_VOLUME_INDEX = "volumeIndex"; 14 | static const String COLUMN_PAGE_INDEX = "pageIndex"; 15 | static const String COLUMN_IMAGE = "image"; 16 | 17 | @override 18 | String createSql() => 19 | baseCreateSql(TABLE_NAME, COLUMN_ID) + 20 | ''' 21 | $COLUMN_BOOK_ID TEXT not null, 22 | $COLUMN_TITLE TEXT not null, 23 | $COLUMN_IMAGE TEXT not null, 24 | $COLUMN_CHAPTER_INDEX INTEGER not null, 25 | $COLUMN_VOLUME_INDEX INTEGER not null, 26 | $COLUMN_PAGE_INDEX INTEGER not null) 27 | '''; 28 | 29 | @override 30 | String tableName() => TABLE_NAME; 31 | 32 | /// 根据[bookId]查询某书是否在书架 33 | /// @return true or false 34 | /// 35 | Future isExist(String bookId) async { 36 | if (bookId == null) return false; 37 | Database db = await getDB(); 38 | List> maps = await db 39 | .query(TABLE_NAME, where: "$COLUMN_BOOK_ID = ?", whereArgs: [bookId]); 40 | return maps.isNotEmpty; 41 | } 42 | 43 | /// 根据[bookId]查询某书详情 44 | /// @return book 45 | /// 46 | Future getBook(String bookId) async { 47 | bool _isExist = await isExist(bookId); 48 | if (!_isExist) return null; 49 | 50 | List books = []; 51 | Database db = await getDB(); 52 | List> maps = await db 53 | .query(TABLE_NAME, where: "$COLUMN_BOOK_ID = ?", whereArgs: [bookId]); 54 | if (maps.isNotEmpty) { 55 | for (Map map in maps) { 56 | NovelBookInfo book = NovelBookInfo.fromDBMap(map); 57 | books.add(book); 58 | } 59 | 60 | return books.first; 61 | } else { 62 | return null; 63 | } 64 | } 65 | 66 | /// 根据[_id]查询某书详情 67 | /// @return book 68 | /// 69 | Future getBookById(int _id) async { 70 | List books = []; 71 | Database db = await getDB(); 72 | List> maps = 73 | await db.query(TABLE_NAME, where: "$COLUMN_ID = ?", whereArgs: [_id]); 74 | if (maps.isNotEmpty) { 75 | for (Map map in maps) { 76 | NovelBookInfo book = NovelBookInfo.fromDBMap(map); 77 | books.add(book); 78 | } 79 | return books.first; 80 | } else { 81 | return null; 82 | } 83 | } 84 | 85 | /// 查询书架上所有小说 86 | /// @return list 87 | /// 88 | Future> getAllBooks() async { 89 | List books = []; 90 | Database db = await getDB(); 91 | List> maps = await db.query(TABLE_NAME); 92 | if (maps.isNotEmpty) { 93 | for (Map map in maps) { 94 | NovelBookInfo book = NovelBookInfo.fromDBMap(map); 95 | books.add(book); 96 | } 97 | } 98 | return books; 99 | } 100 | 101 | /// 添加小说[book]到书架 102 | /// @return _id 103 | /// 104 | Future insertOrReplaceToDB(NovelBookInfo book) async { 105 | if (book == null) return -1; 106 | String id = book?.bookId; 107 | if (book == null || id == null) return -1; 108 | bool _isExist = await isExist(id); 109 | if (!_isExist) {} 110 | Database db = await getDB(); 111 | Map map = book.toDBMap(); 112 | return await db.insert(TABLE_NAME, map); 113 | } 114 | 115 | /// 跟新书架上的小说信息[book] 116 | /// @return true or false 117 | /// 118 | Future updateBook(NovelBookInfo book) async { 119 | String id = book?.bookId; 120 | if (book == null || id == null) return false; 121 | 122 | bool _isExist = await isExist(id); 123 | if (!_isExist) return false; 124 | Database db = await getDB(); 125 | 126 | Map map = book.toDBMap(); 127 | int result = await db 128 | .update(TABLE_NAME, map, where: "$COLUMN_BOOK_ID = ?", whereArgs: [id]); 129 | 130 | return result == 1; 131 | } 132 | 133 | /// 根据[bookId]删除书架上的小说 134 | /// @return true or false 135 | /// 136 | Future deleteBook(String bookId) async { 137 | bool _isExist = await isExist(bookId); 138 | if (!_isExist) return false; 139 | Database db = await getDB(); 140 | 141 | int result = await db 142 | .delete(TABLE_NAME, where: "$COLUMN_BOOK_ID = ?", whereArgs: [bookId]); 143 | 144 | return result == 1; 145 | } 146 | 147 | /// 删除书架上的所有小说 148 | /// @return true or false 149 | /// 150 | Future deleteAllBook() async { 151 | Database db = await getDB(); 152 | int result = await db.delete(TABLE_NAME); 153 | return result == 1; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/app/novel/helper/helper_sp.dart: -------------------------------------------------------------------------------- 1 | class SharedPreferenceHelper{ 2 | 3 | } -------------------------------------------------------------------------------- /lib/app/novel/model/model_novel_cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 5 | import 'package:flutter_novel/app/api/api_novel.dart'; 6 | import 'package:path/path.dart' as p; 7 | import 'package:path_provider/path_provider.dart'; 8 | 9 | class NovelBookCacheModel extends BaseCacheManager{ 10 | static const key = "libCacheNovelData"; 11 | 12 | static NovelBookCacheModel _instance; 13 | 14 | factory NovelBookCacheModel() { 15 | if (_instance == null) { 16 | _instance = new NovelBookCacheModel._(); 17 | } 18 | return _instance; 19 | } 20 | 21 | NovelBookCacheModel._() : super(key); 22 | 23 | Future getFilePath() async { 24 | var directory = await getTemporaryDirectory(); 25 | return p.join(directory.path, key); 26 | } 27 | 28 | Future _getNovelPersistentCacheFile(String url, {Map headers}) async { 29 | FileInfo cacheFile = await getFileFromCache(url); 30 | if (cacheFile != null&&cacheFile.file!=null) { 31 | return cacheFile.file; 32 | }else { 33 | removeFile(url); 34 | try { 35 | var download = await webHelper.downloadFile(url, authHeaders: headers); 36 | return download.file; 37 | } catch (e) { 38 | return null; 39 | } 40 | } 41 | } 42 | 43 | Future getCacheChapterContent(String chapterLink) async{ 44 | File targetFile = await _getNovelPersistentCacheFile(NovelApi.QUERY_BOOK_CHAPTER_CONTENT.replaceAll("{link}", chapterLink)); 45 | 46 | if (targetFile == null) { 47 | return null; 48 | } else { 49 | Uint8List bytes = await targetFile.readAsBytes(); 50 | return utf8.decode(bytes, allowMalformed: true); 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /lib/app/novel/model/zssq/model_book_db.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_novel/app/novel/entity/entity_novel_info.dart'; 2 | import 'package:flutter_novel/app/novel/helper/helper_db.dart'; 3 | import 'package:flutter_novel/base/structure/base_model.dart'; 4 | 5 | class NovelBookDBModel extends BaseModel { 6 | final DBHelper _dbHelper; 7 | 8 | NovelBookShelfInfo bookshelfInfo = NovelBookShelfInfo(); 9 | 10 | NovelBookDBModel(this._dbHelper); 11 | 12 | Future> getSavedBook() { 13 | return _dbHelper.getAllBooks(); 14 | } 15 | 16 | void addBook(NovelBookInfo book){ 17 | _dbHelper.insertOrReplaceToDB(book); 18 | if(!bookshelfInfo.currentBookShelf.contains(book)) { 19 | bookshelfInfo.currentBookShelf.add(book); 20 | } 21 | } 22 | void removeBook(String bookId){ 23 | _dbHelper.deleteBook(bookId).then((isSuccess){ 24 | NovelBookInfo targetBook; 25 | for(NovelBookInfo bookInfo in bookshelfInfo.currentBookShelf){ 26 | if(bookInfo.bookId==bookId){ 27 | targetBook=bookInfo; 28 | break; 29 | } 30 | } 31 | 32 | if(targetBook!=null) { 33 | bookshelfInfo.currentBookShelf.remove(targetBook); 34 | } 35 | }); 36 | } 37 | 38 | void updateBookInfo(NovelBookInfo book){ 39 | _dbHelper.updateBook(book).then((isSuccess){ 40 | if(isSuccess){ 41 | for(NovelBookInfo bookInfo in bookshelfInfo.currentBookShelf){ 42 | if(bookInfo.bookId==book.bookId){ 43 | bookInfo=book; 44 | break; 45 | } 46 | } 47 | } 48 | }); 49 | 50 | } 51 | 52 | void getBookInfo() { 53 | 54 | } 55 | } 56 | 57 | class NovelBookShelfInfo { 58 | List currentBookShelf = []; 59 | } 60 | -------------------------------------------------------------------------------- /lib/app/novel/model/zssq/model_book_net.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_novel/app/api/api_novel.dart'; 2 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_chapter.dart'; 3 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_key_word_search.dart'; 4 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_recommend.dart'; 5 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_review.dart'; 6 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_source.dart'; 7 | import 'package:flutter_novel/app/novel/entity/entity_novel_detail.dart'; 8 | import 'package:flutter_novel/app/novel/entity/entity_novel_short_comment.dart'; 9 | import 'package:flutter_novel/base/structure/base_model.dart'; 10 | 11 | 12 | class NovelBookNetModel extends BaseModel{ 13 | 14 | final NovelApi _api; 15 | 16 | NovelBookIntroContentEntity bookIntroContentEntity = NovelBookIntroContentEntity(); 17 | 18 | NovelBookNetModel(this._api); 19 | 20 | void getBookIntroduction(){ 21 | 22 | } 23 | 24 | Future>> getSearchWord(String keyWord) async{ 25 | return _api.getSearchWord(keyWord); 26 | } 27 | Future>> getHotSearchWord() async{ 28 | return _api.getHotSearchWord(); 29 | } 30 | Future> searchTargetKeyWord(String keyword) async{ 31 | return _api.searchTargetKeyWord(keyword); 32 | } 33 | 34 | Future> getNovelDetailInfo(String bookId) async{ 35 | return _api.getNovelDetailInfo(bookId); 36 | } 37 | 38 | Future> getNovelShortReview(String bookId,{String sort: 'updated', int start: 0, int limit: 20}) async{ 39 | return _api.getNovelShortReview(bookId,sort: sort,start: start,limit: limit); 40 | } 41 | 42 | Future> getNovelBookReview(String bookId,{String sort: 'updated', int start: 0, int limit: 20}) async{ 43 | return _api.getNovelBookReview(bookId,sort: sort,start: start,limit: limit); 44 | } 45 | Future> getNovelBookRecommend(String bookId) async{ 46 | return _api.getNovelBookRecommend(bookId); 47 | } 48 | 49 | Future> getNovelBookCatalog(String sourceId) async{ 50 | return await _api.getNovelCatalog(sourceId); 51 | } 52 | 53 | Future>> getNovelBookSource(String bookId) async{ 54 | return await _api.getNovelSource(bookId); 55 | } 56 | } 57 | 58 | class NovelBookIntroContentEntity { 59 | NovelDetailInfo detailInfo; 60 | NovelShortComment shortComment; 61 | NovelBookReview bookReviewInfo; 62 | NovelBookRecommend bookRecommendInfo; 63 | } 64 | 65 | class SearchContentEntity { 66 | 67 | List searchHotWord = []; 68 | List autoCompleteSearchWord = []; 69 | NovelKeyWordSearch keyWordSearchResult; 70 | 71 | } -------------------------------------------------------------------------------- /lib/app/novel/view/novel_about.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NovelAbout extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | padding: EdgeInsets.all(10), 8 | child: ListView( 9 | children: [ 10 | Padding( 11 | padding: EdgeInsets.all(20), 12 | child: Row( 13 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 14 | children: [ 15 | Expanded(child: Text("掘金")), 16 | Text("lwlizhe") 17 | ], 18 | ), 19 | ), 20 | Padding( 21 | padding: EdgeInsets.all(20), 22 | child: Row( 23 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 24 | children: [ 25 | Expanded(child: Text("github")), 26 | Text("lwlizhe") 27 | ], 28 | )), 29 | Padding( 30 | padding: EdgeInsets.only(left: 20,right: 20,top: 20), 31 | child: Row( 32 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 33 | children: [ 34 | Expanded(child: Text("此处是一个面子工程")), 35 | ], 36 | )), 37 | Padding( 38 | padding: EdgeInsets.only(left: 20,right: 20), 39 | child: Row( 40 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 41 | children: [ 42 | Expanded(child: Text("应该要做的很高大上的那种")), 43 | ], 44 | )), 45 | Padding( 46 | padding: EdgeInsets.only(left: 20,right: 20), 47 | child: Row( 48 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 49 | children: [ 50 | Expanded(child: Text("既要高雅又要接地气")), 51 | ], 52 | )), 53 | Padding( 54 | padding: EdgeInsets.only(left: 20,right: 20), 55 | child: Row( 56 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 57 | children: [ 58 | Expanded(child: Text("既要内容丰富又要简洁明朗")), 59 | ], 60 | )), 61 | Padding( 62 | padding: EdgeInsets.only(left: 20,right: 20,top: 20), 63 | child: Row( 64 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 65 | children: [ 66 | Expanded(child: Text("但是本咸鱼就是要逆天而行")), 67 | ], 68 | )), 69 | Padding( 70 | padding: EdgeInsets.only(left: 20,right: 20), 71 | child: Row( 72 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 73 | children: [ 74 | Expanded(child: Text("才不是懒得做,哼!")), 75 | ], 76 | )), 77 | Padding( 78 | padding: EdgeInsets.only(left: 20,right: 20), 79 | child: Row( 80 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 81 | children: [ 82 | Expanded(child: Text("收藏过……呸,star过xxx,直播xxx")), 83 | ], 84 | )), 85 | Divider( 86 | height: 50, 87 | color: Colors.transparent, 88 | ), 89 | Padding( 90 | padding: EdgeInsets.all(20), 91 | child: Row( 92 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 93 | children: [ 94 | Expanded(child: Text("随缘立项,佛性开发,先挖几个坑,反正穷的只剩挖坑想法了,到时候填不填再说。")), 95 | ], 96 | )), 97 | Padding( 98 | padding: EdgeInsets.all(20), 99 | child: Row( 100 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 101 | children: [ 102 | Expanded(child: Text("坑1:外接纹理?")), 103 | ], 104 | )), 105 | Padding( 106 | padding: EdgeInsets.all(20), 107 | child: Row( 108 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 109 | children: [ 110 | Expanded(child: Text("坑2:基于openGL的AR或者全景照片查看器、全景视频播放器?")), 111 | ], 112 | )), 113 | ], 114 | ), 115 | ); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/app/novel/view/novel_book_find.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_vector_icons/flutter_vector_icons.dart'; 3 | 4 | class NovelBookFindView extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Container( 8 | child: ListView( 9 | children: [ 10 | _FindMenuItem(MenuItemType.TYPE_LEADER_BOARD), 11 | _FindMenuItem(MenuItemType.OTHER), 12 | ], 13 | ), 14 | ); 15 | } 16 | } 17 | 18 | class _FindMenuItem extends StatelessWidget { 19 | final MenuItemType itemType; 20 | 21 | _FindMenuItem(this.itemType); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return Container( 26 | padding: EdgeInsets.all(20), 27 | child: Row( 28 | crossAxisAlignment: CrossAxisAlignment.center, 29 | children: [ 30 | Padding( 31 | padding: EdgeInsets.only(right: 10), 32 | child: Builder(builder: (context) { 33 | switch (itemType) { 34 | case MenuItemType.TYPE_LEADER_BOARD: 35 | return Icon( 36 | Feather.bar_chart_2, 37 | size: 18, 38 | ); 39 | case MenuItemType.OTHER: 40 | default: 41 | return Icon( 42 | Feather.check_square, 43 | size: 18, 44 | ); } 45 | }), 46 | ), 47 | Expanded(child: Builder(builder: (context) { 48 | switch (itemType) { 49 | case MenuItemType.TYPE_LEADER_BOARD: 50 | return Text("排行榜",style: TextStyle(fontSize: 16),); 51 | case MenuItemType.OTHER: 52 | default: 53 | return Text("还没想好放啥",style: TextStyle(fontSize: 16),); 54 | } 55 | })), 56 | Padding( 57 | padding: EdgeInsets.only(left: 10), 58 | child: Icon( 59 | Icons.arrow_forward_ios, 60 | size: 16, 61 | ), 62 | ) 63 | ], 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | enum MenuItemType { TYPE_LEADER_BOARD, OTHER } 70 | -------------------------------------------------------------------------------- /lib/app/novel/view/novel_book_leader_board.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/router/manager_router.dart'; 3 | import 'package:flutter_novel/base/structure/base_view.dart'; 4 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 5 | 6 | class NovelLeaderBoardView extends BaseStatelessView { 7 | static NovelLeaderBoardView getPageView(APPRouterRequestOption option) { 8 | return NovelLeaderBoardView(); 9 | } 10 | 11 | @override 12 | Widget buildView(BuildContext context, BaseViewModel viewModel) { 13 | return null; 14 | } 15 | 16 | @override 17 | BaseViewModel buildViewModel(BuildContext context) { 18 | return null; 19 | } 20 | 21 | @override 22 | void loadData(BuildContext context, BaseViewModel viewModel) {} 23 | } 24 | -------------------------------------------------------------------------------- /lib/app/novel/view/novel_book_menu.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/lib/app/novel/view/novel_book_menu.dart -------------------------------------------------------------------------------- /lib/app/novel/view/novel_book_search_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_key_word_search.dart'; 4 | import 'package:flutter_novel/app/novel/view_model/view_model_novel_search.dart'; 5 | import 'package:flutter_novel/app/router/manager_router.dart'; 6 | import 'package:flutter_novel/base/structure/base_view.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class NovelSearchResultView 10 | extends BaseStatelessView { 11 | final String searchKeyWord; 12 | 13 | NovelSearchResultView(this.searchKeyWord); 14 | 15 | static NovelSearchResultView getPageView(APPRouterRequestOption option) { 16 | return NovelSearchResultView(option.params["search_key"]); 17 | } 18 | 19 | @override 20 | Widget buildView(BuildContext context, NovelBookSearchViewModel viewModel) { 21 | NovelKeyWordSearch keyWordSearchResult = 22 | viewModel?.contentEntity?.keyWordSearchResult; 23 | 24 | return Scaffold( 25 | appBar: AppBar( 26 | title: Text("关于$searchKeyWord的书籍"), 27 | ), 28 | body: Builder(builder: (context) { 29 | if (keyWordSearchResult == null) { 30 | return Container( 31 | alignment: Alignment.center, 32 | child: Text("正在查询"), 33 | ); 34 | } else { 35 | return ListView.separated( 36 | itemBuilder: (context, index) { 37 | return Container( 38 | child: InkWell( 39 | onTap: (){ 40 | APPRouter.instance.route(APPRouterRequestOption(APPRouter.ROUTER_NAME_NOVEL_INTRO,context,params: {"bookId": keyWordSearchResult?.books[index]?.id})); 41 | }, 42 | child: Row( 43 | mainAxisAlignment: MainAxisAlignment.center, 44 | children: [ 45 | Expanded( 46 | flex: 1, 47 | child: CachedNetworkImage( 48 | imageUrl: Uri.decodeComponent(keyWordSearchResult 49 | .books[index].cover 50 | .split("/agent/") 51 | .last), 52 | ), 53 | ), 54 | SizedBox( 55 | width: 10, 56 | ), 57 | Expanded( 58 | flex: 4, 59 | child: Column( 60 | crossAxisAlignment: CrossAxisAlignment.start, 61 | children: [ 62 | Text( 63 | keyWordSearchResult.books[index].title, 64 | style: TextStyle( 65 | fontSize: 18, color: Colors.black), 66 | ), 67 | Divider( 68 | height: 5, 69 | color: Colors.transparent, 70 | ), 71 | Text( 72 | keyWordSearchResult.books[index].shortIntro, 73 | style: TextStyle( 74 | fontSize: 16, color: Colors.grey[600]), 75 | maxLines: 2, 76 | overflow: TextOverflow.ellipsis, 77 | ), 78 | Divider( 79 | height: 5, 80 | color: Colors.transparent, 81 | ), 82 | Row( 83 | children: [ 84 | Icon(Icons.account_circle), 85 | SizedBox( 86 | width: 5, 87 | ), 88 | Text( 89 | keyWordSearchResult.books[index].author, 90 | style: TextStyle( 91 | fontSize: 14, color: Colors.grey), 92 | ) 93 | ], 94 | ), 95 | ], 96 | )) 97 | ], 98 | ), 99 | ), 100 | ); 101 | }, 102 | separatorBuilder: (context, index) { 103 | return Divider( 104 | height: 5, 105 | color: Colors.grey, 106 | ); 107 | }, 108 | itemCount: keyWordSearchResult.books.length); 109 | } 110 | }), 111 | ); 112 | } 113 | 114 | @override 115 | NovelBookSearchViewModel buildViewModel(BuildContext context) { 116 | return NovelBookSearchViewModel(Provider.of(context)); 117 | } 118 | 119 | @override 120 | void loadData(BuildContext context, NovelBookSearchViewModel viewModel) { 121 | viewModel.searchTargetKeyWord(searchKeyWord); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /lib/app/novel/view/novel_book_shelf.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/app/novel/entity/entity_novel_info.dart'; 4 | import 'package:flutter_novel/app/novel/view/novel_book_reader.dart'; 5 | import 'package:flutter_novel/app/novel/view_model/view_model_novel_shelf.dart'; 6 | import 'package:flutter_novel/app/router/manager_router.dart'; 7 | import 'package:flutter_novel/base/structure/base_view.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | class NovelBookShelfView extends BaseStatelessView { 11 | @override 12 | Widget buildView(BuildContext context, NovelBookShelfViewModel viewModel) { 13 | var currentBookShelfInfo = viewModel.bookshelfInfo; 14 | 15 | if (currentBookShelfInfo?.currentBookShelf == null || 16 | currentBookShelfInfo.currentBookShelf.length == 0) { 17 | return Container( 18 | alignment: Alignment.center, 19 | child: InkWell( 20 | child: Text("没有内容,点击搜索添加"), 21 | onTap: () { 22 | APPRouter.instance.route(APPRouterRequestOption( 23 | APPRouter.ROUTER_NAME_NOVEL_SEARCH, context)); 24 | }, 25 | ), 26 | ); 27 | } else { 28 | return GridView.builder( 29 | itemCount: currentBookShelfInfo.currentBookShelf.length + 1, 30 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 31 | crossAxisCount: 3, 32 | crossAxisSpacing: 5, 33 | mainAxisSpacing: 5, 34 | childAspectRatio: 3 / 4), 35 | itemBuilder: (context, index) { 36 | if (index <= currentBookShelfInfo.currentBookShelf.length - 1) { 37 | return Container( 38 | color: Colors.white, 39 | child: InkWell( 40 | child: NovelItemWidget( 41 | currentBookShelfInfo.currentBookShelf[index]), 42 | onTap: () { 43 | var currentBookShelf = 44 | currentBookShelfInfo.currentBookShelf[index]; 45 | APPRouter.instance.route(NovelBookReaderView.buildIntent( 46 | context, 47 | currentBookShelf)); 48 | }, 49 | ), 50 | ); 51 | } else { 52 | return Container( 53 | color: Colors.white, 54 | child: IconButton( 55 | icon: Icon(Icons.add), 56 | onPressed: () { 57 | APPRouter.instance.route(APPRouterRequestOption( 58 | APPRouter.ROUTER_NAME_NOVEL_SEARCH, context)); 59 | }), 60 | ); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | @override 67 | void loadData(BuildContext context, NovelBookShelfViewModel viewModel) { 68 | viewModel?.getSavedBook(); 69 | } 70 | 71 | @override 72 | NovelBookShelfViewModel buildViewModel(BuildContext context) { 73 | return NovelBookShelfViewModel( 74 | Provider.of(context), 75 | Provider.of(context), 76 | ); 77 | } 78 | } 79 | 80 | class NovelItemWidget extends StatefulWidget { 81 | final NovelBookInfo bookInfo; 82 | 83 | NovelItemWidget(this.bookInfo); 84 | 85 | @override 86 | _NovelItemWidgetState createState() => _NovelItemWidgetState(); 87 | } 88 | 89 | class _NovelItemWidgetState extends State 90 | with AutomaticKeepAliveClientMixin { 91 | @override 92 | Widget build(BuildContext context) { 93 | super.build(context); 94 | return Container( 95 | child: Column( 96 | crossAxisAlignment: CrossAxisAlignment.center, 97 | children: [ 98 | new Flexible( 99 | flex: 1, 100 | child: CachedNetworkImage( 101 | imageUrl: widget.bookInfo.cover, 102 | fit: BoxFit.cover, 103 | fadeOutDuration: new Duration(seconds: 1), 104 | fadeInDuration: new Duration(seconds: 1), 105 | ), 106 | ), 107 | Text(widget.bookInfo.title) 108 | ], 109 | ), 110 | ); 111 | } 112 | 113 | @override 114 | bool get wantKeepAlive => true; 115 | } 116 | -------------------------------------------------------------------------------- /lib/app/novel/view/widget/novel_book_intro_book_review_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_novel/app/api/api_novel.dart'; 5 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_review.dart'; 6 | import 'package:flutter_novel/app/widget/widget_tag_view.dart'; 7 | import 'package:flutter_novel/base/util/utils_time.dart'; 8 | import 'package:flutter_vector_icons/flutter_vector_icons.dart'; 9 | 10 | class NovelIntroBookReviewView extends StatelessWidget { 11 | final NovelBookReview reviewInfo; 12 | 13 | NovelIntroBookReviewView(this.reviewInfo); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | if (reviewInfo == null || reviewInfo.reviews == null) { 18 | return Container( 19 | alignment: Alignment.center, 20 | child: Text("正在查询中……"), 21 | padding: EdgeInsets.all(20), 22 | ); 23 | } else { 24 | return Container( 25 | padding: EdgeInsets.all(20), 26 | color: Colors.white, 27 | child: Column( 28 | children: [ 29 | Row( 30 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 31 | children: [ 32 | Text( 33 | "热门书评", 34 | style: TextStyle(fontSize: 16), 35 | ), 36 | InkWell( 37 | child: Row(children: [ 38 | Icon(Icons.edit, color: Colors.green, size: 15), 39 | Text('写书评', 40 | style: 41 | TextStyle(fontSize: 14, color: Colors.green)) 42 | ], mainAxisSize: MainAxisSize.min), 43 | onTap: () {}) 44 | ], 45 | ), 46 | ListView.separated( 47 | padding: EdgeInsets.only(top: 5), 48 | physics: NeverScrollableScrollPhysics(), 49 | shrinkWrap: true, 50 | primary: false, 51 | itemBuilder: (_, index) => 52 | _ItemReview(review: reviewInfo?.reviews[index]), 53 | separatorBuilder: (_, index) => Divider( 54 | height: 20, 55 | indent: 0.0, 56 | color: Colors.grey, 57 | ), 58 | itemCount: reviewInfo?.reviews?.length ?? 0), 59 | InkWell( 60 | child: Container( 61 | padding: EdgeInsets.fromLTRB(0, 5, 0, 5), 62 | child: Text('全部书评', 63 | style: 64 | TextStyle(color: Colors.green, fontSize: 14)), 65 | alignment: Alignment.center), 66 | onTap: (reviewInfo?.reviews?.length ?? 0) == 0 ? null : () {}) 67 | ], 68 | ), 69 | ); 70 | } 71 | } 72 | } 73 | 74 | class _ItemReview extends StatelessWidget { 75 | final Reviews review; 76 | 77 | _ItemReview({Key key, @required this.review}) : super(key: key); 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return Material( 82 | color: Colors.white, 83 | child: InkWell( 84 | child: Container( 85 | padding: EdgeInsets.symmetric(horizontal: 0, vertical: 5), 86 | child: Column( 87 | crossAxisAlignment: CrossAxisAlignment.start, 88 | children: [ 89 | /// 作者 90 | Row(children: [ 91 | ClipOval( 92 | child: CachedNetworkImage( 93 | width: 25, 94 | height: 25, 95 | imageUrl: NovelApi.READER_IMAGE_URL + 96 | review?.author?.avatar, 97 | fit: BoxFit.cover, 98 | // errorWidget: (context, url, error) => 99 | // Image.asset("img/loading_4.png"), 100 | fadeOutDuration: new Duration(seconds: 1), 101 | fadeInDuration: new Duration(seconds: 1), 102 | ), 103 | ), 104 | SizedBox( 105 | width: 5, 106 | ), 107 | Text('${review?.author?.nickname}', 108 | style: TextStyle(fontSize: 14, color: Colors.grey)), 109 | SizedBox( 110 | width: 5, 111 | ), 112 | TagView( 113 | tag: 'Lv${review?.author?.lv}', 114 | textColor: 115 | review.author.lv > 5 ? Colors.blueAccent : null, 116 | borderColor: 117 | review.author.lv > 5 ? Colors.blueAccent : null) 118 | ]), 119 | SizedBox( 120 | height: 5, 121 | ), 122 | 123 | // /// 评分 124 | // Row(children: [ 125 | // SmoothStarRating( 126 | // rating: commentInfo?.rating?.toDouble(), 127 | // size: 15, 128 | // allowHalfRating: false, 129 | // color: starColor, 130 | // borderColor: Colors.grey), 131 | // Text('${commentInfo?.ratingDesc}', style: TextStyles.textGrey12) 132 | // ]), 133 | // SizedBox(height: 5,), 134 | 135 | /// content 136 | Text('${review?.content}', 137 | style: TextStyle(fontSize: 14, color: Colors.grey[850]), 138 | maxLines: 3, 139 | overflow: TextOverflow.ellipsis), 140 | SizedBox( 141 | height: 5, 142 | ), 143 | 144 | /// 时间/回复/赞 145 | Row(children: [ 146 | Text('${TimeUtils.friendlyDateTime(review?.created)}', 147 | style: TextStyle(fontSize: 12, color: Colors.grey)), 148 | Row(mainAxisSize: MainAxisSize.min, children: [ 149 | Icon(Feather.thumbs_up, size: 15, color: Colors.grey), 150 | SizedBox( 151 | height: 5, 152 | ), 153 | Text('${review?.likeCount}', 154 | style: TextStyle(fontSize: 12, color: Colors.grey)) 155 | ]) 156 | ], mainAxisAlignment: MainAxisAlignment.spaceBetween) 157 | ], 158 | ), 159 | ), 160 | onTap: () {}), 161 | ); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /lib/app/novel/view/widget/novel_book_intro_bottom_menu_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/entity/entity_novel_detail.dart'; 3 | import 'package:flutter_novel/app/novel/entity/entity_novel_info.dart'; 4 | import 'package:flutter_novel/app/novel/view/novel_book_reader.dart'; 5 | import 'package:flutter_novel/app/novel/view_model/view_model_novel_shelf.dart'; 6 | import 'package:flutter_novel/app/router/manager_router.dart'; 7 | import 'package:flutter_novel/base/structure/base_view.dart'; 8 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 9 | import 'package:flutter_novel/base/util/utils_toast.dart'; 10 | import 'package:provider/provider.dart'; 11 | 12 | class NovelIntroBottomMenuView 13 | extends BaseStatefulView { 14 | final NovelDetailInfo bookInfo; 15 | 16 | NovelIntroBottomMenuView(this.bookInfo); 17 | 18 | @override 19 | BaseStatefulViewState, NovelBookShelfViewModel> buildState() { 20 | return NovelIntroBottomMenuViewState(); 21 | } 22 | } 23 | 24 | class NovelIntroBottomMenuViewState extends BaseStatefulViewState{ 25 | @override 26 | Widget buildView(BuildContext context, NovelBookShelfViewModel viewModel) { 27 | if (widget.bookInfo == null) { 28 | return Container(); 29 | } else { 30 | List currentBookShelf = 31 | viewModel.bookshelfInfo.currentBookShelf; 32 | 33 | NovelBookInfo currentBookInfo = NovelBookInfo() 34 | ..bookId = widget.bookInfo.id 35 | ..cover = widget.bookInfo.cover 36 | ..title = widget.bookInfo.title; 37 | bool isBookShelfBook = false; 38 | 39 | for (NovelBookInfo info in currentBookShelf) { 40 | if (widget.bookInfo.id == info.bookId) { 41 | currentBookInfo = info; 42 | isBookShelfBook = true; 43 | break; 44 | } 45 | } 46 | 47 | return Container( 48 | child: Column( 49 | children: [ 50 | Divider( 51 | height: 1, 52 | color: Colors.grey[350], 53 | ), 54 | Row( 55 | children: [ 56 | Expanded( 57 | child: InkWell( 58 | onTap: () { 59 | 60 | if(!isBookShelfBook) { 61 | viewModel.addBookToShelf(NovelBookInfo() 62 | ..bookId = widget.bookInfo.id 63 | ..title = widget.bookInfo.title 64 | ..cover = Uri.decodeComponent( 65 | widget.bookInfo.cover 66 | .split("/agent/") 67 | .last)); 68 | }else{ 69 | viewModel.removeBookFromShelf(widget.bookInfo.id); 70 | } 71 | 72 | }, 73 | child: Container( 74 | color: Colors.white, 75 | padding: EdgeInsets.all(15), 76 | child: Row( 77 | mainAxisAlignment: MainAxisAlignment.center, 78 | crossAxisAlignment: CrossAxisAlignment.center, 79 | children: [ 80 | Icon(isBookShelfBook ? Icons.remove : Icons.add), 81 | SizedBox( 82 | width: 5, 83 | ), 84 | Text(isBookShelfBook ? "不追了" : "追书") 85 | ], 86 | ), 87 | ), 88 | ), 89 | ), 90 | Expanded( 91 | child: InkWell( 92 | onTap: () { 93 | APPRouter.instance.route( 94 | NovelBookReaderView.buildIntent( 95 | context, 96 | currentBookInfo)); 97 | }, 98 | child: Container( 99 | color: Colors.green, 100 | child: Container( 101 | padding: EdgeInsets.all(15), 102 | child: Row( 103 | mainAxisAlignment: MainAxisAlignment.center, 104 | crossAxisAlignment: CrossAxisAlignment.center, 105 | children: [ 106 | Text(isBookShelfBook ? "继续阅读" : "开始阅读"), 107 | ], 108 | )), 109 | ), 110 | ), 111 | ), 112 | Expanded( 113 | child: InkWell( 114 | onTap: () { 115 | ToastUtils.showToast("叮~迅雷手机下载助手提示您,作者摸鱼了……虽然这玩意自动缓存"); 116 | }, 117 | child: Container( 118 | color: Colors.white, 119 | padding: EdgeInsets.all(15), 120 | child: Row( 121 | mainAxisAlignment: MainAxisAlignment.center, 122 | crossAxisAlignment: CrossAxisAlignment.center, 123 | children: [ 124 | Icon(Icons.file_download), 125 | SizedBox( 126 | width: 5, 127 | ), 128 | Text("下载") 129 | ], 130 | )), 131 | ), 132 | ) 133 | ], 134 | ) 135 | ], 136 | ), 137 | ); 138 | } 139 | } 140 | 141 | @override 142 | NovelBookShelfViewModel buildViewModel(BuildContext context) { 143 | return NovelBookShelfViewModel( 144 | Provider.of(context), 145 | Provider.of(context), 146 | ); 147 | } 148 | 149 | @override 150 | void loadData(BuildContext context, NovelBookShelfViewModel viewModel) {} 151 | 152 | @override 153 | void initData() { 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/app/novel/view/widget/novel_book_intro_copyright_notice_view.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lwlizhe/flutter_novel/4d16a9d6d0d7c7753578b997b22e0de52f6c1061/lib/app/novel/view/widget/novel_book_intro_copyright_notice_view.dart -------------------------------------------------------------------------------- /lib/app/novel/view/widget/novel_book_intro_header_tag_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/entity/entity_novel_detail.dart'; 3 | import 'package:flutter_novel/app/widget/widget_expand_text_view.dart'; 4 | import 'package:flutter_novel/app/widget/widget_tag_view.dart'; 5 | import 'package:flutter_novel/base/util/utils_color.dart'; 6 | import 'package:flutter_novel/base/util/utils_time.dart'; 7 | 8 | class NovelBookIntroHeaderTagView extends StatelessWidget { 9 | final NovelDetailInfo detailInfo; 10 | 11 | NovelBookIntroHeaderTagView(this.detailInfo); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Container( 16 | color: Colors.white, 17 | width: double.infinity, 18 | padding: EdgeInsets.all(15), 19 | child: Column(children: [ 20 | Text('简介', style: TextStyle(fontSize: 22, color: Colors.grey[900])), 21 | SizedBox( 22 | height: 5, 23 | ), 24 | 25 | /// 标签 26 | Wrap( 27 | children: tags(detailInfo), 28 | spacing: 5, 29 | runSpacing: 3, 30 | ), 31 | SizedBox( 32 | height: 5, 33 | ), 34 | ExpandText( 35 | '${detailInfo?.longIntro}', 36 | style: TextStyle(fontSize: 14, color: Colors.grey), 37 | maxLength: 2, 38 | ), 39 | SizedBox( 40 | height: 5, 41 | ), 42 | Row( 43 | children: [ 44 | Expanded( 45 | child: Text( 46 | "目录", 47 | style: TextStyle(fontSize: 14, color: Colors.grey[850]), 48 | ), 49 | flex: 1, 50 | ), 51 | Expanded( 52 | child: Text( 53 | '[${detailInfo != null ? (detailInfo.isSerial ? "更新:${TimeUtils.friendlyDateTime(detailInfo?.updated)}" : "完结") : ""}]\t${detailInfo?.lastChapter}', 54 | maxLines: 1, 55 | overflow: TextOverflow.ellipsis, 56 | ), 57 | flex: 4, 58 | ), 59 | ], 60 | ), 61 | ], crossAxisAlignment: CrossAxisAlignment.start)); 62 | } 63 | 64 | List tags(NovelDetailInfo detailInfo) { 65 | List widgets = []; 66 | if (detailInfo != null) { 67 | detailInfo.tags.forEach( 68 | (tag) => widgets.add( 69 | TagView( 70 | tag: tag, 71 | bgColor: ColorUtils.strToColor(tag), 72 | padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), 73 | textColor: Colors.white, 74 | ), 75 | ), 76 | ); 77 | } 78 | 79 | return widgets; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/app/novel/view/widget/novel_book_intro_recommend_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_recommend.dart'; 4 | 5 | class NovelIntroBookRecommendView extends StatelessWidget { 6 | final NovelBookRecommend recommendInfo; 7 | 8 | NovelIntroBookRecommendView(this.recommendInfo); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return interestedView(context); 13 | } 14 | 15 | Widget interestedView(BuildContext context) { 16 | Widget result = Container( 17 | color: Colors.white, 18 | alignment: Alignment.center, 19 | child: Text("正在查询中……"), 20 | padding: EdgeInsets.all(20), 21 | ); 22 | 23 | if (recommendInfo != null) { 24 | result = Container( 25 | padding: EdgeInsets.all(20), 26 | color: Colors.white, 27 | child: Column( 28 | children: [ 29 | Row(children: [ 30 | Text('你可能感兴趣的', style: TextStyle(fontSize: 16)), 31 | InkWell( 32 | child: Text('更多', 33 | style: TextStyle(fontSize: 14, color: Colors.green)), 34 | ) 35 | ], mainAxisAlignment: MainAxisAlignment.spaceBetween), 36 | GridView.builder( 37 | padding: EdgeInsets.all(0), 38 | physics: NeverScrollableScrollPhysics(), 39 | shrinkWrap: true, 40 | primary: false, 41 | gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 42 | childAspectRatio: 9 / 16, 43 | crossAxisCount: 4, 44 | crossAxisSpacing: 5), 45 | itemBuilder: (_, index) => _ItemPictureBook( 46 | book: recommendInfo.books[index], 47 | ), 48 | itemCount: 4) 49 | ], 50 | ), 51 | ); 52 | } 53 | 54 | return result; 55 | } 56 | } 57 | 58 | class _ItemPictureBook extends StatelessWidget { 59 | final Books book; 60 | final VoidCallback onPressed; 61 | 62 | _ItemPictureBook({Key key, @required this.book, this.onPressed}) 63 | : super(key: key); 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return Material( 68 | color: Colors.white, 69 | child: InkWell( 70 | child: Container( 71 | child: Column( 72 | children: [ 73 | CachedNetworkImage( 74 | imageUrl: 75 | Uri.decodeComponent(book.cover.split("/agent/").last)), 76 | Text('${book?.title}', 77 | style: TextStyle(fontSize: 14), 78 | maxLines: 1, 79 | overflow: TextOverflow.ellipsis) 80 | ], 81 | crossAxisAlignment: CrossAxisAlignment.center, 82 | mainAxisAlignment: MainAxisAlignment.center, 83 | ), 84 | ), 85 | onTap: onPressed), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/app/novel/view_model/view_model_novel_intro.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/model/zssq/model_book_net.dart'; 3 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 4 | 5 | class NovelBookIntroViewModel extends BaseViewModel { 6 | NovelBookNetModel _netBookModel; 7 | 8 | NovelBookIntroContentEntity get contentEntity =>_netBookModel.bookIntroContentEntity; 9 | 10 | NovelBookIntroViewModel(this._netBookModel); 11 | 12 | void getNovelInfo(String bookId) { 13 | getDetailInfo(bookId); 14 | getNovelShortReview(bookId); 15 | getNovelBookReview(bookId); 16 | getNovelBookRecommend(bookId); 17 | } 18 | 19 | void getDetailInfo(String bookId) async { 20 | var result = await _netBookModel.getNovelDetailInfo(bookId); 21 | if (result.isSuccess && result?.data != null) { 22 | contentEntity.detailInfo = result.data; 23 | notifyListeners(); 24 | } 25 | } 26 | 27 | void getNovelShortReview(String bookId) async { 28 | var result = await _netBookModel.getNovelShortReview(bookId,limit: 2); 29 | if (result.isSuccess && result?.data != null) { 30 | contentEntity.shortComment = result.data; 31 | notifyListeners(); 32 | } 33 | } 34 | 35 | void getNovelBookReview(String bookId) async { 36 | var result = await _netBookModel.getNovelBookReview(bookId,limit: 2); 37 | if (result.isSuccess && result?.data != null) { 38 | contentEntity.bookReviewInfo = result.data; 39 | notifyListeners(); 40 | } 41 | } 42 | 43 | void getNovelBookRecommend(String bookId) async { 44 | var result = await _netBookModel.getNovelBookRecommend(bookId); 45 | if (result.isSuccess && result?.data != null) { 46 | contentEntity.bookRecommendInfo = result.data; 47 | notifyListeners(); 48 | } 49 | } 50 | 51 | @override 52 | Widget getProviderContainer() { 53 | return null; 54 | } 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /lib/app/novel/view_model/view_model_novel_search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/model/zssq/model_book_net.dart'; 3 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 4 | 5 | class NovelBookSearchViewModel extends BaseViewModel { 6 | final NovelBookNetModel _netBookModel; 7 | 8 | SearchContentEntity contentEntity = SearchContentEntity(); 9 | 10 | NovelBookSearchViewModel(this._netBookModel); 11 | 12 | void getSearchWord(String keyWord) async { 13 | var result=await _netBookModel.getSearchWord(keyWord); 14 | if(result.isSuccess&&result?.data!=null&&result.data.length>0) { 15 | contentEntity.autoCompleteSearchWord.clear(); 16 | contentEntity.autoCompleteSearchWord.addAll(result.data); 17 | notifyListeners(); 18 | } 19 | } 20 | void getHotSearchWord() async { 21 | var result=await _netBookModel.getHotSearchWord(); 22 | if(result.isSuccess&&result?.data!=null&&result.data.length>0) { 23 | contentEntity.searchHotWord.clear(); 24 | contentEntity.searchHotWord.addAll(result.data); 25 | notifyListeners(); 26 | } 27 | } 28 | 29 | void searchTargetKeyWord(String keyword) async { 30 | var searchResult= await _netBookModel.searchTargetKeyWord(keyword); 31 | if(searchResult.isSuccess&&searchResult?.data!=null) { 32 | contentEntity.keyWordSearchResult=searchResult.data; 33 | notifyListeners(); 34 | } 35 | } 36 | 37 | @override 38 | Widget getProviderContainer() { 39 | return null; 40 | } 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /lib/app/novel/view_model/view_model_novel_shelf.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/entity/entity_novel_info.dart'; 3 | import 'package:flutter_novel/app/novel/model/zssq/model_book_db.dart'; 4 | import 'package:flutter_novel/app/novel/model/zssq/model_book_net.dart'; 5 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 6 | 7 | class NovelBookShelfViewModel extends BaseViewModel { 8 | NovelBookDBModel _dbBookModel; 9 | NovelBookNetModel _netBookModel; 10 | 11 | NovelBookShelfInfo get bookshelfInfo =>_dbBookModel?.bookshelfInfo; 12 | 13 | NovelBookShelfViewModel(this._dbBookModel, this._netBookModel); 14 | 15 | @override 16 | Widget getProviderContainer() { 17 | return null; 18 | } 19 | 20 | void addBookToShelf(NovelBookInfo book) async{ 21 | _dbBookModel?.addBook(book); 22 | bookshelfInfo?.currentBookShelf?.add(book); 23 | notifyListeners(); 24 | } 25 | void removeBookFromShelf(String bookId) async{ 26 | _dbBookModel?.removeBook(bookId); 27 | NovelBookInfo targetBook; 28 | for(NovelBookInfo bookInfo in bookshelfInfo.currentBookShelf){ 29 | if(bookInfo.bookId==bookId){ 30 | targetBook=bookInfo; 31 | break; 32 | } 33 | } 34 | 35 | bookshelfInfo.currentBookShelf.remove(targetBook); 36 | notifyListeners(); 37 | 38 | } 39 | 40 | void getSavedBook() { 41 | _dbBookModel?.getSavedBook()?.then((data){ 42 | bookshelfInfo.currentBookShelf=data; 43 | if(!isDisposed&&hasListeners) { 44 | notifyListeners(); 45 | } 46 | }); 47 | 48 | // Future.delayed(Duration(seconds: 5)).then((data){ 49 | // bookshelfInfo.currentBookShelf= [ 50 | // NovelBookInfo() 51 | // ..cover = 52 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 53 | // ..title = "剑来", 54 | // NovelBookInfo() 55 | // ..cover = 56 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 57 | // ..title = "剑来", 58 | // NovelBookInfo() 59 | // ..cover = 60 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 61 | // ..title = "剑来", 62 | // NovelBookInfo() 63 | // ..cover = 64 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 65 | // ..title = "剑来", 66 | // NovelBookInfo() 67 | // ..cover = 68 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 69 | // ..title = "剑来", 70 | // NovelBookInfo() 71 | // ..cover = 72 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 73 | // ..title = "剑来", 74 | // NovelBookInfo() 75 | // ..cover = 76 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 77 | // ..title = "剑来", 78 | // NovelBookInfo() 79 | // ..cover = 80 | // "http://img.1391.com/api/v1/bookcenter/cover/1/2014980/2014980_713632b9b37d405f84865792cdae14f3.jpg/" 81 | // ..title = "剑来", 82 | // ]; 83 | // if(!isDisposed&&hasListeners) { 84 | // notifyListeners(); 85 | // } 86 | // }); 87 | } 88 | 89 | void getBookIntroduction() { 90 | _netBookModel?.getBookIntroduction(); 91 | } 92 | } 93 | 94 | 95 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/cache/novel_config_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/manager_reader_page.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | class NovelConfigManager { 7 | static const String KEY_CONFIG_BRIGHTNESS = "key_config_brightness"; 8 | static const String KEY_CONFIG_FONT_SIZE = "key_config_font_size"; 9 | static const String KEY_CONFIG_LINE_HEIGHT = "key_config_line_height"; 10 | static const String KEY_CONFIG_PARAGRAPH_SPACING = "key_config_paragraph_spacing"; 11 | static const String KEY_CONFIG_ANIMATION_MODE = "key_config_animation_mode"; 12 | static const String KEY_CONFIG_BG_COLOR = "key_config_bg_color"; 13 | static const String KEY_CONFIG_LAST_READ_INFO = "key_config_last_read_info"; 14 | 15 | static const double VALUE_DEFAULT_CONFIG_BRIGHTNESS = 0.2; 16 | 17 | static const int VALUE_DEFAULT_FONT_SIZE = 20; 18 | static const int VALUE_DEFAULT_LINE_HEIGHT = 30; 19 | static const int VALUE_DEFAULT_PARAGRAPH_SPACING = 10; 20 | 21 | static NovelConfigManager _instance; 22 | 23 | double brightness; 24 | int fontSize; 25 | int lineHeight; 26 | int paragraphSpacing; 27 | int animationMode; 28 | Color bgColor; 29 | 30 | factory NovelConfigManager() { 31 | if (_instance == null) { 32 | _instance = new NovelConfigManager._(); 33 | } 34 | return _instance; 35 | } 36 | 37 | NovelConfigManager._() { 38 | getUserBrightnessConfig().then((value) { 39 | brightness = value; 40 | }); 41 | getUserFontSizeConfig().then((value) { 42 | fontSize = value; 43 | }); 44 | getUserConfigAnimationMode().then((value) { 45 | animationMode = value; 46 | }); 47 | } 48 | 49 | Future getUserBrightnessConfig() async { 50 | if (brightness == null) { 51 | SharedPreferences prefs = await SharedPreferences.getInstance(); 52 | brightness = prefs.getDouble(KEY_CONFIG_BRIGHTNESS); 53 | brightness ??= VALUE_DEFAULT_CONFIG_BRIGHTNESS; 54 | } 55 | return brightness; 56 | } 57 | 58 | void setUserBrightnessConfig(double data) async { 59 | SharedPreferences prefs = await SharedPreferences.getInstance(); 60 | await prefs.setDouble(KEY_CONFIG_BRIGHTNESS, data).then((value){ 61 | brightness = data; 62 | }); 63 | } 64 | 65 | Future getUserFontSizeConfig() async { 66 | if(fontSize==null) { 67 | SharedPreferences prefs = await SharedPreferences.getInstance(); 68 | fontSize = prefs.getInt(KEY_CONFIG_FONT_SIZE); 69 | fontSize ??= VALUE_DEFAULT_FONT_SIZE; 70 | } 71 | return fontSize; 72 | } 73 | 74 | void setUserFontSizeConfig(int size) async { 75 | SharedPreferences prefs = await SharedPreferences.getInstance(); 76 | await prefs.setInt(KEY_CONFIG_FONT_SIZE, size).then((value){ 77 | fontSize=size; 78 | }); 79 | } 80 | 81 | Future getUserLineHeightConfig() async { 82 | if(lineHeight==null) { 83 | SharedPreferences prefs = await SharedPreferences.getInstance(); 84 | lineHeight = prefs.getInt(KEY_CONFIG_LINE_HEIGHT); 85 | lineHeight ??= VALUE_DEFAULT_LINE_HEIGHT; 86 | } 87 | return lineHeight; 88 | } 89 | 90 | void setUserLineHeightConfig(int height) async { 91 | SharedPreferences prefs = await SharedPreferences.getInstance(); 92 | await prefs.setInt(KEY_CONFIG_LINE_HEIGHT, height).then((value){ 93 | lineHeight=height; 94 | }); 95 | } 96 | 97 | Future getUserParagraphSpacingConfig() async { 98 | if(paragraphSpacing==null) { 99 | SharedPreferences prefs = await SharedPreferences.getInstance(); 100 | paragraphSpacing = prefs.getInt(KEY_CONFIG_PARAGRAPH_SPACING); 101 | paragraphSpacing ??= VALUE_DEFAULT_PARAGRAPH_SPACING; 102 | } 103 | return paragraphSpacing; 104 | } 105 | 106 | void setUserParagraphSpacingConfig(int spacing) async { 107 | SharedPreferences prefs = await SharedPreferences.getInstance(); 108 | await prefs.setInt(KEY_CONFIG_PARAGRAPH_SPACING, spacing).then((value){ 109 | paragraphSpacing=spacing; 110 | }); 111 | } 112 | 113 | Future getUserConfigAnimationMode() async { 114 | if(animationMode==null) { 115 | SharedPreferences prefs = await SharedPreferences.getInstance(); 116 | animationMode = prefs.getInt(KEY_CONFIG_ANIMATION_MODE); 117 | animationMode??=ReaderPageManager.TYPE_ANIMATION_SIMULATION_TURN; 118 | } 119 | 120 | return animationMode; 121 | } 122 | 123 | void setUserConfigAnimationMode(int mode) async { 124 | SharedPreferences prefs = await SharedPreferences.getInstance(); 125 | await prefs.setInt(KEY_CONFIG_ANIMATION_MODE, mode).then((value){ 126 | animationMode=mode; 127 | }); 128 | } 129 | 130 | Future getUserConfigBgColor() async { 131 | if(bgColor==null) { 132 | SharedPreferences prefs = await SharedPreferences.getInstance(); 133 | int color=prefs.getInt(KEY_CONFIG_BG_COLOR); 134 | color??=0xfffff2cc; 135 | bgColor=Color(color); 136 | } 137 | 138 | return bgColor; 139 | } 140 | 141 | void setUserConfigBgColor(Color bgColor) async { 142 | SharedPreferences prefs = await SharedPreferences.getInstance(); 143 | await prefs.setInt(KEY_CONFIG_BG_COLOR, bgColor.value).then((value){ 144 | this.bgColor=bgColor; 145 | }); 146 | } 147 | 148 | Future getLastReadNovelInfoJson() async { 149 | String infoJson = ""; 150 | 151 | SharedPreferences prefs = await SharedPreferences.getInstance(); 152 | infoJson = prefs.getString(KEY_CONFIG_LAST_READ_INFO); 153 | 154 | return infoJson; 155 | } 156 | 157 | void setLastReadNovelInfoJson(String dataJson) async { 158 | SharedPreferences prefs = await SharedPreferences.getInstance(); 159 | await prefs.setString(KEY_CONFIG_LAST_READ_INFO, dataJson); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/cache/novel_content_cache_manager.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_cache_manager/flutter_cache_manager.dart'; 4 | import 'package:path/path.dart' as p; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | class NovelPersistentCacheManager extends BaseCacheManager { 8 | static const key = "libCacheNovelData"; 9 | 10 | static NovelPersistentCacheManager _instance; 11 | 12 | factory NovelPersistentCacheManager() { 13 | if (_instance == null) { 14 | _instance = new NovelPersistentCacheManager._(); 15 | } 16 | return _instance; 17 | } 18 | 19 | NovelPersistentCacheManager._() : super(key); 20 | 21 | Future getFilePath() async { 22 | var directory = await getTemporaryDirectory(); 23 | return p.join(directory.path, key); 24 | } 25 | 26 | Future getNovelPersistentCacheFile(String url, {Map headers}) async { 27 | var cacheFile = await getFileFromCache(url); 28 | if (cacheFile != null) { 29 | return cacheFile.file; 30 | } 31 | try { 32 | var download = await webHelper.downloadFile(url, authHeaders: headers); 33 | return download.file; 34 | } catch (e) { 35 | return null; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/content/helper/animation/animation_page_base.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/manager_reader_page.dart'; 3 | import 'package:flutter_novel/app/novel/view_model/view_model_novel_reader.dart'; 4 | import 'package:flutter_novel/base/util/utils_screen.dart'; 5 | 6 | abstract class BaseAnimationPage{ 7 | 8 | Offset mTouch=Offset(0,0); 9 | 10 | AnimationController mAnimationController; 11 | 12 | Size currentSize=Size(ScreenUtils.getScreenWidth(),ScreenUtils.getScreenHeight()); 13 | 14 | // @protected 15 | // ReaderContentViewModel contentModel=ReaderContentViewModel.instance; 16 | 17 | NovelReaderViewModel readerViewModel; 18 | 19 | // void setData(ReaderChapterPageContentConfig prePageConfig,ReaderChapterPageContentConfig currentPageConfig,ReaderChapterPageContentConfig nextPageConfig){ 20 | // currentPageContentConfig=pageConfig; 21 | // } 22 | 23 | void setSize(Size size){ 24 | currentSize=size; 25 | // mTouch=Offset(currentSize.width, currentSize.height); 26 | } 27 | void setContentViewModel(NovelReaderViewModel viewModel){ 28 | readerViewModel=viewModel; 29 | // mTouch=Offset(currentSize.width, currentSize.height); 30 | } 31 | 32 | void onDraw(Canvas canvas); 33 | void onTouchEvent(TouchEvent event); 34 | void setAnimationController(AnimationController controller){ 35 | mAnimationController=controller; 36 | } 37 | 38 | bool isShouldAnimatingInterrupt(){ 39 | return false; 40 | } 41 | 42 | bool isCanGoNext(){ 43 | return readerViewModel.isCanGoNext(); 44 | } 45 | bool isCanGoPre(){ 46 | return readerViewModel.isCanGoPre(); 47 | } 48 | 49 | bool isCancelArea(); 50 | bool isConfirmArea(); 51 | 52 | Animation getCancelAnimation(AnimationController controller,GlobalKey canvasKey); 53 | Animation getConfirmAnimation(AnimationController controller,GlobalKey canvasKey); 54 | Simulation getFlingAnimationSimulation(AnimationController controller,DragEndDetails details); 55 | 56 | } 57 | 58 | enum ANIMATION_TYPE { TYPE_CONFIRM, TYPE_CANCEL,TYPE_FILING } 59 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/content/helper/animation/animation_page_slide.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/animation/animation_page_base.dart'; 4 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/manager_reader_page.dart'; 5 | import 'package:flutter_novel/app/novel/view_model/view_model_novel_reader.dart'; 6 | 7 | /// 滑动动画 /// 8 | /// ps 正在研究怎么加上惯性 (ScrollPhysics:可滑动组件的滑动控制器,android 对应:ClampingScrollPhysics,ScrollController呢?)/// 9 | /// AnimationController 有fling动画,不过需要传入滑动距离 10 | /// ScrollPhysics 提供了滑动信息,createBallisticSimulation 方法需要传入一个position(初始化的时候创建) 和 velocity(手势监听的DragEndDetails中有速度) 11 | /// 实在不行直接用小部件实现? 12 | /// 13 | /// 结论:自己算个毛,交给模拟器实现去…… 14 | class SlidePageAnimation extends BaseAnimationPage { 15 | ClampingScrollPhysics physics; 16 | 17 | Offset mStartPoint = Offset(0, 0); 18 | double mStartDy = 0; 19 | double currentMoveDy = 0; 20 | 21 | /// 滑动偏移量 22 | double dy = 0; 23 | 24 | /// 上次滑动的index 25 | int lastIndex = 0; 26 | 27 | /// 翻到下一页 28 | bool isTurnToNext = true; 29 | 30 | AnimationController _currentAnimationController; 31 | 32 | Tween currentAnimationTween; 33 | Animation currentAnimation; 34 | 35 | SlidePageAnimation() : super() { 36 | physics = ClampingScrollPhysics(); 37 | } 38 | 39 | @override 40 | void setContentViewModel(NovelReaderViewModel viewModel) { 41 | super.setContentViewModel(viewModel); 42 | viewModel.registerContentOperateCallback((operate){ 43 | mStartPoint = Offset(0, 0); 44 | mStartDy = 0; 45 | dy = 0; 46 | lastIndex = 0; 47 | currentMoveDy = 0; 48 | }); 49 | } 50 | 51 | @override 52 | Animation getCancelAnimation( 53 | AnimationController controller, GlobalKey canvasKey) { 54 | return null; 55 | } 56 | 57 | @override 58 | Animation getConfirmAnimation( 59 | AnimationController controller, GlobalKey canvasKey) { 60 | return null; 61 | } 62 | 63 | @override 64 | Simulation getFlingAnimationSimulation( 65 | AnimationController controller, DragEndDetails details) { 66 | ClampingScrollSimulation simulation; 67 | simulation = ClampingScrollSimulation( 68 | position: mTouch.dy, 69 | velocity: details.velocity.pixelsPerSecond.dy, 70 | tolerance: Tolerance.defaultTolerance, 71 | ); 72 | _currentAnimationController = controller; 73 | return simulation; 74 | } 75 | 76 | @override 77 | void onDraw(Canvas canvas) { 78 | drawBottomPage(canvas); 79 | } 80 | 81 | @override 82 | void onTouchEvent(TouchEvent event) { 83 | if (event.touchPos == null) { 84 | return; 85 | } 86 | 87 | switch (event.action) { 88 | case TouchEvent.ACTION_DOWN: 89 | if (!dy.isNaN && !dy.isInfinite) { 90 | mStartPoint = event.touchPos; 91 | mStartDy = currentMoveDy; 92 | dy = 0; 93 | } 94 | 95 | break; 96 | case TouchEvent.ACTION_MOVE: 97 | if (!mTouch.dy.isInfinite && !mStartPoint.dy.isInfinite) { 98 | double tempDy = event.touchPos.dy - mStartPoint.dy; 99 | if (!currentSize.height.isInfinite && 100 | currentSize.height != 0 && 101 | currentSize.height != null && 102 | !dy.isInfinite && 103 | !currentMoveDy.isInfinite) { 104 | int currentIndex = (tempDy + mStartDy) ~/ currentSize.height; 105 | 106 | if (lastIndex != currentIndex) { 107 | if (currentIndex < lastIndex) { 108 | if (isCanGoNext()) { 109 | readerViewModel.nextPage(); 110 | } else { 111 | return; 112 | } 113 | } else if (currentIndex + 1 > lastIndex) { 114 | if (isCanGoPre()) { 115 | readerViewModel.prePage(); 116 | } else { 117 | return; 118 | } 119 | } 120 | } 121 | 122 | mTouch = event.touchPos; 123 | dy = mTouch.dy - mStartPoint.dy; 124 | isTurnToNext = mTouch.dy - mStartPoint.dy < 0; 125 | lastIndex = currentIndex; 126 | if (!dy.isInfinite && !currentMoveDy.isInfinite) { 127 | currentMoveDy = mStartDy + dy; 128 | } 129 | } 130 | } 131 | break; 132 | case TouchEvent.ACTION_UP: 133 | case TouchEvent.ACTION_CANCEL: 134 | break; 135 | default: 136 | break; 137 | } 138 | } 139 | 140 | @override 141 | bool isShouldAnimatingInterrupt() { 142 | return true; 143 | } 144 | 145 | void drawBottomPage(Canvas canvas) { 146 | double actualOffset = currentMoveDy < 0 147 | ? -((currentMoveDy).abs() % currentSize.height) 148 | : (currentMoveDy) % currentSize.height; 149 | 150 | canvas.save(); 151 | if (actualOffset < 0) { 152 | if (readerViewModel.getNextPage()?.pagePicture != null) { 153 | canvas.translate(0, actualOffset + currentSize.height); 154 | canvas.drawPicture(readerViewModel.getNextPage().pagePicture); 155 | } else { 156 | if (!isCanGoNext()) { 157 | dy = 0; 158 | actualOffset = 0; 159 | currentMoveDy = 0; 160 | 161 | if (_currentAnimationController != null && 162 | !_currentAnimationController.isCompleted) { 163 | _currentAnimationController.stop(); 164 | } 165 | } 166 | } 167 | } else if (actualOffset > 0) { 168 | if (readerViewModel.getPrePage()?.pagePicture != null) { 169 | canvas.translate(0, actualOffset - currentSize.height); 170 | canvas.drawPicture(readerViewModel.getPrePage().pagePicture); 171 | } else { 172 | if (!isCanGoPre()) { 173 | dy = 0; 174 | lastIndex = 0; 175 | actualOffset = 0; 176 | currentMoveDy = 0; 177 | 178 | if (_currentAnimationController != null && 179 | !_currentAnimationController.isCompleted) { 180 | _currentAnimationController.stop(); 181 | } 182 | } 183 | } 184 | } 185 | 186 | canvas.restore(); 187 | canvas.save(); 188 | 189 | if (readerViewModel.getCurrentPage().pagePicture != null) { 190 | canvas.translate(0, actualOffset); 191 | canvas.drawPicture(readerViewModel.getCurrentPage().pagePicture); 192 | } 193 | 194 | canvas.restore(); 195 | } 196 | 197 | void drawStatic(Canvas canvas) {} 198 | 199 | @override 200 | bool isCancelArea() { 201 | return null; 202 | } 203 | 204 | @override 205 | bool isConfirmArea() { 206 | return null; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/content/helper/animation/controller_animation_with_listener_number.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/scheduler.dart'; 4 | 5 | class AnimationControllerWithListenerNumber extends AnimationController { 6 | final ObserverList statusListeners = 7 | ObserverList(); 8 | 9 | /// Creates an animation controller. 10 | /// 11 | /// * `value` is the initial value of the animation. If defaults to the lower 12 | /// bound. 13 | /// 14 | /// * [duration] is the length of time this animation should last. 15 | /// 16 | /// * [debugLabel] is a string to help identify this animation during 17 | /// debugging (used by [toString]). 18 | /// 19 | /// * [lowerBound] is the smallest value this animation can obtain and the 20 | /// value at which this animation is deemed to be dismissed. It cannot be 21 | /// null. 22 | /// 23 | /// * [upperBound] is the largest value this animation can obtain and the 24 | /// value at which this animation is deemed to be completed. It cannot be 25 | /// null. 26 | /// 27 | /// * `vsync` is the [TickerProvider] for the current context. It can be 28 | /// changed by calling [resync]. It is required and must not be null. See 29 | /// [TickerProvider] for advice on obtaining a ticker provider. 30 | AnimationControllerWithListenerNumber({ 31 | double value, 32 | this.duration, 33 | this.reverseDuration, 34 | this.debugLabel, 35 | this.animationBehavior = AnimationBehavior.normal, 36 | @required TickerProvider vsync, 37 | }) : super( 38 | value: value, 39 | duration: duration, 40 | reverseDuration: reverseDuration, 41 | debugLabel: debugLabel, 42 | lowerBound: 0.0, 43 | upperBound: 1.0, 44 | animationBehavior: animationBehavior, 45 | vsync: vsync); 46 | 47 | /// Creates an animation controller with no upper or lower bound for its value. 48 | /// 49 | /// * [value] is the initial value of the animation. 50 | /// 51 | /// * [duration] is the length of time this animation should last. 52 | /// 53 | /// * [debugLabel] is a string to help identify this animation during 54 | /// debugging (used by [toString]). 55 | /// 56 | /// * `vsync` is the [TickerProvider] for the current context. It can be 57 | /// changed by calling [resync]. It is required and must not be null. See 58 | /// [TickerProvider] for advice on obtaining a ticker provider. 59 | /// 60 | /// This constructor is most useful for animations that will be driven using a 61 | /// physics simulation, especially when the physics simulation has no 62 | /// pre-determined bounds. 63 | AnimationControllerWithListenerNumber.unbounded({ 64 | double value = 0.0, 65 | this.duration, 66 | this.reverseDuration, 67 | this.debugLabel, 68 | @required TickerProvider vsync, 69 | this.animationBehavior = AnimationBehavior.preserve, 70 | }) : super.unbounded( 71 | value: value, 72 | duration: duration, 73 | reverseDuration: reverseDuration, 74 | debugLabel: debugLabel, 75 | animationBehavior: animationBehavior, 76 | vsync: vsync); 77 | 78 | 79 | /// A label that is used in the [toString] output. Intended to aid with 80 | /// identifying animation controller instances in debug output. 81 | final String debugLabel; 82 | 83 | /// The behavior of the controller when [AccessibilityFeatures.disableAnimations] 84 | /// is true. 85 | /// 86 | /// Defaults to [AnimationBehavior.normal] for the [new AnimationController] 87 | /// constructor, and [AnimationBehavior.preserve] for the 88 | /// [new AnimationController.unbounded] constructor. 89 | final AnimationBehavior animationBehavior; 90 | 91 | /// Returns an [Animation] for this animation controller, so that a 92 | /// pointer to this object can be passed around without allowing users of that 93 | /// pointer to mutate the [AnimationController] state. 94 | Animation get view => this; 95 | 96 | /// The length of time this animation should last. 97 | /// 98 | /// If [reverseDuration] is specified, then [duration] is only used when going 99 | /// [forward]. Otherwise, it specifies the duration going in both directions. 100 | Duration duration; 101 | 102 | /// The length of time this animation should last when going in [reverse]. 103 | /// 104 | /// The value of [duration] us used if [reverseDuration] is not specified or 105 | /// set to null. 106 | Duration reverseDuration; 107 | 108 | 109 | @override 110 | void addStatusListener(listener) { 111 | statusListeners.add(listener); 112 | super.addStatusListener(listener); 113 | } 114 | 115 | @override 116 | void removeStatusListener(AnimationStatusListener listener) { 117 | statusListeners.remove(listener); 118 | super.removeStatusListener(listener); 119 | } 120 | 121 | bool isListenerEmpty() { 122 | return statusListeners.isEmpty; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/content/helper/helper_reader_animation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ReaderAnimationHelper{ 4 | 5 | Offset currentTouchOffset; 6 | 7 | BasePageAnimation animation; 8 | 9 | ReaderAnimationHelper(this.currentTouchOffset,BasePageAnimation animation); 10 | 11 | Canvas draw(Canvas canvas){ 12 | return canvas; 13 | } 14 | 15 | } 16 | 17 | abstract class BasePageAnimation{ 18 | 19 | void draw(Canvas canvas); 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/content/widget_reader_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/manager_reader_page.dart'; 5 | 6 | class NovelPagePainter extends CustomPainter { 7 | ReaderPageManager pageManager; 8 | TouchEvent currentTouchData; 9 | int currentPageIndex; 10 | int currentChapterId; 11 | 12 | NovelPagePainter({this.pageManager}); 13 | 14 | void setCurrentTouchEvent(TouchEvent event) { 15 | currentTouchData = event; 16 | pageManager.setCurrentTouchEvent(currentTouchData); 17 | } 18 | 19 | @override 20 | void paint(Canvas canvas, Size size) { 21 | 22 | ///-------------------background----------------/// 23 | // var _bgPaint = Paint() 24 | // ..isAntiAlias = true 25 | // ..style = PaintingStyle.fill //填充 26 | // ..color = Color(0xfffff2cc); //背景为纸黄色 27 | // canvas.drawRect(Offset.zero & size, _bgPaint); 28 | // 29 | ///-----------------animation-------------------/// 30 | 31 | if (pageManager != null) { 32 | pageManager.setPageSize(size); 33 | pageManager.onPageDraw(canvas); 34 | } 35 | 36 | } 37 | 38 | @override 39 | bool shouldRepaint(CustomPainter oldDelegate) { 40 | return pageManager.shouldRepaint(oldDelegate,this); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/menu/manager_menu_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/base/util/utils_screen.dart'; 4 | 5 | const Duration _bottomSheetDuration = Duration(milliseconds: 200); 6 | 7 | typedef void OnMenuItemClicked(MenuOperateEnum operateName, var operateData); 8 | 9 | enum MenuOperateEnum { 10 | OPERATE_NEXT_CHAPTER, 11 | OPERATE_PRE_CHAPTER, 12 | OPERATE_JUMP_CHAPTER, 13 | OPERATE_OPEN_CATALOG, 14 | OPERATE_TOGGLE_NIGHT_MODE, 15 | OPERATE_OPEN_SETTING, 16 | OPERATE_SETTING_FONT_SIZE, 17 | OPERATE_SETTING_LINE_HEIGHT, 18 | OPERATE_SETTING_PARAGRAPH_SPACING, 19 | OPERATE_SETTING_ANIMATION_MODE, 20 | OPERATE_SETTING_BG_COLOR, 21 | OPERATE_SELECT_CHAPTER, 22 | } 23 | 24 | class NovelMenuManager { 25 | static AnimationController createAnimationController(TickerProvider vsync) { 26 | return AnimationController( 27 | duration: _bottomSheetDuration, 28 | debugLabel: 'BottomSheet', 29 | vsync: vsync, 30 | ); 31 | } 32 | } 33 | 34 | class NovelPagePanGestureRecognizer extends PanGestureRecognizer { 35 | bool isMenuOpen; 36 | 37 | NovelPagePanGestureRecognizer(this.isMenuOpen); 38 | 39 | void setMenuOpen(bool isOpen) { 40 | isMenuOpen = isOpen; 41 | } 42 | 43 | @override 44 | String get debugDescription => "novel page pan gesture recognizer"; 45 | 46 | @override 47 | void addPointer(PointerDownEvent event) { 48 | if (!isMenuOpen) { 49 | super.addPointer(event); 50 | } 51 | } 52 | } 53 | 54 | class NovelMenuLayoutDelegate extends SingleChildLayoutDelegate { 55 | NovelMenuLayoutDelegate(this.progress, this.direction); 56 | 57 | final double progress; 58 | final MenuDirection direction; 59 | 60 | @override 61 | BoxConstraints getConstraintsForChild(BoxConstraints constraints) { 62 | return BoxConstraints( 63 | minWidth: 64 | direction == MenuDirection.DIRECTION_LEFT ? 0 : constraints.maxWidth, 65 | maxWidth: direction == MenuDirection.DIRECTION_LEFT 66 | ? ScreenUtils.getScreenWidth() / 4*3 67 | : constraints.maxWidth, 68 | minHeight: 0.0, 69 | maxHeight: direction == MenuDirection.DIRECTION_LEFT 70 | ? constraints.maxHeight 71 | : constraints.maxHeight * 9.0 / 16.0, 72 | ); 73 | } 74 | 75 | @override 76 | Offset getPositionForChild(Size size, Size childSize) { 77 | double offsetDy = 0.0; 78 | double offsetDx = 0.0; 79 | 80 | switch (direction) { 81 | case MenuDirection.DIRECTION_BOTTOM: 82 | offsetDy = size.height - childSize.height * progress; 83 | break; 84 | case MenuDirection.DIRECTION_TOP: 85 | offsetDy = -childSize.height * (1 - progress); 86 | break; 87 | case MenuDirection.DIRECTION_LEFT: 88 | offsetDx = -childSize.width * (1 - progress); 89 | break; 90 | } 91 | 92 | return Offset(offsetDx, offsetDy); 93 | } 94 | 95 | @override 96 | bool shouldRelayout(NovelMenuLayoutDelegate oldDelegate) { 97 | return progress != oldDelegate.progress; 98 | } 99 | } 100 | 101 | enum MenuDirection { 102 | DIRECTION_BOTTOM, 103 | DIRECTION_TOP, 104 | DIRECTION_LEFT, 105 | } 106 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/menu/widget_reader_catalog_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_chapter.dart'; 3 | import 'package:flutter_novel/app/novel/widget/reader/menu/manager_menu_widget.dart'; 4 | import 'package:flutter_novel/app/widget/scrollable_positioned_list/scrollable_positioned_list.dart'; 5 | 6 | class NovelCatalogMenu extends StatefulWidget { 7 | final NovelBookChapter bookChapter; 8 | final OnMenuItemClicked _menuItemClickedCallback; 9 | final int currentChapterIndex; 10 | 11 | NovelCatalogMenu(this.bookChapter, this.currentChapterIndex, 12 | this._menuItemClickedCallback, Key key) 13 | : super(key: key); 14 | 15 | @override 16 | _NovelCatalogMenuState createState() => _NovelCatalogMenuState(); 17 | } 18 | 19 | class _NovelCatalogMenuState extends State { 20 | ItemScrollController primaryISC; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | primaryISC = ItemScrollController(); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return Container( 31 | color: Colors.black, 32 | child: SafeArea( 33 | child: ScrollablePositionedList.builder( 34 | itemBuilder: (context, index) { 35 | return InkWell( 36 | onTap: () { 37 | widget._menuItemClickedCallback( 38 | MenuOperateEnum.OPERATE_SELECT_CHAPTER, index); 39 | }, 40 | child: Container( 41 | padding: EdgeInsets.all(15), 42 | child: Text( 43 | widget?.bookChapter?.chapters[index].title, 44 | style: TextStyle(fontSize: 20, height: 1.5,color: Colors.white), 45 | ), 46 | ), 47 | ); 48 | }, 49 | itemCount: widget?.bookChapter?.chapters?.length, 50 | itemScrollController: primaryISC, 51 | initialScrollIndex: widget.currentChapterIndex, 52 | ), 53 | ), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/menu/widget_reader_top_menu.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NovelTopMenu extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return Container( 7 | color: Colors.black, 8 | child: InkWell( 9 | child: Padding( 10 | padding: const EdgeInsets.all(20), 11 | child: Text( 12 | "top menu", 13 | style: TextStyle(fontSize: 14, color: Colors.grey), 14 | )), 15 | onTap: () { 16 | print("menuItem clicked"); 17 | }, 18 | ), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/model/model_reader_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter_novel/app/novel/entity/entity_novel_book_chapter.dart'; 4 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/manager_reader_page.dart'; 5 | import 'package:flutter_novel/app/novel/view_model/view_model_novel_reader.dart'; 6 | 7 | class NovelReaderConfigModel { 8 | 9 | 10 | NovelReaderViewModel viewModel; 11 | 12 | NovelBookChapter catalog; 13 | bool isMenuOpen=false; 14 | 15 | ReaderConfigEntity configEntity = ReaderConfigEntity(); 16 | 17 | NovelReaderConfigModel(this.viewModel); 18 | 19 | void clear() { 20 | viewModel = null; 21 | catalog = null; 22 | configEntity = null; 23 | isMenuOpen=false; 24 | } 25 | } 26 | 27 | class ReaderConfigEntity { 28 | /// 翻页动画类型 29 | int currentAnimationMode = ReaderPageManager.TYPE_ANIMATION_SIMULATION_TURN; 30 | 31 | /// 背景色 32 | Color currentCanvasBgColor = Color(0xfffff2cc); 33 | 34 | int currentPageIndex = 0; 35 | int currentChapterIndex = 0; 36 | String novelId; 37 | 38 | int fontSize = 20; 39 | int lineHeight = 30; 40 | int paragraphSpacing = 10; 41 | 42 | Offset pageSize; 43 | 44 | int contentPadding=10; 45 | int titleHeight=25; 46 | int bottomTipHeight=20; 47 | 48 | int titleFontSize=20; 49 | int bottomTipFontSize=20; 50 | 51 | ReaderConfigEntity( 52 | {this.currentAnimationMode, 53 | this.currentCanvasBgColor, 54 | this.currentPageIndex, 55 | this.currentChapterIndex, 56 | this.novelId, 57 | this.fontSize, 58 | this.lineHeight, 59 | this.paragraphSpacing, 60 | this.pageSize}); 61 | 62 | ReaderConfigEntity copy() { 63 | return ReaderConfigEntity( 64 | currentAnimationMode: this.currentAnimationMode, 65 | currentCanvasBgColor: this.currentCanvasBgColor, 66 | currentPageIndex: this.currentPageIndex, 67 | currentChapterIndex: this.currentChapterIndex, 68 | novelId: this.novelId, 69 | fontSize: this.fontSize, 70 | lineHeight: this.lineHeight, 71 | paragraphSpacing: this.paragraphSpacing, 72 | pageSize: this.pageSize, 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/widget/widget_novel_reader_error.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/helper_reader_content.dart'; 4 | 5 | class NovelReaderErrorPageWidget extends StatelessWidget { 6 | final ReaderContentDataValue dataValue; 7 | 8 | NovelReaderErrorPageWidget(this.dataValue); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | Widget result; 13 | 14 | switch (dataValue.contentState) { 15 | case ContentState.STATE_NORMAL: 16 | result = Container(width: 0,height: 0,); 17 | break; 18 | case ContentState.STATE_NOT_FOUND: 19 | result = Container( 20 | color: Colors.transparent, 21 | width: double.infinity, 22 | height: double.infinity, 23 | alignment: Alignment.center, 24 | child: Text("当前章节不存在\n点击屏幕调出菜单,跳转到下一章"), 25 | ); 26 | break; 27 | case ContentState.STATE_NET_ERROR: 28 | result = Container( 29 | color: Colors.transparent, 30 | width: double.infinity, 31 | height: double.infinity, 32 | alignment: Alignment.center, 33 | child: Column( 34 | mainAxisSize: MainAxisSize.min, 35 | children: [ 36 | Text("加载出错,点击重试按钮再次尝试"), 37 | MaterialButton( 38 | onPressed: () {}, 39 | child: Text("重试"), 40 | ) 41 | ], 42 | ), 43 | ); 44 | break; 45 | } 46 | 47 | return result; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/app/novel/widget/reader/widget/widget_novel_reader_loadding.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_novel/app/novel/widget/reader/content/helper/helper_reader_content.dart'; 4 | 5 | class NovelReaderLoadingPageWidget extends StatelessWidget { 6 | final ReaderContentDataValue dataValue; 7 | 8 | NovelReaderLoadingPageWidget(this.dataValue); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | Widget result; 13 | 14 | result = Container( 15 | color: Colors.transparent, 16 | width: double.infinity, 17 | height: double.infinity, 18 | alignment: Alignment.center, 19 | child: CircularProgressIndicator(), 20 | ); 21 | 22 | return result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/app/provider_setup.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_novel/app/novel/helper/helper_db.dart'; 2 | import 'package:flutter_novel/app/novel/helper/helper_sp.dart'; 3 | import 'package:flutter_novel/app/novel/model/model_novel_cache.dart'; 4 | import 'package:flutter_novel/app/novel/model/zssq/model_book_db.dart'; 5 | import 'package:flutter_novel/app/novel/model/zssq/model_book_net.dart'; 6 | import 'package:flutter_novel/base/structure/provider/config_provider.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | import 'api/api_novel.dart'; 10 | 11 | List providers = [] 12 | ..addAll(independentServices) 13 | ..addAll(dependentServices) 14 | ..addAll(uiConsumableProviders); 15 | 16 | /// 静态资源,这样也可以不用写单例了吧 17 | List independentServices = [ 18 | Provider.value(value: NovelApi()), 19 | Provider.value(value: DBHelper()), 20 | Provider.value(value: NovelBookCacheModel()), 21 | Provider.value(value: SharedPreferenceHelper()), 22 | ]; 23 | 24 | List dependentServices = [ 25 | ProxyProvider( 26 | update: (context, api, netModel) => NovelBookNetModel(api), 27 | ), 28 | ProxyProvider( 29 | update: (context, db, dbModel) => NovelBookDBModel(db), 30 | ), 31 | ConfigProvider().getProviderContainer(), 32 | ]; 33 | 34 | List uiConsumableProviders = [ 35 | ProxyProvider( 36 | update: (context, configProvider, nightModeConfig) => 37 | Provider.of(context, listen: false).nightState, 38 | ), 39 | 40 | ]; 41 | -------------------------------------------------------------------------------- /lib/app/router/manager_router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/novel/view/novel_book_intro.dart'; 3 | import 'package:flutter_novel/app/novel/view/novel_book_reader.dart'; 4 | import 'package:flutter_novel/app/novel/view/novel_book_search.dart'; 5 | import 'package:flutter_novel/app/novel/view/novel_book_search_result.dart'; 6 | import 'package:flutter_novel/app/novel/view/novel_book_leader_board.dart'; 7 | import 'package:flutter_novel/base/router/base_router_manager.dart'; 8 | 9 | class APPRouter extends BaseRouterManager { 10 | static const String ROUTER_NAME_NOVEL_INTRO = "app://novel/intro"; 11 | static const String ROUTER_NAME_NOVEL_SEARCH = "app://novel/search"; 12 | static const String ROUTER_NAME_NOVEL_SEARCH_RESULT = "app://novel/search_result"; 13 | static const String ROUTER_NAME_NOVEL_READER = "app://novel/reader"; 14 | static const String ROUTER_NAME_NOVEL_LEADER_BOARD = "app://novel/leader_board"; 15 | 16 | // 工厂模式 : 单例公开访问点 17 | factory APPRouter() => _getInstance(); 18 | 19 | static APPRouter get instance => _getInstance(); 20 | 21 | // 静态私有成员,没有初始化 22 | static APPRouter _instance; 23 | 24 | // 私有构造函数 25 | APPRouter._internal() { 26 | // 初始化 27 | } 28 | 29 | // 静态、同步、私有访问点 30 | static APPRouter _getInstance() { 31 | if (_instance == null) { 32 | _instance = new APPRouter._internal(); 33 | } 34 | return _instance; 35 | } 36 | 37 | 38 | void route(APPRouterRequestOption option) { 39 | if (option == null) { 40 | return; 41 | } 42 | 43 | switch (option.targetName) { 44 | case ROUTER_NAME_NOVEL_INTRO: 45 | jumpToTarget(option, NovelBookIntroView.getPageView(option)); 46 | break; 47 | case ROUTER_NAME_NOVEL_SEARCH: 48 | jumpToTarget(option, NovelSearchView.getPageView()); 49 | break; 50 | case ROUTER_NAME_NOVEL_SEARCH_RESULT: 51 | jumpToTarget(option, NovelSearchResultView.getPageView(option)); 52 | break; 53 | case ROUTER_NAME_NOVEL_READER: 54 | jumpToTarget(option, NovelBookReaderView.getPageView(option)); 55 | break; 56 | case ROUTER_NAME_NOVEL_LEADER_BOARD: 57 | jumpToTarget(option, NovelLeaderBoardView.getPageView(option)); 58 | break; 59 | } 60 | } 61 | } 62 | 63 | class APPRouterRequestOption extends RouterRequestOption { 64 | Map params; 65 | 66 | APPRouterRequestOption(String targetName, BuildContext context, {this.params}) 67 | : super(targetName, context); 68 | } 69 | -------------------------------------------------------------------------------- /lib/app/widget/scrollable_positioned_list/element_registry.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// A registry to track some [Element]s in the tree. 8 | class RegistryWidget extends StatefulWidget { 9 | /// Creates a [RegistryWidget]. 10 | const RegistryWidget({Key key, this.elementNotifier, this.child}) 11 | : super(key: key); 12 | 13 | /// The widget below this widget in the tree. 14 | final Widget child; 15 | 16 | /// Contains the current set of all [Element]s created by 17 | /// [RegisteredElementWidget]s in the tree below this widget. 18 | /// 19 | /// Note that if there is another [RegistryWidget] in this widget's subtree 20 | /// that registry, and not this one, will collect elements in its subtree. 21 | final ValueNotifier> elementNotifier; 22 | 23 | @override 24 | State createState() => _RegistryWidgetState(); 25 | } 26 | 27 | /// A widget whose [Element] will be added its nearest ancestor 28 | /// [RegistryWidget]. 29 | class RegisteredElementWidget extends ProxyWidget { 30 | /// Creates a [RegisteredElementWidget]. 31 | const RegisteredElementWidget({Key key, Widget child}) 32 | : super(key: key, child: child); 33 | 34 | @override 35 | Element createElement() => _RegisteredElement(this); 36 | } 37 | 38 | class _RegistryWidgetState extends State { 39 | final Set registeredElements = {}; 40 | 41 | @override 42 | Widget build(BuildContext context) => _InheritedRegistryWidget( 43 | state: this, 44 | child: widget.child, 45 | ); 46 | } 47 | 48 | class _InheritedRegistryWidget extends InheritedWidget { 49 | final _RegistryWidgetState state; 50 | 51 | const _InheritedRegistryWidget( 52 | {Key key, @required this.state, @required Widget child}) 53 | : super(key: key, child: child); 54 | 55 | @override 56 | bool updateShouldNotify(InheritedWidget oldWidget) => true; 57 | } 58 | 59 | class _RegisteredElement extends ProxyElement { 60 | _RegisteredElement(ProxyWidget widget) : super(widget); 61 | 62 | @override 63 | void notifyClients(ProxyWidget oldWidget) {} 64 | 65 | _RegistryWidgetState _registryWidgetState; 66 | 67 | @override 68 | void mount(Element parent, dynamic newSlot) { 69 | super.mount(parent, newSlot); 70 | final _InheritedRegistryWidget _inheritedRegistryWidget = 71 | inheritFromWidgetOfExactType(_InheritedRegistryWidget); 72 | _registryWidgetState = _inheritedRegistryWidget.state; 73 | _registryWidgetState.registeredElements.add(this); 74 | _registryWidgetState.widget.elementNotifier?.value = 75 | _registryWidgetState.registeredElements; 76 | } 77 | 78 | @override 79 | void didChangeDependencies() { 80 | super.didChangeDependencies(); 81 | final _InheritedRegistryWidget _inheritedRegistryWidget = 82 | inheritFromWidgetOfExactType(_InheritedRegistryWidget); 83 | _registryWidgetState = _inheritedRegistryWidget.state; 84 | _registryWidgetState.registeredElements.add(this); 85 | _registryWidgetState.widget.elementNotifier?.value = 86 | _registryWidgetState.registeredElements; 87 | } 88 | 89 | @override 90 | void unmount() { 91 | _registryWidgetState.registeredElements.remove(this); 92 | _registryWidgetState.widget.elementNotifier?.value = 93 | _registryWidgetState.registeredElements; 94 | super.unmount(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/app/widget/scrollable_positioned_list/item_positions_listener.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'item_positions_notifier.dart'; 8 | import 'scrollable_positioned_list.dart'; 9 | 10 | /// Provides a listenable iterable of [itemPositions] of items that are on 11 | /// screen and their locations. 12 | abstract class ItemPositionsListener { 13 | /// Creates an [ItemPositionsListener] that can be used by a 14 | /// [ScrollablePositionedList] to return the current position of items. 15 | factory ItemPositionsListener.create() => ItemPositionsNotifier(); 16 | 17 | /// The position of items that are at least partially visible in the viewport. 18 | ValueListenable> get itemPositions; 19 | } 20 | 21 | /// Position information for an item in the list. 22 | class ItemPosition { 23 | /// Create an [ItemPosition]. 24 | const ItemPosition( 25 | {@required this.index, 26 | @required this.itemLeadingEdge, 27 | @required this.itemTrailingEdge}); 28 | 29 | /// Index of the item. 30 | final int index; 31 | 32 | /// Distance in proportion of the viewport's main axis length from the leading 33 | /// edge of the viewport to the leading edge of the item. 34 | /// 35 | /// May be negative if the item is partially visible. 36 | final double itemLeadingEdge; 37 | 38 | /// Distance in proportion of the viewport's main axis length from the leading 39 | /// edge of the viewport to the trailing edge of the item. 40 | /// 41 | /// May be greater than one if the item is partially visible. 42 | final double itemTrailingEdge; 43 | 44 | @override 45 | bool operator ==(dynamic other) { 46 | if (other.runtimeType != runtimeType) return false; 47 | final ItemPosition otherPosition = other; 48 | return otherPosition.index == index && 49 | otherPosition.itemLeadingEdge == itemLeadingEdge && 50 | otherPosition.itemTrailingEdge == itemTrailingEdge; 51 | } 52 | 53 | @override 54 | int get hashCode => 55 | 31 * (31 * (7 + index.hashCode) + itemLeadingEdge.hashCode) + 56 | itemTrailingEdge.hashCode; 57 | 58 | @override 59 | String toString() => 60 | 'ItemPosition(index: $index, itemLeadingEdge: $itemLeadingEdge, itemTrailingEdge: $itemTrailingEdge)'; 61 | } 62 | -------------------------------------------------------------------------------- /lib/app/widget/scrollable_positioned_list/item_positions_notifier.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | 7 | import 'item_positions_listener.dart'; 8 | 9 | /// Internal implementation of [ItemPositionsListener]. 10 | class ItemPositionsNotifier implements ItemPositionsListener { 11 | @override 12 | final ValueNotifier> itemPositions = ValueNotifier([]); 13 | } 14 | -------------------------------------------------------------------------------- /lib/app/widget/scrollable_positioned_list/post_mount_callback.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Fuchsia Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// Widget whose [Element] calls a callback when the element is mounted. 8 | class PostMountCallback extends StatelessWidget { 9 | /// Creates a [PostMountCallback] widget. 10 | const PostMountCallback({@required this.child, this.callback, Key key}) 11 | : super(key: key); 12 | 13 | /// The widget below this widget in the tree. 14 | final Widget child; 15 | 16 | /// Callback to call when the element for this widget is mounted. 17 | final void Function() callback; 18 | 19 | @override 20 | StatelessElement createElement() => _PostMountCallbackElement(this); 21 | 22 | @override 23 | Widget build(BuildContext context) => child; 24 | } 25 | 26 | class _PostMountCallbackElement extends StatelessElement { 27 | _PostMountCallbackElement(PostMountCallback widget) : super(widget); 28 | 29 | @override 30 | void mount(Element parent, dynamic newSlot) { 31 | super.mount(parent, newSlot); 32 | final PostMountCallback postMountCallback = widget; 33 | postMountCallback.callback?.call(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/app/widget/widget_expand_text_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const Duration _animationDuration = Duration(milliseconds: 300); 4 | 5 | /// 这是expand_widget[https://pub.dev/packages/expand_widget] 的中国特色社会主义改造版 6 | 7 | class ExpandText extends StatefulWidget { 8 | final String minMessage, maxMessage; 9 | final Color arrowColor; 10 | final double arrowSize; 11 | 12 | final Duration animationDuration; 13 | final String text; 14 | final int maxLength; 15 | 16 | final TextStyle style; 17 | final StrutStyle strutStyle; 18 | final TextAlign textAlign; 19 | 20 | final GlobalKey<_ExpandTextState> textKey; 21 | final bool isHiddenArrow; 22 | 23 | const ExpandText(this.text, 24 | {this.textKey, 25 | this.minMessage = 'Show more', 26 | this.maxMessage = 'Show less', 27 | this.arrowColor, 28 | this.arrowSize = 27, 29 | this.animationDuration = _animationDuration, 30 | this.maxLength = 8, 31 | this.style, 32 | this.strutStyle, 33 | this.textAlign, 34 | this.isHiddenArrow}) 35 | : super(key: textKey); 36 | 37 | @override 38 | _ExpandTextState createState() => _ExpandTextState(); 39 | 40 | void toggle() { 41 | if (textKey != null && textKey.currentState != null) { 42 | textKey.currentState.toggle(); 43 | } 44 | } 45 | } 46 | 47 | class _ExpandTextState extends State 48 | with TickerProviderStateMixin { 49 | /// Custom animations curves for both height & arrow controll. 50 | static final Animatable _easeInTween = 51 | CurveTween(curve: Curves.easeInOutCubic); 52 | static final Animatable _halfTween = 53 | Tween(begin: 0.0, end: 0.5); 54 | 55 | /// General animation controller. 56 | AnimationController _controller; 57 | 58 | /// Animation for controlling the height of the widget. 59 | Animation _iconTurns; 60 | 61 | bool _isExpanded = false; 62 | 63 | @override 64 | void initState() { 65 | super.initState(); 66 | 67 | /// Initializing the animation controller with the [duration] parameter. 68 | _controller = AnimationController( 69 | duration: widget.animationDuration, 70 | vsync: this, 71 | ); 72 | 73 | /// Initializing the animation, depending on the [_easeInTween] curve. 74 | _iconTurns = _controller.drive(_halfTween.chain(_easeInTween)); 75 | } 76 | 77 | @override 78 | void dispose() { 79 | _controller.dispose(); 80 | super.dispose(); 81 | } 82 | 83 | /// Method called when the user clicks on the expand arrow. 84 | void _handleTap() { 85 | setState(() { 86 | _isExpanded = !_isExpanded; 87 | _isExpanded ? _controller.forward() : _controller.reverse(); 88 | }); 89 | } 90 | 91 | void toggle() { 92 | _handleTap(); 93 | } 94 | 95 | /// Builds the widget itself. If the [_isExpanded] parameters is [true], 96 | /// the [child] parameter will contain the child information, passed to 97 | /// this instance of the object. 98 | Widget _buildChildren(BuildContext context, Widget child) { 99 | return LayoutBuilder(builder: (context, size) { 100 | final TextPainter textPainter = TextPainter( 101 | text: TextSpan( 102 | text: widget.text, 103 | style: widget.style, 104 | ), 105 | textDirection: TextDirection.rtl, 106 | maxLines: widget.maxLength, 107 | )..layout(maxWidth: size.maxWidth); 108 | 109 | return textPainter.didExceedMaxLines&&(widget?.isHiddenArrow==null||!widget.isHiddenArrow) 110 | ? Column( 111 | mainAxisSize: MainAxisSize.min, 112 | children: [ 113 | AnimatedSize( 114 | vsync: this, 115 | duration: widget.animationDuration, 116 | alignment: Alignment.topCenter, 117 | curve: Curves.easeInOutCubic, 118 | child: ConstrainedBox( 119 | constraints: BoxConstraints(), 120 | child: child, 121 | ), 122 | ), 123 | ExpandArrow( 124 | minMessage: widget.minMessage, 125 | maxMessage: widget.maxMessage, 126 | color: widget.arrowColor, 127 | size: widget.arrowSize, 128 | animation: _iconTurns, 129 | onTap: _handleTap, 130 | ), 131 | ], 132 | ) 133 | : child; 134 | }); 135 | } 136 | 137 | @override 138 | Widget build(BuildContext context) { 139 | return AnimatedBuilder( 140 | animation: _controller.view, 141 | builder: _buildChildren, 142 | child: Text( 143 | widget.text, 144 | textAlign: widget.textAlign, 145 | overflow: TextOverflow.fade, 146 | style: widget.style, 147 | maxLines: _isExpanded ? null : widget.maxLength, 148 | ), 149 | ); 150 | } 151 | } 152 | 153 | class ExpandArrow extends StatefulWidget { 154 | final String minMessage, maxMessage; 155 | final Animation animation; 156 | final Function onTap; 157 | final Color color; 158 | final double size; 159 | 160 | const ExpandArrow({ 161 | this.minMessage, 162 | this.maxMessage, 163 | @required this.animation, 164 | @required this.onTap, 165 | this.color, 166 | this.size, 167 | }); 168 | 169 | @override 170 | _ExpandArrowState createState() => _ExpandArrowState(); 171 | } 172 | 173 | class _ExpandArrowState extends State { 174 | @override 175 | Widget build(BuildContext context) { 176 | return Tooltip( 177 | message: _message, 178 | child: InkResponse( 179 | child: RotationTransition( 180 | turns: widget.animation, 181 | child: Icon( 182 | Icons.expand_more, 183 | color: widget.color ?? Theme.of(context).textTheme.caption.color, 184 | size: widget.size, 185 | ), 186 | ), 187 | onTap: widget.onTap, 188 | ), 189 | ); 190 | } 191 | 192 | /// Shows a tooltip message depending on the [animation] state. 193 | String get _message => 194 | widget.animation.value == 0 ? widget.minMessage : widget.maxMessage; 195 | } 196 | -------------------------------------------------------------------------------- /lib/app/widget/widget_tag_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// refer:https://github.com/shichunlei/flutter_app/blob/555d4e6b9714695629e4286f4d3b9d585fd4713d/lib/ui/tagview.dart 4 | class TagView extends StatelessWidget { 5 | final String tag; 6 | final Color textColor; 7 | final Color borderColor; 8 | final EdgeInsetsGeometry padding; 9 | final Color bgColor; 10 | final VoidCallback onPressed; 11 | final double borderRadius; 12 | final double fontSize; 13 | 14 | TagView( 15 | {Key key, 16 | @required this.tag, 17 | this.textColor, 18 | this.borderColor, 19 | this.padding, 20 | this.bgColor, 21 | this.onPressed, 22 | this.borderRadius: 3.0, 23 | this.fontSize = 11.5}) 24 | : super(key: key); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Material( 29 | type: MaterialType.transparency, 30 | child: InkWell( 31 | onTap: onPressed, 32 | child: Container( 33 | padding: padding ?? EdgeInsets.symmetric(horizontal: 5), 34 | child: Text( 35 | tag, 36 | style: TextStyle( 37 | color: textColor ?? Color(0xFF9A9AA7), 38 | fontSize: fontSize, 39 | ), 40 | ), 41 | decoration: BoxDecoration( 42 | borderRadius: BorderRadius.all(Radius.circular(borderRadius)), 43 | border: 44 | Border.all(width: 0.5, color: borderColor ?? Color(0xFF9A9AA7)), 45 | color: bgColor ?? Colors.transparent, 46 | ), 47 | ), 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/base/constant/constant_text_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TextStyleConstant { 4 | 5 | static TextStyle textStyle({double fontSize: 12, 6 | Color color: Colors.white, 7 | FontWeight fontWeight}) { 8 | return TextStyle( 9 | fontSize: fontSize, 10 | color: color, 11 | decoration: TextDecoration.none, 12 | fontWeight: fontWeight); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/base/db/manager_db.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:path/path.dart'; 5 | import 'package:sqflite/sqflite.dart'; 6 | import 'package:synchronized/synchronized.dart'; 7 | import 'package:meta/meta.dart'; 8 | 9 | class DBManager { 10 | static const _VERSION = 1; 11 | static const _DB_NAME = "flutter_novel.db"; 12 | Database _db; 13 | final _lock = Lock(); 14 | 15 | factory DBManager() => _getInstance(); 16 | 17 | static DBManager get instance => _getInstance(); 18 | static DBManager _instance; 19 | 20 | DBManager._internal(); 21 | 22 | static DBManager _getInstance() { 23 | if (_instance == null) { 24 | _instance = new DBManager._internal(); 25 | } 26 | return _instance; 27 | } 28 | 29 | Future init() async { 30 | // DB path 31 | var databasesPath = await getDatabasesPath(); 32 | String path = join(databasesPath, _DB_NAME); 33 | if (!await Directory(dirname(path)).exists()) { 34 | try { 35 | await Directory(dirname(path)).create(recursive: true); 36 | } catch (e) { 37 | print(e); 38 | } 39 | } 40 | _db = await openDatabase(path, 41 | version: _VERSION, onCreate: (Database db, int version) async {}); 42 | } 43 | 44 | Future isTableExits(String tableName) async { 45 | await getDB(); 46 | var res = await _db.rawQuery( 47 | "select * from Sqlite_master where type = 'table' and name = '$tableName'"); 48 | return res != null && res.isNotEmpty; 49 | } 50 | 51 | Future getDB() async { 52 | if (_db == null) { 53 | await _lock.synchronized(() async { 54 | // Check again once entering the synchronized block 55 | if (_db == null) { 56 | await init(); 57 | } 58 | }); 59 | } 60 | return _db; 61 | } 62 | 63 | void close() { 64 | _db?.close(); 65 | _db = null; 66 | } 67 | } 68 | 69 | /// Base provider 70 | abstract class BaseDBProvider { 71 | bool isTableExits = false; 72 | 73 | String createSql(); 74 | 75 | String tableName(); 76 | 77 | String baseCreateSql(String name, String columnId) { 78 | return ''' 79 | create table $name ( 80 | $columnId integer primary key autoincrement, 81 | '''; 82 | } 83 | 84 | @mustCallSuper 85 | Future createTable(String name, String createSql) async { 86 | isTableExits = await DBManager.instance.isTableExits(name); 87 | if (!isTableExits) { 88 | Database db = await DBManager.instance.getDB(); 89 | return await db.execute(createSql); 90 | } 91 | } 92 | 93 | @mustCallSuper 94 | Future getDB() async { 95 | await createTable(tableName(), createSql()); 96 | return await DBManager.instance.getDB(); 97 | } 98 | } -------------------------------------------------------------------------------- /lib/base/http/manager_net_request.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | 3 | class NetRequestManager { 4 | // 工厂模式 5 | factory NetRequestManager() => _getInstance(); 6 | 7 | static NetRequestManager get instance => _getInstance(); 8 | static NetRequestManager _instance; 9 | 10 | NetRequestManager._internal() { 11 | // 初始化 12 | _init(); 13 | } 14 | 15 | Dio _dio; 16 | 17 | static NetRequestManager _getInstance() { 18 | if (_instance == null) { 19 | _instance = new NetRequestManager._internal(); 20 | } 21 | return _instance; 22 | } 23 | 24 | void _init() { 25 | _dio = Dio(new BaseOptions( 26 | connectTimeout: 5000, 27 | receiveTimeout: 100000, 28 | )); 29 | 30 | _dio.interceptors 31 | .add(InterceptorsWrapper(onRequest: (RequestOptions options) { 32 | print("请求之前"); 33 | // Do something before request is sent 34 | return options; //continue 35 | }, onResponse: (Response response) { 36 | print("响应之前"); 37 | // Do something with response data 38 | return response; // continue 39 | }, onError: (DioError e) { 40 | print("错误之前"); 41 | // Do something with response error 42 | return e; //continue 43 | })); 44 | } 45 | 46 | Future> getRequest( 47 | String url, { 48 | Map queryParameters, 49 | Options options, 50 | CancelToken cancelToken, 51 | ProgressCallback onReceiveProgress, 52 | }) async { 53 | var response = await _dio.get(url, queryParameters: queryParameters,options: options,cancelToken: cancelToken,onReceiveProgress: onReceiveProgress); 54 | return response; 55 | } 56 | } 57 | 58 | abstract class RequestCallback { 59 | void onSuccess(T requestData); 60 | 61 | void onFailed(Exception e); 62 | } 63 | -------------------------------------------------------------------------------- /lib/base/router/base_router_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class BaseRouterManager { 4 | 5 | const BaseRouterManager(); 6 | 7 | @protected 8 | void jumpToTarget(RouterRequestOption option,Widget targetRouterWidget) { 9 | Navigator.push( 10 | option.context, 11 | option.customRouter == null 12 | ? MaterialPageRoute(builder: (context) { 13 | return targetRouterWidget; 14 | }) 15 | : option.customRouter); 16 | } 17 | 18 | } 19 | 20 | class RouterRequestOption { 21 | String targetName; 22 | 23 | BuildContext context; 24 | 25 | Route customRouter; 26 | 27 | RouterRequestOption(this.targetName, this.context, {this.customRouter}); 28 | } 29 | -------------------------------------------------------------------------------- /lib/base/sp/manager_sp.dart: -------------------------------------------------------------------------------- 1 | import 'package:shared_preferences/shared_preferences.dart'; 2 | 3 | class SharedPreferenceManager { 4 | // 工厂模式 5 | factory SharedPreferenceManager() => _getInstance(); 6 | 7 | static SharedPreferenceManager get instance => _getInstance(); 8 | static SharedPreferenceManager _instance; 9 | 10 | SharedPreferences prefs; 11 | 12 | SharedPreferenceManager._internal() { 13 | // 初始化 14 | _init(); 15 | } 16 | 17 | static SharedPreferenceManager _getInstance() { 18 | if (_instance == null) { 19 | _instance = new SharedPreferenceManager._internal(); 20 | } 21 | return _instance; 22 | } 23 | 24 | void _init() { 25 | SharedPreferences.getInstance().then((data) { 26 | prefs = data; 27 | }); 28 | } 29 | 30 | Future getSP() async { 31 | return prefs ??= await SharedPreferences.getInstance(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/base/structure/base_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/sp/manager_sp.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | /// model 处理数据库、网络请求等返回的数据,进行数据转换、存储等 6 | abstract class BaseModel{ 7 | 8 | @protected 9 | SharedPreferenceManager mSPManager = SharedPreferenceManager.instance; 10 | 11 | bool isDisposed=false; 12 | 13 | } -------------------------------------------------------------------------------- /lib/base/structure/base_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/structure/base_view_model.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | abstract class BaseStatelessView 6 | extends StatelessWidget { 7 | 8 | const BaseStatelessView({Key key}) : super(key: key); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | // M viewModel = buildViewModel(context, false); 13 | 14 | Widget resultWidget; 15 | 16 | M viewModel=buildViewModel(context); 17 | 18 | if (viewModel != null) { 19 | resultWidget = ChangeNotifierProvider(create: (context) { 20 | loadData(context, viewModel); 21 | return viewModel; 22 | }, child: Consumer( 23 | builder: (BuildContext context, M viewModel, Widget child) { 24 | return buildView(context, viewModel); 25 | })); 26 | } else { 27 | loadData(context, null); 28 | resultWidget = buildView(context, null); 29 | } 30 | return resultWidget; 31 | } 32 | 33 | Widget buildView(BuildContext context, M viewModel); 34 | 35 | /// 为什么buildViewModel方法要放以一个抽象自己构建出来?直接让父Widget构建出来传过来不更好么? 36 | /// 因为我发现像tabLayout会触发viewModel的dispose方法……但是如果以父widget传入,那么viewModel是final的,自然会触发已经dispose的provider不能再次绑定的错误 37 | M buildViewModel(BuildContext context); 38 | 39 | /// 需要使用viewModel加载数据、或者页面刷新重新配置数据 40 | void loadData(BuildContext context, M viewModel); 41 | 42 | bool isEnableLoadingView() { 43 | return false; 44 | } 45 | } 46 | 47 | abstract class BaseStatefulView 48 | extends StatefulWidget { 49 | 50 | const BaseStatefulView({Key key}) : super(key: key); 51 | 52 | @override 53 | State createState() { 54 | return buildState(); 55 | } 56 | 57 | BaseStatefulViewState buildState(); 58 | } 59 | 60 | abstract class BaseStatefulViewState extends State { 62 | 63 | M viewModel; 64 | 65 | @override 66 | void initState() { 67 | super.initState(); 68 | initData(); 69 | } 70 | 71 | @override 72 | Widget build(BuildContext context) { 73 | 74 | viewModel=buildViewModel(context); 75 | 76 | Widget resultWidget; 77 | if (viewModel != null&&isBindViewModel()) { 78 | resultWidget = ChangeNotifierProvider(create: (context) { 79 | loadData(context, viewModel); 80 | return viewModel; 81 | }, child: Consumer( 82 | builder: (BuildContext context, M viewModel, Widget child) { 83 | return buildView(context, viewModel); 84 | })); 85 | } else { 86 | loadData(context,viewModel); 87 | resultWidget = buildView(context, viewModel); 88 | } 89 | 90 | return resultWidget; 91 | } 92 | 93 | Widget buildView(BuildContext context, M viewModel); 94 | 95 | /// 初始化数据 96 | void initData(); 97 | 98 | M buildViewModel(BuildContext context); 99 | 100 | /// 需要使用viewModel加载数据、或者页面刷新重新配置数据 101 | void loadData(BuildContext context, M viewModel); 102 | 103 | bool isBindViewModel(){ 104 | return true; 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /lib/base/structure/base_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/structure/provider/base_provider.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | /// 对model的数据进行处理,是跟view逻辑相关的部分 6 | abstract class BaseViewModel extends BaseProvider{ 7 | 8 | LoadingStateEnum isLoading=LoadingStateEnum.IDLE; 9 | 10 | bool isDisposed=false; 11 | 12 | @protected 13 | void refreshRequestState(LoadingStateEnum state){ 14 | isLoading=state; 15 | } 16 | 17 | @override 18 | void dispose() { 19 | super.dispose(); 20 | isDisposed=true; 21 | } 22 | } 23 | 24 | enum LoadingStateEnum { LOADING, IDLE, ERROR } -------------------------------------------------------------------------------- /lib/base/structure/provider/app_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/structure/provider/state_provider.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import 'base_provider.dart'; 6 | 7 | class APPInfoProvider with ChangeNotifier { 8 | 9 | List _currentProviders = [PageStateProvider()]; 10 | 11 | static APPInfoProvider get instance => _getInstance(); 12 | 13 | // 单例公开访问点 14 | factory APPInfoProvider() => _getInstance(); 15 | 16 | // 静态私有成员,没有初始化 17 | static APPInfoProvider _instance = APPInfoProvider._(); 18 | 19 | // 私有构造函数 20 | APPInfoProvider._() { 21 | // 具体初始化代码 22 | } 23 | 24 | // 静态、同步、私有访问点 25 | static APPInfoProvider _getInstance() { 26 | _instance ??= APPInfoProvider._(); 27 | return _instance; 28 | } 29 | 30 | /// 如果需要全局的监听,或者跨页面的消息传递,需要在这里注册 31 | /// 注意一点,添加之后会触发整个APP的刷新,所以会触发initData,注意不要在initData中改变全局属性配置,否则又会触发APP刷新,进而导致无限循环。 32 | /// 推荐还是直接在代码中写到这里,这里是备选方案。 33 | void addGlobalProvider(BaseProvider provider) { 34 | _currentProviders.add(provider); 35 | notifyListeners(); 36 | } 37 | 38 | List getProvidersList(BuildContext context) { 39 | List providers = []; 40 | 41 | for (BaseProvider currentProvider in _currentProviders) { 42 | providers.add(currentProvider.getProviderContainer()); 43 | } 44 | 45 | providers.add(ChangeNotifierProvider.value(value: APPInfoProvider())); 46 | providers.add(Provider.value(value: 50)); 47 | 48 | return providers; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/base/structure/provider/base_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class BaseProvider extends ChangeNotifier{ 4 | 5 | Widget getProviderContainer(); 6 | 7 | } -------------------------------------------------------------------------------- /lib/base/structure/provider/config_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/structure/provider/base_provider.dart'; 3 | import 'package:flutter_novel/base/widget/view_common_loading.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | enum AppNightState { 7 | STATE_NIGHT, 8 | STATE_DAY, 9 | } 10 | 11 | class ConfigProvider extends BaseProvider { 12 | 13 | AppNightState nightState = AppNightState.STATE_NIGHT; 14 | 15 | @override 16 | Widget getProviderContainer() { 17 | return ChangeNotifierProvider(builder: (BuildContext context) { 18 | return ConfigProvider(); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/base/structure/provider/state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/structure/provider/base_provider.dart'; 3 | import 'package:flutter_novel/base/widget/view_common_loading.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | enum PageState { 7 | STATE_IDLE, 8 | STATE_LOADING, 9 | STATE_LOAD_FAILED, 10 | STATE_LOAD_SUCCESS 11 | } 12 | 13 | class PageStateProvider extends BaseProvider { 14 | PageState currentState; 15 | 16 | Widget mLoadingView = CommonLoadingView(); 17 | 18 | void startLoading() { 19 | currentState = PageState.STATE_LOADING; 20 | notifyListeners(); 21 | } 22 | 23 | void stopLoading(bool isSuccess, bool isShowTips) { 24 | currentState = isShowTips 25 | ? (isSuccess ? PageState.STATE_LOADING : PageState.STATE_LOAD_FAILED) 26 | : PageState.STATE_IDLE; 27 | notifyListeners(); 28 | } 29 | 30 | void resetState() { 31 | currentState = PageState.STATE_IDLE; 32 | notifyListeners(); 33 | } 34 | 35 | void setLoadingView(Widget newLoadingView){ 36 | mLoadingView=newLoadingView; 37 | } 38 | 39 | @override 40 | Widget getProviderContainer() { 41 | return ChangeNotifierProvider(builder: (BuildContext context) { 42 | return PageStateProvider(); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/base/util/utils_color.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ColorUtils{ 5 | /// 文字转颜色 6 | static Color strToColor(String name) { 7 | assert(name.length > 1); 8 | final int hash = name.hashCode & 0xffff; 9 | final double hue = (360.0 * hash / (1 << 15)) % 360.0; 10 | return HSVColor.fromAHSV(1.0, hue, 0.4, 0.90).toColor(); 11 | } 12 | 13 | /// 随机颜色 14 | static Color randomRGB() { 15 | return Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), 16 | Random().nextInt(255)); 17 | } 18 | 19 | static Color randomARGB() { 20 | Random random = new Random(); 21 | return Color.fromARGB(random.nextInt(180), random.nextInt(255), 22 | random.nextInt(255), random.nextInt(255)); 23 | } 24 | } -------------------------------------------------------------------------------- /lib/base/util/utils_navigator.dart: -------------------------------------------------------------------------------- 1 | class NavigatorUtils { 2 | static void push(){ 3 | 4 | } 5 | 6 | static void pop(){ 7 | 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /lib/base/util/utils_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class ScreenUtils{ 6 | 7 | static double getScreenHeight(){ 8 | return MediaQueryData.fromWindow(window).size.height; 9 | } 10 | 11 | static double getScreenWidth(){ 12 | return MediaQueryData.fromWindow(window).size.width; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /lib/base/util/utils_toast.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/constant/custom_color.dart'; 3 | import 'package:fluttertoast/fluttertoast.dart'; 4 | 5 | class ToastUtils{ 6 | 7 | static void showToast(String msg){ 8 | Fluttertoast.showToast( 9 | msg: msg, 10 | toastLength: Toast.LENGTH_SHORT, 11 | gravity: ToastGravity.BOTTOM, 12 | timeInSecForIos: 1, 13 | backgroundColor: CustomColor.blackA99, 14 | textColor: Colors.white, 15 | fontSize: 16.0 16 | ); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /lib/base/widget/base_list_item_holder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | 4 | abstract class BaseItemHolder extends StatefulWidget { 5 | 6 | final T currentHolderData; 7 | 8 | BaseItemHolder(this.currentHolderData); 9 | 10 | BaseItemHolderState initState(); 11 | 12 | @override 13 | State createState() { 14 | return initState().setHolderData(currentHolderData); 15 | } 16 | 17 | } 18 | 19 | abstract class BaseItemHolderState 20 | extends State { 21 | T mCurrentHolderData; 22 | 23 | BaseItemHolderState setHolderData(T data) { 24 | mCurrentHolderData = data; 25 | return this; 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return buildView(context); 31 | } 32 | 33 | Widget buildView(BuildContext context); 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/base/widget/base_list_item_holder_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/base/widget/base_list_item_holder.dart'; 3 | 4 | typedef void OnItemTaped(BuildContext context,T data, int index,int currentItemType); 5 | 6 | abstract class BaseListItemHolderBuilder { 7 | List currentListData; 8 | 9 | BaseListItemHolderBuilder(); 10 | 11 | Widget build(BuildContext context, int index, List listData, 12 | {OnItemTaped itemTapCallback}) { 13 | BaseItemHolder result; 14 | 15 | currentListData = listData; 16 | 17 | result = getItemHolder(context, index, getItemType(listData[index], index)); 18 | 19 | return GestureDetector( 20 | child: result, 21 | onTap: () { 22 | if (itemTapCallback != null) { 23 | itemTapCallback( 24 | context,listData[index], index, getItemType(listData[index], index)); 25 | } 26 | }); 27 | } 28 | 29 | BaseItemHolder getItemHolder( 30 | BuildContext context, int index, int itemType); 31 | 32 | int getItemType(T data, int index) { 33 | return 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/base/widget/view_common_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CommonLoadingView extends StatefulWidget { 4 | @override 5 | _CommonLoadingViewState createState() => _CommonLoadingViewState(); 6 | } 7 | 8 | class _CommonLoadingViewState extends State { 9 | @override 10 | Widget build(BuildContext context) { 11 | return Container( 12 | alignment: Alignment.center, 13 | child: Text("默认的通用loading页面"), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_novel/app/main/main_page_view.dart'; 3 | import 'package:flutter_novel/app/novel/view/novel_book_intro.dart'; 4 | import 'package:flutter_novel/app/provider_setup.dart'; 5 | import 'package:flutter_novel/base/structure/provider/config_provider.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | void main() async { 9 | runApp(MyApp()); 10 | } 11 | 12 | class MyApp extends StatelessWidget { 13 | // This widget is the root of your application. 14 | @override 15 | Widget build(BuildContext context) { 16 | return MultiProvider( 17 | providers: providers, 18 | child: Consumer(builder: 19 | (BuildContext context, ConfigProvider appInfo, Widget child) { 20 | return MaterialApp( 21 | // showPerformanceOverlay: true, 22 | // checkerboardOffscreenLayers: true, // 使用了saveLayer的图形会显示为棋盘格式并随着页面刷新而闪烁 23 | // checkerboardRasterCacheImages: true, // 做了缓存的静态图片在刷新页面时不会改变棋盘格的颜色;如果棋盘格颜色变了说明被重新缓存了,这是我们要避免的 24 | 25 | title: 'Flutter Novel Reader', 26 | theme: ThemeData(primaryColor:Colors.white,), 27 | // home: NovelBookIntroView("592fe687c60e3c4926b040ca")); 28 | home: MainPageView()); 29 | })); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_novel 2 | description: A new Flutter Novel Reader Application. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | # The following adds the Cupertino Icons font to your application. 24 | # Use with the CupertinoIcons class for iOS style icons. 25 | cupertino_icons: ^0.1.2 26 | flutter_vector_icons: ^0.2.1 27 | carousel_slider: ^1.3.0 28 | fluttertoast: ^3.1.0 29 | dio: 2.1.0 30 | 31 | json_annotation: ^3.0.0 32 | 33 | cached_network_image: ^2.0.0-rc 34 | image_picker: ^0.6.0+9 35 | rxdart: ^0.22.6 36 | provider: ^3.0.0+1 37 | 38 | extended_nested_scroll_view: ^0.3.6 39 | 40 | flutter_isolate: ^1.0.0+11 41 | flutter_easyrefresh: ^2.0.4 42 | flutter_widgets: ^0.1.6 43 | 44 | screen: ^0.0.5 45 | sqflite: ^1.1.7+3 46 | shared_preferences: ^0.5.4+6 47 | autocomplete_textfield: ^1.7.3 48 | 49 | # flutter 团队维护的一个工具合集 50 | palette_generator: ^0.2.0 51 | 52 | dev_dependencies: 53 | flutter_test: 54 | sdk: flutter 55 | 56 | build_runner: ^1.7.1 57 | json_serializable: ^3.2.3 58 | 59 | # For information on the generic Dart part of this file, see the 60 | # following page: https://dart.dev/tools/pub/pubspec 61 | 62 | # The following section is specific to Flutter. 63 | flutter: 64 | 65 | # The following line ensures that the Material Icons font is 66 | # included with your application, so that you can use the icons in 67 | # the material Icons class. 68 | uses-material-design: true 69 | 70 | # To add assets to your application, add an assets section, like this: 71 | # assets: 72 | # - images/a_dot_burr.jpeg 73 | # - images/a_dot_ham.jpeg 74 | 75 | # An image asset can refer to one or more resolution-specific "variants", see 76 | # https://flutter.dev/assets-and-images/#resolution-aware. 77 | 78 | # For details regarding adding assets from package dependencies, see 79 | # https://flutter.dev/assets-and-images/#from-packages 80 | 81 | # To add custom fonts to your application, add a fonts section here, 82 | # in this "flutter" section. Each entry in this list should have a 83 | # "family" key with the font family name, and a "fonts" key with a 84 | # list giving the asset and other descriptors for the font. For 85 | # example: 86 | # fonts: 87 | # - family: Schyler 88 | # fonts: 89 | # - asset: fonts/Schyler-Regular.ttf 90 | # - asset: fonts/Schyler-Italic.ttf 91 | # style: italic 92 | # - family: Trajan Pro 93 | # fonts: 94 | # - asset: fonts/TrajanPro.ttf 95 | # - asset: fonts/TrajanPro_Bold.ttf 96 | # weight: 700 97 | # 98 | # For details regarding fonts from package dependencies, 99 | # see https://flutter.dev/custom-fonts/#from-packages 100 | assets: 101 | - img/ -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:flutter_novel/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(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 | --------------------------------------------------------------------------------