├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── android.zip ├── app │ ├── bookApp.jks │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── woshilll │ │ │ │ └── book_app │ │ │ │ ├── ConfigType.java │ │ │ │ ├── MainActivity.kt │ │ │ │ └── WoshilllPlugin.java │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── 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 │ │ │ ├── raw │ │ │ └── keep.xml │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── key.properties └── settings.gradle ├── book_private.html ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 1024x1024.jpg │ │ ├── 120x120 1.jpg │ │ ├── 120x120.jpg │ │ ├── 152x152.jpg │ │ ├── 167x167.jpg │ │ ├── 180x180.jpg │ │ ├── 20x20.jpg │ │ ├── 29x29 1.jpg │ │ ├── 29x29.jpg │ │ ├── 40x40 1.jpg │ │ ├── 40x40 2.jpg │ │ ├── 40x40.jpg │ │ ├── 58x58 1.jpg │ │ ├── 58x58.jpg │ │ ├── 60x60.jpg │ │ ├── 76x76.jpg │ │ ├── 80x80 1.jpg │ │ ├── 80x80.jpg │ │ ├── 87x87.jpg │ │ └── Contents.json │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── README.md │ │ ├── ios (1) 1.jpg │ │ ├── ios (2).jpg │ │ ├── ios.jpg │ │ ├── ipad (2).jpg │ │ └── ipad.jpg │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ ├── Runner-Bridging-Header.h │ └── WoshilllPlugin.swift ├── lib ├── api │ └── http_manager.dart ├── app_binding.dart ├── app_controller.dart ├── db │ ├── base_db_provider.dart │ └── sql_manager.dart ├── di.dart ├── log │ └── log.dart ├── main.dart ├── mapper │ ├── book_db_provider.dart │ └── chapter_db_provider.dart ├── model │ ├── base.dart │ ├── book │ │ ├── book.dart │ │ └── book.g.dart │ ├── book_with_chapters.dart │ ├── chapter │ │ ├── chapter.dart │ │ └── chapter.g.dart │ ├── message.dart │ ├── read_page_type.dart │ └── search │ │ └── search_history.dart ├── module │ └── book │ │ ├── home │ │ ├── book_home_binding.dart │ │ ├── book_home_controller.dart │ │ └── book_home_screen.dart │ │ ├── read │ │ ├── component │ │ │ ├── bottom.dart │ │ │ ├── content.dart │ │ │ ├── content_bottom.dart │ │ │ ├── content_gen.dart │ │ │ ├── content_page.dart │ │ │ ├── cover.dart │ │ │ ├── custom_drawer.dart │ │ │ ├── drawer.dart │ │ │ ├── list.dart │ │ │ ├── my_text_painter.dart │ │ │ ├── page_gen.dart │ │ │ ├── point.dart │ │ │ └── slide.dart │ │ ├── read_binding.dart │ │ ├── read_controller.dart │ │ └── read_screen.dart │ │ ├── readMoreSetting │ │ ├── component │ │ │ └── page_style_bottom.dart │ │ ├── read_more_setting_binding.dart │ │ ├── read_more_setting_controller.dart │ │ └── read_more_setting_screen.dart │ │ └── readSetting │ │ ├── component │ │ └── read_setting_config.dart │ │ ├── read_setting_binding.dart │ │ ├── read_setting_controller.dart │ │ └── read_setting_screen.dart ├── resource │ └── image │ │ └── screen_h.png ├── route │ ├── route_pages.dart │ └── routes.dart ├── theme │ └── color.dart └── util │ ├── bar_util.dart │ ├── bottom_bar_build.dart │ ├── channel_utils.dart │ ├── chapter_compare.dart │ ├── constant.dart │ ├── content_fliter.dart │ ├── custom_page_view.dart │ ├── dialog_build.dart │ ├── drag_overlay.dart │ ├── font_util.dart │ ├── future_do.dart │ ├── html_parse_util.dart │ ├── keep_alive_wrapper.dart │ ├── limit_util.dart │ ├── list_item.dart │ ├── no_shadow_scroll_behavior.dart │ ├── notify │ ├── counter_notify.dart │ └── object_notify.dart │ ├── parse_book.dart │ ├── parse_network_book.dart │ ├── path_util.dart │ ├── random_user_agent.dart │ ├── save_util.dart │ ├── system_utils.dart │ ├── time_util.dart │ └── toast.dart ├── pubspec.yaml ├── test └── test.dart └── web ├── favicon.png ├── icons ├── Icon-192.png ├── Icon-512.png ├── Icon-maskable-192.png └── Icon-maskable-512.png ├── index.html └── manifest.json /.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 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | #/android/key.properties 48 | #/android/*.jks 49 | #*.jks 50 | -------------------------------------------------------------------------------- /.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: 3595343e20a61ff16d14e8ecc25f364276bb1b8b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 小说阅读 2 | 3 | ## 废了。。。 4 | 5 | 更新后无法上架IOS了。。。给我说4.3,后来我回复邮件也不行,懒得搞了。有想体验可以下载个testflight,加入这个[测试链接](https://testflight.apple.com/join/ubhOVPdq)就好了 6 | 7 | ## 功能 8 | 9 | - 本地小说解析 10 | - 网络小说解析 11 | 12 | 13 | ## 演示 14 | 15 | ### 其它APP分享 16 | 17 | 18 | 19 | ### 解析分享 20 | 21 | 22 | 23 | ### 本地书籍 24 | 25 | 26 | 27 | ### 阅读 28 | 29 | 30 | 31 | ### 阅读设置 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ### 查看网络小说 40 | 41 | 42 | 43 | ## 更新历史 44 | 45 | ### 1.0.15 46 | 47 | - 增加了分享,可通过分享链接解析小说,避免搜索无用功 48 | 49 | ### 1.0.16 50 | 51 | - 缓存小说 52 | - 网络原因会照成章节内容解析不全,因此增加重新解析该章节 53 | 54 | ### 1.0.17 55 | 56 | - 删除搜索功能,可通过复制链接到APP解析 57 | - 小说重命名 58 | 59 | ### 1.0.18 60 | 61 | - 可以解析分页小说链接(对于一些小说网站,频繁访问可能导致访问限制) 62 | - 每日第一次打开小说会尝试更新 63 | 64 | ## Support 65 | 66 | Support of your project name 67 | If you have any questions, please contact us by this email and we give reply to you as soon as possible. 68 | Email: iamlilili@yeah.net 69 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | dart_code_metrics: 31 | anti-patterns: 32 | - long-method 33 | - long-parameter-list 34 | metrics: 35 | cyclomatic-complexity: 20 36 | maximum-nesting-level: 5 37 | number-of-parameters: 4 38 | source-lines-of-code: 50 39 | metrics-exclude: 40 | - test/** 41 | rules: 42 | - newline-before-return 43 | - no-boolean-literal-compare 44 | - no-empty-block 45 | - prefer-trailing-comma 46 | - prefer-conditional-expressions 47 | - no-equal-then-else -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | #key.properties 12 | **/*.keystore 13 | #**/*.jks 14 | -------------------------------------------------------------------------------- /android/android.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/android.zip -------------------------------------------------------------------------------- /android/app/bookApp.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/app/bookApp.jks -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if (keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | compileSdkVersion 31 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | 46 | sourceSets { 47 | main.java.srcDirs += 'src/main/kotlin' 48 | } 49 | 50 | defaultConfig { 51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 52 | applicationId "com.woshilll.book_app" 53 | minSdkVersion 23 54 | targetSdkVersion 30 55 | versionCode flutterVersionCode.toInteger() 56 | versionName flutterVersionName 57 | } 58 | 59 | signingConfigs { 60 | release { 61 | keyAlias keystoreProperties['keyAlias'] 62 | keyPassword keystoreProperties['keyPassword'] 63 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 64 | storePassword keystoreProperties['storePassword'] 65 | } 66 | } 67 | 68 | buildTypes { 69 | release { 70 | minifyEnabled false 71 | shrinkResources false 72 | crunchPngs false // or true 73 | lintOptions { 74 | checkReleaseBuilds false 75 | abortOnError false 76 | } 77 | // profile { 78 | // matchingFallbacks = ['debug', 'release'] 79 | // } 80 | signingConfig signingConfigs.release 81 | } 82 | } 83 | } 84 | 85 | flutter { 86 | source '../..' 87 | } 88 | 89 | dependencies { 90 | // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 91 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version' 92 | testImplementation 'junit:junit:4.12' 93 | androidTestImplementation 'androidx.test:runner:1.1.1' 94 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 95 | } 96 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/woshilll/book_app/ConfigType.java: -------------------------------------------------------------------------------- 1 | package com.woshilll.book_app; 2 | 3 | public enum ConfigType { 4 | INT,BOOLEAN,MAP,DOUBLE 5 | } 6 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/woshilll/book_app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.woshilll.book_app 2 | 3 | import android.Manifest.permission.READ_EXTERNAL_STORAGE 4 | import android.Manifest.permission.WRITE_EXTERNAL_STORAGE 5 | import android.os.Bundle 6 | import android.view.KeyEvent 7 | import io.flutter.embedding.android.FlutterActivity 8 | import io.flutter.embedding.engine.FlutterEngine 9 | import android.content.Intent 10 | import android.content.pm.PackageManager 11 | import android.net.Uri 12 | import androidx.core.app.ActivityCompat 13 | import java.lang.Exception 14 | import java.util.* 15 | 16 | class MainActivity : FlutterActivity() { 17 | private lateinit var woshilllPlugin: WoshilllPlugin 18 | 19 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 20 | super.configureFlutterEngine(flutterEngine) 21 | } 22 | 23 | 24 | override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { 25 | val volumeFlag: Boolean = woshilllPlugin.getConfigValue("volumeFlag", ConfigType.BOOLEAN) as Boolean 26 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeFlag) { 27 | // 音量减 28 | woshilllPlugin.sendVolumeChange(false) 29 | return false 30 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeFlag) { 31 | // 音量加 32 | woshilllPlugin.sendVolumeChange(true) 33 | return false 34 | } 35 | return super.onKeyUp(keyCode, event) 36 | } 37 | 38 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 39 | val volumeFlag: Boolean = woshilllPlugin.getConfigValue("volumeFlag", ConfigType.BOOLEAN) as Boolean 40 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeFlag) { 41 | // 音量减 42 | return true 43 | } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeFlag) { 44 | // 音量加 45 | return true 46 | } 47 | return super.onKeyDown(keyCode, event) 48 | } 49 | 50 | override fun onCreate(savedInstanceState: Bundle?) { 51 | super.onCreate(savedInstanceState) 52 | this.woshilllPlugin = WoshilllPlugin(this.flutterEngine?.dartExecutor?.binaryMessenger, this) 53 | receiveActionSend(intent) 54 | } 55 | 56 | override fun onNewIntent(intent: Intent) { 57 | super.onNewIntent(intent) 58 | receiveActionSend(intent) 59 | } 60 | 61 | private fun processIntent(intent: Intent): Map { 62 | try { 63 | // 这个会在下面进行解释 64 | val uri: Uri? = intent.getParcelableExtra(Intent.EXTRA_STREAM) 65 | // 进行权限请求 66 | val permission = ActivityCompat.checkSelfPermission( 67 | this, 68 | READ_EXTERNAL_STORAGE 69 | ) 70 | val permissionStorage: Array = arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE) 71 | if (permission != PackageManager.PERMISSION_GRANTED) { 72 | ActivityCompat.requestPermissions( 73 | this, 74 | permissionStorage, 75 | 1 76 | ) 77 | } 78 | // 获取Intent中携带的数据 79 | val resolver = this.contentResolver 80 | if (uri != null) { 81 | resolver.openInputStream(uri).use { 82 | val reader = it?.reader() 83 | val content = reader?.readText() 84 | val name = uri.path?.substring(uri.path!!.lastIndexOf('/') + 1) 85 | return mapOf("name" to name, "content" to content) 86 | } 87 | } 88 | } catch (e: Exception) { 89 | } 90 | return mapOf() 91 | } 92 | 93 | private fun receiveActionSend(intent: Intent) { 94 | val action: String? = intent.action 95 | val type: String = if (intent.type != null) intent.type!! else "unknown" 96 | // 判断Intent action,如果是SEND则调用下列代码,还有一个类型为ACTION_SEND_MULTIPLE 97 | if (Intent.ACTION_SEND == (action)) { 98 | // 如果是text类型 99 | if (type.startsWith("text/")) { 100 | val map = processIntent(intent) 101 | // 调用Flutter方法对接收的文件内容进行处理 102 | woshilllPlugin.sendBookPath(map) 103 | } 104 | } 105 | } 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/woshilll/book_app/WoshilllPlugin.java: -------------------------------------------------------------------------------- 1 | package com.woshilll.book_app; 2 | 3 | import android.content.res.Resources; 4 | import android.provider.Settings; 5 | import android.view.Window; 6 | import android.view.WindowManager; 7 | import androidx.annotation.NonNull; 8 | import io.flutter.plugin.common.BinaryMessenger; 9 | import io.flutter.plugin.common.MethodCall; 10 | import io.flutter.plugin.common.MethodChannel; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | public class WoshilllPlugin implements MethodChannel.MethodCallHandler { 17 | private final MethodChannel channel; 18 | private final MainActivity activity; 19 | /** 20 | * 存放配置信息 21 | */ 22 | private final Map configMap = new HashMap<>(16); 23 | 24 | /** 25 | * 插件初始化 26 | * 27 | * @param messenger 28 | * @param activity 29 | */ 30 | public WoshilllPlugin(BinaryMessenger messenger, MainActivity activity) { 31 | channel = new MethodChannel(messenger, "woshill/plugin"); 32 | channel.setMethodCallHandler(this); 33 | this.activity = activity; 34 | 35 | } 36 | 37 | /** 38 | * 方法响应 39 | * 40 | * @param call 41 | * @param result 42 | */ 43 | @Override 44 | public void onMethodCall(@NonNull @NotNull MethodCall call, @NonNull @NotNull MethodChannel.Result result) { 45 | try { 46 | switch (call.method) { 47 | case "setConfig": 48 | configMap.put(call.argument("key"), call.argument("value")); 49 | break; 50 | } 51 | } catch (Exception e) { 52 | e.printStackTrace(); 53 | } 54 | } 55 | 56 | /** 57 | * 音量物理键改变 58 | * 59 | * @param up true加音量 false减音量 60 | */ 61 | public void sendVolumeChange(boolean up) { 62 | channel.invokeMethod("bookVolumeChange", up); 63 | } 64 | 65 | /** 66 | * 发送小说解析 67 | * 68 | * @param book 小说对象 69 | */ 70 | public void sendBookPath(Map book) { 71 | channel.invokeMethod("bookPath", book); 72 | } 73 | 74 | /** 75 | * 获取配置信息 有默认值 76 | * 77 | * @param key 78 | * @param configType 79 | * @return 80 | */ 81 | public Object getConfigValue(String key, ConfigType configType) { 82 | Object res = configMap.get(key); 83 | if (res == null) { 84 | switch (configType) { 85 | case INT: 86 | res = 0; 87 | break; 88 | case MAP: 89 | res = new HashMap<>(2); 90 | break; 91 | case BOOLEAN: 92 | res = false; 93 | break; 94 | case DOUBLE: 95 | res = 0.0d; 96 | break; 97 | } 98 | } 99 | return res; 100 | } 101 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/raw/keep.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.5.21' 3 | repositories { 4 | //google() 5 | //jcenter() 6 | maven { url 'https://maven.aliyun.com/repository/public/' } 7 | maven { url 'https://maven.aliyun.com/repository/google/'} 8 | maven { url 'https://maven.aliyun.com/repository/jcenter/'} 9 | mavenLocal() 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | classpath 'com.android.tools.build:gradle:4.1.0' 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | //google() 22 | //jcenter() 23 | maven { url 'https://maven.aliyun.com/repository/public/' } 24 | maven { url 'https://maven.aliyun.com/repository/google/'} 25 | maven { url 'https://maven.aliyun.com/repository/jcenter/'} 26 | mavenLocal() 27 | mavenCentral() 28 | } 29 | } 30 | 31 | rootProject.buildDir = '../build' 32 | subprojects { 33 | project.buildDir = "${rootProject.buildDir}/${project.name}" 34 | project.evaluationDependsOn(':app') 35 | } 36 | 37 | task clean(type: Delete) { 38 | delete rootProject.buildDir 39 | } 40 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/key.properties: -------------------------------------------------------------------------------- 1 | storePassword=123456 2 | keyPassword=123456 3 | keyAlias=key 4 | storeFile=bookApp.jks 5 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /book_private.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 隐私协议 6 | 7 | 8 |
9 |

隐私政策

10 | 轻阅读不会收集任何信息。轻阅读不会更新本隐私权政策。您在同意轻阅读服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于轻阅读服务使用协议不可分割的一部分。 11 |
12 |
    13 |
  1. 无需登录
  2. 14 |
  3. 不会申请任何权限,除网络和存储以外
  4. 15 |
  5. 网络请求:用于解析链接
  6. 16 |
  7. 存储:用于将小说存储在本地
  8. 17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - DKImagePickerController/Core (4.3.2): 3 | - DKImagePickerController/ImageDataManager 4 | - DKImagePickerController/Resource 5 | - DKImagePickerController/ImageDataManager (4.3.2) 6 | - DKImagePickerController/PhotoGallery (4.3.2): 7 | - DKImagePickerController/Core 8 | - DKPhotoGallery 9 | - DKImagePickerController/Resource (4.3.2) 10 | - DKPhotoGallery (0.0.17): 11 | - DKPhotoGallery/Core (= 0.0.17) 12 | - DKPhotoGallery/Model (= 0.0.17) 13 | - DKPhotoGallery/Preview (= 0.0.17) 14 | - DKPhotoGallery/Resource (= 0.0.17) 15 | - SDWebImage 16 | - SwiftyGif 17 | - DKPhotoGallery/Core (0.0.17): 18 | - DKPhotoGallery/Model 19 | - DKPhotoGallery/Preview 20 | - SDWebImage 21 | - SwiftyGif 22 | - DKPhotoGallery/Model (0.0.17): 23 | - SDWebImage 24 | - SwiftyGif 25 | - DKPhotoGallery/Preview (0.0.17): 26 | - DKPhotoGallery/Model 27 | - DKPhotoGallery/Resource 28 | - SDWebImage 29 | - SwiftyGif 30 | - DKPhotoGallery/Resource (0.0.17): 31 | - SDWebImage 32 | - SwiftyGif 33 | - file_picker (0.0.1): 34 | - DKImagePickerController/PhotoGallery 35 | - Flutter 36 | - Flutter (1.0.0) 37 | - FMDB (2.7.5): 38 | - FMDB/standard (= 2.7.5) 39 | - FMDB/standard (2.7.5) 40 | - path_provider_ios (0.0.1): 41 | - Flutter 42 | - "permission_handler (5.1.0+2)": 43 | - Flutter 44 | - SDWebImage (5.12.3): 45 | - SDWebImage/Core (= 5.12.3) 46 | - SDWebImage/Core (5.12.3) 47 | - share_plus (0.0.1): 48 | - Flutter 49 | - shared_preferences_ios (0.0.1): 50 | - Flutter 51 | - sqflite (0.0.2): 52 | - Flutter 53 | - FMDB (>= 2.7.5) 54 | - SwiftyGif (5.4.3) 55 | - url_launcher_ios (0.0.1): 56 | - Flutter 57 | - woshilll_flutter_plugin (0.0.1): 58 | - Flutter 59 | 60 | DEPENDENCIES: 61 | - file_picker (from `.symlinks/plugins/file_picker/ios`) 62 | - Flutter (from `Flutter`) 63 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 64 | - permission_handler (from `.symlinks/plugins/permission_handler/ios`) 65 | - share_plus (from `.symlinks/plugins/share_plus/ios`) 66 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 67 | - sqflite (from `.symlinks/plugins/sqflite/ios`) 68 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 69 | - woshilll_flutter_plugin (from `.symlinks/plugins/woshilll_flutter_plugin/ios`) 70 | 71 | SPEC REPOS: 72 | trunk: 73 | - DKImagePickerController 74 | - DKPhotoGallery 75 | - FMDB 76 | - SDWebImage 77 | - SwiftyGif 78 | 79 | EXTERNAL SOURCES: 80 | file_picker: 81 | :path: ".symlinks/plugins/file_picker/ios" 82 | Flutter: 83 | :path: Flutter 84 | path_provider_ios: 85 | :path: ".symlinks/plugins/path_provider_ios/ios" 86 | permission_handler: 87 | :path: ".symlinks/plugins/permission_handler/ios" 88 | share_plus: 89 | :path: ".symlinks/plugins/share_plus/ios" 90 | shared_preferences_ios: 91 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 92 | sqflite: 93 | :path: ".symlinks/plugins/sqflite/ios" 94 | url_launcher_ios: 95 | :path: ".symlinks/plugins/url_launcher_ios/ios" 96 | woshilll_flutter_plugin: 97 | :path: ".symlinks/plugins/woshilll_flutter_plugin/ios" 98 | 99 | SPEC CHECKSUMS: 100 | DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d 101 | DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 102 | file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 103 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 104 | FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a 105 | path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 106 | permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 107 | SDWebImage: 53179a2dba77246efa8a9b85f5c5b21f8f43e38f 108 | share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 109 | shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad 110 | sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 111 | SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 112 | url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de 113 | woshilll_flutter_plugin: ec22e45d9929817b403332a23feb0d4a798ba749 114 | 115 | PODFILE CHECKSUM: a9b1149ad1f99ffa65b8f1bba547ea3f639a5861 116 | 117 | COCOAPODS: 1.11.3 118 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.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 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | 7 | override func application( 8 | _ application: UIApplication, 9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 10 | ) -> Bool { 11 | let controller : FlutterViewController = window?.rootViewController as! FlutterViewController 12 | WoshilllPlugin.register(messenger: controller.binaryMessenger) 13 | GeneratedPluginRegistrant.register(with: self) 14 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 15 | } 16 | 17 | override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { 18 | do { 19 | let text = try String(contentsOfFile: url.path, encoding: .utf8) 20 | WoshilllPlugin.seedBookPath(path: ["name": url.absoluteString, "content": text]) 21 | } catch { 22 | do { 23 | let enc = CFStringConvertEncodingToNSStringEncoding(CFStringEncoding(CFStringEncodings.GB_18030_2000.rawValue)) 24 | let text = try NSString(contentsOfFile: url.path, encoding: enc) as String 25 | WoshilllPlugin.seedBookPath(path: ["name": url.absoluteString, "content": text]) 26 | } catch {} 27 | } 28 | return super.application(app, open: url, options: options) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024x1024.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120x120 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/120x120 1.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/120x120.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/120x120.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/152x152.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/152x152.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/167x167.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/167x167.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/180x180.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/180x180.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/20x20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/20x20.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29 1.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/29x29.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40 1.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40 2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40 2.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/40x40.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58x58 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/58x58 1.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/58x58.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/58x58.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/60x60.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/60x60.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/76x76.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/76x76.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80 1.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/80x80.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/87x87.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/AppIcon.appiconset/87x87.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40x40.jpg", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60x60.jpg", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29x29.jpg", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58x58.jpg", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87x87.jpg", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80x80.jpg", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120x120.jpg", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "120x120 1.jpg", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "180x180.jpg", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "filename" : "20x20.jpg", 59 | "idiom" : "ipad", 60 | "scale" : "1x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "40x40 1.jpg", 65 | "idiom" : "ipad", 66 | "scale" : "2x", 67 | "size" : "20x20" 68 | }, 69 | { 70 | "filename" : "29x29 1.jpg", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "58x58 1.jpg", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "29x29" 80 | }, 81 | { 82 | "filename" : "40x40 2.jpg", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "80x80 1.jpg", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "40x40" 92 | }, 93 | { 94 | "filename" : "76x76.jpg", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "152x152.jpg", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "76x76" 104 | }, 105 | { 106 | "filename" : "167x167.jpg", 107 | "idiom" : "ipad", 108 | "scale" : "2x", 109 | "size" : "83.5x83.5" 110 | }, 111 | { 112 | "filename" : "1024x1024.jpg", 113 | "idiom" : "ios-marketing", 114 | "scale" : "1x", 115 | "size" : "1024x1024" 116 | } 117 | ], 118 | "info" : { 119 | "author" : "xcode", 120 | "version" : 1 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "ios.jpg", 5 | "idiom" : "iphone", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "ios (1) 1.jpg", 10 | "idiom" : "iphone", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "ios (2).jpg", 15 | "idiom" : "iphone", 16 | "scale" : "3x" 17 | }, 18 | { 19 | "filename" : "ipad.jpg", 20 | "idiom" : "ipad", 21 | "scale" : "1x" 22 | }, 23 | { 24 | "filename" : "ipad (2).jpg", 25 | "idiom" : "ipad", 26 | "scale" : "2x" 27 | } 28 | ], 29 | "info" : { 30 | "author" : "xcode", 31 | "version" : 1 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ios (1) 1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/ios (1) 1.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ios (2).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/ios (2).jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ios.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/ios.jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ipad (2).jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/ipad (2).jpg -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/ipad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/ios/Runner/Assets.xcassets/LaunchImage.imageset/ipad.jpg -------------------------------------------------------------------------------- /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 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPhotoLibraryUsageDescription 6 | App需要您的同意,用于保存数据到手机 7 | UISupportsDocumentBrowser 8 | 9 | LSApplicationCategoryType 10 | 11 | CFBundleDevelopmentRegion 12 | $(DEVELOPMENT_LANGUAGE) 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | 轻阅读 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | $(FLUTTER_BUILD_NUMBER) 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | CADisableMinimumFrameDurationOnPhone 51 | 52 | CFBundleDocumentTypes 53 | 54 | 55 | CFBundleTypeName 56 | Text 57 | LSHandlerRank 58 | Alternate 59 | LSItemContentTypes 60 | 61 | public.text 62 | public.plain-text 63 | 64 | 65 | 66 | UIApplicationSupportsIndirectInputEvents 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/WoshilllPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WoshilllPlugin.swift 3 | // Runner 4 | // 5 | // Created by 李洋 on 2022/10/23. 6 | // 7 | 8 | import Flutter 9 | import UIKit 10 | 11 | public class WoshilllPlugin { 12 | static var channel: FlutterMethodChannel? 13 | static func register(messenger: FlutterBinaryMessenger) { 14 | channel = FlutterMethodChannel(name: "woshill/plugin", binaryMessenger: messenger) 15 | } 16 | 17 | static func seedBookPath(path: [String: String]) { 18 | channel?.invokeMethod("bookPath", arguments: path) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/api/http_manager.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'dart:io'; 3 | 4 | class HttpManager { 5 | static HttpClient? _httpClient; 6 | 7 | factory HttpManager() => getInstance(); 8 | 9 | static HttpManager get instance => getInstance(); 10 | static HttpManager? _instance; 11 | 12 | static HttpManager getInstance() { 13 | _instance ??= HttpManager._init(); 14 | 15 | return _instance!; 16 | } 17 | 18 | HttpManager._init() { 19 | _httpClient ??= HttpClient(); 20 | } 21 | 22 | static HttpClient? get httpClient => _httpClient; 23 | } 24 | -------------------------------------------------------------------------------- /lib/app_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | class AppBinding extends Bindings { 4 | @override 5 | void dependencies() { 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /lib/app_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | class AppController extends GetxController { 5 | var screenColor = Colors.white; 6 | var screenColorModel = BlendMode.darken; 7 | setScreenStyle(Color color, {BlendMode mode = BlendMode.darken}) { 8 | screenColor = color; 9 | screenColorModel = mode; 10 | update(["fullScreen"]); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/db/base_db_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/db/sql_manager.dart'; 2 | import 'package:book_app/model/base.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | 7 | abstract class BaseDbProvider { 8 | bool isTableExits = false; 9 | 10 | createTableString(); 11 | 12 | tableName(); 13 | 14 | ///创建表sql语句 15 | tableBaseString(String sql) { 16 | return sql; 17 | } 18 | 19 | Future getDataBase() async { 20 | return await open(); 21 | } 22 | 23 | ///super 函数对父类进行初始化 24 | @mustCallSuper 25 | prepare(name, String createSql) async { 26 | isTableExits = await SqlManager.isTableExits(name); 27 | if (!isTableExits) { 28 | Database db = await SqlManager.getCurrentDatabase(); 29 | 30 | return await db.execute(createSql); 31 | } 32 | } 33 | 34 | @mustCallSuper 35 | open() async { 36 | if (!isTableExits) { 37 | await prepare(tableName(), createTableString()); 38 | } 39 | 40 | return await SqlManager.getCurrentDatabase(); 41 | } 42 | 43 | Future commonDelete(id) async { 44 | Database db = await getDataBase(); 45 | await db.delete(tableName(), where: "id = ?", whereArgs: [id]); 46 | } 47 | 48 | Future commonInsert(Base base) async { 49 | Database db = await getDataBase(); 50 | 51 | return await db.insert(tableName(), base.toJson()); 52 | } 53 | 54 | Future commonUpdate(Base base) async { 55 | Database db = await getDataBase(); 56 | await db.update(tableName(), base.toJson(), where: "id = ?", whereArgs: [base.id]); 57 | } 58 | 59 | Future commonBatchInsert(List list) async { 60 | Database db = await getDataBase(); 61 | var batch = db.batch(); 62 | for (var element in list) { 63 | batch.insert(tableName(), element.toJson()); 64 | } 65 | batch.commit(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/db/sql_manager.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart'; 2 | import 'package:sqflite/sqflite.dart'; 3 | 4 | class SqlManager { 5 | static const _version = 1; 6 | static const _dataBaseName = "book"; 7 | static late Database _database; 8 | 9 | 10 | static init() async { 11 | var databasesPath = await getDatabasesPath(); 12 | String path = join(databasesPath, _dataBaseName); 13 | _database = await openDatabase(path, version: _version, onCreate: (Database db, int version) async{}); 14 | } 15 | 16 | static Future getCurrentDatabase() async { 17 | return _database; 18 | } 19 | 20 | static isTableExits(String tableName) async { 21 | await getCurrentDatabase(); 22 | var res = await _database.rawQuery("select * from Sqlite_master where type = 'table' and name = '$tableName'"); 23 | 24 | return res.isNotEmpty; 25 | } 26 | 27 | static close() { 28 | _database.close(); 29 | } 30 | } -------------------------------------------------------------------------------- /lib/di.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/api/http_manager.dart'; 2 | import 'package:book_app/db/sql_manager.dart'; 3 | import 'package:book_app/util/save_util.dart'; 4 | class DependencyInjection { 5 | static Future init() async { 6 | await SqlManager.init(); 7 | await SaveUtil.init(); 8 | HttpManager.getInstance(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/log/log.dart: -------------------------------------------------------------------------------- 1 | import 'package:logger/logger.dart'; 2 | 3 | class Log { 4 | static final Logger _logger = Logger( 5 | printer: PrefixPrinter(PrettyPrinter()), 6 | ); 7 | 8 | static void v(dynamic message) { 9 | _logger.v(message); 10 | } 11 | 12 | static void d(dynamic message) { 13 | _logger.d(message); 14 | } 15 | 16 | static void i(dynamic message) { 17 | _logger.i(message); 18 | } 19 | 20 | static void w(dynamic message) { 21 | _logger.w(message); 22 | } 23 | 24 | static void e(dynamic message) { 25 | _logger.e(message); 26 | } 27 | 28 | static void wtf(dynamic message) { 29 | _logger.wtf(message); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/app_binding.dart'; 2 | import 'package:book_app/app_controller.dart'; 3 | import 'package:book_app/di.dart'; 4 | import 'package:book_app/route/route_pages.dart'; 5 | import 'package:book_app/route/routes.dart'; 6 | import 'package:book_app/util/bar_util.dart'; 7 | import 'package:book_app/util/constant.dart'; 8 | import 'package:book_app/util/dialog_build.dart'; 9 | import 'package:book_app/util/save_util.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 12 | import 'package:get/get.dart'; 13 | 14 | void main() async { 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | await DependencyInjection.init(); 17 | runApp(const App()); 18 | transparentBar(); 19 | } 20 | 21 | class App extends StatelessWidget { 22 | const App({Key? key}) : super(key: key); 23 | 24 | // This widget is the root of your application. 25 | @override 26 | Widget build(BuildContext context) { 27 | Get.put(AppController()); 28 | return GetBuilder( 29 | id: "fullScreen", 30 | builder: (controller) { 31 | return ColorFiltered( 32 | colorFilter: ColorFilter.mode(controller.screenColor, controller.screenColorModel), 33 | child: GetMaterialApp( 34 | debugShowCheckedModeBanner: false, 35 | enableLog: true, 36 | initialRoute: Routes.bookHome, 37 | defaultTransition: Transition.rightToLeft, 38 | getPages: RoutePages.routes, 39 | initialBinding: AppBinding(), 40 | smartManagement: SmartManagement.keepFactory, 41 | title: '轻阅读', 42 | builder: EasyLoading.init(), 43 | ), 44 | ); 45 | }, 46 | ); 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lib/mapper/book_db_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/db/base_db_provider.dart'; 2 | import 'package:book_app/model/book/book.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | class BookDbProvider extends BaseDbProvider { 6 | /// 表名 7 | final String name = "book"; 8 | 9 | /// 表字段 10 | /// 主键 11 | final String columnId = "id"; 12 | /// 书名 13 | final String columnName = "name"; 14 | /// 书的介绍 15 | final String columnDescription = "description"; 16 | /// 书的作者 17 | final String columnAuthor = "author"; 18 | /// 书的封面 19 | final String columnIndex = "indexImg"; 20 | /// 当前阅读的章节 21 | final String columnCurChapter = "curChapter"; 22 | /// 当前阅读的章节里的页数 23 | final String columnCurPage = "curPage"; 24 | final String columnUrl = "url"; 25 | final String columnType = "type"; 26 | final String columnUpdateTime = "updateTime"; 27 | @override 28 | createTableString() { 29 | return ''' 30 | create table $name 31 | ( 32 | $columnId integer primary key AUTOINCREMENT, 33 | $columnName text not null, 34 | $columnDescription text, 35 | $columnAuthor text, 36 | $columnIndex text, 37 | $columnCurChapter integer, 38 | $columnCurPage integer, 39 | $columnUrl text not null, 40 | $columnType integer not null, 41 | $columnUpdateTime text 42 | ) 43 | '''; 44 | } 45 | 46 | @override 47 | tableName() { 48 | return name; 49 | } 50 | 51 | /// 获取书籍详情 52 | Future getBookById(id) async { 53 | Database db = await getDataBase(); 54 | List> maps = await db.rawQuery("select * from $name where $columnId = $id"); 55 | if (maps.isEmpty) { 56 | return null; 57 | } 58 | return Book.fromJson(maps[0]); 59 | } 60 | 61 | /// 获取书籍列表详情 62 | Future> getBooks() async { 63 | Database db = await getDataBase(); 64 | List> maps = await db.rawQuery("select * from $name order by $columnId asc"); 65 | List list = []; 66 | for (var element in maps) { 67 | list.add(Book.fromJson(element)); 68 | } 69 | return list; 70 | } 71 | /// 获取书籍数量 72 | Future getBookCount(url) async { 73 | Database db = await getDataBase(); 74 | return Sqflite.firstIntValue(await db.rawQuery("select count(*) from $name where $columnUrl = ?", [url])); 75 | } 76 | 77 | /// 更新阅读章节 78 | updateCurChapter(id, chapterId, page) async{ 79 | Database db = await getDataBase(); 80 | await db.rawUpdate("update $name set $columnCurChapter = ?, $columnCurPage = ? where $columnId = ?", [chapterId, page, id]); 81 | } 82 | 83 | /// 更新名称 84 | updateName(id, newName) async{ 85 | Database db = await getDataBase(); 86 | await db.rawUpdate("update $name set $columnName = ? where $columnId = ?", [newName, id]); 87 | } 88 | 89 | updateTime(id, String updateTime) async{ 90 | Database db = await getDataBase(); 91 | await db.rawUpdate("update $name set $columnUpdateTime = ? where $columnId = ?", [updateTime, id]); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /lib/mapper/chapter_db_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/db/base_db_provider.dart'; 2 | import 'package:book_app/model/chapter/chapter.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | /// 章节 6 | class ChapterDbProvider extends BaseDbProvider { 7 | /// 表名 8 | final String name = "chapter"; 9 | 10 | /// 表字段 11 | /// 主键 12 | final String columnId = "id"; 13 | final String columnBookId = "bookId"; 14 | /// 书名 15 | final String columnName = "name"; 16 | /// 书的介绍 17 | final String columnContent = "content"; 18 | final String columnUrl = "url"; 19 | @override 20 | createTableString() { 21 | return ''' 22 | create table $name 23 | ( 24 | $columnId integer primary key AUTOINCREMENT, 25 | $columnName text not null, 26 | $columnBookId integer not null, 27 | $columnContent text, 28 | $columnUrl text not null 29 | ) 30 | '''; 31 | } 32 | 33 | @override 34 | tableName() { 35 | return name; 36 | } 37 | 38 | /// 获取章节详情 39 | Future getChapterById(id) async { 40 | Database db = await getDataBase(); 41 | List> maps = await db.rawQuery("select * from $name where $columnId = $id"); 42 | if (maps.isEmpty) { 43 | return null; 44 | } 45 | return Chapter.fromJson(maps[0]); 46 | } 47 | 48 | /// 获取章节列表详情 49 | Future> getChapters(startId, bookId) async { 50 | Database db = await getDataBase(); 51 | List> maps = await db.rawQuery("select $columnId, $columnName, $columnUrl from $name where $columnBookId = ? ${startId == null ? '' : 'and id >= $startId' } order by $columnId asc", [bookId]); 52 | List list = []; 53 | for (var element in maps) { 54 | list.add(Chapter.fromJson(element)); 55 | } 56 | return list; 57 | } 58 | 59 | Future getNextChapter(startId, bookId) async { 60 | Database db = await getDataBase(); 61 | List> maps = await db.rawQuery("select $columnId, $columnName, $columnUrl from $name where $columnBookId = ? and id > ? order by $columnId asc limit 1", [bookId, startId]); 62 | List list = []; 63 | for (var element in maps) { 64 | list.add(Chapter.fromJson(element)); 65 | } 66 | if (list.isEmpty) { 67 | return null; 68 | } 69 | return list.first; 70 | } 71 | 72 | /// 获取章节列表详情 73 | Future getChapterCount(bookId) async { 74 | Database db = await getDataBase(); 75 | return Sqflite.firstIntValue(await db.rawQuery("select count(*) from $name where $columnBookId = ?", [bookId])); 76 | } 77 | 78 | /// 获取当前章节位置 79 | Future getCurChapterCount(bookId, chapterId) async { 80 | if (chapterId == null) { 81 | return 0; 82 | } 83 | Database db = await getDataBase(); 84 | return Sqflite.firstIntValue(await db.rawQuery("select count(*) from $name where $columnBookId = ? and $columnId <= ?", [bookId, chapterId])); 85 | } 86 | 87 | Future updateContent(id, content) async{ 88 | Database db = await getDataBase(); 89 | await db.rawUpdate( 90 | ''' 91 | update $name 92 | set $columnContent = ? 93 | where $columnId = ? 94 | ''', 95 | [content, id] 96 | ); 97 | } 98 | 99 | Future deleteByBookId(bookId) async{ 100 | Database db = await getDataBase(); 101 | await db.rawDelete( 102 | ''' 103 | delete from $name 104 | where $columnBookId = ? 105 | ''', 106 | [bookId] 107 | ); 108 | } 109 | 110 | Future getNext(bookId, curChapterId) async{ 111 | Database db = await getDataBase(); 112 | List> maps = await db.rawQuery("select * from $name where $columnBookId = $bookId and $columnId > $curChapterId limit 1"); 113 | if (maps.isEmpty) { 114 | return null; 115 | } 116 | return Chapter.fromJson(maps[0]); 117 | } 118 | 119 | Future getUnCacheCount(int id) async{ 120 | Database db = await getDataBase(); 121 | return Sqflite.firstIntValue(await db.rawQuery("select count(*) from $name where $columnBookId = ? and ($columnContent is null or trim($columnContent) = '')", [id])); 122 | } 123 | 124 | Future> getChaptersWithContent(int bookId) async{ 125 | Database db = await getDataBase(); 126 | List> maps = await db.rawQuery("select $columnId, $columnName, $columnContent, $columnUrl from $name where $columnBookId = ? order by $columnId asc", [bookId]); 127 | List list = []; 128 | for (var element in maps) { 129 | list.add(Chapter.fromJson(element)); 130 | } 131 | return list; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/model/base.dart: -------------------------------------------------------------------------------- 1 | abstract class Base { 2 | int? id; 3 | Map toJson(); 4 | } -------------------------------------------------------------------------------- /lib/model/book/book.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/base.dart'; 2 | import 'package:book_app/model/chapter/chapter.dart'; 3 | import 'package:json_annotation/json_annotation.dart'; 4 | part 'book.g.dart'; 5 | 6 | @JsonSerializable() 7 | class Book extends Base{ 8 | int? id; 9 | String? name; 10 | String? description; 11 | String? author; 12 | String? indexImg; 13 | int? curChapter; 14 | int? curPage; 15 | String? url; 16 | /// 1-网络 2-本地 3-文本分享 17 | int? type; 18 | List? chapters; 19 | String? updateTime; 20 | String? curTotal; 21 | 22 | Book({this.id, this.name, this.description, this.author, this.indexImg, 23 | this.curChapter, this.curPage, this.url, this.type = 1, this.chapters, this.updateTime}); 24 | 25 | factory Book.fromJson(Map json) => _$BookFromJson(json); 26 | 27 | @override 28 | Map toJson() => { 29 | 'id': id, 30 | 'name': name, 31 | 'description': description, 32 | 'author': author, 33 | 'indexImg': indexImg, 34 | 'curChapter': curChapter, 35 | 'curPage': curPage, 36 | 'url': url, 37 | 'type': type, 38 | 'updateTime': updateTime, 39 | }; 40 | 41 | @override 42 | String toString() { 43 | return 'Book{id: $id, name: $name, description: $description, author: $author, indexImg: $indexImg, curChapter: $curChapter, curPage: $curPage, url: $url, type: $type, updateTime: $updateTime}'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/model/book/book.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'book.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Book _$BookFromJson(Map json) => Book( 10 | id: json['id'] as int?, 11 | name: json['name'] as String?, 12 | description: json['description'] as String?, 13 | author: json['author'] as String?, 14 | indexImg: json['indexImg'] as String?, 15 | updateTime: json['updateTime'] as String?, 16 | curChapter: json['curChapter'] as int?, 17 | curPage: json['curPage'] as int?, 18 | url: json['url'] as String?, 19 | type: json['type'] as int? ?? 1, 20 | chapters: (json['chapters'] as List?) 21 | ?.map((e) => Chapter.fromJson(e as Map)) 22 | .toList(), 23 | ); 24 | 25 | Map _$BookToJson(Book instance) => { 26 | 'id': instance.id, 27 | 'name': instance.name, 28 | 'description': instance.description, 29 | 'author': instance.author, 30 | 'indexImg': instance.indexImg, 31 | 'curChapter': instance.curChapter, 32 | 'curPage': instance.curPage, 33 | 'url': instance.url, 34 | 'type': instance.type, 35 | 'chapters': instance.chapters, 36 | 'updateTime': instance.updateTime, 37 | }; 38 | -------------------------------------------------------------------------------- /lib/model/book_with_chapters.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:book_app/model/book/book.dart'; 4 | import 'package:book_app/model/chapter/chapter.dart'; 5 | 6 | class BookWithChapters { 7 | /// 书 8 | final Book _book; 9 | /// 章节 10 | final List _chapters; 11 | /// 已下载章节 12 | final List _downloadChapters = []; 13 | /// 通知 14 | Timer? _downloadTimer; 15 | /// 下载完成 16 | bool _downloadComplete = false; 17 | /// 是否中断 18 | bool _interruptDownload = false; 19 | BookWithChapters(this._book, this._chapters); 20 | 21 | /// 下载过程回调 22 | /// 书-已下载数-总数-是否完成 23 | void downloadCallback(Function(Book, int, int, bool) downloadCallback) { 24 | if (complete) { 25 | downloadCallback(_book, _downloadChapters.length, _chapters.length, _downloadComplete); 26 | _downloadTimer?.cancel(); 27 | return; 28 | } 29 | _downloadTimer?.cancel(); 30 | _downloadTimer = Timer.periodic(const Duration(seconds: 1), (timer) { 31 | downloadCallback(_book, _downloadChapters.length, _chapters.length, _downloadComplete); 32 | }); 33 | } 34 | 35 | void dispose() { 36 | _downloadTimer?.cancel(); 37 | } 38 | 39 | void downloadChaptersAdd(Chapter chapter) { 40 | _downloadChapters.add(chapter); 41 | } 42 | 43 | /// 下载完成 44 | void downloadComplete(bool complete) { 45 | _downloadComplete = complete; 46 | } 47 | 48 | /// 中断 49 | void interrupt() { 50 | dispose(); 51 | _interruptDownload = true; 52 | } 53 | 54 | bool get complete => _downloadComplete; 55 | 56 | Book get book => _book; 57 | 58 | List get chapters => _chapters; 59 | 60 | bool get interruptDownload => _interruptDownload; 61 | } -------------------------------------------------------------------------------- /lib/model/chapter/chapter.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/base.dart'; 2 | import 'package:json_annotation/json_annotation.dart'; 3 | 4 | part 'chapter.g.dart'; 5 | @JsonSerializable() 6 | class Chapter extends Base{ 7 | int? id; 8 | int? bookId; 9 | String? name; 10 | String? content; 11 | String? url; 12 | @JsonKey(ignore: true) 13 | double height; 14 | 15 | Chapter({this.id, this.bookId, this.name, this.content, this.url, this.height = 0}); 16 | 17 | factory Chapter.fromJson(Map json) => _$ChapterFromJson(json); 18 | 19 | @override 20 | Map toJson() => _$ChapterToJson(this); 21 | 22 | @override 23 | String toString() { 24 | return 'Chapter{id: $id, bookId: $bookId, name: $name, content: $content, url: $url}'; 25 | } 26 | 27 | @override 28 | int get hashCode => name?.hashCode ?? url.hashCode; 29 | 30 | @override 31 | bool operator ==(Object other) { 32 | if (other is! Chapter) { 33 | return false; 34 | } 35 | if (name == other.name || url == other.url) { 36 | return true; 37 | } 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/model/chapter/chapter.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'chapter.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Chapter _$ChapterFromJson(Map json) => Chapter( 10 | id: json['id'] as int?, 11 | bookId: json['bookId'] as int?, 12 | name: json['name'] as String?, 13 | content: json['content'] as String?, 14 | url: json['url'] as String?, 15 | ); 16 | 17 | Map _$ChapterToJson(Chapter instance) => { 18 | 'id': instance.id, 19 | 'bookId': instance.bookId, 20 | 'name': instance.name, 21 | 'content': instance.content, 22 | 'url': instance.url, 23 | }; 24 | -------------------------------------------------------------------------------- /lib/model/message.dart: -------------------------------------------------------------------------------- 1 | class Message { 2 | final MessageType type; 3 | dynamic data; 4 | 5 | Message(this.type, this.data); 6 | 7 | } 8 | 9 | enum MessageType { 10 | parseNetworkBook, 11 | parseTextBook, 12 | killParse 13 | } -------------------------------------------------------------------------------- /lib/model/read_page_type.dart: -------------------------------------------------------------------------------- 1 | /// 阅读页样式 2 | enum ReadPageType { 3 | /// 平滑 4 | // smooth, 5 | /// 平滑动画 6 | /// another_transformer_page_view 7 | // smooth_1, 8 | // smooth_2, 9 | // smooth_3, 10 | // smooth_4, 11 | // smooth_5, 12 | // smooth_6, 13 | // /// 覆盖 14 | // cover, 15 | // /// 仿真 16 | // emulation, 17 | /// 点击 18 | point, 19 | /// 滑动翻页 20 | slide, 21 | /// 上下滑动 22 | slideUpDown, 23 | /// 上下平滑 24 | // list, 25 | } 26 | ReadPageType getReadPageTypeByStr(String? str) { 27 | switch(str) { 28 | case "slide": 29 | return ReadPageType.slide; 30 | case "slideUpDown": 31 | return ReadPageType.slideUpDown; 32 | case "point": 33 | return ReadPageType.point; 34 | } 35 | return ReadPageType.slide; 36 | } 37 | -------------------------------------------------------------------------------- /lib/model/search/search_history.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert' as convert; 2 | 3 | import 'package:book_app/model/base.dart'; 4 | 5 | class SearchHistory extends Base{ 6 | String? label; 7 | String? site; 8 | 9 | SearchHistory({this.label, this.site}); 10 | 11 | @override 12 | Map toJson() => { 13 | 'label': label, 14 | 'site': site, 15 | }; 16 | factory SearchHistory.fromJson(Map json) => SearchHistory(site: json["site"], label: json["label"]); 17 | static List fromList(List? data) { 18 | List res = []; 19 | if (data == null) { 20 | return res; 21 | } 22 | for (var value in data) { 23 | res.add(SearchHistory.fromJson(convert.jsonDecode(value))); 24 | } 25 | return res; 26 | } 27 | 28 | @override 29 | String toString() { 30 | return 'SearchHistory{label: $label, site: $site}'; 31 | } 32 | static List defaultList() { 33 | List res = []; 34 | res.add(SearchHistory(label: "神马小说", site: "https://quark.sm.cn/s?q=%s&from=smor&safe=1")); 35 | res.add(SearchHistory(label: "360搜索", site: "https://m.so.com/s?q=%s")); 36 | res.add(SearchHistory(label: "必应搜索", site: "https://cn.bing.com/search?q=%s")); 37 | return res; 38 | } 39 | } -------------------------------------------------------------------------------- /lib/module/book/home/book_home_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:get/get.dart'; 2 | 3 | import 'book_home_controller.dart'; 4 | 5 | 6 | class BookHomeBinding implements Bindings { 7 | @override 8 | void dependencies() { 9 | Get.lazyPut(() => BookHomeController()); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/module/book/read/component/content.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/component/content_bottom.dart'; 2 | import 'package:book_app/module/book/read/read_controller.dart'; 3 | import 'package:book_app/theme/color.dart'; 4 | import 'package:book_app/util/system_utils.dart'; 5 | import 'package:book_app/util/toast.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | Widget content(context, index, ReadController controller) { 9 | if (controller.pages.isEmpty || controller.pages.length < index) { 10 | return Container(); 11 | } 12 | return Column( 13 | children: [ 14 | SizedBox(height: controller.rotateScreen ? controller.pageGen.screenTop / 2 : controller.pageGen.screenTop,), 15 | Expanded( 16 | child: SizedBox( 17 | width: controller.pages[index].width, 18 | child: Text.rich( 19 | TextSpan( 20 | children: [ 21 | if (controller.pages[index].index == 1) 22 | TextSpan( 23 | text: "${controller.pages[index].chapterName}\n", 24 | style: TextStyle( 25 | color: hexToColor(controller.readSettingConfig.fontColor), 26 | fontSize: controller.pageGen.titleStyle.fontSize, 27 | fontWeight: controller.pageGen.titleStyle.fontWeight, 28 | fontFamily: controller.pageGen.titleStyle.fontFamily 29 | ), 30 | ), 31 | TextSpan( 32 | text: controller.pages[index].content, 33 | style: TextStyle( 34 | color: hexToColor(controller.readSettingConfig.fontColor), 35 | fontSize: controller.pageGen.contentStyle.fontSize, 36 | fontWeight: controller.pageGen.contentStyle.fontWeight, 37 | fontFamily: controller.pageGen.contentStyle.fontFamily 38 | )), 39 | ] 40 | ), 41 | textAlign: TextAlign.start, 42 | textScaleFactor: MediaQuery.of(globalContext).textScaleFactor, 43 | textWidthBasis: TextWidthBasis.longestLine, 44 | locale: WidgetsBinding.instance.window.locale, 45 | textDirection: TextDirection.ltr, 46 | strutStyle: StrutStyle( 47 | forceStrutHeight: true, 48 | fontSize: controller.pageGen.contentStyle.fontSize, 49 | height: controller.pageGen.contentStyle.height, 50 | ), 51 | ), 52 | ), 53 | ), 54 | if (controller.pages[index].noContent) 55 | Container( 56 | alignment: Alignment.center, 57 | child: GestureDetector( 58 | child: Row( 59 | mainAxisAlignment: MainAxisAlignment.center, 60 | children: [ 61 | Icon(Icons.refresh, color: Theme.of(context).primaryColor, size: 25,), 62 | Text("重新加载", style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 20, height: 1),) 63 | ], 64 | ), 65 | onTap: () async { 66 | if (controller.book!.type == 1) { 67 | await controller.reloadPage(); 68 | } else { 69 | Toast.toast(toast: "本地导入文章,无法加载"); 70 | } 71 | }, 72 | ), 73 | ), 74 | contentBottom(context, index, controller), 75 | ], 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /lib/module/book/read/component/content_bottom.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:book_app/module/book/read/read_controller.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | Widget contentBottom(context, index, ReadController controller) { 6 | return Container( 7 | height: controller.pageGen.screenBottom, 8 | width: MediaQuery.of(context).size.width, 9 | margin: const EdgeInsets.only(bottom: 4), 10 | child: Row( 11 | children: [ 12 | const SizedBox(width: 25,), 13 | Expanded( 14 | child: Text( 15 | "${controller.pages[index].chapterName}", 16 | maxLines: 1, 17 | style: const TextStyle(fontSize: 12, color: Colors.grey), 18 | ), 19 | ), 20 | Text( 21 | "${controller.pages[index].index}/${controller.calThisChapterTotalPage(index)}", 22 | style: const TextStyle(fontSize: 12, color: Colors.grey), 23 | ), 24 | const SizedBox(width: 25,), 25 | ], 26 | ), 27 | ); 28 | } -------------------------------------------------------------------------------- /lib/module/book/read/component/content_gen.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/mapper/chapter_db_provider.dart'; 2 | import 'package:book_app/model/book/book.dart'; 3 | import 'package:book_app/model/chapter/chapter.dart'; 4 | import 'package:book_app/util/font_util.dart'; 5 | import 'package:book_app/util/html_parse_util.dart'; 6 | 7 | 8 | final ChapterDbProvider _chapterDbProvider = ChapterDbProvider(); 9 | 10 | contentGen(Chapter chapter, Book book) async{ 11 | await _getContent(chapter, book); 12 | } 13 | 14 | /// 获取章节内容 15 | _getContent(Chapter chapter, Book book) async{ 16 | // 查找内容 17 | Chapter? temp = await _chapterDbProvider.getChapterById(chapter.id); 18 | var content = temp?.content; 19 | if ((content == null || content.isEmpty) && book.type == 1) { 20 | Chapter? nextChapter = await _chapterDbProvider.getNextChapter(chapter.id, book.id); 21 | content = await HtmlParseUtil.parseContent(chapter.name!, chapter.url!, nextChapter?.url); 22 | // 格式化文本 23 | content = FontUtil.formatContent(content); 24 | await _chapterDbProvider.updateContent(chapter.id, content); 25 | } 26 | // 赋值 27 | chapter.content = content; 28 | } -------------------------------------------------------------------------------- /lib/module/book/read/component/content_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ContentPage { 4 | String content; 5 | int index; 6 | int? chapterId; 7 | String? chapterName; 8 | double width; 9 | double height; 10 | bool noContent; 11 | TextStyle textStyle; 12 | 13 | ContentPage(this.content, this.index, this.chapterId, this.chapterName, this.width, this.height, this.textStyle, {this.noContent = false}); 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /lib/module/book/read/component/cover.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/read_controller.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import 'content.dart'; 6 | /// 覆盖 7 | Widget cover() { 8 | return GetBuilder( 9 | id: ReadRefreshKey.content, 10 | builder: (controller) { 11 | return Listener( 12 | child: Container(), 13 | // onPointerDown: (e) { 14 | // controller.autoPageCancel(); 15 | // controller.xMove = e.position.dx; 16 | // }, 17 | // onPointerUp: (e) async { 18 | // double move = e.position.dx - controller.xMove; 19 | // // 滑动了五十距离, 且当前为0 20 | // if (move > 50 && controller.pageIndex.count == 0) { 21 | // await controller.prePage(); 22 | // } else if (move < -50 && 23 | // controller.pageIndex.count == controller.pages.length - 1) { 24 | // await controller.nextPage(); 25 | // } 26 | // }, 27 | ); 28 | }, 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/module/book/read/component/custom_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomDrawer extends StatelessWidget { 4 | final double elevation; 5 | final Widget child; 6 | final double widthPercent; 7 | const CustomDrawer({ 8 | Key? key, 9 | this.elevation = 16.0, 10 | required this.child, 11 | this.widthPercent = 0.7, 12 | }) : assert( 13 | widthPercent < 1.0 && widthPercent > 0.0), 14 | super(key: key); 15 | @override 16 | Widget build(BuildContext context) { 17 | assert(debugCheckHasMaterialLocalizations(context)); 18 | final double _width = MediaQuery.of(context).size.width * widthPercent; 19 | return Semantics( 20 | scopesRoute: true, 21 | namesRoute: true, 22 | explicitChildNodes: true, 23 | child: ConstrainedBox( 24 | constraints: BoxConstraints.expand(width: _width), 25 | child: Material( 26 | elevation: elevation, 27 | child: child, 28 | color: Colors.black, 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/module/book/read/component/drawer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:book_app/model/chapter/chapter.dart'; 4 | import 'package:book_app/module/book/read/read_controller.dart'; 5 | import 'package:book_app/util/limit_util.dart'; 6 | import 'package:book_app/util/no_shadow_scroll_behavior.dart'; 7 | import 'package:draggable_scrollbar/draggable_scrollbar.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:get/get.dart'; 11 | 12 | import 'custom_drawer.dart'; 13 | class Drawer extends GetView { 14 | Drawer({Key? key}) : super(key: key); 15 | final ScrollController scrollController = ScrollController(); 16 | @override 17 | Widget build(BuildContext context) { 18 | return GetBuilder( 19 | id: "drawer", 20 | builder: (controller) { 21 | Future.delayed(const Duration(milliseconds: 100), (){ 22 | scrollController.jumpTo(controller.pages.isEmpty ? 0 : (controller.chapters.indexWhere((element) => controller.pages[controller.pageIndex.count].chapterId == element.id)) * 41); 23 | }); 24 | return Container( 25 | width: MediaQuery.of(context).size.width * .7, 26 | color: Colors.black, 27 | child: Column( 28 | crossAxisAlignment: CrossAxisAlignment.start, 29 | children: [ 30 | Container( 31 | height: 41, 32 | alignment: Alignment.centerLeft, 33 | margin: 34 | const EdgeInsets.only(left: 15), 35 | child: Text( 36 | "共${controller.chapters.length}章", 37 | style: 38 | const TextStyle(fontSize: 14, color: Colors.grey), 39 | ), 40 | ), 41 | MediaQuery.removePadding( 42 | context: context, 43 | child: Expanded( 44 | child: DraggableScrollbar.rrect( 45 | controller: scrollController, 46 | child: ListView.builder( 47 | itemBuilder: 48 | (context, index) { 49 | return InkWell( 50 | child: Container( 51 | padding: 52 | const EdgeInsets.only(left: 10, right: 20), 53 | alignment: Alignment.centerLeft, 54 | child: Text( 55 | "${controller.chapters[index] 56 | .name}", 57 | maxLines: 2, 58 | style: 59 | controller.pages.isEmpty ? null : controller.pages[controller.pageIndex.count].chapterId == 60 | controller.chapters[index].id 61 | ? const TextStyle( 62 | color: Colors.lightBlue) 63 | : const TextStyle( 64 | color: Colors.grey), 65 | ), 66 | ), 67 | onTap: () async { 68 | ReadController controller = Get.find(); 69 | await controller.jumpChapter(controller.chapters.indexWhere((element) => element.id == controller.chapters[index].id), pop: false, clearCount: true); 70 | controller.zoomDrawerController.toggle?.call(); 71 | }, 72 | ); 73 | }, 74 | itemCount: controller.chapters.length, 75 | controller: scrollController, 76 | itemExtent: 41, 77 | ), 78 | ) 79 | ), 80 | removeTop: true, 81 | ) 82 | ], 83 | ), 84 | ); 85 | }, 86 | ); 87 | } 88 | } -------------------------------------------------------------------------------- /lib/module/book/read/component/list.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/read_controller.dart'; 2 | import 'package:book_app/theme/color.dart'; 3 | import 'package:book_app/util/toast.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | Widget list(context, ReadController controller) { 7 | return Column( 8 | children: [ 9 | SizedBox(height: controller.rotateScreen ? controller.pageGen.screenTop / 2 : controller.pageGen.screenTop,), 10 | Expanded( 11 | child: SizedBox( 12 | width: 0, 13 | child: ListView.builder( 14 | itemBuilder: (context, index) { 15 | if (controller.pages[index].noContent) { 16 | return Container( 17 | alignment: Alignment.center, 18 | child: GestureDetector( 19 | child: Row( 20 | mainAxisAlignment: MainAxisAlignment.center, 21 | children: [ 22 | Icon(Icons.refresh, color: Theme.of(context).primaryColor, size: 25,), 23 | Text("重新加载", style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 20, height: 1),) 24 | ], 25 | ), 26 | onTap: () async { 27 | if (controller.book!.type == 1) { 28 | await controller.reloadPage(); 29 | } else { 30 | Toast.toast(toast: "本地导入文章,无法加载"); 31 | } 32 | }, 33 | ), 34 | ); 35 | } 36 | return Text.rich( 37 | TextSpan( 38 | children: [ 39 | if (controller.pages[index].index == 1) 40 | TextSpan( 41 | text: "${controller.pages[index].chapterName}\n", 42 | style: TextStyle( 43 | color: hexToColor(controller.readSettingConfig.fontColor), 44 | fontSize: controller.pageGen.titleStyle.fontSize, 45 | fontWeight: controller.pageGen.titleStyle.fontWeight, 46 | fontFamily: controller.pageGen.titleStyle.fontFamily 47 | ), 48 | ), 49 | TextSpan( 50 | text: controller.pages[index].content, 51 | style: TextStyle( 52 | color: hexToColor(controller.readSettingConfig.fontColor), 53 | fontSize: controller.pageGen.contentStyle.fontSize, 54 | fontWeight: controller.pageGen.contentStyle.fontWeight, 55 | fontFamily: controller.pageGen.contentStyle.fontFamily 56 | )), 57 | ] 58 | ), 59 | textAlign: TextAlign.start, 60 | textScaleFactor: MediaQuery.of(context).textScaleFactor, 61 | textWidthBasis: TextWidthBasis.longestLine, 62 | locale: WidgetsBinding.instance.window.locale, 63 | textDirection: TextDirection.ltr, 64 | strutStyle: StrutStyle( 65 | forceStrutHeight: true, 66 | fontSize: controller.pageGen.contentStyle.fontSize, 67 | height: controller.pageGen.contentStyle.height, 68 | ), 69 | ); 70 | }, 71 | itemCount: controller.pages.length, 72 | ), 73 | ), 74 | ), 75 | Container( 76 | height: controller.pageGen.screenBottom, 77 | width: MediaQuery.of(context).size.width, 78 | margin: const EdgeInsets.only(bottom: 4), 79 | child: Row( 80 | children: [ 81 | const SizedBox(width: 25,), 82 | Expanded( 83 | child: Text( 84 | "name", 85 | maxLines: 1, 86 | style: const TextStyle(fontSize: 12, color: Colors.grey), 87 | ), 88 | ), 89 | Text( 90 | "index", 91 | style: const TextStyle(fontSize: 12, color: Colors.grey), 92 | ), 93 | const SizedBox(width: 25,), 94 | ], 95 | ), 96 | ) 97 | ], 98 | ); 99 | } -------------------------------------------------------------------------------- /lib/module/book/read/component/my_text_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/component/content_page.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class MyTextPainter extends CustomPainter { 5 | final TextPainter _painter; 6 | final ContentPage contentPage; 7 | 8 | 9 | MyTextPainter(this._painter, this.contentPage); 10 | @override 11 | void paint(Canvas canvas, Size size) { 12 | _painter.text = TextSpan( 13 | text: contentPage.content, 14 | style: contentPage.textStyle 15 | ); 16 | } 17 | 18 | @override 19 | bool shouldRepaint(covariant MyTextPainter oldDelegate) { 20 | return contentPage.index != oldDelegate.contentPage.index; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /lib/module/book/read/component/page_gen.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/log/log.dart'; 2 | import 'package:book_app/model/book/book.dart'; 3 | import 'package:book_app/model/chapter/chapter.dart'; 4 | import 'package:book_app/module/book/read/component/content_gen.dart'; 5 | import 'package:book_app/module/book/readSetting/component/read_setting_config.dart'; 6 | import 'package:book_app/theme/color.dart'; 7 | import 'package:book_app/util/font_util.dart'; 8 | import 'package:book_app/util/system_utils.dart'; 9 | import 'package:flutter/material.dart'; 10 | 11 | import 'content_page.dart'; 12 | 13 | class PageGen{ 14 | final TextPainter _painter = TextPainter( 15 | textAlign: TextAlign.start, 16 | textDirection: TextDirection.ltr, 17 | locale: WidgetsBinding.instance.window.locale, 18 | textScaleFactor: MediaQuery.of(globalContext).textScaleFactor, 19 | textWidthBasis: TextWidthBasis.longestLine 20 | ); 21 | late TextStyle _contentStyle; 22 | late double _screenWidth; 23 | late double _titleHeight; 24 | late double _screenHeight; 25 | late double _screenTop; 26 | late double _screenBottom; 27 | late double _wordHeight; 28 | final double _paddingWidth = 40; 29 | late double _screenLeft; 30 | late double _screenRight; 31 | final TextStyle _titleStyle = const TextStyle( 32 | fontSize: 25, 33 | fontWeight: FontWeight.bold); 34 | 35 | PageGen(ReadSettingConfig readSettingConfig) { 36 | _contentStyle = _readSettingConfigToTextStyle(readSettingConfig); 37 | _initSize(); 38 | } 39 | 40 | TextStyle _readSettingConfigToTextStyle(ReadSettingConfig readSettingConfig) { 41 | return TextStyle( 42 | color: hexToColor(readSettingConfig.fontColor), 43 | fontSize: readSettingConfig.fontSize, 44 | height: readSettingConfig.fontHeight, 45 | fontWeight: FontUtil.intToFontWeight(readSettingConfig.fontWeight), 46 | fontFamily: FontUtil.getFontFamily()); 47 | } 48 | 49 | 50 | Future> genPages(Chapter chapter, Book book, Function(List)? finishFunc) async{ 51 | _painter.strutStyle = StrutStyle( 52 | forceStrutHeight: true, 53 | fontSize: contentStyle.fontSize, 54 | height: contentStyle.height, 55 | ); 56 | await contentGen(chapter, book); 57 | List list = await _genPages(chapter); 58 | if (finishFunc != null) { 59 | finishFunc(list); 60 | } 61 | return list; 62 | } 63 | 64 | Future> _genPages(Chapter chapter) async { 65 | List list = []; 66 | _calTitleHeight(chapter.name); 67 | _calWordHeightAndWidth(); 68 | int maxLines = _calMaxLines(firstPage: true); 69 | String content = chapter.content??""; 70 | if (content.isEmpty) { 71 | list.add( 72 | ContentPage("", 1, chapter.id, chapter.name, _contentWidth(), 0, _contentStyle, noContent: true)); 73 | return list; 74 | } 75 | _painter.text = TextSpan(text: content, style: _contentStyle); 76 | _painter.maxLines = maxLines; 77 | // 统计第一页字符偏移量 78 | _painter.layout(maxWidth: _contentWidth()); 79 | double paintWidth = _painter.width; 80 | double paintHeight = _painter.height; 81 | int offset = 82 | _painter.getPositionForOffset(Offset(paintWidth, paintHeight)).offset; 83 | // 得到第一页偏移量 84 | int i = 1; 85 | do { 86 | String subContent = content.substring(0, offset); 87 | list.add( 88 | ContentPage(subContent, i, chapter.id, chapter.name, _contentWidth(), paintHeight, _contentStyle)); 89 | i++; 90 | if (i == 2) { 91 | maxLines = _calMaxLines(); 92 | } 93 | content = content.substring(offset); 94 | if (content.startsWith("\n")) { 95 | content = content.substring(1); 96 | } 97 | _painter.text = TextSpan(text: content, style: _contentStyle); 98 | _painter.maxLines = maxLines; 99 | _painter.layout(maxWidth: _contentWidth()); 100 | paintWidth = _painter.width; 101 | paintHeight = _painter.height; 102 | offset = 103 | _painter.getPositionForOffset(Offset(paintWidth, paintHeight)).offset; 104 | await Future.delayed(const Duration(microseconds: 200)); 105 | } while (offset < content.characters.length && content.trim().isNotEmpty); 106 | if (offset > 0 && content.trim().isNotEmpty) { 107 | list.add( 108 | ContentPage(content, i, chapter.id, chapter.name, _contentWidth(), paintHeight, _contentStyle)); 109 | } 110 | return list; 111 | } 112 | 113 | 114 | 115 | changeContentStyle(ReadSettingConfig readSettingConfig) { 116 | _contentStyle = _readSettingConfigToTextStyle(readSettingConfig); 117 | } 118 | 119 | 120 | 121 | 122 | _calTitleHeight(String? title) { 123 | _painter.text = TextSpan(text: title, style: _titleStyle); 124 | _painter.layout(maxWidth: _screenWidth); 125 | var cal = _painter.computeLineMetrics()[0]; 126 | _titleHeight = cal.height; 127 | } 128 | double _contentWidth() { 129 | return _screenWidth - _paddingWidth - _screenLeft - _screenRight; 130 | } 131 | 132 | void _initSize() { 133 | MediaQueryData data = MediaQuery.of(globalContext); 134 | _screenWidth = data.size.width; 135 | _screenHeight = data.size.height; 136 | _screenLeft = data.padding.left; 137 | _screenRight = data.padding.right; 138 | _screenBottom = 16; 139 | double top = data.padding.top; 140 | if (top < 33) { 141 | top = 33; 142 | } 143 | _screenTop = top; 144 | } 145 | 146 | /// 计算词宽和词高 147 | _calWordHeightAndWidth() { 148 | _painter.text = TextSpan(text: "哈", style: _contentStyle); 149 | _painter.layout(maxWidth: 100); 150 | var cal = _painter.computeLineMetrics()[0]; 151 | _wordHeight = cal.height; 152 | } 153 | 154 | int _calMaxLines({bool firstPage = false}) { 155 | double extend = 0; 156 | if (firstPage) { 157 | extend = _titleHeight + _wordHeight; 158 | } 159 | double _remainHeight = (_screenHeight - 160 | _screenTop - _screenBottom - extend) % 161 | _wordHeight; 162 | if (_remainHeight < (_wordHeight / 2)) { 163 | _remainHeight = _wordHeight / 2; 164 | } 165 | return (_screenHeight - 166 | _screenTop - _screenBottom - extend - (_remainHeight ~/ 1)) ~/ 167 | _wordHeight; 168 | } 169 | 170 | void heightWidthSwap([bool flag = false]) { 171 | double temp = _screenHeight; 172 | _screenHeight = _screenWidth; 173 | _screenWidth = temp; 174 | if (flag) { 175 | _screenLeft = _screenTop; 176 | _screenRight = _screenTop; 177 | } else { 178 | _screenLeft = 0; 179 | _screenRight = 0; 180 | } 181 | } 182 | 183 | double get screenRight => _screenRight; 184 | 185 | double get screenLeft => _screenLeft; 186 | 187 | double get paddingWidth => _paddingWidth; 188 | 189 | double get wordHeight => _wordHeight; 190 | 191 | double get screenBottom => _screenBottom; 192 | 193 | double get screenTop => _screenTop; 194 | 195 | double get screenHeight => _screenHeight; 196 | 197 | double get titleHeight => _titleHeight; 198 | 199 | double get screenWidth => _screenWidth; 200 | 201 | TextStyle get contentStyle => _contentStyle; 202 | TextStyle get titleStyle => _titleStyle; 203 | TextPainter get textPainter => _painter; 204 | } -------------------------------------------------------------------------------- /lib/module/book/read/component/point.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/read_controller.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | import 'content.dart'; 6 | 7 | /// 点击 8 | Widget point() { 9 | return GetBuilder( 10 | id: ReadRefreshKey.content, 11 | builder: (controller) { 12 | if (controller.pages.isEmpty) { 13 | return Container(); 14 | } 15 | return content(controller.context, controller.pageIndex.count, controller); 16 | }, 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/module/book/read/component/slide.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/read_page_type.dart'; 2 | import 'package:book_app/module/book/read/read_controller.dart'; 3 | import 'package:book_app/util/custom_page_view.dart'; 4 | import 'package:book_app/util/keep_alive_wrapper.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | import 'content.dart'; 9 | /// 滑动翻页 10 | Widget slide({Axis scrollDirection = Axis.horizontal}) { 11 | return GetBuilder( 12 | id: ReadRefreshKey.content, 13 | builder: (controller) { 14 | return Listener( 15 | child: CustomPageView.builder( 16 | scrollDirection: scrollDirection, 17 | controller: controller.contentPageController, 18 | itemCount: controller.pages.length, 19 | itemBuilder: (context, index) { 20 | return KeepAliveWrapper(content(context, index, controller)); 21 | }, 22 | onPageChanged: (index) async { 23 | 24 | }, 25 | onPageStartChanged: (_) { 26 | controller.isSliding.update(true); 27 | }, 28 | onPageEndChanged: (index) { 29 | controller.isSliding.update(false); 30 | controller.pageIndex.setCount(index); 31 | }, 32 | ), 33 | onPointerDown: (e) { 34 | controller.autoPageCancel(); 35 | if (controller.readPageType == ReadPageType.slideUpDown) { 36 | // 上下滑动 37 | controller.xMove = e.position.dy; 38 | } else { 39 | controller.xMove = e.position.dx; 40 | } 41 | }, 42 | onPointerUp: (e) async { 43 | double move = 0; 44 | if (controller.readPageType == ReadPageType.slideUpDown) { 45 | // 上下滑动 46 | move = e.position.dy - controller.xMove; 47 | } else { 48 | move = e.position.dx - controller.xMove; 49 | } 50 | // 滑动了五十距离, 且当前为0 51 | if (move > 50 && controller.pageIndex.count == 0) { 52 | await controller.prePage(); 53 | } else if (move < -50 && 54 | controller.pageIndex.count == controller.pages.length - 1) { 55 | await controller.nextPage(); 56 | } 57 | }, 58 | ); 59 | }, 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /lib/module/book/read/read_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/read_controller.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | class ReadBinding implements Bindings { 5 | @override 6 | void dependencies() { 7 | Get.put(ReadController()); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /lib/module/book/read/read_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/read_page_type.dart'; 2 | import 'package:book_app/module/book/read/component/bottom.dart'; 3 | import 'package:book_app/module/book/read/component/drawer.dart' as dr; 4 | import 'package:book_app/module/book/read/component/point.dart'; 5 | import 'package:book_app/module/book/read/component/slide.dart'; 6 | import 'package:book_app/module/book/read/read_controller.dart'; 7 | import 'package:book_app/theme/color.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; 11 | import 'package:get/get.dart'; 12 | 13 | class ReadScreen extends GetView{ 14 | const ReadScreen({Key? key}) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | controller.context = context; 19 | return WillPopScope( 20 | child: ZoomDrawer( 21 | controller: controller.zoomDrawerController, 22 | menuScreen: dr.Drawer(), 23 | mainScreen: GetBuilder( 24 | id: ReadRefreshKey.background, 25 | builder: (controller) { 26 | return Container( 27 | color: hexToColor( 28 | controller.readSettingConfig.backgroundColor), 29 | child: _body(context), 30 | ); 31 | }, 32 | ), 33 | angle: 0, 34 | mainScreenScale: 0, 35 | mainScreenTapClose: true, 36 | style: DrawerStyle.DefaultStyle, 37 | showShadow: true, 38 | ), 39 | onWillPop: () async { 40 | await controller.popRead(); 41 | return false; 42 | }, 43 | ); 44 | } 45 | 46 | Widget _body(context) { 47 | return GetBuilder( 48 | id: ReadRefreshKey.page, 49 | builder: (controller) { 50 | return GestureDetector( 51 | child: _content(), 52 | behavior: HitTestBehavior.opaque, 53 | onTap: () { 54 | if (controller.zoomDrawerController.isOpen!()) { 55 | controller.zoomDrawerController.toggle!.call(); 56 | } 57 | }, 58 | onTapUp: (e) async { 59 | if (controller.zoomDrawerController.isOpen!()) { 60 | return; 61 | } 62 | if (e.globalPosition.dx < controller.pageGen.screenWidth / 3) { 63 | if (controller.readPageType == ReadPageType.point) { 64 | await controller.prePage(); 65 | } 66 | } else if (e.globalPosition.dx > (controller.pageGen.screenWidth / 3 * 2)) { 67 | // if (!controller.loading) { 68 | if (controller.readPageType == ReadPageType.point) { 69 | await controller.nextPage(); 70 | } 71 | // } 72 | } else { 73 | // 中间 74 | // Get.toNamed(Routes.readBottom); 75 | controller.initBrightness(); 76 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge).then((value) { 77 | bottom(context); 78 | }); 79 | 80 | } 81 | }, 82 | ); 83 | } 84 | ); 85 | } 86 | 87 | _content() { 88 | switch (controller.readPageType) { 89 | case ReadPageType.slide: 90 | return slide(); 91 | case ReadPageType.point: 92 | return point(); 93 | case ReadPageType.slideUpDown: 94 | return slide(scrollDirection: Axis.vertical); 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /lib/module/book/readMoreSetting/component/page_style_bottom.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/read_page_type.dart'; 2 | import 'package:book_app/module/book/read/read_controller.dart'; 3 | import 'package:book_app/module/book/readMoreSetting/read_more_setting_controller.dart'; 4 | import 'package:book_app/util/bottom_bar_build.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:get/get.dart'; 7 | 8 | pageStyleBottom(context, ReadMoreSettingController controller) { 9 | var list = [ 10 | ["滑动翻页", ReadPageType.slide], 11 | ["上下滑动翻页", ReadPageType.slideUpDown], 12 | ["点击翻页", ReadPageType.point], 13 | ]; 14 | ReadController readController = Get.find(); 15 | Get.bottomSheet(BottomBarBuild( 16 | "选项", 17 | list.map((e) => BottomBarBuildItem( 18 | "${e.first}", 19 | () { 20 | Get.back(); 21 | ReadPageType readPageType = e[1] as ReadPageType; 22 | if (readController.readPageType != readPageType) { 23 | readController.setPageType(readPageType); 24 | controller.fresh(); 25 | Navigator.of(context).pop(); 26 | } 27 | }, 28 | )).toList() 29 | )); 30 | // showModalBottomSheet( 31 | // context: context, 32 | // builder: (context) { 33 | // return Opacity( 34 | // opacity: .7, 35 | // child: SizedBox( 36 | // height: 51 * list.length + 16, 37 | // child:, 38 | // ), 39 | // ); 40 | // }); 41 | } 42 | -------------------------------------------------------------------------------- /lib/module/book/readMoreSetting/read_more_setting_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/readMoreSetting/read_more_setting_controller.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | class ReadMoreSettingBinding implements Bindings { 5 | @override 6 | void dependencies() { 7 | Get.put(ReadMoreSettingController()); 8 | } 9 | } -------------------------------------------------------------------------------- /lib/module/book/readMoreSetting/read_more_setting_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/app_controller.dart'; 2 | import 'package:book_app/theme/color.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_colorpicker/flutter_colorpicker.dart'; 5 | import 'package:get/get.dart'; 6 | 7 | class ReadMoreSettingController extends GetxController{ 8 | bool autoPage = false; 9 | int autoPageRate = 5; 10 | bool goodEyes = false; 11 | AppController appController = Get.find(); 12 | String goodEyesHex = "#FFF9DE"; 13 | 14 | @override 15 | void onInit() { 16 | super.onInit(); 17 | var nowScreenColor = colorToHex(appController.screenColor, includeHashSign: true, enableAlpha: false); 18 | goodEyes = goodEyesHex == nowScreenColor; 19 | } 20 | void setAutoPage(bool value) { 21 | autoPage = value; 22 | update(["moreSetting"]); 23 | } 24 | 25 | void setAutoPageRate(int value) { 26 | autoPageRate = value; 27 | update(["moreSetting"]); 28 | } 29 | 30 | void setGoodEyes(bool value) { 31 | if (value) { 32 | appController.setScreenStyle(hexToColor(goodEyesHex)); 33 | } else { 34 | appController.setScreenStyle(Colors.white); 35 | } 36 | goodEyes = value; 37 | update(["moreSetting"]); 38 | } 39 | 40 | void pop() { 41 | Get.back(result: {'autoPage': autoPage, 'autoPageRate': autoPageRate}); 42 | } 43 | 44 | void fresh() { 45 | update(["moreSetting"]); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /lib/module/book/readMoreSetting/read_more_setting_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/log/log.dart'; 2 | import 'package:book_app/model/read_page_type.dart'; 3 | import 'package:book_app/module/book/read/read_controller.dart'; 4 | import 'package:book_app/module/book/readMoreSetting/component/page_style_bottom.dart'; 5 | import 'package:book_app/module/book/readMoreSetting/read_more_setting_controller.dart'; 6 | import 'package:book_app/theme/color.dart'; 7 | import 'package:book_app/util/list_item.dart'; 8 | import 'package:book_app/util/system_utils.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_switch/flutter_switch.dart'; 11 | import 'package:get/get.dart'; 12 | import 'package:numberpicker/numberpicker.dart'; 13 | 14 | class ReadMoreSettingScreen extends GetView { 15 | ReadMoreSettingScreen({Key? key}) : super(key: key); 16 | final ReadController _readController = Get.find(); 17 | @override 18 | Widget build(BuildContext context) { 19 | return WillPopScope( 20 | child: Scaffold( 21 | appBar: AppBar( 22 | title: const Text("更多设置"), 23 | centerTitle: true, 24 | elevation: 0, 25 | backgroundColor: backgroundColor(), 26 | ), 27 | backgroundColor: backgroundColor(), 28 | body: _body(context), 29 | ), 30 | onWillPop: () async { 31 | controller.pop(); 32 | return false; 33 | }, 34 | ); 35 | } 36 | 37 | Widget _body(context) { 38 | return GetBuilder( 39 | id: 'moreSetting', 40 | builder: (controller) { 41 | return Column( 42 | mainAxisAlignment: MainAxisAlignment.start, 43 | children: [ 44 | const SizedBox(height: 10,), 45 | ListItem( 46 | "自动翻页", 47 | FlutterSwitch( 48 | value: controller.autoPage, 49 | height: 25, 50 | width: 50, 51 | onToggle: (value) { 52 | Log.i(value); 53 | controller.setAutoPage(value); 54 | } 55 | ), 56 | textColor: textColor() 57 | ), 58 | Container( 59 | padding: const EdgeInsets.only(left: 15, right: 15), 60 | child: Divider( 61 | height: 1, 62 | color: Colors.grey[300], 63 | ), 64 | ), 65 | ListItem( 66 | "翻页速度", 67 | NumberPicker( 68 | value: controller.autoPageRate, 69 | minValue: 3, 70 | maxValue: 30, 71 | itemCount: 1, 72 | itemHeight: 30, 73 | itemWidth: 50, 74 | textMapper: (str) { 75 | return "${str}s/页"; 76 | }, 77 | textStyle: TextStyle(fontSize: 16, color: textColor()), 78 | selectedTextStyle: TextStyle(fontSize: 16, color: textColor()), 79 | onChanged: (value) { 80 | controller.setAutoPageRate(value); 81 | }, 82 | ), 83 | textColor: textColor() 84 | ), 85 | Container( 86 | padding: const EdgeInsets.only(left: 15, right: 15), 87 | child: Divider( 88 | height: 1, 89 | color: Colors.grey[300], 90 | ), 91 | ), 92 | ListItem("护眼模式", FlutterSwitch( 93 | value: controller.goodEyes, 94 | height: 25, 95 | width: 50, 96 | onToggle: (value) { 97 | controller.setGoodEyes(value); 98 | } 99 | ), 100 | textColor: textColor() 101 | ), 102 | Container( 103 | padding: const EdgeInsets.only(left: 15, right: 15), 104 | child: Divider( 105 | height: 1, 106 | color: Colors.grey[300], 107 | ), 108 | ), 109 | ListItem("翻页样式", 110 | GestureDetector( 111 | child: Row( 112 | children: [ 113 | Text(_pageStyleStr(_readController.readPageType), style: TextStyle(color: textColor(), height: 1, fontSize: 14),), 114 | Icon(Icons.keyboard_arrow_right, color: textColor(), size: 25,) 115 | ], 116 | ), 117 | onTap: () { 118 | pageStyleBottom(context, controller); 119 | }, 120 | ), 121 | textColor: textColor() 122 | , 123 | ), 124 | ], 125 | ); 126 | }, 127 | ); 128 | } 129 | 130 | String _pageStyleStr(ReadPageType pageType) { 131 | switch(pageType) { 132 | case ReadPageType.point: 133 | return "点击翻页"; 134 | case ReadPageType.slide: 135 | return "滑动翻页"; 136 | case ReadPageType.slideUpDown: 137 | return "上下滑动翻页"; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/module/book/readSetting/component/read_setting_config.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/base.dart'; 2 | 3 | class ReadSettingConfig extends Base{ 4 | String backgroundColor; 5 | double fontSize; 6 | String fontColor; 7 | double fontHeight; 8 | int fontWeight; 9 | 10 | ReadSettingConfig(this.backgroundColor, this.fontSize, this.fontColor, this.fontHeight, this.fontWeight); 11 | 12 | static ReadSettingConfig defaultConfig() { 13 | return ReadSettingConfig("#FFF2E2", 20, "#000000", 2.5, 3); 14 | } 15 | 16 | static ReadSettingConfig defaultDarkConfig(fontSize, fontHeight) { 17 | return ReadSettingConfig("#2F2E2E", fontSize, "#a9a9a9", fontHeight, 3); 18 | } 19 | 20 | factory ReadSettingConfig.fromJson(Map json) => ReadSettingConfig(json["backgroundColor"], json["fontSize"], json["fontColor"], json["fontHeight"]??=1.8, json["fontWeight"]??=3); 21 | @override 22 | Map toJson() => { 23 | 'backgroundColor': backgroundColor, 24 | 'fontSize': fontSize, 25 | 'fontColor': fontColor, 26 | 'fontHeight': fontHeight, 27 | 'fontWeight': fontWeight, 28 | }; 29 | 30 | static List getCommonBackgroundColors() { 31 | return ["#FAF9DE", "#FFF2E2", "#FDE6E0", "#E3EDCD", "#DCE2F1", "#E9EBFE"]; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /lib/module/book/readSetting/read_setting_binding.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/readSetting/read_setting_controller.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | class ReadSettingBinding implements Bindings { 5 | @override 6 | void dependencies() { 7 | Get.put(ReadSettingController()); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /lib/module/book/readSetting/read_setting_controller.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/read/read_controller.dart'; 2 | import 'package:book_app/module/book/readSetting/component/read_setting_config.dart'; 3 | import 'package:get/get.dart'; 4 | 5 | class ReadSettingController extends GetxController { 6 | 7 | late ReadSettingConfig config; 8 | @override 9 | void onInit() { 10 | super.onInit(); 11 | config = Get.arguments["config"]; 12 | } 13 | 14 | fontSizeAdd() { 15 | if (config.fontSize >= 40) { 16 | return; 17 | } 18 | config.fontSize = config.fontSize + 1; 19 | update(["setting"]); 20 | } 21 | 22 | fontSizeSub() { 23 | if (config.fontSize <= 10) { 24 | return; 25 | } 26 | config.fontSize = config.fontSize - 1; 27 | update(["setting"]); 28 | } 29 | 30 | fontWeightAdd() { 31 | if (config.fontWeight >= 8) { 32 | return; 33 | } 34 | config.fontWeight = config.fontWeight + 1; 35 | update(["setting"]); 36 | } 37 | 38 | fontWeightSub() { 39 | if (config.fontWeight <= 0) { 40 | return; 41 | } 42 | config.fontWeight = config.fontWeight - 1; 43 | update(["setting"]); 44 | } 45 | 46 | void setColor(String selectColorHex, flag) { 47 | if (flag) { 48 | config.backgroundColor = selectColorHex; 49 | } else { 50 | config.fontColor = selectColorHex; 51 | } 52 | update(["setting"]); 53 | } 54 | 55 | void setDefault() { 56 | ReadController readController = Get.find(); 57 | var defaultConfig = ReadSettingConfig.defaultConfig(); 58 | config = readController.isDark ? ReadSettingConfig.defaultDarkConfig(defaultConfig.fontSize, defaultConfig.fontHeight) : defaultConfig; 59 | update(["setting"]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/module/book/readSetting/read_setting_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:book_app/module/book/read/read_controller.dart'; 4 | import 'package:book_app/module/book/readSetting/read_setting_controller.dart'; 5 | import 'package:book_app/theme/color.dart'; 6 | import 'package:book_app/util/constant.dart'; 7 | import 'package:book_app/util/font_util.dart'; 8 | import 'package:book_app/util/no_shadow_scroll_behavior.dart'; 9 | import 'package:book_app/util/save_util.dart'; 10 | import 'package:flutter_colorpicker/flutter_colorpicker.dart'; 11 | import 'package:get/get.dart'; 12 | import 'package:flutter/material.dart'; 13 | 14 | class ReadSettingScreen extends GetView { 15 | const ReadSettingScreen({Key? key}) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Scaffold( 20 | appBar: AppBar( 21 | title: const Text("自定义设置"), 22 | centerTitle: true, 23 | leading: GestureDetector( 24 | child: const Icon(Icons.arrow_back_ios), 25 | onTap: () => Get.back(), 26 | ), 27 | elevation: 0, 28 | backgroundColor: backgroundColor(), 29 | ), 30 | backgroundColor: backgroundColor(), 31 | body: _body(context), 32 | ); 33 | } 34 | 35 | 36 | Widget _body(context) { 37 | return Stack( 38 | children: [ 39 | Positioned( 40 | top: 30, 41 | left: 50, 42 | right: 50, 43 | child: GetBuilder( 44 | id: 'setting', 45 | builder: (controller) { 46 | return Card( 47 | color: hexToColor(controller.config.backgroundColor), 48 | elevation: 10, 49 | child: Container( 50 | height: 500, 51 | alignment: Alignment.topLeft, 52 | padding: const EdgeInsets.all(10), 53 | child: ScrollConfiguration( 54 | behavior: NoShadowScrollBehavior(), 55 | child: SingleChildScrollView( 56 | child: Text.rich( 57 | TextSpan( 58 | children: const [ 59 | TextSpan(text: "1.你可以设置背景色\n\n"), 60 | TextSpan(text: "2.你可以设置字体颜色\n\n"), 61 | TextSpan(text: "3.你可以设置字体大小\n\n"), 62 | TextSpan(text: "4.你可以设置字体粗细\n\n"), 63 | TextSpan(text: "5.接下来是一段测试\n\n"), 64 | TextSpan(text: "得,好美,它如深山里的一泓泉水,带着清澈和甘甜,温润心灵;它如初春的那抹新绿,清新自然,点缀生命;它如花笺里的兰花,恬淡生香,芬芳怡人;它如清晨小草上的露珠,晶莹剔透,不染风尘。懂得,是蓝天与白云的相拥;是清风与花香的缠绵;是润物细无声的点点春雨;是清晨坐拥的满怀阳光。"), 65 | ], 66 | style: TextStyle( 67 | fontSize: controller.config.fontSize, 68 | color: hexToColor(controller.config.fontColor), 69 | fontFamily: FontUtil.getFontFamily(), 70 | fontWeight: FontUtil.intToFontWeight(controller.config.fontWeight) 71 | ) 72 | ) 73 | ), 74 | ), 75 | ), 76 | ), 77 | ); 78 | }, 79 | ), 80 | ), 81 | Positioned( 82 | bottom: 0, 83 | left: 0, 84 | right: 0, 85 | child: Card( 86 | color: backgroundColorL2() ?? Colors.white, 87 | child: SizedBox( 88 | height: 150, 89 | child: Column( 90 | children: [ 91 | Expanded( 92 | flex: 1, 93 | child: Row( 94 | mainAxisAlignment: MainAxisAlignment.spaceAround, 95 | children: [ 96 | GestureDetector( 97 | child: Text("阅读底色", style: TextStyle(color: textColor() ?? Colors.black),), 98 | onTap: () => _colorPicker(context, true), 99 | ), 100 | GestureDetector( 101 | child: Text("字体颜色", style: TextStyle(color: textColor() ?? Colors.black)), 102 | onTap: () => _colorPicker(context, false), 103 | ), 104 | GestureDetector( 105 | child: Text("Aa-", style: TextStyle(color: textColor() ?? Colors.black)), 106 | onTap: () => controller.fontSizeSub(), 107 | ), 108 | GestureDetector( 109 | child: Text("Aa+", style: TextStyle(color: textColor() ?? Colors.black)), 110 | onTap: () => controller.fontSizeAdd(), 111 | ), 112 | GestureDetector( 113 | child: Text("B-", style: TextStyle(color: textColor() ?? Colors.black)), 114 | onTap: () => controller.fontWeightSub(), 115 | ), 116 | GestureDetector( 117 | child: Text("B+", style: TextStyle(color: textColor() ?? Colors.black)), 118 | onTap: () => controller.fontWeightAdd(), 119 | ), 120 | ], 121 | ), 122 | ), 123 | Expanded( 124 | flex: 1, 125 | child: Row( 126 | mainAxisAlignment: MainAxisAlignment.center, 127 | children: [ 128 | const SizedBox(width: 20,), 129 | Expanded( 130 | child: InkWell( 131 | borderRadius: const BorderRadius.all(Radius.circular(8)), 132 | child: Container( 133 | height: 35, 134 | alignment: Alignment.center, 135 | decoration: BoxDecoration( 136 | borderRadius: const BorderRadius.all(Radius.circular(8)), 137 | border: Border.all(color: textColor() ?? Theme.of(context).primaryColor) 138 | ), 139 | child: Text("恢复默认设置", style: TextStyle(color: textColor() ?? Theme.of(context).primaryColor, fontSize: 15),), 140 | ), 141 | onTap: () => controller.setDefault(), 142 | ), 143 | ), 144 | const SizedBox(width: 20,), 145 | Expanded( 146 | child: InkWell( 147 | borderRadius: const BorderRadius.all(Radius.circular(8)), 148 | child: Container( 149 | height: 35, 150 | alignment: Alignment.center, 151 | decoration: BoxDecoration( 152 | borderRadius: const BorderRadius.all(Radius.circular(8)), 153 | border: Border.all(color: textColor() ?? Theme.of(context).primaryColor) 154 | ), 155 | child: Text("保存设置", style: TextStyle(color: textColor() ?? Theme.of(context).primaryColor, fontSize: 15)), 156 | ), 157 | onTap: () { 158 | String data = json.encode(controller.config); 159 | SaveUtil.setString(Constant.readSettingConfig, data); 160 | Get.back(result: {"config": controller.config}); 161 | }, 162 | ), 163 | ), 164 | const SizedBox(width: 20,), 165 | ], 166 | ), 167 | ), 168 | ], 169 | ), 170 | ), 171 | ), 172 | ) 173 | ], 174 | ); 175 | } 176 | 177 | _colorPicker(context, flag) { 178 | String preHex; 179 | if (flag) { 180 | // 背景色 181 | ReadController readController = Get.find(); 182 | if (readController.isDark) { 183 | return; 184 | } 185 | preHex = controller.config.backgroundColor; 186 | } else { 187 | // 字体颜色 188 | preHex = controller.config.fontColor; 189 | } 190 | Color pre = hexToColor(preHex); 191 | String selectColorHex = ""; 192 | showDialog( 193 | context: context, 194 | builder: (context) { 195 | return AlertDialog( 196 | backgroundColor: backgroundColorL2(), 197 | content: SingleChildScrollView( 198 | child: ColorPicker( 199 | pickerColor: pre, 200 | onColorChanged: (color) { 201 | selectColorHex = colorToHex(color, includeHashSign: true, enableAlpha: false); 202 | }, 203 | showLabel: false, 204 | enableAlpha: false, 205 | pickerAreaHeightPercent: .8, 206 | ), 207 | ), 208 | actions: [ 209 | ElevatedButton( 210 | child: Text("确定", style: TextStyle(color: textColor()),), 211 | style: ButtonStyle( 212 | backgroundColor: MaterialStateProperty.all(backgroundColor()) 213 | ), 214 | onPressed: () { 215 | if (selectColorHex.isNotEmpty) { 216 | controller.setColor(selectColorHex, flag); 217 | } 218 | Navigator.of(context).pop(); 219 | }, 220 | ) 221 | ], 222 | ); 223 | }, 224 | ); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/resource/image/screen_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/lib/resource/image/screen_h.png -------------------------------------------------------------------------------- /lib/route/route_pages.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/module/book/home/book_home_binding.dart'; 2 | import 'package:book_app/module/book/home/book_home_screen.dart'; 3 | import 'package:book_app/module/book/readMoreSetting/read_more_setting_binding.dart'; 4 | import 'package:book_app/module/book/readMoreSetting/read_more_setting_screen.dart'; 5 | import 'package:book_app/module/book/readSetting/read_setting_binding.dart'; 6 | import 'package:book_app/module/book/readSetting/read_setting_screen.dart'; 7 | import 'package:book_app/module/book/read/read_binding.dart'; 8 | import 'package:book_app/module/book/read/read_screen.dart'; 9 | import 'package:book_app/route/routes.dart'; 10 | import 'package:get/get.dart'; 11 | 12 | 13 | class RoutePages { 14 | static const initial = Routes.splash; 15 | static final routes = [ 16 | GetPage( 17 | name: Routes.bookHome, 18 | page: () => const BookHomeScreen(), 19 | binding: BookHomeBinding() 20 | ), 21 | GetPage( 22 | name: Routes.read, 23 | page: () => const ReadScreen(), 24 | binding: ReadBinding() 25 | ), 26 | GetPage( 27 | name: Routes.readSetting, 28 | page: () => const ReadSettingScreen(), 29 | binding: ReadSettingBinding() 30 | ), 31 | GetPage( 32 | name: Routes.readMoreSetting, 33 | page: () => ReadMoreSettingScreen(), 34 | binding: ReadMoreSettingBinding() 35 | ), 36 | ]; 37 | } 38 | -------------------------------------------------------------------------------- /lib/route/routes.dart: -------------------------------------------------------------------------------- 1 | /// 路由路径页 2 | abstract class Routes { 3 | static const splash = '/'; 4 | 5 | 6 | /// 小说 7 | static const bookHome = '/book/home'; 8 | static const read = '/book/read'; 9 | static const readSetting = '/book/read/setting'; 10 | static const readMoreSetting = '/book/read/moreSetting'; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/theme/color.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:get/get.dart'; 3 | 4 | Color hexToColor(String hex) { 5 | assert(RegExp(r'^#([0-9a-fA-F]{6})|([0-9a-fA-F]{8})$').hasMatch(hex), 6 | 'hex color must be #rrggbb or #rrggbbaa'); 7 | 8 | return Color( 9 | int.parse(hex.substring(1), radix: 16) + 10 | (hex.length == 7 ? 0xff000000 : 0x00000000), 11 | ); 12 | } 13 | 14 | Color? backgroundColor() { 15 | return Get.isPlatformDarkMode ? Colors.black : null; 16 | } 17 | 18 | Color? backgroundColorL2() { 19 | return Get.isPlatformDarkMode ? hexToColor("#2F2E2E") : null; 20 | } 21 | 22 | Color? textColor() { 23 | return Get.isPlatformDarkMode ? hexToColor("#a9a9a9") : null; 24 | } 25 | -------------------------------------------------------------------------------- /lib/util/bar_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | 6 | transparentBar() { 7 | if (Platform.isAndroid) { 8 | // 以下两行 设置android状态栏为透明的沉浸。写在组件渲染之后,是为了在渲染后进行set赋值,覆盖状态栏,写在渲染之前MaterialApp组件会覆盖掉这个值。 9 | SystemUiOverlayStyle systemUiOverlayStyle = 10 | const SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent); 11 | SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 12 | } 13 | } -------------------------------------------------------------------------------- /lib/util/bottom_bar_build.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/theme/color.dart'; 2 | import 'package:book_app/util/bar_util.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:get/get.dart'; 5 | 6 | // ignore: must_be_immutable 7 | class BottomBarBuild extends StatelessWidget { 8 | final String title; 9 | Color? backgroundColor; 10 | Color? titleColor; 11 | final List items; 12 | BottomBarBuild(this.title, this.items, {Key? key, this.backgroundColor, this.titleColor}) : super(key: key){ 13 | backgroundColor = backgroundColor ?? (Get.isPlatformDarkMode ? Colors.black : Colors.white); 14 | titleColor = titleColor ?? (Get.isPlatformDarkMode ? hexToColor("#a9a9a9") : Colors.black); 15 | } 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | transparentBar(); 20 | return Card( 21 | color: backgroundColor, 22 | child: SizedBox( 23 | height: (items.length + 1) * 50 + items.length * 1 + MediaQuery.of(context).padding.bottom, 24 | child: ListView.separated( 25 | physics: const NeverScrollableScrollPhysics(), 26 | cacheExtent: (items.length + 1) * 50 + items.length * 1, 27 | itemCount: items.length + 1, 28 | itemBuilder: (context, index) { 29 | if (index == 0) { 30 | return Container( 31 | height: 50, 32 | alignment: Alignment.center, 33 | child: Text(title, style: TextStyle(height: 1, fontSize: 14, color: titleColor),), 34 | ); 35 | } 36 | return InkWell( 37 | child: Container( 38 | height: 50, 39 | alignment: Alignment.center, 40 | child: items[index - 1].useWidget ? items[index - 1].titleWidget : Text(items[index - 1].title, style: TextStyle(height: 1, fontSize: 14, color: titleColor),), 41 | ), 42 | onTap: () { 43 | items[index - 1].function(); 44 | }, 45 | onLongPress: () { 46 | if (items[index - 1].longFunction != null) { 47 | items[index - 1].longFunction!(); 48 | } 49 | }, 50 | ); 51 | }, 52 | separatorBuilder: (context, index) { 53 | return Divider( 54 | height: 1, 55 | color: Colors.grey[200], 56 | ); 57 | }, 58 | ), 59 | ), 60 | ); 61 | } 62 | 63 | } 64 | class BottomBarBuildItem { 65 | final String title; 66 | final Function function; 67 | final Function? longFunction; 68 | final Widget? titleWidget; 69 | bool _useWidget = false; 70 | BottomBarBuildItem(this.title, this.function, {this.titleWidget, this.longFunction}) { 71 | if (titleWidget != null) { 72 | _useWidget = true; 73 | } 74 | } 75 | 76 | bool get useWidget => _useWidget; 77 | } -------------------------------------------------------------------------------- /lib/util/channel_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/services.dart'; 4 | 5 | class ChannelUtils { 6 | static const methodChannel = MethodChannel('woshill/plugin'); 7 | 8 | static setConfig(String key, Object value) async { 9 | if (Platform.isAndroid) { 10 | await methodChannel.invokeMethod("setConfig", {"key": key, "value": value}); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /lib/util/chapter_compare.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/model/chapter/chapter.dart'; 2 | 3 | List chapterCompare(List oldList, List newList) { 4 | List remainList = []; 5 | Set set = {}; 6 | set.addAll(oldList); 7 | for (var element in newList) { 8 | if (set.add(element)) { 9 | remainList.add(element); 10 | } 11 | } 12 | return remainList; 13 | } -------------------------------------------------------------------------------- /lib/util/constant.dart: -------------------------------------------------------------------------------- 1 | class Constant { 2 | /// 小说搜索历时 3 | static const String searchHistoryKey = "search_history"; 4 | /// splash页 5 | static const String splashTrue = "splash_true"; 6 | /// 老人版 7 | static const String oldManTrue = "old_man_true"; 8 | /// 阅读背景 9 | static const String readBackgroundColor = "read_background_color"; 10 | /// 阅读配置 11 | static const String readSettingConfig = "read_setting_config"; 12 | /// 登录token 13 | static const String token = "token"; 14 | /// 阅读点击类型 15 | static const String readType = "read_type"; 16 | 17 | /// 启动页 18 | static const String initRoute = "initRoute"; 19 | 20 | static const String privateRead = "privateRead1.1.0"; 21 | 22 | /// ------------------------- 插件 start ------------------------- 23 | static const String pluginVolumeFlag = "volumeFlag"; 24 | 25 | /// ------------------------- 插件 end ------------------------- 26 | } 27 | -------------------------------------------------------------------------------- /lib/util/content_fliter.dart: -------------------------------------------------------------------------------- 1 | /// 需要过滤的词汇 2 | List _contentFilter() { 3 | return [ 4 | "笔趣阁", 5 | "小说网", 6 | "书屋", 7 | "首页", 8 | "上一页", 9 | "下一页", 10 | "上一章", 11 | "下一章", 12 | "加入书签", 13 | "投票推荐", 14 | "目录", 15 | "手机版", 16 | "推荐阅读" 17 | ]; 18 | } 19 | 20 | String _contentRStr() { 21 | List rStr = []; 22 | for (var value in _contentFilter()) { 23 | rStr.add("($value)"); 24 | } 25 | return rStr.join("|"); 26 | } 27 | RegExp contentFilterRegExp() { 28 | return RegExp(_contentRStr()); 29 | } -------------------------------------------------------------------------------- /lib/util/custom_page_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/rendering.dart'; 3 | import 'package:flutter/gestures.dart' show DragStartBehavior; 4 | 5 | ///自定义的PageView 6 | ///新增了页面加载开始和结束的回调 7 | 8 | final PageController _defaultPageController = PageController(); 9 | const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); 10 | 11 | class CustomPageView extends StatefulWidget { 12 | /// Creates a scrollable list that works page by page from an explicit [List] 13 | /// of widgets. 14 | /// 15 | /// This constructor is appropriate for page views with a small number of 16 | /// children because constructing the [List] requires doing work for every 17 | /// child that could possibly be displayed in the page view, instead of just 18 | /// those children that are actually visible. 19 | CustomPageView({ 20 | Key? key, 21 | this.scrollDirection = Axis.horizontal, 22 | this.reverse = false, 23 | PageController? controller, 24 | this.physics, 25 | this.pageSnapping = true, 26 | this.onPageChanged, 27 | this.onPageEndChanged, 28 | this.onPageStartChanged, 29 | List children = const [], 30 | this.dragStartBehavior = DragStartBehavior.start, 31 | }) : controller = controller ?? _defaultPageController, 32 | childrenDelegate = SliverChildListDelegate(children), 33 | super(key: key); 34 | 35 | /// Creates a scrollable list that works page by page using widgets that are 36 | /// created on demand. 37 | /// 38 | /// This constructor is appropriate for page views with a large (or infinite) 39 | /// number of children because the builder is called only for those children 40 | /// that are actually visible. 41 | /// 42 | /// Providing a non-null [itemCount] lets the [CustomPageView] compute the maximum 43 | /// scroll extent. 44 | /// 45 | /// [itemBuilder] will be called only with indices greater than or equal to 46 | /// zero and less than [itemCount]. 47 | /// 48 | /// [CustomPageView.builder] by default does not support child reordering. If 49 | /// you are planning to change child order at a later time, consider using 50 | /// [CustomPageView] or [CustomPageView.custom]. 51 | CustomPageView.builder({ 52 | Key? key, 53 | this.scrollDirection = Axis.horizontal, 54 | this.reverse = false, 55 | PageController? controller, 56 | this.physics, 57 | this.pageSnapping = true, 58 | this.onPageChanged, 59 | this.onPageEndChanged, 60 | this.onPageStartChanged, 61 | required IndexedWidgetBuilder itemBuilder, 62 | int? itemCount, 63 | this.dragStartBehavior = DragStartBehavior.start, 64 | }) : controller = controller ?? _defaultPageController, 65 | childrenDelegate = 66 | SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), 67 | super(key: key); 68 | 69 | CustomPageView.custom({ 70 | Key? key, 71 | this.scrollDirection = Axis.horizontal, 72 | this.reverse = false, 73 | PageController? controller, 74 | this.physics, 75 | this.pageSnapping = true, 76 | this.onPageChanged, 77 | this.onPageEndChanged, 78 | this.onPageStartChanged, 79 | required this.childrenDelegate, 80 | this.dragStartBehavior = DragStartBehavior.start, 81 | }) : controller = controller ?? _defaultPageController, 82 | super(key: key); 83 | 84 | /// The axis along which the page view scrolls. 85 | /// 86 | /// Defaults to [Axis.horizontal]. 87 | final Axis scrollDirection; 88 | 89 | /// Whether the page view scrolls in the reading direction. 90 | /// 91 | /// For example, if the reading direction is left-to-right and 92 | /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from 93 | /// left to right when [reverse] is false and from right to left when 94 | /// [reverse] is true. 95 | /// 96 | /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view 97 | /// scrolls from top to bottom when [reverse] is false and from bottom to top 98 | /// when [reverse] is true. 99 | /// 100 | /// Defaults to false. 101 | final bool reverse; 102 | 103 | /// An object that can be used to control the position to which this page 104 | /// view is scrolled. 105 | final PageController controller; 106 | 107 | /// How the page view should respond to user input. 108 | /// 109 | /// For example, determines how the page view continues to animate after the 110 | /// user stops dragging the page view. 111 | /// 112 | /// The physics are modified to snap to page boundaries using 113 | /// [PageScrollPhysics] prior to being used. 114 | /// 115 | /// Defaults to matching platform conventions. 116 | final ScrollPhysics? physics; 117 | 118 | /// Set to false to disable page snapping, useful for custom scroll behavior. 119 | final bool pageSnapping; 120 | 121 | /// Called whenever the page in the center of the viewport changes. 122 | final ValueChanged? onPageChanged; 123 | final ValueChanged? onPageEndChanged; 124 | final ValueChanged? onPageStartChanged; 125 | 126 | /// A delegate that provides the children for the [CustomPageView]. 127 | /// 128 | /// The [CustomPageView.custom] constructor lets you specify this delegate 129 | /// explicitly. The [CustomPageView] and [CustomPageView.builder] constructors create a 130 | /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], 131 | /// respectively. 132 | final SliverChildDelegate childrenDelegate; 133 | 134 | /// {@macro flutter.widgets.scrollable.dragStartBehavior} 135 | final DragStartBehavior dragStartBehavior; 136 | 137 | @override 138 | _CustomPageViewState createState() => _CustomPageViewState(); 139 | } 140 | 141 | class _CustomPageViewState extends State { 142 | int _lastReportedPage = 0; 143 | 144 | @override 145 | void initState() { 146 | super.initState(); 147 | _lastReportedPage = widget.controller.initialPage; 148 | } 149 | 150 | AxisDirection _getDirection(BuildContext context) { 151 | switch (widget.scrollDirection) { 152 | case Axis.horizontal: 153 | assert(debugCheckHasDirectionality(context)); 154 | final TextDirection textDirection = Directionality.of(context); 155 | final AxisDirection axisDirection = 156 | textDirectionToAxisDirection(textDirection); 157 | return widget.reverse 158 | ? flipAxisDirection(axisDirection) 159 | : axisDirection; 160 | case Axis.vertical: 161 | return widget.reverse ? AxisDirection.up : AxisDirection.down; 162 | } 163 | } 164 | 165 | @override 166 | Widget build(BuildContext context) { 167 | final AxisDirection axisDirection = _getDirection(context); 168 | final ScrollPhysics? physics = widget.pageSnapping 169 | ? _kPagePhysics.applyTo(widget.physics) 170 | : widget.physics; 171 | 172 | return NotificationListener( 173 | onNotification: (ScrollNotification notification) { 174 | ///滑动开始 175 | if (notification is ScrollStartNotification) { 176 | if (widget.onPageStartChanged != null) { 177 | final PageMetrics metrics = notification.metrics as PageMetrics; 178 | widget.onPageStartChanged!(metrics.page!.round()); 179 | } 180 | } 181 | 182 | ///滑动中 183 | if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) { 184 | final PageMetrics metrics = notification.metrics as PageMetrics; 185 | final int currentPage = metrics.page!.round(); 186 | if (currentPage != _lastReportedPage) { 187 | _lastReportedPage = currentPage; 188 | widget.onPageChanged!(currentPage); 189 | } 190 | } 191 | 192 | ///滑动结束 193 | if (notification.depth == 0 && notification is ScrollEndNotification) { 194 | if (widget.onPageEndChanged != null) { 195 | final PageMetrics metrics = notification.metrics as PageMetrics; 196 | widget.onPageEndChanged!(metrics.page!.round()); 197 | } 198 | } 199 | return false; 200 | }, 201 | child: Scrollable( 202 | dragStartBehavior: widget.dragStartBehavior, 203 | axisDirection: axisDirection, 204 | controller: widget.controller, 205 | physics: physics, 206 | viewportBuilder: (BuildContext context, ViewportOffset position) { 207 | return Viewport( 208 | cacheExtent: 0.0, 209 | axisDirection: axisDirection, 210 | offset: position, 211 | slivers: [ 212 | SliverFillViewport( 213 | viewportFraction: widget.controller.viewportFraction, 214 | delegate: widget.childrenDelegate, 215 | ), 216 | ], 217 | ); 218 | }, 219 | ), 220 | ); 221 | } 222 | 223 | @override 224 | void debugFillProperties(DiagnosticPropertiesBuilder description) { 225 | super.debugFillProperties(description); 226 | description 227 | .add(EnumProperty('scrollDirection', widget.scrollDirection)); 228 | description.add( 229 | FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')); 230 | description.add(DiagnosticsProperty( 231 | 'controller', widget.controller, 232 | showName: false)); 233 | description.add(DiagnosticsProperty( 234 | 'physics', widget.physics, 235 | showName: false)); 236 | description.add(FlagProperty('pageSnapping', 237 | value: widget.pageSnapping, ifFalse: 'snapping disabled')); 238 | } 239 | } -------------------------------------------------------------------------------- /lib/util/dialog_build.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/theme/color.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class DialogBuild extends StatelessWidget { 5 | final String title; 6 | final String cancelText; 7 | final String confirmText; 8 | final Widget body; 9 | final Function? cancelFunction; 10 | final Function? confirmFunction; 11 | 12 | const DialogBuild(this.title, this.body, {Key? key, this.cancelText = "取消", this.confirmText = "确定", this.cancelFunction, this.confirmFunction}) : super(key: key); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return AlertDialog( 17 | title: Text(title, style: TextStyle(color: textColor()),), 18 | backgroundColor: backgroundColorL2(), 19 | titlePadding: const EdgeInsets.all(10), 20 | titleTextStyle: const TextStyle(color: Colors.black87, fontSize: 16), 21 | content: body, 22 | contentPadding: const EdgeInsets.all(10), 23 | //中间显示内容的文本样式 24 | contentTextStyle: const TextStyle(color: Colors.black54, fontSize: 14), 25 | actions: [ 26 | ElevatedButton( 27 | child: Text(cancelText, style: TextStyle(color: textColor()),), 28 | style: ButtonStyle( 29 | backgroundColor: MaterialStateProperty.all(backgroundColor()) 30 | ), 31 | onPressed: () { 32 | if (cancelFunction != null) { 33 | cancelFunction!(); 34 | } else { 35 | Navigator.of(context).pop(); 36 | } 37 | }, 38 | ), 39 | ElevatedButton( 40 | child: Text(confirmText, style: TextStyle(color: textColor()),), 41 | style: ButtonStyle( 42 | backgroundColor: MaterialStateProperty.all(backgroundColor()) 43 | ), 44 | onPressed: () { 45 | if (confirmFunction != null) { 46 | confirmFunction!(); 47 | } else { 48 | Navigator.of(context).pop(); 49 | } 50 | }, 51 | ) 52 | ], 53 | ); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /lib/util/drag_overlay.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/log/log.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class DragOverlay { 5 | static Widget? view; 6 | static OverlayEntry? _holder; 7 | 8 | static void remove() { 9 | if (_holder != null) { 10 | _holder?.remove(); 11 | _holder = null; 12 | } 13 | view = null; 14 | } 15 | 16 | static void show(BuildContext context, Widget view) { 17 | remove(); 18 | DragOverlay.view = view; 19 | OverlayEntry overlayEntry = OverlayEntry(builder: (context){ 20 | return Positioned( 21 | top: MediaQuery.of(context).size.height *0.7, 22 | child: _buildDraggable(context), 23 | ); 24 | }); 25 | Overlay.of(context)!.insert(overlayEntry); 26 | _holder = overlayEntry; 27 | } 28 | 29 | static _buildDraggable(context){ 30 | return Draggable( 31 | child: DragOverlay.view!, 32 | feedback: DragOverlay.view!, 33 | onDragStarted: (){ 34 | 35 | }, 36 | onDragEnd: (detail){ 37 | //放手时候创建一个DragTarget 38 | createDragTarget(offset:detail.offset,context:context); 39 | }, 40 | //当拖拽的时候就展示空 41 | childWhenDragging: Container(), 42 | ignoringFeedbackSemantics: false, 43 | ); 44 | } 45 | 46 | static void createDragTarget({required Offset offset,required BuildContext context}){ 47 | if(_holder != null){ 48 | _holder?.remove(); 49 | } 50 | _holder = OverlayEntry(builder: (context){ 51 | bool isLeft = true; 52 | if(offset.dx + 100 > MediaQuery.of(context).size.width / 2){ 53 | isLeft = false; 54 | } 55 | double maxY = MediaQuery.of(context).size.height - 100; 56 | 57 | return Positioned( 58 | top: offset.dy < 50 ? 50 : offset.dy > maxY ? maxY : offset.dy, 59 | left: isLeft ? 0:null, 60 | right: isLeft ? null : 0, 61 | child: DragTarget( 62 | onWillAccept: (data){ 63 | Log.i(data); 64 | ///返回true 会将data数据添加到candidateData列表中,false时会将data添加到rejectData 65 | return true; 66 | }, 67 | onAccept: (data){ 68 | Log.i(data); 69 | }, 70 | onLeave: (data){ 71 | Log.i(data); 72 | }, 73 | builder: (BuildContext context,List incoming,List rejected){ 74 | return _buildDraggable(context); 75 | }, 76 | ), 77 | ); 78 | }); 79 | Overlay.of(context)!.insert(_holder!); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/util/font_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 字体工具 4 | class FontUtil { 5 | /// 字符转全角 6 | static String alphanumericToFullLength(str) { 7 | if (str == null) { 8 | return ""; 9 | } 10 | var temp = str.codeUnits; 11 | //a-zA-Z0-9!,.@#$%^&*()@?;\u0022\u0027}{ 12 | final regex = RegExp(r"^[\u0021-\u007E]+$"); 13 | final string = temp.map((rune) { 14 | final char = String.fromCharCode(rune); 15 | if (char == " ") { 16 | return "\u3000"; 17 | } 18 | return regex.hasMatch(char) ? String.fromCharCode(rune + 65248) : char; 19 | }); 20 | return string.join(); 21 | } 22 | 23 | /// 字符转半角 24 | static String alphanumericToHalfLength(String str) { 25 | var runes = str.codeUnits; 26 | final regex = RegExp(r'^[\uFF01-\uFF5E]+$'); 27 | final string = runes.map((rune) { 28 | final char = String.fromCharCode(rune); 29 | return regex.hasMatch(char) ? String.fromCharCode(rune - 65248) : char; 30 | }); 31 | return string.join(); 32 | } 33 | 34 | static String formatContent(String content) { 35 | if (content.isEmpty) { 36 | return content; 37 | } 38 | content = content 39 | .replaceAll(" ", "") 40 | .replaceAll("\u3000", ""); 41 | content = "\u3000\u3000" + content; 42 | List list = []; 43 | List codes = content.codeUnits; 44 | for (int i = 0; i < codes.length; i++) { 45 | final char = String.fromCharCode(codes[i]); 46 | if (char != "\n") { 47 | list.add(char); 48 | } else { 49 | if (list.isNotEmpty) { 50 | if (list[list.length - 1].contains("\n")) { 51 | continue; 52 | } 53 | } 54 | list.add("\n\u3000\u3000"); 55 | } 56 | } 57 | return list.join(); 58 | } 59 | 60 | static String? getFontFamily() { 61 | return null; 62 | } 63 | 64 | static String toDBC(String input) { 65 | var c = input.codeUnits; 66 | var s = ''; 67 | for (var i = 0; i < c.length; i++) { 68 | if (c[i] == 32) { 69 | // 半角空格 70 | s = s + String.fromCharCode(12288); 71 | } else if (c[i] < 127) { 72 | // 半角英文字符 73 | //如果前后为换行符,则不转换 74 | if (c[i] == 10) { 75 | s = s + String.fromCharCode(c[i]); 76 | } else { 77 | s = s + String.fromCharCode(c[i] + 65248); 78 | } 79 | } else { 80 | // 非半角字符 81 | s = s + String.fromCharCode(c[i]); 82 | } 83 | } 84 | return s; 85 | } 86 | 87 | static FontWeight intToFontWeight(int weight) { 88 | switch(weight) { 89 | case 0: 90 | return FontWeight.w100; 91 | case 1: 92 | return FontWeight.w200; 93 | case 2: 94 | return FontWeight.w300; 95 | case 3: 96 | return FontWeight.w400; 97 | case 4: 98 | return FontWeight.w500; 99 | case 5: 100 | return FontWeight.w600; 101 | case 6: 102 | return FontWeight.w700; 103 | case 7: 104 | return FontWeight.w800; 105 | case 8: 106 | return FontWeight.w900; 107 | default: 108 | return FontWeight.w400; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/util/future_do.dart: -------------------------------------------------------------------------------- 1 | class FutureDo { 2 | /// 延迟执行后续方法 3 | static void doAfterExecutor(Function() afterExecutor, {Function? preExecutor, final int milliseconds = 1000}) { 4 | if (preExecutor != null) { 5 | preExecutor(); 6 | } 7 | Future.delayed(Duration(milliseconds: milliseconds), (){ 8 | afterExecutor(); 9 | }); 10 | } 11 | 12 | /// 延迟300毫秒执行后续方法 13 | static void doAfterExecutor300(Function() afterExecutor, {Function? preExecutor}) { 14 | doAfterExecutor(afterExecutor, preExecutor: preExecutor, milliseconds: 300); 15 | } 16 | } -------------------------------------------------------------------------------- /lib/util/html_parse_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:book_app/model/chapter/chapter.dart'; 5 | import 'package:book_app/model/message.dart'; 6 | import 'package:book_app/util/content_fliter.dart'; 7 | import 'package:book_app/util/random_user_agent.dart'; 8 | import 'package:html/parser.dart'; 9 | import 'package:html/dom.dart'; 10 | import 'package:book_app/api/http_manager.dart'; 11 | import 'package:book_app/log/log.dart'; 12 | import 'package:fast_gbk/fast_gbk.dart'; 13 | final RegExp chinese = RegExp(r"[\u4E00-\u9FA5]"); 14 | final RegExp contentFilter = contentFilterRegExp(); 15 | final RegExp nextPageReg = RegExp(r"下[1一]?页"); 16 | class HtmlParseUtil { 17 | static final List ignoreContentHtmlTag = ["a", "option", "h1", "h2", "strong", "font", "button", "script"]; 18 | static Future> parseChapter(String url, {Function(String? url)? img, Function(String name)? name, Function(int page)? pageFunc, String? originUrl, int page = 1}) async{ 19 | Document document = parse(await getFileString(url)); 20 | var body = document.body; 21 | if (img != null) { 22 | var imgs = document.getElementsByTagName("img"); 23 | if (imgs.isNotEmpty) { 24 | for (var imgE in imgs) { 25 | String? _uri = imgE.attributes['src']; 26 | String? _width = imgE.attributes['width']; 27 | String? _height = imgE.attributes['height']; 28 | if (_uri != null && _width != null && _height != null) { 29 | if (_uri.startsWith("/")) { 30 | _uri = Uri.parse(url).origin + _uri; 31 | } 32 | img(_uri); 33 | break; 34 | } 35 | } 36 | } 37 | } 38 | if (name != null) { 39 | var _h1S = document.getElementsByTagName("h1"); 40 | String? _bookName; 41 | if (_h1S.isNotEmpty) { 42 | _bookName = _h1S.first.text; 43 | } 44 | name(_bookName ?? "网络小说"); 45 | } 46 | List res = await _getChapterAllTags(body!); 47 | var aTags = res; 48 | Map>> parseMap = {}; 49 | int index = 0; 50 | for (int i = 0; i < aTags.length; i++) { 51 | var aTag = aTags[i]; 52 | Map chapter = {}; 53 | if (!aTag.attributes.containsKey("href")) { 54 | continue; 55 | } 56 | String src = aTag.attributes["href"]!; 57 | if (skip(src)) { 58 | continue; 59 | } 60 | String title = aTag.text; 61 | chapter["url"] = src; 62 | chapter["name"] = title; 63 | String prefix = parseSrcPrefix(src); 64 | if (prefix == "") { 65 | continue; 66 | } 67 | chapter["index"] = index; 68 | index = index + 1; 69 | if (parseMap.containsKey(prefix)) { 70 | List> chapters = parseMap[prefix]!; 71 | chapters.removeWhere((element) => element["name"] == chapter["name"]); 72 | chapters.add(chapter); 73 | parseMap[prefix] = chapters; 74 | } else { 75 | List> chapters = []; 76 | chapters.add(chapter); 77 | parseMap[prefix] = chapters; 78 | } 79 | } 80 | String maxKey = ""; 81 | int maxValue = 0; 82 | parseMap.forEach((key, value) { 83 | if (value.length > maxValue) { 84 | maxValue = value.length; 85 | maxKey = key; 86 | } 87 | }); 88 | List> trueChapters = parseMap[maxKey]!; 89 | var chapters = format(trueChapters, originUrl ?? url); 90 | for (var a in aTags) { 91 | if (a.text.contains(nextPageReg)) { 92 | String? nextPageUrl = a.attributes["href"]; 93 | if (nextPageUrl == null || nextPageUrl == "javascript:") { 94 | break; 95 | } 96 | // nextPageUrl = url.substring(0, url.lastIndexOf("/")) + nextPageUrl.substring(nextPageUrl.lastIndexOf('/')); 97 | nextPageUrl = _getNextPageUrl(url, nextPageUrl); 98 | originUrl ??= url; 99 | if (pageFunc != null) { 100 | pageFunc(page); 101 | } 102 | await Future.delayed(const Duration(milliseconds: 1500)); 103 | var nextPageData = await parseChapter(nextPageUrl, originUrl: originUrl, page: page + 1, pageFunc: pageFunc); 104 | chapters.addAll(nextPageData[1]); 105 | break; 106 | } 107 | } 108 | return [originUrl ?? url, chapters]; 109 | } 110 | 111 | static bool skip(String url) { 112 | return url.endsWith("/") || url.endsWith(".php") || url.endsWith(".js") || url.endsWith(".css"); 113 | } 114 | 115 | static String parseSrcPrefix(String src) { 116 | // 以 '/' 或者不带 '/'开头如 xxxx/xxx.htm这种大概率是的 117 | if (src.startsWith("/")) { 118 | if (!src.contains("/", 1)) { 119 | if (src.endsWith(".html") || src.endsWith(".htm")) { 120 | return "/" + src.substring(src.lastIndexOf(".")); 121 | } else { 122 | return ""; 123 | } 124 | } 125 | return src.substring(0, src.indexOf("/", 1)); 126 | } else if (src.startsWith("https")) { 127 | if (!src.contains("/", 8)) { 128 | return ""; 129 | } 130 | return src.substring(0, src.indexOf("/", 8)); 131 | } else if (src.startsWith("http")) { 132 | if (!src.contains("/", 7)) { 133 | return ""; 134 | } 135 | return src.substring(0, src.indexOf("/", 7)); 136 | } else { 137 | // 大概率是xxx.? 或xxx/xxx.?开头 138 | if (src.indexOf("/") > 0) { 139 | // xxx/xxx.?开头 140 | return src.substring(0, src.indexOf("/")); 141 | } else { 142 | // xxx.?开头 通常是.html结尾 143 | if (src.endsWith(".html") || src.endsWith(".htm")) { 144 | return src.substring(src.lastIndexOf(".")); 145 | } 146 | } 147 | } 148 | return ""; 149 | } 150 | static Map headers() { 151 | return { 152 | "Accept": "text/html;charset=UTF-8", 153 | "Accept-Encoding": "gzip, deflate, br", 154 | "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", 155 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/96.0.4664.110", 156 | }; 157 | } 158 | 159 | static List format(List> chapters, String url) { 160 | String childUrl = chapters[0]["url"]; 161 | Uri uri = Uri.parse(url); 162 | if (childUrl.startsWith("//")) { 163 | url = uri.scheme + ":"; 164 | } else if (childUrl.startsWith("/")) { 165 | url = uri.origin; 166 | } else if (childUrl.startsWith("http") || childUrl.startsWith("www")) { 167 | url = ""; 168 | } else { 169 | if (!url.endsWith("/")) { 170 | url = url.substring(0, url.lastIndexOf("/") + 1); 171 | } 172 | } 173 | List returnChapters = []; 174 | RegExp chapterMatch = RegExp(r"^第.*章|^\d+$"); 175 | for (var element in chapters) { 176 | var name = element["name"] as String; 177 | if (name.contains(RegExp("(正序)|(倒序)|(首页)|(尾页)|(上一页)|(下一页)"))) { 178 | if (!chapterMatch.hasMatch(name)) { 179 | continue; 180 | } 181 | } 182 | returnChapters.add(Chapter(name: element["name"].trim(), url: url + element["url"])); 183 | } 184 | return returnChapters; 185 | } 186 | 187 | 188 | static Future parseContent(String chapterName, String url, String? nextChapterUrl, {String? originPageId, int maxPage = 0}) async{ 189 | try { 190 | var html = await getFileString(url); 191 | Document document = parse(html); 192 | var body = document.body; 193 | List elements = []; 194 | getElement(elements, body!); 195 | Element? contentElement = findMaxChineseElement(elements); 196 | if (contentElement == null) { 197 | return ""; 198 | } 199 | // 有没有下一页 200 | String content = contentElement.innerHtml; 201 | if (!content.contains("
")) { 202 | content = contentElement.parent!.innerHtml; 203 | } 204 | // 最多应该有10页 205 | for (var a in body.getElementsByTagName("a")) { 206 | if (maxPage >= 10) { 207 | break; 208 | } 209 | if (a.text.contains(nextPageReg)) { 210 | String? nextPageUrl = a.attributes["href"]; 211 | if (nextPageUrl == null) { 212 | break; 213 | } 214 | nextPageUrl = _getNextPageUrl(url, nextPageUrl); 215 | originPageId ??= url; 216 | if (nextChapterUrl != null && nextChapterUrl.isNotEmpty) { 217 | if (nextPageUrl.contains(nextChapterUrl) || nextPageUrl == nextChapterUrl) { 218 | break; 219 | } 220 | } 221 | await Future.delayed(const Duration(milliseconds: 1500)); 222 | var nextPageContent = await parseContent(chapterName, nextPageUrl, nextChapterUrl,originPageId: originPageId, maxPage: maxPage + 1); 223 | content += nextPageContent; 224 | break; 225 | } 226 | } 227 | return _removeChapterName(chapterName, _beautifulFormat(_beautyUnknownTag(_beautyNotes(_beautyScript(_beautyBrAndP(_trim(content))))))); 228 | } catch(err) { 229 | Log.e(err); 230 | return ""; 231 | } 232 | } 233 | static getElement(List elements, Element parent) { 234 | if (ignoreContentHtmlTag.contains(parent.localName)) { 235 | return; 236 | } 237 | if (parent.children.isEmpty && parent.innerHtml.trim().isNotEmpty) { 238 | elements.add(parent); 239 | return; 240 | } 241 | 242 | List children = parent.children; 243 | int flag = 0; 244 | for (var child in children) { 245 | if (child.children.isEmpty || child.innerHtml.trim().isEmpty) { 246 | flag++; 247 | } else { 248 | flag = 0; 249 | } 250 | if (flag >= 3) { 251 | elements.add(parent); 252 | return; 253 | } 254 | getElement(elements, child); 255 | } 256 | } 257 | 258 | static Element? findMaxChineseElement(List elements) { 259 | Element? returnElement; 260 | int max = 0; 261 | for (var element in elements) { 262 | var content = element.innerHtml.replaceAll(RegExp(r"([\s\S]*?)"), ""); 263 | var allMatch = chinese.allMatches(content); 264 | if (allMatch.length > max) { 265 | returnElement = element; 266 | max = allMatch.length; 267 | } 268 | } 269 | return returnElement; 270 | } 271 | 272 | 273 | static getFileString(String url) async{ 274 | Log.i("发起请求 --- $url"); 275 | var res = await getString(url); 276 | return res; 277 | } 278 | static Future getString(String url, {bool retry = false, int retryTimes = 0}) async{ 279 | // var client = retry ? HttpClient() : HttpManager.httpClient!; 280 | var client = HttpClient(); 281 | client.badCertificateCallback = (X509Certificate cert, String host, int port) { 282 | return true; 283 | }; 284 | try { 285 | var request = await client.getUrl(Uri.parse(url)).timeout(const Duration(seconds: 10)); 286 | request.headers.remove("User-Agent", "Dart/2.16 (dart:io)"); 287 | request.headers.add("Accept", "text/html;charset=UTF-8"); 288 | request.headers.add("content-type", "text/html; charset=utf-8"); 289 | request.headers.add("User-Agent", randomUserAgent()); 290 | var response = await request.close().timeout(const Duration(seconds: 10)); 291 | List> dataBytes = await response.toList(); 292 | return decodeToStr(dataBytes); 293 | } catch(e) { 294 | if (retryTimes > 0) { 295 | rethrow; 296 | } 297 | await Future.delayed(const Duration(milliseconds: 1500)); 298 | return await getString(url, retry: true, retryTimes: retryTimes + 1); 299 | } 300 | } 301 | 302 | static String decodeToStr(List> dataBytes) { 303 | try{ 304 | return utf8Decode(dataBytes); 305 | }catch(_) { 306 | return gbkDecode(dataBytes); 307 | } 308 | } 309 | 310 | static String utf8Decode(List> dataBytesList) { 311 | List dataBytes = []; 312 | for (var value in dataBytesList) { 313 | dataBytes.addAll(value); 314 | } 315 | return utf8.decode(dataBytes); 316 | } 317 | 318 | static String gbkDecode(List> dataBytesList) { 319 | List dataBytes = []; 320 | for (var value in dataBytesList) { 321 | dataBytes.addAll(value); 322 | } 323 | return gbk.decode(dataBytes); 324 | } 325 | 326 | /// 获取所有的章节 327 | static Future> _getChapterAllTags(Element body) async{ 328 | return body.getElementsByTagName("a"); 329 | } 330 | 331 | static String _removeChapterName(String chapterName, String beautifulFormat) { 332 | try { 333 | chapterName = chapterName.replaceAll(" ", ""); 334 | return beautifulFormat.replaceAll(RegExp(".*$chapterName.*"), ""); 335 | } catch(_) { 336 | return beautifulFormat; 337 | } 338 | } 339 | 340 | static String _getNextPageUrl(String url, String nextPageUrl) { 341 | var uri = Uri.parse(url); 342 | /// //aaa/cvvv 343 | if (nextPageUrl.startsWith("//")) { 344 | return uri.scheme + ":" + nextPageUrl; 345 | } 346 | /// /aaa/cvvv 347 | if (nextPageUrl.startsWith("/")) { 348 | return uri.origin + nextPageUrl; 349 | } 350 | /// http://aaa/cvvv 351 | if (nextPageUrl.startsWith("http")) { 352 | return nextPageUrl; 353 | } 354 | if (nextPageUrl.startsWith("www")) { 355 | return uri.scheme + ":" + "//" + nextPageUrl; 356 | } 357 | if (!url.endsWith("/")) { 358 | url += "/"; 359 | } 360 | return url + nextPageUrl; 361 | } 362 | } 363 | 364 | String _trim(String text) { 365 | return text 366 | .replaceAll(" ", "") 367 | .replaceAll(">", "") 368 | .replaceAll(" ", ""); 369 | } 370 | String _beautyBrAndP(String text) { 371 | return text.replaceAll(RegExp(r"]*>"), "") 372 | .replaceAll("

", "
").replaceAll(RegExp(r"]*>"), "") 373 | .replaceAll(RegExp(r"]*>"), "").replaceAll("", "") 374 | .replaceAll("", "
").replaceAll("
", "\n") 375 | ; 376 | } 377 | String _beautyScript(String text) { 378 | return text.replaceAll(RegExp(r"<[a-zA-Z]+.*?>([\s\S]*?)"), ""); 379 | } 380 | String _beautyNotes(String text) { 381 | return text.replaceAll(RegExp(r""), ""); 382 | } 383 | String _beautyUnknownTag(String text) { 384 | return text.replaceAll(RegExp(r"<\.*>|<.*>"), ""); 385 | } 386 | String _beautifulFormat(String str) { 387 | var strList = str.split('\n'); 388 | List newStr = []; 389 | for (var element in strList) { 390 | if (element.isEmpty) { 391 | continue; 392 | } 393 | var match = chinese.allMatches(element); 394 | if (match.isEmpty || match.length <= 1) { 395 | continue; 396 | } 397 | if (element.contains(contentFilter)) { 398 | continue; 399 | } 400 | newStr.add(element.replaceAll(" ", "").replaceAll("\u3000", "")); 401 | } 402 | return newStr.join('\n').trim(); 403 | } -------------------------------------------------------------------------------- /lib/util/keep_alive_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class KeepAliveWrapper extends StatefulWidget { 4 | final Widget child; 5 | 6 | const KeepAliveWrapper(this.child, {Key? key}) : super(key: key); 7 | 8 | @override 9 | _KeepAliveWrapperState createState() => _KeepAliveWrapperState(); 10 | } 11 | 12 | class _KeepAliveWrapperState extends State 13 | with AutomaticKeepAliveClientMixin { 14 | @override 15 | Widget build(BuildContext context) { 16 | super.build(context); 17 | return widget.child; 18 | } 19 | 20 | @override 21 | bool get wantKeepAlive => true; 22 | } -------------------------------------------------------------------------------- /lib/util/limit_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | class LimitUtil { 4 | static const deFaultDurationTime = 300; 5 | static Timer? timer; 6 | 7 | // 防抖函数 8 | static debounce(Function doSomething, {durationTime = deFaultDurationTime}) { 9 | timer?.cancel(); 10 | timer = Timer(Duration(milliseconds: durationTime), () { 11 | doSomething; 12 | }); 13 | } 14 | 15 | // 节流函数 16 | static const String deFaultThrottleId = 'DeFaultThrottleId'; 17 | static Map startTimeMap = {deFaultThrottleId: 0}; 18 | static throttle(Function doSomething, {String throttleId = deFaultThrottleId, durationTime = deFaultDurationTime, Function? continueClick}) { 19 | int currentTime = DateTime.now().millisecondsSinceEpoch; 20 | if (currentTime - (startTimeMap[throttleId] ?? 0) > durationTime) { 21 | doSomething.call(); 22 | startTimeMap[throttleId] = DateTime.now().millisecondsSinceEpoch; 23 | } else { 24 | continueClick?.call(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/util/list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // ignore: non_constant_identifier_names 4 | Widget ListItem( 5 | String left, 6 | Widget right, 7 | {Color? backgroundColor, 8 | Color? textColor}) 9 | { 10 | return Container( 11 | height: 50, 12 | color: backgroundColor, 13 | child: Container( 14 | padding: const EdgeInsets.only(left: 15, right: 15, top: 12, bottom: 12), 15 | child: Row( 16 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 17 | children: [ 18 | Text(left, style: TextStyle(color: textColor, fontSize: 16, height: 1), ), 19 | right 20 | ], 21 | ), 22 | ), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /lib/util/no_shadow_scroll_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoShadowScrollBehavior extends ScrollBehavior { 4 | @override 5 | Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { 6 | switch (getPlatform(context)) { 7 | case TargetPlatform.iOS: 8 | case TargetPlatform.macOS: 9 | return child; 10 | case TargetPlatform.android: 11 | case TargetPlatform.fuchsia: 12 | case TargetPlatform.linux: 13 | case TargetPlatform.windows: 14 | return GlowingOverscrollIndicator( 15 | child: child, 16 | //不显示头部水波纹 17 | showLeading: false, 18 | //不显示尾部水波纹 19 | showTrailing: false, 20 | axisDirection: axisDirection, 21 | color: Colors.transparent, 22 | ); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/util/notify/counter_notify.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 数值变化监听 4 | class CounterNotify extends ChangeNotifier { 5 | int _count = 0; 6 | int get count => _count; 7 | 8 | 9 | addCount([int number = 1]) { 10 | _count += number; 11 | notifyListeners(); 12 | } 13 | 14 | setCount(int number) { 15 | _count = number; 16 | notifyListeners(); 17 | } 18 | 19 | resetCount() { 20 | _count = 0; 21 | notifyListeners(); 22 | } 23 | } -------------------------------------------------------------------------------- /lib/util/notify/object_notify.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// 对象变化监听 4 | class ObjectNotify extends ChangeNotifier { 5 | late T _data; 6 | T get data => _data; 7 | 8 | ObjectNotify(T data) { 9 | _data = data; 10 | } 11 | 12 | update(T data) { 13 | _data = data; 14 | notifyListeners(); 15 | } 16 | } -------------------------------------------------------------------------------- /lib/util/parse_book.dart: -------------------------------------------------------------------------------- 1 | import 'package:book_app/log/log.dart'; 2 | import 'package:book_app/mapper/book_db_provider.dart'; 3 | import 'package:book_app/mapper/chapter_db_provider.dart'; 4 | import 'package:book_app/model/book/book.dart'; 5 | import 'package:book_app/module/book/home/book_home_controller.dart'; 6 | import 'package:book_app/theme/color.dart'; 7 | import 'package:book_app/util/dialog_build.dart'; 8 | import 'package:book_app/util/toast.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:get/get.dart'; 11 | 12 | import 'html_parse_util.dart'; 13 | 14 | parseBookByShare(String bookName, String content) async{ 15 | Get.dialog( 16 | DialogBuild( 17 | "分享小说", 18 | Text.rich( 19 | TextSpan( 20 | text: "是否解析来自其它APP分享的小说", 21 | children: [ 22 | TextSpan(text: bookName, style: const TextStyle(color: Colors.lightBlueAccent)) 23 | ], 24 | style: TextStyle(color: textColor(), fontSize: 14) 25 | ) 26 | ), 27 | confirmFunction: () { 28 | Get.back(); 29 | Future.delayed(const Duration(milliseconds: 500), () { 30 | BookHomeController bookHomeController = Get.find(); 31 | bookHomeController.parseBookText(content.split("\n"), bookName); 32 | }); 33 | }, 34 | ) 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/util/parse_network_book.dart: -------------------------------------------------------------------------------- 1 | import 'dart:isolate'; 2 | 3 | import 'package:book_app/log/log.dart'; 4 | import 'package:book_app/model/book/book.dart'; 5 | import 'package:book_app/model/message.dart'; 6 | import 'package:book_app/util/html_parse_util.dart'; 7 | import 'package:book_app/util/toast.dart'; 8 | 9 | class ParseNetworkBook { 10 | String url; 11 | String? name; 12 | final SendPort sendPort; 13 | Isolate? _isolate; 14 | 15 | ParseNetworkBook(this.url, this.sendPort, {this.name}); 16 | 17 | Future> parseInBackground() async{ 18 | final p = ReceivePort(); 19 | _isolate = await Isolate.spawn(_parse, p.sendPort,); 20 | return await p.first; 21 | } 22 | 23 | kill() { 24 | final _isolate = this._isolate; 25 | if (_isolate != null) { 26 | _isolate.kill(); 27 | } 28 | } 29 | 30 | _parse(SendPort p) async{ 31 | try { 32 | String? img; 33 | var results = (await HtmlParseUtil.parseChapter(url, img: (imgUrl) { 34 | img = imgUrl; 35 | }, 36 | pageFunc: (page) { 37 | sendPort.send(Message(MessageType.parseNetworkBook, page)); 38 | }, 39 | name: (_bookName) { 40 | name = name ?? _bookName; 41 | })); 42 | url = results[0]; 43 | var chapters = results[1]; 44 | final Book book = Book(url: url, name: name, indexImg: img); 45 | var day = DateTime.now(); 46 | book.updateTime = "${day.year}-${day.month}-${day.day}"; 47 | return Isolate.exit(p, [book, chapters]); 48 | } catch (err) { 49 | Log.e(err); 50 | return Isolate.exit(p, []); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /lib/util/path_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:path_provider/path_provider.dart'; 4 | 5 | class PathUtil { 6 | static Future getSavePath(String path) async{ 7 | Directory? dir; 8 | if (Platform.isAndroid) { 9 | dir = await getExternalStorageDirectory(); 10 | } else { 11 | dir = await getApplicationDocumentsDirectory(); 12 | } 13 | Directory _bookDir = Directory("${dir!.path}/$path"); 14 | if (!_bookDir.existsSync()) { 15 | _bookDir.createSync(); 16 | } 17 | return _bookDir.path; 18 | } 19 | } -------------------------------------------------------------------------------- /lib/util/random_user_agent.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | List _userAgent = [ 4 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36", 5 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB7.0)", 6 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", 7 | "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", 8 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; ) AppleWebKit/534.12 (KHTML, like Gecko) Maxthon/3.0 Safari/534.12", 9 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)", 10 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E; SE 2.X MetaSr 1.0)", 11 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.33 Safari/534.3 SE 2.X MetaSr 1.0", 12 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E)", 13 | "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1 QQBrowser/6.9.11079.201", 14 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E) QQBrowser/6.9.11079.201", 15 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)", 16 | "Mozilla/5.0(Macintosh;U;IntelMacOSX10_6_8;en-us)AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50", 17 | "Mozilla/5.0(Windows;U;WindowsNT6.1;en-us)AppleWebKit/534.50(KHTML,likeGecko)Version/5.1Safari/534.50", 18 | "Mozilla/5.0(compatible;MSIE9.0;WindowsNT6.1;Trident/5.0;", 19 | "Mozilla/4.0(compatible;MSIE8.0;WindowsNT6.0;Trident/4.0)", 20 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT6.0)", 21 | "Mozilla/4.0(compatible;MSIE6.0;WindowsNT5.1)", 22 | "Mozilla/5.0(Macintosh;IntelMacOSX10.6;rv:2.0.1)Gecko/20100101Firefox/4.0.1", 23 | "Mozilla/5.0(WindowsNT6.1;rv:2.0.1)Gecko/20100101Firefox/4.0.1", 24 | "Opera/9.80(Macintosh;IntelMacOSX10.6.8;U;en)Presto/2.8.131Version/11.11", 25 | "Opera/9.80(WindowsNT6.1;U;en)Presto/2.8.131Version/11.11", 26 | "Mozilla/5.0(Macintosh;IntelMacOSX10_7_0)AppleWebKit/535.11(KHTML,likeGecko)Chrome/17.0.963.56Safari/535.11", 27 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;Maxthon2.0)", 28 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;TencentTraveler4.0)", 29 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1)", 30 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;TheWorld)", 31 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;Trident/4.0;SE2.XMetaSr1.0;SE2.XMetaSr1.0;.NETCLR2.0.50727;SE2.XMetaSr1.0)", 32 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;360SE)", 33 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1;AvantBrowser)", 34 | "Mozilla/4.0(compatible;MSIE7.0;WindowsNT5.1)", 35 | "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/96.0.4664.110" 36 | ]; 37 | 38 | String randomUserAgent() { 39 | return _userAgent[Random().nextInt(_userAgent.length)]; 40 | } 41 | -------------------------------------------------------------------------------- /lib/util/save_util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:book_app/model/base.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | class SaveUtil { 7 | static late SharedPreferences _sharedPreferences; 8 | 9 | static Future init() async{ 10 | _sharedPreferences = await SharedPreferences.getInstance(); 11 | } 12 | 13 | static void setModel(key, Base model) { 14 | _sharedPreferences.setString(key, model.toJson().toString()); 15 | } 16 | 17 | static void setModelList(key, List list) { 18 | List data = []; 19 | for (var element in list) { 20 | data.add(json.encode(element.toJson())); 21 | } 22 | _sharedPreferences.setStringList(key, data); 23 | } 24 | 25 | static String? getModel(key) { 26 | return _sharedPreferences.getString(key); 27 | } 28 | static List? getModelList(key) { 29 | return _sharedPreferences.getStringList(key); 30 | } 31 | 32 | static void setTrue(key, {bool isTrue = true}) { 33 | _sharedPreferences.setBool(key, isTrue); 34 | } 35 | static bool? getTrue(key) { 36 | return _sharedPreferences.getBool(key); 37 | } 38 | 39 | static void setString(key, str) { 40 | _sharedPreferences.setString(key, str); 41 | } 42 | static String? getString(key) { 43 | return _sharedPreferences.getString(key); 44 | } 45 | 46 | static void remove(key) { 47 | _sharedPreferences.remove(key); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/util/system_utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | getAppBarTop() { 3 | 4 | } 5 | /// 获取状态栏高度 6 | double getStatusBarHeight(BuildContext context) { 7 | return MediaQuery.of(context).padding.top; 8 | } 9 | late BuildContext globalContext; 10 | -------------------------------------------------------------------------------- /lib/util/time_util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TimeUtil { 4 | static String getSystemTime() { 5 | var date = DateTime.now(); 6 | return "${date.hour < 10 ? '0' + date.hour.toString() : date.hour}:${date.minute < 10 ? '0' + date.minute.toString() : date.minute}"; 7 | } 8 | 9 | static String formatTime(int time) { 10 | String str = ""; 11 | if (time ~/ 3600 > 0) { 12 | int hour = time ~/ 3600; 13 | str += "0$hour:"; 14 | time = time % 3600; 15 | } 16 | if (time ~/ 60 > 0) { 17 | int minutes = time ~/ 60; 18 | str += minutes > 9 ? "$minutes:" : "0$minutes:"; 19 | time = time % 60; 20 | } else { 21 | str += "00:"; 22 | } 23 | str += time > 9 ? "$time" : "0$time"; 24 | return str; 25 | } 26 | 27 | static String getMonthStr(int index) { 28 | return "${index + 1}月"; 29 | } 30 | 31 | static String getYearStr(int diff) { 32 | return "${(DateTime.now().year - diff)}年"; 33 | } 34 | 35 | static String getChineseDayDiff(DateTime selectedDay) { 36 | DateTime now = DateTime.now(); 37 | DateTimeRange range = DateUtils.datesOnly(DateTimeRange(start: selectedDay, end: now)); 38 | int dayDiff = range.duration.inDays; 39 | if (dayDiff == 0) { 40 | return "今天"; 41 | } 42 | if (dayDiff > 0 && dayDiff <= 3) { 43 | switch(dayDiff) { 44 | case 1: 45 | return "昨天"; 46 | case 2: 47 | return "前天"; 48 | case 3: 49 | return "大前天"; 50 | } 51 | } 52 | if (dayDiff < 0 && dayDiff >= -3) { 53 | switch(dayDiff) { 54 | case -1: 55 | return "明天"; 56 | case -2: 57 | return "后天"; 58 | case -3: 59 | return "大后天"; 60 | } 61 | } 62 | return "${selectedDay.day}号"; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/util/toast.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_easyloading/flutter_easyloading.dart'; 4 | 5 | class Toast { 6 | 7 | /// 吐司 8 | static void toast({String toast = "加载中...", Duration? duration}) { 9 | EasyLoading.showToast(toast, duration: duration); 10 | } 11 | 12 | /// 长吐司 13 | static void toastL({String toast = "加载中..."}) { 14 | EasyLoading.show(status: toast, maskType: EasyLoadingMaskType.clear); 15 | } 16 | 17 | /// 长吐司 18 | static void toastLWithDismiss(Future Function() executor, {String toast = "加载中..."}) async{ 19 | EasyLoading.show(status: toast, maskType: EasyLoadingMaskType.clear); 20 | executor().then((value) => cancel()).catchError((_) => cancel()); 21 | } 22 | 23 | /// 取消吐司 24 | static void cancel() { 25 | EasyLoading.dismiss(); 26 | } 27 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: book_app 2 | description: 轻阅读 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.1.3+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | flutter_localizations: 33 | sdk: flutter 34 | 35 | 36 | # The following adds the Cupertino Icons font to your application. 37 | # Use with the CupertinoIcons class for iOS style icons. 38 | cupertino_icons: ^1.0.2 39 | get: ^4.6.1 40 | flutter_easyloading: ^3.0.3 41 | shared_preferences: ^2.0.8 42 | sqflite: ^2.0.0+4 43 | json_annotation: ^4.1.0 44 | logger: ^1.1.0 45 | cached_network_image: ^3.1.0 46 | # 选色板 47 | flutter_colorpicker: ^1.0.3 48 | flutter_switch: ^0.3.2 49 | numberpicker: ^2.1.1 50 | file_picker: ^4.6.1 51 | permission_handler: ^8.3.0 52 | fast_gbk: ^1.0.0 53 | woshilll_flutter_plugin: 54 | git: 55 | url: git@github.com:woshilll/woshilll_flutter_plugin.git 56 | flutter_zoom_drawer: ^2.3.1+1 57 | draggable_scrollbar: ^0.1.0 58 | share_plus: ^4.0.1 59 | path_provider: ^2.0.9 60 | url_launcher: ^6.0.9 61 | 62 | dev_dependencies: 63 | flutter_test: 64 | sdk: flutter 65 | 66 | # The "flutter_lints" package below contains a set of recommended lints to 67 | # encourage good coding practices. The lint set provided by the package is 68 | # activated in the `analysis_options.yaml` file located at the root of your 69 | # package. See that file for information about deactivating specific lint 70 | # rules and activating additional ones. 71 | flutter_lints: ^1.0.0 72 | json_serializable: ^6.1.4 73 | build_runner: ^2.1.4 74 | dart_code_metrics: ^4.11.0 75 | 76 | # For information on the generic Dart part of this file, see the 77 | # following page: https://dart.dev/tools/pub/pubspec 78 | 79 | # The following section is specific to Flutter. 80 | flutter: 81 | 82 | # The following line ensures that the Material Icons font is 83 | # included with your application, so that you can use the icons in 84 | # the material Icons class. 85 | uses-material-design: true 86 | assets: 87 | - lib/resource/ 88 | - lib/resource/image/screen_h.png 89 | # To add assets to your application, add an assets section, like this: 90 | # assets: 91 | # - images/a_dot_burr.jpeg 92 | # - images/a_dot_ham.jpeg 93 | 94 | # An image asset can refer to one or more resolution-specific "variants", see 95 | # https://flutter.dev/assets-and-images/#resolution-aware. 96 | 97 | # For details regarding adding assets from package dependencies, see 98 | # https://flutter.dev/assets-and-images/#from-packages 99 | # fonts: 100 | # - family: KaiTi 101 | # fonts: 102 | # - asset: lib/resource/font/KaiTi.ttf 103 | # To add custom fonts to your application, add a fonts section here, 104 | # in this "flutter" section. Each entry in this list should have a 105 | # "family" key with the font family name, and a "fonts" key with a 106 | # list giving the asset and other descriptors for the font. For 107 | # example: 108 | # fonts: 109 | # - family: Schyler 110 | # fonts: 111 | # - asset: fonts/Schyler-Regular.ttf 112 | # - asset: fonts/Schyler-Italic.ttf 113 | # style: italic 114 | # - family: Trajan Pro 115 | # fonts: 116 | # - asset: fonts/TrajanPro.ttf 117 | # - asset: fonts/TrajanPro_Bold.ttf 118 | # weight: 700 119 | # 120 | # For details regarding fonts from package dependencies, 121 | # see https://flutter.dev/custom-fonts/#from-packages 122 | -------------------------------------------------------------------------------- /test/test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:book_app/log/log.dart'; 4 | import 'package:book_app/util/html_parse_util.dart'; 5 | 6 | void main() async{ 7 | final RegExp nextPageReg = RegExp(r"下[1一]?页"); 8 | Log.i("页".contains(nextPageReg)); 9 | } 10 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woshilll/book_app/0076d8c22a01f5c3005b2fd510b3d085d6bea8b4/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | book_app 30 | 31 | 32 | 33 | 36 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "book_app", 3 | "short_name": "book_app", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "小说阅读", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------