├── .gitignore ├── .metadata ├── LICENSE ├── README-zh.md ├── README.md ├── android.iml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── yotaku │ │ │ └── light │ │ │ └── MainActivity.java │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── lightlogo_dark20_color_132x44.png │ │ └── product_logo_translate_color_144.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── lightlogo_dark20_color_132x44.png │ │ └── product_logo_translate_color_144.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── lightlogo_dark20_color_132x44.png │ │ └── product_logo_translate_color_144.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── lightlogo_dark20_color_132x44.png │ │ └── product_logo_translate_color_144.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── lightlogo_dark20_color_132x44.png │ │ └── product_logo_translate_color_144.png │ │ └── values │ │ ├── dimens.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── assets ├── background │ ├── bg1.png │ ├── bg10.png │ ├── bg11.png │ ├── bg2.png │ ├── bg3.png │ ├── bg4.png │ ├── bg5.png │ ├── bg6.png │ ├── bg7.jpg │ ├── bg8.png │ └── bg9.png ├── config.ini ├── default_cover.jpg ├── font │ ├── Avenir.ttf │ ├── FZYouH.ttf │ ├── HYQH.ttf │ ├── Lora-Regular.ttf │ └── Noto Serif.ttf └── light.db ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── main.dart └── src │ ├── app.dart │ ├── model │ ├── book.dart │ ├── message.dart │ ├── read_mode.dart │ ├── selected_list_model.dart │ ├── tieba_post.dart │ └── tieba_topic.dart │ ├── service │ ├── app_service.dart │ ├── baidu_service.dart │ ├── book_service.dart │ ├── config.dart │ ├── db.dart │ ├── file_service.dart │ ├── initial.dart │ ├── local.dart │ ├── mock_book.dart │ ├── search.dart │ └── tieba.dart │ ├── utils │ ├── custom_text_painter.dart │ └── page_calculator.dart │ ├── view │ ├── chat │ │ └── chat_screen.dart │ ├── explore │ │ └── explore.dart │ ├── home.dart │ ├── profile │ │ └── profile.dart │ ├── reader │ │ ├── mask.dart │ │ ├── menu.dart │ │ ├── page.dart │ │ ├── reader.dart │ │ ├── setting_list.dart │ │ └── setting_pannel.dart │ ├── search │ │ ├── search.dart │ │ ├── search_item.dart │ │ └── tieba │ │ │ ├── detail.dart │ │ │ ├── search_tieba.dart │ │ │ └── tieba.dart │ ├── shelf │ │ ├── book_item.dart │ │ ├── entity_item.dart │ │ ├── import_book.dart │ │ └── shelf.dart │ └── web │ │ ├── page.dart │ │ └── select_items.dart │ └── widgets │ ├── custom_button.dart │ ├── custom_indicator.dart │ ├── custom_page_route.dart │ ├── custom_slider.dart │ ├── dialog_item.dart │ ├── image_view.dart │ ├── item_button.dart │ ├── label.dart │ └── select_bottom_bar.dart ├── light.iml ├── light_android.iml └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .atom/ 3 | .idea 4 | .vscode/ 5 | .packages 6 | .pub/ 7 | pubspec.lock 8 | build/ 9 | ios/.generated/ 10 | packages 11 | .flutter-plugins 12 | 13 | key.properties 14 | *.log -------------------------------------------------------------------------------- /.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: 3ea4d06340a97a1e9d7cae97567c64e0569dcaa2 8 | channel: beta 9 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # Light 2 | 3 | ![Light logo](https://user-images.githubusercontent.com/17924777/39092072-762deace-4636-11e8-8acd-447a03c7556e.png) 4 | 5 | Light是一个用Flutter开发的轻小说阅读器。 6 | 7 | 作为学习Flutter期间编写的软件,存在很多不足之处,现在已不再维护。 8 | 9 | 如果你对重构的Light感兴趣,请点击[https://github.com/creatint/light](https://github.com/creatint/light),欢迎提出建议。 10 | 11 | 12 | 13 | ## Features 14 | - [x] 支持ePub2、ePub3文件 15 | - [x] 支持utf-8编码 16 | - [x] 支持latin-1编码 17 | - [x] 支持扫描、导入本地文件 18 | - [x] 自定义字高 19 | - [x] 自定义行高 20 | - [x] 多主题 21 | - [ ] 日、夜间模式切换 22 | - [ ] 搜索、关注、阅读贴吧 23 | - [ ] 剩余页数 24 | - [ ] 垂直、水平滚动 25 | - [ ] 解析ePub文件封面 26 | - [ ] 添加、删除、搜索标签 27 | - [ ] 处理内、外链 28 | - [ ] 贴吧优化阅读 29 | - [ ] 贴吧离线阅读 30 | - [ ] 在线搜索 31 | - [ ] Wifi导书 32 | - [ ] 好友、聊天 33 | - [ ] 书籍分享 34 | - [ ] 文字语音朗读 35 | - [ ] 用户设置 36 | 37 | ## 截屏 38 | 分页显示 | 本地导入 39 | :-------------------------:|:-------------------------: 40 | ![分页显示](https://user-images.githubusercontent.com/17924777/39093416-24e27484-4652-11e8-9eaa-96b610508d80.gif) | ![本地导入](https://user-images.githubusercontent.com/17924777/39093132-18904792-464d-11e8-9bda-4f30abec0504.gif) 41 | 42 | 搜索贴吧 | 浏览帖子 43 | :-------------------------:|:-------------------------: 44 | ![搜索贴吧](https://user-images.githubusercontent.com/17924777/39093389-d2d79c64-4651-11e8-9b19-07490ccbb44a.gif) | ![浏览帖子](https://user-images.githubusercontent.com/17924777/39093405-0108874c-4652-11e8-9e79-884a1f6961a9.gif) 45 | 46 | ## 用法 47 | ``` 48 | git clone https://github.com/creatint/light 49 | flutter packages get 50 | flutter run 51 | flutter build apk --release 52 | ``` 53 | 想要开始使用Flutter框架请浏览在线文档[documentation](https://flutter.io/). 54 | 55 | 56 | ## 联系我 57 | Email: creatint@163.com 58 | 59 | QQ: 565864175 60 | 61 | 编程交流群: [![编程技术交流](https://pub.idqqimg.com/wpa/images/group.png)](//shang.qq.com/wpa/qunwpa?idkey=b34e5d3956950dc053efdd7aef63ef75151c01cfff48a951c8fc53d6349b454a) 62 | 63 | 二次元交流群: [![Yotaku](https://pub.idqqimg.com/wpa/images/group.png)](//shang.qq.com/wpa/qunwpa?idkey=2fea46b70c9a73fcbfedd08ee64ed9d6d8c554baa63dc2402082226675e825e7) 64 | 65 | ## License 66 | Light使用GPL-3.0开源许可证,查看[证书](https://github.com/creatint/light/blob/master/LICENSE)。 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Light 2 | 3 | ![Light logo](https://user-images.githubusercontent.com/17924777/39092072-762deace-4636-11e8-8acd-447a03c7556e.png) 4 | 5 | Light is a light novel e-reader build with [Flutter](https://flutter.io). 6 | 7 | As an app written in the study of Flutter, it has many defects. 8 | Confirmed habits are hard to get rid of, so this repository has been no longer maintained. 9 | 10 | If you are interested in my new [Light](https://github.com/creatint/light), please click this [https://github.com/creatint/light](https://github.com/creatint/light), welcome your advice. 11 | 12 | [中文版](README-zh.md) 13 | 14 | ## Features 15 | - [x] ePub 2 and ePub 3 support. 16 | - [x] Utf-8 support. 17 | - [x] Latin-1 support. 18 | - [x] Scan and Import Local Files. 19 | - [x] Custom Text Size. 20 | - [x] Custom Line Height. 21 | - [x] Multi Themes. 22 | - [ ] Day mode / Night mode. 23 | - [ ] Search / Mark / Reading Tieba. 24 | - [ ] Reading Pages left. 25 | - [ ] Vertical or/and Horizontal scrolling. 26 | - [ ] Parse epub cover image. 27 | - [ ] Add / Delete / Search Mark. 28 | - [ ] Handle Internal and External Links. 29 | - [ ] Tieba Optimize reading. 30 | - [ ] Tieba Offline Reading. 31 | - [ ] Book Search Online. 32 | - [ ] Import Book from Wifi. 33 | - [ ] Friend / Chat. 34 | - [ ] Book Share. 35 | - [ ] TTS - Text to Speech Support. 36 | - [ ] User Profile. 37 | 38 | ## Screeshot 39 | Paging | Import 40 | :-------------------------:|:-------------------------: 41 | ![Paging](https://user-images.githubusercontent.com/17924777/39093416-24e27484-4652-11e8-9eaa-96b610508d80.gif) | ![Import](https://user-images.githubusercontent.com/17924777/39093132-18904792-464d-11e8-9bda-4f30abec0504.gif) 42 | 43 | Tieba.baidu.com | Posts 44 | :-------------------------:|:-------------------------: 45 | ![Tieba.com](https://user-images.githubusercontent.com/17924777/39093389-d2d79c64-4651-11e8-9b19-07490ccbb44a.gif) | ![Posts](https://user-images.githubusercontent.com/17924777/39093405-0108874c-4652-11e8-9e79-884a1f6961a9.gif) 46 | 47 | ## Usage 48 | ``` 49 | git clone https://github.com/creatint/light 50 | flutter packages get 51 | flutter run 52 | flutter build apk --release 53 | ``` 54 | For help getting started with Flutter, view our online 55 | [documentation](https://flutter.io/). 56 | 57 | 58 | ## Contact 59 | Email: creatint@163.com 60 | 61 | QQ: 565864175 62 | 63 | QQ Group for Coding: [![](https://pub.idqqimg.com/wpa/images/group.png)](//shang.qq.com/wpa/qunwpa?idkey=b34e5d3956950dc053efdd7aef63ef75151c01cfff48a951c8fc53d6349b454a) 64 | 65 | QQ Group for ACGN: [![Yotaku](https://pub.idqqimg.com/wpa/images/group.png)](//shang.qq.com/wpa/qunwpa?idkey=2fea46b70c9a73fcbfedd08ee64ed9d6d8c554baa63dc2402082226675e825e7) 66 | 67 | ## License 68 | 69 | Light is available under the GPL-3.0 license. See the [LICENSE](https://github.com/creatint/light/blob/master/LICENSE) file. -------------------------------------------------------------------------------- /android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /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 | apply plugin: 'com.android.application' 15 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 16 | 17 | def keystorePropertiesFile = rootProject.file("key.properties") 18 | def keystoreProperties = new Properties() 19 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 20 | 21 | android { 22 | compileSdkVersion 27 23 | 24 | lintOptions { 25 | disable 'InvalidPackage' 26 | } 27 | 28 | defaultConfig { 29 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 30 | applicationId "cn.yotaku.light" 31 | minSdkVersion 16 32 | targetSdkVersion 27 33 | versionCode 1 34 | versionName "1.0" 35 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 36 | } 37 | 38 | signingConfigs { 39 | release { 40 | keyAlias keystoreProperties['keyAlias'] 41 | keyPassword keystoreProperties['keyPassword'] 42 | storeFile file(keystoreProperties['storeFile']) 43 | storePassword keystoreProperties['storePassword'] 44 | } 45 | } 46 | buildTypes { 47 | release { 48 | signingConfig signingConfigs.release 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 60 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 17 | 21 | 28 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /android/app/src/main/java/cn/yotaku/light/MainActivity.java: -------------------------------------------------------------------------------- 1 | package cn.yotaku.light; 2 | 3 | import android.content.ContextWrapper; 4 | import android.content.Intent; 5 | import android.content.IntentFilter; 6 | import android.net.Uri; 7 | import android.os.BatteryManager; 8 | import android.os.Build.VERSION; 9 | import android.os.Build.VERSION_CODES; 10 | import android.os.Bundle; 11 | import android.provider.Settings; 12 | 13 | import io.flutter.app.FlutterActivity; 14 | import io.flutter.plugins.GeneratedPluginRegistrant; 15 | import io.flutter.plugin.common.MethodCall; 16 | import io.flutter.plugin.common.MethodChannel; 17 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler; 18 | import io.flutter.plugin.common.MethodChannel.Result; 19 | 20 | import java.io.BufferedReader; 21 | import java.io.File; 22 | import java.io.FileInputStream; 23 | import java.io.InputStreamReader; 24 | 25 | public class MainActivity extends FlutterActivity { 26 | private static final String CHANNEL = "light.yotaku.cn/system"; 27 | 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | GeneratedPluginRegistrant.registerWith(this); 32 | 33 | new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( 34 | new MethodCallHandler() { 35 | @Override 36 | public void onMethodCall(MethodCall call, Result result) { 37 | try { 38 | if (call.method.equals("getBatteryLevel")) { 39 | int batteryLevel = getBatteryLevel(); 40 | 41 | if (batteryLevel != -1) { 42 | result.success(batteryLevel); 43 | } else { 44 | result.error("UNAVAILABLE", "Battery level not available.", null); 45 | } 46 | } else if (call.method.equals("openApplicationSettings")) { 47 | boolean res = openApplicationSettings(); 48 | result.success(res); 49 | } else if (call.method.equals("readFile")) { 50 | // result.success(decodeGbkFile(call.argument("path"))); 51 | // String str = decodeGbkFile(call.argument("path")); 52 | // String str = readFile("hahahaha"); 53 | // System.out.print(call.argument("path").toString()); 54 | String path = call.argument("path"); 55 | String res = readFile(path); 56 | result.success(res); 57 | // result.success("hahaha"); 58 | } else { 59 | result.notImplemented(); 60 | } 61 | } catch (Exception e) { 62 | System.out.println("Error: " + e.getMessage()); 63 | } 64 | } 65 | } 66 | 67 | ); 68 | } 69 | 70 | private int getBatteryLevel() { 71 | int batteryLevel = -1; 72 | if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { 73 | BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE); 74 | batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY); 75 | } else { 76 | Intent intent = new ContextWrapper(getApplicationContext()). 77 | registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 78 | batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) / 79 | intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); 80 | } 81 | 82 | return batteryLevel; 83 | } 84 | 85 | private String readFile(String path) { 86 | 87 | String result = null; 88 | // result = path; 89 | // return result; 90 | try { 91 | File f=new File(path); 92 | int length=(int)f.length(); 93 | byte[] buff=new byte[length]; 94 | FileInputStream fin=new FileInputStream(f); 95 | int flength = fin.read(buff); 96 | fin.close(); 97 | result=new String(buff,"gbk"); 98 | }catch (Exception e){ 99 | return e.getMessage(); 100 | } 101 | return result; 102 | } 103 | 104 | 105 | private boolean openApplicationSettings() { 106 | Uri packageURI = Uri.parse("package:" + "cn.yotaku.light"); 107 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageURI); 108 | startActivity(intent); 109 | return true; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 16 | 17 | 18 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/lightlogo_dark20_color_132x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-hdpi/lightlogo_dark20_color_132x44.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/product_logo_translate_color_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-hdpi/product_logo_translate_color_144.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/lightlogo_dark20_color_132x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-mdpi/lightlogo_dark20_color_132x44.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/product_logo_translate_color_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-mdpi/product_logo_translate_color_144.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/lightlogo_dark20_color_132x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xhdpi/lightlogo_dark20_color_132x44.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/product_logo_translate_color_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xhdpi/product_logo_translate_color_144.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/lightlogo_dark20_color_132x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xxhdpi/lightlogo_dark20_color_132x44.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/product_logo_translate_color_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xxhdpi/product_logo_translate_color_144.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/lightlogo_dark20_color_132x44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xxxhdpi/lightlogo_dark20_color_132x44.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/product_logo_translate_color_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/app/src/main/res/mipmap-xxxhdpi/product_logo_translate_color_144.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 100.0dip 4 | -4.0dip 5 | 60.0dip 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.0.1' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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-4.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /assets/background/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg1.png -------------------------------------------------------------------------------- /assets/background/bg10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg10.png -------------------------------------------------------------------------------- /assets/background/bg11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg11.png -------------------------------------------------------------------------------- /assets/background/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg2.png -------------------------------------------------------------------------------- /assets/background/bg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg3.png -------------------------------------------------------------------------------- /assets/background/bg4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg4.png -------------------------------------------------------------------------------- /assets/background/bg5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg5.png -------------------------------------------------------------------------------- /assets/background/bg6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg6.png -------------------------------------------------------------------------------- /assets/background/bg7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg7.jpg -------------------------------------------------------------------------------- /assets/background/bg8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg8.png -------------------------------------------------------------------------------- /assets/background/bg9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/background/bg9.png -------------------------------------------------------------------------------- /assets/config.ini: -------------------------------------------------------------------------------- 1 | version=0.0.1 2 | database=light.db 3 | storage=Yotaku -------------------------------------------------------------------------------- /assets/default_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/default_cover.jpg -------------------------------------------------------------------------------- /assets/font/Avenir.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/font/Avenir.ttf -------------------------------------------------------------------------------- /assets/font/FZYouH.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/font/FZYouH.ttf -------------------------------------------------------------------------------- /assets/font/HYQH.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/font/HYQH.ttf -------------------------------------------------------------------------------- /assets/font/Lora-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/font/Lora-Regular.ttf -------------------------------------------------------------------------------- /assets/font/Noto Serif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/font/Noto Serif.ttf -------------------------------------------------------------------------------- /assets/light.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/assets/light.db -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | *.pbxuser 16 | *.mode1v3 17 | *.mode2v3 18 | *.perspectivev3 19 | 20 | !default.pbxuser 21 | !default.mode1v3 22 | !default.mode2v3 23 | !default.perspectivev3 24 | 25 | xcuserdata 26 | 27 | *.moved-aside 28 | 29 | *.pyc 30 | *sync/ 31 | Icon? 32 | .tags* 33 | 34 | /Flutter/app.flx 35 | /Flutter/app.zip 36 | /Flutter/flutter_assets/ 37 | /Flutter/App.framework 38 | /Flutter/Flutter.framework 39 | /Flutter/Generated.xcconfig 40 | /ServiceDefinitions.json 41 | 42 | Pods/ 43 | -------------------------------------------------------------------------------- /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 | UIRequiredDeviceCapabilities 24 | 25 | arm64 26 | 27 | MinimumOSVersion 28 | 8.0 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 7 | [GeneratedPluginRegistrant registerWithRegistry:self]; 8 | // Override point for customization after application launch. 9 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 10 | } 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | light 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | arm64 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | import 'package:light/src/service/initial.dart'; 6 | import 'package:light/src/service/config.dart'; 7 | import 'package:light/src/service/db.dart'; 8 | import 'package:light/src/app.dart'; 9 | 10 | Future prefs; 11 | 12 | void main() async { 13 | SharedPreferences prefs = await initial(); 14 | Config config = await Config.getInstance1(); 15 | DB db = await DB.getInstance(config); 16 | runApp(new App(prefs: prefs, config: config, db: db,)); 17 | // return; 18 | // initial().then((SharedPreferences prefs) { 19 | // Config.getInstance1().then((Config config){ 20 | // DB.getInstance(config).then((DB db) { 21 | // runApp(new App( 22 | // prefs: prefs, 23 | // config: config, 24 | // db: db, 25 | // )); 26 | // }); 27 | // }); 28 | // }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/services.dart'; 4 | //import 'package:flutter_webview_plugin/flutter_webview_plugin.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | import 'package:light/src/service/config.dart'; 8 | import 'package:light/src/service/db.dart'; 9 | //import 'package:light/src/service/initial.dart'; 10 | import 'package:light/src/view/home.dart'; 11 | //import 'package:light/src/view/search/tieba/tieba.dart'; 12 | //import 'package:light/src/view/search/tieba/detail.dart'; 13 | //import 'package:light/src/model/tieba_topic.dart'; 14 | //import 'package:light/src/view/chat/chat_screen.dart'; 15 | //import 'package:light/src/view/shelf/import_book.dart'; 16 | 17 | final ThemeData _kGalleryLightTheme = new ThemeData( 18 | brightness: Brightness.light, 19 | primarySwatch: Colors.blue, 20 | ); 21 | 22 | final ThemeData _kGalleryDarkTheme = new ThemeData( 23 | brightness: Brightness.dark, 24 | primarySwatch: Colors.blue, 25 | ); 26 | 27 | class App extends StatefulWidget { 28 | final SharedPreferences prefs; 29 | final Config config; 30 | final DB db; 31 | 32 | App({@required this.config, @required this.prefs, @required this.db}); 33 | 34 | @override 35 | _AppState createState() => new _AppState(); 36 | } 37 | 38 | class _AppState extends State { 39 | bool _useLightTheme = true; 40 | 41 | @override 42 | void initState() { 43 | super.initState(); 44 | print('init app'); 45 | SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return new MaterialApp( 51 | title: 'Light', 52 | debugShowCheckedModeBanner: false, 53 | theme: _useLightTheme ? _kGalleryLightTheme : _kGalleryDarkTheme, 54 | routes: { 55 | // '/': (BuildContext context) => new ImportBook( 56 | // key: new Key('start'), 57 | // isRoot: true, 58 | // path: '/storage/emulated/0/DuoKan/Downloads/MiCloudBooks', 59 | //// path: '/storage/emulated/0/DuoKan', 60 | // ), 61 | '/': (BuildContext context) => new Home( 62 | useLightTheme: _useLightTheme, 63 | prefs: widget.prefs, 64 | onThemeChanged: (bool value) { 65 | setState(() { 66 | print('set _useLightTheme to $value'); 67 | _useLightTheme = value; 68 | }); 69 | }, 70 | ), 71 | // '/': (BuildContext context) => new Tieba(fname: '爱书的下克上'), 72 | // '/': (BuildContext context) => new ChatScreen( 73 | // appAccount: '13244414819', 74 | // username: 'Creaty', 75 | // ), 76 | // '/': (BuildContext context) => new Tieba(fname: '爆肝工程师的异世界狂想曲'), 77 | // '/': (BuildContext context) => new Detail( 78 | // topic: new TiebaTopic(title: 'VRchat,科普一下VR平台最火的二次元游戏', 79 | // url: 'http://tieba.baidu.com/mo/q---0C6E0C5D10B08D2558E1AF076714E837%3AFG%3D1--1-3-0--2--wapp_1521603526919_731/m?kz=5561118240&new_word=&pinf=1_2_60&pn=0&lp=6005', 80 | // tiebaUrl: 'http://tieba.baidu.com', 81 | // clickTimes: 12, 82 | // replyTimes: 384), 83 | // ), 84 | // "/": (_) => new WebviewScaffold( 85 | // url: 'http://tieba.baidu.com/mo/q---0C6E0C5D10B08D2558E1AF076714E837%3AFG%3D1--1-3-0--2--wapp_1521603526919_731/m?kz=5608792899&is_bakan=0&lp=5010&pinf=1_2_0', 86 | // appBar: new AppBar( 87 | // title: new Text("Widget webview"), 88 | // ), 89 | // withZoom: true, 90 | // ) 91 | }, 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/model/book.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | import 'package:light/src/service/file_service.dart'; 5 | import 'package:light/src/service/book_service.dart'; 6 | 7 | ///book数据可能是从线上获取,也可能是本地存储 8 | ///无论从线上线下获取到的book数据,封面不一定存在 9 | /// 225 130 101 599 10 | enum BookType { txt, epub, pdf, url, urls } 11 | 12 | class Book { 13 | Book( 14 | {@required this.title, 15 | this.description, 16 | this.coverUri, 17 | this.uri, 18 | this.type, 19 | this.createAt, 20 | this.updateAt}); 21 | 22 | Book.fromEntity({@required FileSystemEntity entity}) 23 | : assert(null != entity), 24 | title = 25 | new RegExp(r'([^/]+)\.[^./]+$').firstMatch(entity.path).group(1), 26 | description = null, 27 | coverUri = null, 28 | uri = entity.path, 29 | type = getSuffix(entity), 30 | createAt = new DateTime.now().toIso8601String(), 31 | updateAt = new DateTime.now().toIso8601String(); 32 | 33 | final String title; 34 | final String description; 35 | final String coverUri; 36 | final String uri; 37 | final String type; 38 | final String createAt; 39 | final String updateAt; 40 | BookService bookService; 41 | 42 | Book.fromMap({@required Map map}) 43 | : assert(null != map), 44 | title = map['title'], 45 | description = map['description'], 46 | coverUri = map['cover_uri'], 47 | uri = map['uri'], 48 | type = map['type'], 49 | createAt = map['create_at'], 50 | updateAt = map['update_at']; 51 | 52 | @override 53 | String toString() => '{title: $title, type: $type, uri: $uri}\n'; 54 | 55 | // BookType get bookType => BookType.values.firstWhere((t) => t.toString() == this.type); 56 | BookType get bookType => BookType.values.firstWhere((t) { 57 | // print(t.toString()); 58 | // print(this.type); 59 | return t.toString() == 'BookType.' + this.type; 60 | }); 61 | 62 | Map getMap() { 63 | return { 64 | 'title': title, 65 | 'description': description, 66 | 'cover_uri': coverUri, 67 | 'type': type, 68 | 'uri': uri, 69 | 'create_at': createAt 70 | }; 71 | } 72 | 73 | /// 用于记录分页数据 74 | String get recordsName => title + '_records'; 75 | 76 | /// 用于纪录阅读进度 77 | String get processName => title + '_process'; 78 | 79 | Future delete() async { 80 | if (null == bookService) { 81 | bookService = new BookService(); 82 | } 83 | return await bookService.deleteBook(this); 84 | } 85 | } 86 | 87 | /// 章节数据 88 | class Chapter { 89 | Chapter( 90 | {@required this.id, 91 | @required this.title, 92 | @required this.offset, 93 | @required this.length}); 94 | 95 | List subChapters = []; 96 | 97 | final int id; 98 | final String title; 99 | final int offset; 100 | final int length; 101 | 102 | @override 103 | String toString() { 104 | return 'Chapter{id: $id, title: $title, offset: $offset, length: $length}'; 105 | } 106 | } 107 | 108 | BookType getBookType(FileSystemEntity entity) { 109 | String suffix = getSuffix(entity); 110 | return BookType.values 111 | .firstWhere((t) => t.toString() == 'BookType.' + suffix); 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/model/message.dart: -------------------------------------------------------------------------------- 1 | //import 'dart:convert'; 2 | 3 | ///ping:ping/pong 4 | ///事件:event 5 | ///文件:file 6 | ///文本:text 7 | ///图片:image 8 | ///语音:voice 9 | ///视频:video 10 | ///位置:location 11 | ///链接:link 12 | enum MessageType { 13 | ping, 14 | pong, 15 | event, 16 | file, 17 | text, 18 | image, 19 | voice, 20 | video, 21 | location, 22 | link 23 | } 24 | 25 | class Message { 26 | Message( 27 | {this.version, 28 | this.topicId, 29 | this.toAccount, 30 | this.toUsername, 31 | this.fromAccount, 32 | this.fromUsername, 33 | this.msgId, 34 | this.msgType, 35 | this.timestamp, 36 | this.data}); 37 | 38 | final double version; 39 | final String topicId; 40 | final String msgId; 41 | final String toAccount; 42 | final String toUsername; 43 | final String fromAccount; 44 | final String fromUsername; 45 | final MessageType msgType; 46 | final int timestamp; 47 | final String data; 48 | 49 | String getContent() { 50 | return data; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/src/model/read_mode.dart: -------------------------------------------------------------------------------- 1 | //import 'dart:io'; 2 | //import 'dart:convert'; 3 | import 'package:flutter/material.dart'; 4 | //import 'package:light/src/service/file_service.dart'; 5 | 6 | ///閲讀主題類型 7 | ///[color]純色背景 8 | ///[image]圖片背景 9 | ///[texture]通過reapeat填充的背景 10 | enum ReadModeType { color, image, texture } 11 | 12 | 13 | class ReadMode { 14 | ReadMode({ 15 | this.id, 16 | this.type, 17 | this.fontColor, 18 | this.backgroundColor, 19 | this.imageUri, 20 | }); 21 | 22 | ///用於解析從數據庫讀取的數據 23 | ReadMode.fromMap(Map map) 24 | : this.id = int.parse(map['id']), 25 | this.type = ReadModeType.values 26 | .firstWhere((v) => v.toString() == 'ReadModeType.' + map['type']), 27 | this.fontColor = new Color(int.parse(map['font_color'])), 28 | this.backgroundColor = map['background_color'], 29 | this.imageUri = map['image_uri']; 30 | 31 | final int id; 32 | final ReadModeType type; 33 | final Color fontColor; 34 | final Color backgroundColor; 35 | final String imageUri; 36 | 37 | BoxFit get fit => type == ReadModeType.image 38 | ? BoxFit.cover 39 | : type == ReadModeType.texture ? BoxFit.none : null; 40 | 41 | ImageRepeat get repeat => type == ReadModeType.image 42 | ? ImageRepeat.noRepeat 43 | : type == ReadModeType.texture ? ImageRepeat.repeat : null; 44 | 45 | DecorationImage get image => null != imageUri 46 | ? new DecorationImage( 47 | fit: fit, repeat: repeat, image: new AssetImage(imageUri)) 48 | : null; 49 | DecorationImage get buttonImage => null != imageUri 50 | ? new DecorationImage( 51 | fit: fit, repeat: ImageRepeat.repeat, image: new AssetImage(imageUri)) 52 | : null; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/model/selected_list_model.dart: -------------------------------------------------------------------------------- 1 | //import 'dart:io'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | typedef void SetterCallback(T element, List list); 5 | typedef E GetterCallback(T element, List list); 6 | 7 | class SelectedListModel { 8 | SelectedListModel({ 9 | @required handleRemove(T element, List list), 10 | @required handleIndexOf(T element, List list) 11 | }) 12 | : _handleIndexOf = handleIndexOf, 13 | _handleRemove = handleRemove; 14 | 15 | final SetterCallback _handleRemove; 16 | final GetterCallback _handleIndexOf; 17 | List _list = []; 18 | 19 | List get list => _list; 20 | 21 | void addAll(List list) { 22 | list.forEach((T ele) { 23 | add(ele); 24 | }); 25 | } 26 | 27 | void add(T ele) { 28 | remove(ele); 29 | _list.add(ele); 30 | } 31 | 32 | void remove(T ele) => _handleRemove(ele, _list); 33 | 34 | T operator [](int index) => _list[index]; 35 | 36 | int get length => _list.length; 37 | 38 | int indexOf(T ele) => _handleIndexOf(ele, _list); 39 | 40 | void clear() { 41 | _list.clear(); 42 | } 43 | 44 | void forEach(ValueChanged call) => _list.forEach(call); 45 | 46 | bool get isEmpty => _list.isEmpty; 47 | bool get isNotEmpty => _list.isNotEmpty; 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/model/tieba_post.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:html/parser.dart'; 5 | import 'package:html/dom.dart'; 6 | 7 | import 'package:light/src/service/baidu_service.dart'; 8 | 9 | class TiebaPost { 10 | TiebaPost({ 11 | this.data, 12 | }) { 13 | document = parse(data); 14 | } 15 | 16 | TiebaPost.json( 17 | {@required this.raw, this.datetime, this.avatarUrl, this.referer}) { 18 | data = json.decode(raw); 19 | author = data['author']; 20 | content = data['content']['content']; 21 | flour = data['content']['post_no']; 22 | commentNum = data['content']['comment_num']; 23 | } 24 | 25 | String referer; 26 | String raw; 27 | Map data; 28 | Document document; 29 | 30 | String avatarUrl; 31 | Map author; 32 | int commentNum; 33 | String content; 34 | String datetime; 35 | int flour; 36 | RegExp regFlour = new RegExp(r'^(\d+)楼\.'); 37 | 38 | String getFlour() { 39 | return flour.toString(); 40 | // flour = regFlour.firstMatch(data).group(1); 41 | // return flour; 42 | } 43 | 44 | String getNormalUrl(String url) { 45 | if (new RegExp(r'^//').hasMatch(url)) { 46 | return 'https:' + url; 47 | } 48 | return url; 49 | } 50 | 51 | Image getAvatar() { 52 | return getImage(getNormalUrl(avatarUrl), getAuthor(), referer); 53 | } 54 | 55 | String getAuthor() { 56 | return author['user_name']; 57 | // author = document.querySelector('span.g>a').innerHtml; 58 | // return author; 59 | } 60 | 61 | String getDatetime() { 62 | return datetime; 63 | // datetime = document.querySelector('span.b').innerHtml; 64 | // return datetime; 65 | } 66 | 67 | String getContent() { 68 | return content; 69 | // content = document.firstChild.text.replaceFirst(regFlour, '').split('\n'); 70 | // content.removeAt(content.length - 1); 71 | // return content.join('\n'); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/src/model/tieba_topic.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class TiebaTopic{ 4 | TiebaTopic({ 5 | @required this.id, 6 | @required this.title, 7 | @required this.url, 8 | @required this.tiebaUrl, 9 | @required this.clickTimes, 10 | @required this.replyTimes 11 | }); 12 | 13 | final int id; 14 | final String title; 15 | final String url; 16 | final String tiebaUrl; 17 | final int clickTimes; 18 | final int replyTimes; 19 | } -------------------------------------------------------------------------------- /lib/src/service/app_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | import 'package:flutter/foundation.dart' show required; 4 | 5 | class AppService { 6 | static AppService _cache; 7 | 8 | AppService._internal({@required this.prefs}) 9 | : streamController = new StreamController.broadcast(); 10 | 11 | factory AppService({SharedPreferences prefs}) { 12 | if (null == _cache) { 13 | _cache = new AppService._internal(prefs: prefs); 14 | } 15 | return _cache; 16 | } 17 | 18 | /// SharedPreferences实例 19 | final SharedPreferences prefs; 20 | 21 | /// 流控制器 22 | final StreamController streamController; 23 | 24 | /// 获得stream 25 | Stream get stream => streamController.stream; 26 | 27 | /// 发送事件 28 | void add(value) => streamController.add(value); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/service/baidu_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | //import 'package:http/http.dart'; 3 | 4 | Map cache = {}; 5 | 6 | getImage(String url, String key, [String referer]) { 7 | if (cache[key] != null) { 8 | print('有缓存'); 9 | return cache[key]; 10 | } 11 | print('无缓存 name = $key \n url = $url \n referer = $referer'); 12 | cache[key] = new Image.network(url, headers: {'referer': referer}); 13 | return cache[key]; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/service/config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | //import 'dart:convert'; 4 | //import 'dart:typed_data'; 5 | import 'package:path/path.dart'; 6 | //import 'package:flutter/services.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:options_file/options_file.dart'; 9 | 10 | class Config { 11 | static Config _instance; 12 | static OptionsFile _optionFile; 13 | 14 | static Future getInstance1() async { 15 | try { 16 | if (null == _optionFile) { 17 | Directory dir = await getApplicationDocumentsDirectory(); 18 | 19 | String path = join(dir.path, 'config.ini'); 20 | if (!(new File(path).existsSync())) { 21 | // throw new FileSystemException('config.ini不存在 $path'); 22 | print('config.ini不存在 $path'); 23 | return null; 24 | } 25 | _optionFile = new OptionsFile(path); 26 | } 27 | if (null == _instance) { 28 | _instance = new Config._(); 29 | return _instance; 30 | } 31 | return _instance; 32 | } catch(e) { 33 | print('Config加载异常:$e'); 34 | return null; 35 | } 36 | } 37 | 38 | Config._(); 39 | 40 | String getString(String key) { 41 | return _optionFile.getString(key); 42 | } 43 | 44 | int getInt(String key) { 45 | return _optionFile.getInt(key); 46 | } 47 | 48 | bool getBool(String key) { 49 | return getString(key) == 'true'; 50 | } 51 | 52 | double getDouble(String key) { 53 | return double.parse(getString(key)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/service/db.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | import 'package:path/path.dart'; 4 | import 'package:sqflite/sqflite.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | import 'package:light/src/service/config.dart'; 8 | 9 | class DB { 10 | static DB _instance; 11 | static Config _config; 12 | static Database _database; 13 | 14 | DB._(); 15 | 16 | factory DB([String name = 'default']) { 17 | if (null != _instance) 18 | return _instance; 19 | else 20 | return null; 21 | } 22 | 23 | static Future getInstance([Config config]) async { 24 | try { 25 | if (null == _config) { 26 | _config = config; 27 | } 28 | if (null == _database) { 29 | if (null == _config) { 30 | return null; 31 | } 32 | Directory dir = await getApplicationDocumentsDirectory(); 33 | String path = join(dir.path, _config.getString('database')); 34 | if (new File(path).existsSync()) { 35 | _database = await openDatabase(path, version: 3, 36 | onCreate: (Database db, int version) async { 37 | // When creating the db, create the table 38 | print('数据库连接成功 version: $version, path: $path'); 39 | }); 40 | } else { 41 | return null; 42 | } 43 | } 44 | if (null == _instance) { 45 | _instance = new DB._(); 46 | return _instance; 47 | } 48 | return _instance; 49 | } catch (e) { 50 | print('DB加载异常:$e'); 51 | return null; 52 | } 53 | } 54 | 55 | ///INSERT data 56 | Future insert(String table, Map values, 57 | {String nullColumnHack, ConflictAlgorithm conflictAlgorithm}) { 58 | return _database.insert(table, values, 59 | nullColumnHack: nullColumnHack, conflictAlgorithm: conflictAlgorithm); 60 | } 61 | 62 | ///SELECT data 63 | Future>> query(String table, 64 | {bool distinct, 65 | List columns, 66 | String where, 67 | List whereArgs, 68 | String groupBy, 69 | String having, 70 | String orderBy, 71 | int limit, 72 | int offset}) { 73 | return _database.query(table, 74 | distinct: distinct, 75 | columns: columns, 76 | where: where, 77 | whereArgs: whereArgs, 78 | groupBy: groupBy, 79 | having: having, 80 | orderBy: orderBy, 81 | limit: limit, 82 | offset: offset); 83 | } 84 | 85 | Future> rawQuery(String query) { 86 | return _database.rawQuery(query); 87 | } 88 | 89 | Future rawDelete(String sql, List arguments) { 90 | return _database.rawDelete(sql, arguments); 91 | } 92 | 93 | Future execute(String query, List arguments) { 94 | return _database.execute(query, arguments); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/service/file_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | import 'package:path/path.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:path_provider/path_provider.dart'; 6 | 7 | enum FileType { TEXT, EPUB, PDF, OTHER, NOT_FOUND, DIRECTORY } 8 | 9 | RegExp _regFileType = new RegExp(r'([^.\\/]+)$'); 10 | RegExp _regTXT = new RegExp(r'txt'); 11 | RegExp _regPDF = new RegExp(r'pdf'); 12 | RegExp _regEPUB = new RegExp(r'epub'); 13 | RegExp _regName = new RegExp(r'(.+)[^.]+$'); 14 | RegExp _regSuffix = new RegExp(r'.*\.([^.\\/]+)$'); 15 | RegExp _regBasename = new RegExp(r'[^/\\]+$'); 16 | 17 | FileType getType(FileSystemEntity entity) { 18 | if (entity.existsSync()) { 19 | if (FileSystemEntity.isDirectorySync(entity.path)) { 20 | return FileType.DIRECTORY; 21 | } else { 22 | String name = getBasename(entity.path); 23 | FileType type; 24 | if (name == null || name.isEmpty) { 25 | type = FileType.OTHER; 26 | } 27 | String suffix = _regFileType.firstMatch(name)?.group(1); 28 | if (null == suffix || suffix.isEmpty) { 29 | type = FileType.OTHER; 30 | } else if (_regTXT.hasMatch(suffix)) 31 | type = FileType.TEXT; 32 | else if (_regPDF.hasMatch(suffix)) 33 | type = FileType.PDF; 34 | else if (_regEPUB.hasMatch(suffix)) 35 | type = FileType.EPUB; 36 | else 37 | type = FileType.OTHER; 38 | return type; 39 | } 40 | } 41 | return FileType.NOT_FOUND; 42 | } 43 | 44 | String getBasename(var file) { 45 | if (file is String) { 46 | return _regBasename.firstMatch(file)?.group(0); 47 | } else if (file is Directory || file is FileSystemEntity) { 48 | return _regBasename.firstMatch(file.path)?.group(0); 49 | } else { 50 | return ''; 51 | } 52 | } 53 | 54 | String getName(var file) { 55 | String baseName = getBasename(file); 56 | // print('baseName: $baseName'); 57 | String name = _regName.firstMatch(baseName)?.group(1); 58 | // print(_regName.firstMatch(baseName)); 59 | return name; 60 | // return _regName.firstMatch(baseName)?.group(1); 61 | } 62 | 63 | String getSuffix(var file) { 64 | String baseName = getBasename(file); 65 | String suffix = _regSuffix.firstMatch(baseName)?.group(1); 66 | // print(_regSuffix.firstMatch(baseName).group(0)); 67 | // print(_regSuffix.firstMatch(baseName).group(1)); 68 | return suffix; 69 | } 70 | 71 | Future writeFile(String content, String name) async { 72 | Directory dir = await getExternalStorageDirectory(); 73 | String path = join(dir.path, 'Yotaku', name); 74 | try { 75 | print('写入文件 path=$path\ncontent=$content'); 76 | File file = new File(path); 77 | file.writeAsStringSync(content); 78 | return null; 79 | } catch (e) { 80 | print('文件写入失败:$e'); 81 | if (dir.existsSync()) { 82 | print('文件夹存在:${dir.path}'); 83 | } else { 84 | print('文件夹不存在:${dir.path}'); 85 | } 86 | return null; 87 | } 88 | } 89 | 90 | bool isDirectory(FileSystemEntity entity) { 91 | return FileSystemEntity.isDirectorySync(entity.path); 92 | } 93 | 94 | class FileService { 95 | static final Map _cache = {}; 96 | static final Future external = getExternalStorageDirectory(); 97 | 98 | factory FileService([String name = 'default']) { 99 | if (_cache.containsKey(name)) { 100 | return _cache[name]; 101 | } else { 102 | _cache[name] = new FileService._internal(); 103 | return _cache[name]; 104 | } 105 | } 106 | 107 | FileService._internal(); 108 | 109 | Future readFile(String name) async { 110 | try { 111 | return external.then((Directory dir) { 112 | String path = join(dir.path, 'Yotaku', name); 113 | print(path); 114 | File file = new File(path); 115 | print('flag'); 116 | return file.readAsStringSync() as T; 117 | }); 118 | } catch (e) { 119 | print('文件读取失败:$e'); 120 | return null; 121 | } 122 | } 123 | } 124 | 125 | Image getImage(String uri) { 126 | RegExp regasset = new RegExp(r'^asset'); 127 | RegExp regurl = new RegExp(r'^http'); 128 | RegExp regfile = new RegExp(r'^/storage'); 129 | if (regasset.hasMatch(uri)) { 130 | return new Image.asset(uri); 131 | } else if (regurl.hasMatch(uri)) { 132 | return new Image.network(uri); 133 | } else if (regfile.hasMatch(uri)) { 134 | return new Image.file(new File(uri)); 135 | } 136 | return null; 137 | } 138 | 139 | String charsetDetector(RandomAccessFile file) { 140 | String charset; 141 | List bytes = file.readSync(3); 142 | int length = file.lengthSync(); 143 | bool isLatin1 = true; 144 | bool isUtf8 = true; 145 | if (null != bytes && 146 | bytes.length == 3 && 147 | bytes[0] == 0xEF && 148 | bytes[1] == 0xBB && 149 | bytes[2] == 0xBF) { 150 | isLatin1 = false; 151 | } else if (null != bytes && bytes.isNotEmpty) { 152 | //不带bom头,可能是gbk,latin1,utf8,big5 153 | bytes = file.readSync(100 > length ? length : 100); 154 | int i = 0; 155 | do { 156 | if (bytes[i] > 127) { 157 | isLatin1 = false; 158 | } 159 | if ((bytes[i] & 0xC0) != 0x80) { 160 | isUtf8 = false; 161 | } 162 | i++; 163 | } while (i < bytes.length); 164 | } 165 | if (!isLatin1 && !isUtf8) { 166 | charset = 'gbk'; 167 | } else if (!isLatin1 && isUtf8) { 168 | charset = 'utf8'; 169 | } else if (isLatin1 && !isUtf8) { 170 | charset = 'latin1'; 171 | } 172 | return charset; 173 | } 174 | -------------------------------------------------------------------------------- /lib/src/service/initial.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:async'; 3 | //import 'dart:convert'; 4 | import 'dart:typed_data'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:path/path.dart'; 7 | import 'package:path_provider/path_provider.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | import 'package:sqflite/sqflite.dart'; 10 | 11 | import 'package:light/src/service/config.dart'; 12 | import 'package:light/src/service/db.dart'; 13 | 14 | ///判断是否已经安装 15 | Future isInstalled(SharedPreferences prefs) async { 16 | // return false; 17 | if (prefs.getBool('installed') != null && prefs.getBool('installed') == true) 18 | return true; 19 | return false; 20 | } 21 | 22 | ///执行安装 23 | Future initial() async { 24 | SharedPreferences prefs = await SharedPreferences.getInstance(); 25 | if (await isInstalled(prefs)) { 26 | print('Installed, skip......'); 27 | return prefs; 28 | } 29 | print('Installing......'); 30 | 31 | //检查权限 32 | if (!(await checkPermissions())) { 33 | return prefs; 34 | } 35 | 36 | Directory directory = await getApplicationDocumentsDirectory(); 37 | 38 | try { 39 | Config config = await createConfig(directory); 40 | if (null == config) { 41 | throw new Exception('复制config失败'); 42 | } 43 | DB db = await createDB(directory, config); 44 | if (null == db) { 45 | throw new Exception('复制db失败'); 46 | } 47 | // bool dirRes = await createDirectory(config); 48 | // if (!dirRes) { 49 | // throw new Exception('创建文件夹失败'); 50 | // } 51 | bool testRes = await test(dir: directory, db: db, config: config); 52 | if (!testRes) { 53 | throw new Exception('测试失败'); 54 | } 55 | // createConfig(directory).then((Config config) { 56 | // print('flag1'); 57 | // createDB(directory, config).then((DB db) { 58 | // print('flag2'); 59 | // createDirectory(config).then((done) { 60 | // print('flag3'); 61 | // if (done) 62 | // test(dir: directory, db: db, config: config).then((_) { 63 | // return prefs; 64 | // }); 65 | // }); 66 | // }); 67 | // }); 68 | prefs.setBool('installed', true); 69 | } on FileSystemException catch (e) { 70 | print('需要文件读写权限 $e'); 71 | } catch (e) { 72 | print('Install failed. $e'); 73 | } 74 | return prefs; 75 | } 76 | 77 | ///测试权限 78 | Future checkPermissions() async { 79 | Directory dir = await getApplicationDocumentsDirectory(); 80 | String path = join(dir.path, 'test.txt'); 81 | try { 82 | print('尝试写入测试文件 path=$path'); 83 | File file = new File(path); 84 | file.writeAsStringSync('test'); 85 | if (file.readAsStringSync() == 'test') { 86 | print('测试文件写入成功'); 87 | return true; 88 | } else { 89 | print('测试写入文件失败'); 90 | return false; 91 | } 92 | } catch (e) { 93 | print('测试文件写入失败:$e'); 94 | if (dir.existsSync()) { 95 | print('文件夹存在:${dir.path}'); 96 | } else { 97 | print('文件夹存在:${dir.path}'); 98 | } 99 | if (new File(path).existsSync()) { 100 | print('文件存在:$path'); 101 | List list = dir.listSync(); 102 | list.forEach((FileSystemEntity entity) { 103 | print(entity.path); 104 | }); 105 | } else { 106 | print('文件不存在:$path'); 107 | } 108 | return false; 109 | } 110 | } 111 | 112 | ///创建配置文件 113 | Future createConfig(Directory dir) async { 114 | try { 115 | String path = join(dir.path, 'config.ini'); 116 | print('正在复制配置文件config.ini到$path'); 117 | ByteData data = await rootBundle.load(join('assets', 'config.ini')); 118 | List bytes = 119 | data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); 120 | new File(path).writeAsBytesSync(bytes); 121 | return Config.getInstance1(); 122 | } catch (e) { 123 | print('创建配置文件失败:$e'); 124 | return null; 125 | } 126 | } 127 | 128 | ///创建数据库 129 | Future createDB(Directory dir, Config config) async { 130 | print('createDB'); 131 | try { 132 | //数据库路径 133 | String path = join(dir.path, config.getString('database')); 134 | //创建路径 135 | await new Directory(dirname(path)).create(recursive: true); 136 | 137 | //删除数据库 138 | if (new File(path).existsSync()) { 139 | await deleteDatabase(path); 140 | } 141 | print('flag1.5'); 142 | 143 | //读取asset资源 144 | ByteData dbData = await rootBundle.load(join('assets', 'light.db')); 145 | List dbBytes = 146 | dbData.buffer.asUint8List(dbData.offsetInBytes, dbData.lengthInBytes); 147 | //写入app文件夹 148 | await new File(path).writeAsBytes(dbBytes); 149 | print('flag1.6'); 150 | 151 | //新建数据库并打开 152 | Database db = await openDatabase(path, version: 3, 153 | onCreate: (Database db, int version) async { 154 | // When creating the db, create the table 155 | print('数据库创建成功 version: $version, path: $path'); 156 | await db.execute( 157 | "CREATE TABLE Test (id INTEGER PRIMARY KEY, name TEXT, value INTEGER, num REAL)"); 158 | }); 159 | 160 | // Insert some records in a transaction 161 | await db.transaction((txn) async { 162 | int id1 = await txn.rawInsert( 163 | 'INSERT INTO Test(name, value, num) VALUES("some name", 1234, 456.789)'); 164 | print("inserted1: $id1"); 165 | int id2 = await txn.rawInsert( 166 | 'INSERT INTO Test(name, value, num) VALUES(?, ?, ?)', 167 | ["another name", 12345678, 3.1416]); 168 | print("inserted2: $id2"); 169 | }); 170 | 171 | // Update some record 172 | int count = await db.rawUpdate( 173 | 'UPDATE Test SET name = ?, VALUE = ? WHERE name = ?', 174 | ["updated name", "9876", "some name"]); 175 | print("updated: $count"); 176 | 177 | // Get the records 178 | List list = await db.rawQuery('SELECT * FROM Test'); 179 | List expectedList = [ 180 | {"id": 1, "name": "updated name", "value": 9876, "num": 456.789}, 181 | {"id": 2, "name": "another name", "value": 12345678, "num": 3.1416} 182 | ]; 183 | print(list); 184 | print(expectedList); 185 | 186 | // Count the records 187 | count = 188 | Sqflite.firstIntValue(await db.rawQuery("SELECT COUNT(*) FROM Test")); 189 | assert(count == 2); 190 | 191 | // Delete a record 192 | count = 193 | await db.rawDelete('DELETE FROM Test WHERE name = ?', ['another name']); 194 | assert(count == 1); 195 | 196 | db.close(); 197 | 198 | return DB.getInstance(config); 199 | } catch (e) { 200 | print('创建数据库失败:$e'); 201 | return null; 202 | } 203 | } 204 | 205 | /// 创建存储文件夹 206 | Future createDirectory(Config config) async { 207 | Directory exDir = await getExternalStorageDirectory(); 208 | try { 209 | String path = join(exDir.path, config.getString('storage')); 210 | if (!FileSystemEntity.isDirectorySync(path)) { 211 | Directory rootDir = new Directory(path); 212 | rootDir.createSync(recursive: true); 213 | } 214 | return true; 215 | } catch (e) { 216 | print('创建存储文件夹失败:$e'); 217 | return false; 218 | } 219 | } 220 | 221 | ///测试 222 | Future test({Directory dir, DB db, Config config}) async { 223 | print('Install finished.'); 224 | print('测试:'); 225 | 226 | try { 227 | //遍历当前文件夹 228 | print('Files in ${dir.path}:'); 229 | List fileList = dir.listSync(); 230 | fileList.forEach((entity) { 231 | print(entity.path); 232 | }); 233 | 234 | //读取配置文件 235 | print('读取配置文件:'); 236 | Config config = await Config.getInstance1(); 237 | print('version = ' + config.getString('version')); 238 | 239 | //写入数据库 240 | print('写入数据库'); 241 | int id = await db.insert('collection', {'name': 'test name', 'type': '0'}); 242 | print('写入数据id=$id'); 243 | 244 | //查询数据库 245 | print('查询数据库:'); 246 | List list = await db.rawQuery('select * from collection where id=$id'); 247 | print(list); 248 | } catch (e) { 249 | print('测试出错:$e'); 250 | } 251 | return true; 252 | } 253 | -------------------------------------------------------------------------------- /lib/src/service/local.dart: -------------------------------------------------------------------------------- 1 | //import 'dart:async'; 2 | //import 'package:light/src/model/book.dart'; 3 | // 4 | //import 'package:light/src/service/mock_book.dart'; 5 | // 6 | //class Local { 7 | // Future> getBooks(String word) async { 8 | // return mockBooks; 9 | // } 10 | //} -------------------------------------------------------------------------------- /lib/src/service/mock_book.dart: -------------------------------------------------------------------------------- 1 | import 'package:light/src/model/book.dart'; 2 | 3 | List mockBooks = [ 4 | new Book(title: '逃杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 5 | new Book(title: '逃逃杀竞技场逃杀竞技场逃杀竞技场杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 6 | new Book(title: '逃杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 7 | new Book(title: '逃杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 8 | new Book(title: '逃杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 9 | new Book(title: '逃杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 10 | new Book(title: '逃杀竞技场', coverUri: 'http://xs.dmzj.com/img/webpic/3/hdsyzj6896l.jpg'), 11 | ]; -------------------------------------------------------------------------------- /lib/src/service/search.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:http/http.dart' as http; 3 | import 'dart:convert'; 4 | 5 | import 'package:light/src/model/book.dart'; 6 | 7 | bool mock = true; 8 | 9 | Future> getOnlineBooksAll(String word) { 10 | String url = ''; 11 | 12 | if (mock) { 13 | return new Future>.delayed(new Duration(milliseconds: 2000), () => []); 14 | // return new Future>.delayed(new Duration(milliseconds: 2000), () => [ 15 | // new Book(title: '书1'), 16 | // new Book(title: '书2'), 17 | // new Book(title: '书3'), 18 | // ]); 19 | } 20 | return http.get(url).then((response) { 21 | return json.decode(response.body); 22 | }); 23 | } -------------------------------------------------------------------------------- /lib/src/service/tieba.dart: -------------------------------------------------------------------------------- 1 | //import 'package:sqflite/sqflite.dart'; 2 | 3 | -------------------------------------------------------------------------------- /lib/src/utils/custom_text_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class CustomTextPainter extends CustomPainter { 5 | CustomTextPainter({@required this.textPainter}); 6 | 7 | final TextPainter textPainter; 8 | 9 | @override 10 | void paint(Canvas canvas, Size size) { 11 | textPainter.paint(canvas, Offset.zero); 12 | } 13 | 14 | @override 15 | bool shouldRepaint(CustomTextPainter oldDelegate) => true; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/utils/page_calculator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class PageCalculator { 4 | factory PageCalculator({ 5 | String key = 'default', 6 | Size size, 7 | TextStyle textStyle, 8 | TextAlign textAlign, 9 | TextDirection textDirection, 10 | int maxLines, 11 | }) { 12 | if (_cache.containsKey(key)) { 13 | return _cache[key] 14 | ..pageSize = size 15 | ..textStyle = textStyle 16 | ..textAlign = textAlign 17 | ..textDirection = textDirection 18 | ..maxLines = maxLines; 19 | } else { 20 | _cache[key] = new PageCalculator._internal( 21 | size: size, 22 | textStyle: textStyle, 23 | textAlign: textAlign, 24 | textDirection: textDirection, 25 | maxLines: maxLines); 26 | return _cache[key]; 27 | } 28 | } 29 | 30 | PageCalculator._internal({ 31 | Size size, 32 | TextStyle textStyle, 33 | TextAlign textAlign, 34 | TextDirection textDirection, 35 | int maxLines, 36 | }) : this.pageSize = size, 37 | this._textStyle = textStyle, 38 | this._textAlign = textAlign, 39 | this._textDirection = textDirection, 40 | this._maxLines = maxLines, 41 | this.textPainter = new TextPainter( 42 | textAlign: textAlign, 43 | textDirection: textDirection, 44 | maxLines: maxLines, 45 | ellipsis: null 46 | ); 47 | 48 | static Map _cache = {}; 49 | 50 | /// 计算次数 51 | int times = 0; 52 | 53 | /// 页面尺寸 54 | Size pageSize; 55 | 56 | /// 文本样式 57 | TextStyle _textStyle; 58 | 59 | /// 文本排版 60 | TextAlign _textAlign; 61 | 62 | /// 文本阅读方向,默认由左至右 63 | TextDirection _textDirection; 64 | 65 | /// 最大行 66 | int _maxLines; 67 | 68 | /// 文本绘制器 69 | TextPainter textPainter; 70 | 71 | /// 待绘制文本 72 | String content; 73 | 74 | /// 剪切掉的文本 75 | String clipped; 76 | 77 | /// 文本截断位置 78 | TextPosition textPosition; 79 | 80 | /// 待绘制文本长度 81 | int get length => textPosition.offset; 82 | 83 | /// 设置文本样式 84 | set textStyle(TextStyle textStyle) { 85 | if (null == textStyle) return; 86 | _textStyle = textStyle; 87 | } 88 | 89 | /// 设置文本排版 90 | set textAlign(TextAlign textAlign) { 91 | if (null == textAlign) return; 92 | _textAlign = textAlign; 93 | textPainter.textAlign = _textAlign; 94 | } 95 | 96 | /// 设置文本阅读方向 97 | set textDirection(TextDirection textDirection) { 98 | if (null == textDirection) return; 99 | _textDirection = textDirection; 100 | textPainter.textDirection = _textDirection; 101 | } 102 | 103 | /// 设置最大行数 104 | set maxLines(int maxLines) { 105 | if (null == maxLines) return; 106 | _maxLines = maxLines; 107 | textPainter.maxLines = _maxLines; 108 | } 109 | 110 | /// 获取带样式的文本对象 111 | TextSpan getTextSpan(String text) { 112 | return new TextSpan( 113 | text: text, 114 | style: _textStyle, 115 | ); 116 | } 117 | 118 | /// 接收内容 119 | /// 追加内容返回false 120 | /// 计算完毕返回true 121 | bool load(String text) { 122 | if (layout(text)) { 123 | // 未填满整页,需要追加内容 124 | textPosition = 125 | textPainter.getPositionForOffset(pageSize.bottomRight(Offset.zero)); 126 | return false; 127 | } else { 128 | // 已经填满整页 129 | textPosition = 130 | textPainter.getPositionForOffset(pageSize.bottomRight(Offset.zero)); 131 | return true; 132 | } 133 | } 134 | 135 | /// 计算待绘制文本 136 | /// 未超出边界返回true 137 | /// 超出边界返回false 138 | bool layout(String text) { 139 | times++; 140 | text = text ?? ''; 141 | content = text; 142 | textPainter 143 | ..text = getTextSpan(text) 144 | ..layout(maxWidth: pageSize.width); 145 | return !didExceed; 146 | } 147 | 148 | /// 是否超出边界 149 | bool get didExceed { 150 | return textPainter.didExceedMaxLines || 151 | textPainter.size.height > pageSize.height; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/src/view/chat/chat_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | 5 | import 'package:light/src/model/message.dart'; 6 | 7 | class ChatScreen extends StatefulWidget { 8 | ChatScreen({this.isTopic: false, this.appAccount, this.username}); 9 | 10 | final bool isTopic; 11 | final String appAccount; 12 | final String username; 13 | 14 | @override 15 | _ChatScreenState createState() => new _ChatScreenState(); 16 | } 17 | 18 | class _ChatScreenState extends State { 19 | final List _messages = []; 20 | final List _messageItems = []; 21 | final TextEditingController _textController = new TextEditingController(); 22 | final Random random = new Random(new DateTime.now().millisecondsSinceEpoch); 23 | final ScrollPhysics _scrollPhysics = new BouncingScrollPhysics(); 24 | 25 | void _handleTextSubmitted(String text) { 26 | print('handleTextSubmit text=$text'); 27 | _textController.clear(); 28 | text = text?.trim(); 29 | if (null == text || text.isEmpty) { 30 | return; 31 | } 32 | Message message = new Message( 33 | fromAccount: widget.appAccount, 34 | fromUsername: widget.username, 35 | data: text); 36 | MessageItem item = 37 | new MessageItem(message: message, isSelf: random.nextBool()); 38 | setState(() { 39 | _messageItems.insert(0, item); 40 | _messages.insert(0, message); 41 | }); 42 | } 43 | 44 | Widget _buildTextComposer() { 45 | return new IconTheme( 46 | data: new IconThemeData(color: Theme.of(context).accentColor), 47 | child: new Container( 48 | margin: const EdgeInsets.symmetric(horizontal: 8.0), 49 | child: new Row( 50 | crossAxisAlignment: CrossAxisAlignment.end, 51 | children: [ 52 | new Flexible( 53 | child: new ConstrainedBox( 54 | constraints: 55 | new BoxConstraints(maxHeight: 130.0, minHeight: 30.0), 56 | child: new Container( 57 | // padding: const EdgeInsets.symmetric(vertical: 8.0), 58 | child: new SingleChildScrollView( 59 | scrollDirection: Axis.vertical, 60 | reverse: true, 61 | child: new TextField( 62 | maxLines: null, 63 | keyboardType: TextInputType.multiline, 64 | controller: _textController, 65 | onSubmitted: _handleTextSubmitted, 66 | decoration: new InputDecoration( 67 | border: InputBorder.none, hintText: "Send a message"), 68 | ), 69 | ), 70 | ), 71 | ), 72 | ), 73 | new Container( 74 | margin: new EdgeInsets.symmetric(horizontal: 4.0), 75 | child: new IconButton( 76 | icon: new Icon(Icons.send), 77 | onPressed: () => _handleTextSubmitted(_textController.text)), 78 | ), 79 | ], 80 | ), 81 | ), 82 | ); 83 | } 84 | 85 | Widget buildAppBar() { 86 | return new AppBar( 87 | title: new Text('Yotaku'), 88 | ); 89 | } 90 | 91 | Widget buildMessages() { 92 | return new Flexible( 93 | child: new Container( 94 | color: const Color.fromRGBO(242, 245, 250, 1.0), //聊天背景 95 | child: new ListView.builder( 96 | physics: _scrollPhysics, 97 | padding: new EdgeInsets.symmetric(vertical: 8.0), 98 | reverse: true, 99 | itemBuilder: (_, int index) => new MessageItem( 100 | message: _messages[index], 101 | isSelf: random.nextBool(), 102 | // isSelf: widget.appAccount == _messages[index].fromAccount, 103 | ), 104 | itemCount: _messages.length, 105 | ), 106 | ), 107 | ); 108 | } 109 | 110 | Widget buildBody() { 111 | return new Column( 112 | children: [ 113 | buildMessages(), 114 | new Divider( 115 | height: 1.0, 116 | ), 117 | new Flexible( 118 | child: new Container( 119 | child: _buildTextComposer(), 120 | ), 121 | ) 122 | ], 123 | ); 124 | } 125 | 126 | @override 127 | void initState() { 128 | super.initState(); 129 | print('initState'); 130 | } 131 | 132 | @override 133 | Widget build(BuildContext context) { 134 | return new Scaffold( 135 | appBar: new AppBar(title: new Text("Friendlychat")), 136 | body: new Column( 137 | children: [ 138 | buildMessages(), 139 | new Divider(height: 1.0), 140 | new Container( 141 | decoration: new BoxDecoration(color: Theme.of(context).cardColor), 142 | child: _buildTextComposer(), 143 | ), 144 | ], 145 | ), 146 | ); 147 | } 148 | } 149 | 150 | class MessageItem extends StatelessWidget { 151 | MessageItem({Key key, @required this.message, @required this.isSelf}); 152 | 153 | final Message message; 154 | final bool isSelf; 155 | 156 | @override 157 | Widget build(BuildContext context) { 158 | List widgets = [ 159 | new Container( 160 | margin: const EdgeInsets.symmetric(horizontal: 8.0), 161 | child: new CircleAvatar( 162 | child: new Text('C'), 163 | ), 164 | ), 165 | new Expanded( 166 | child: new Column( 167 | crossAxisAlignment: 168 | isSelf ? CrossAxisAlignment.end : CrossAxisAlignment.start, 169 | children: [ 170 | new Offstage( 171 | child: new Container( 172 | margin: const EdgeInsets.only(bottom: 8.0), 173 | child: new Text('Creaty')), 174 | offstage: isSelf, 175 | ), 176 | new Container( 177 | margin: isSelf 178 | ? const EdgeInsets.only(left: 65.0) 179 | : const EdgeInsets.only(right: 65.0), 180 | padding: const EdgeInsets.all(8.0), 181 | decoration: new BoxDecoration( 182 | color: Colors.white, 183 | borderRadius: 184 | new BorderRadius.all(const Radius.circular(14.0))), 185 | child: new Text(message.getContent())) 186 | ], 187 | ), 188 | ) 189 | ]; 190 | if (isSelf) { 191 | widgets = widgets.reversed.toList(); 192 | } 193 | return new Container( 194 | margin: const EdgeInsets.symmetric(vertical: 8.0), 195 | child: new Row( 196 | crossAxisAlignment: CrossAxisAlignment.start, 197 | children: widgets, 198 | ), 199 | ); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/src/view/explore/explore.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | import 'package:light/src/widgets/custom_page_route.dart'; 5 | import 'package:light/src/view/search/search.dart'; 6 | //import 'package:light/src/service/db.dart'; 7 | 8 | class Explore extends StatefulWidget { 9 | Explore( 10 | {@required Key key, 11 | @required this.useLightTheme, 12 | @required this.onThemeChanged}) 13 | : super(key: key); 14 | final bool useLightTheme; 15 | final ValueChanged onThemeChanged; 16 | 17 | @override 18 | _ExploreState createState() => new _ExploreState(); 19 | } 20 | 21 | class _ExploreState extends State { 22 | @override 23 | void initState() { 24 | super.initState(); 25 | print('init Explore state...'); 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return new Scaffold( 31 | appBar: new AppBar( 32 | centerTitle: true, 33 | title: new Text('Light'), 34 | actions: [ 35 | new IconButton( 36 | icon: new Icon(Icons.search), 37 | onPressed: () { 38 | Navigator.of(context).push( 39 | new CustomPageRoute(builder: (BuildContext context) { 40 | return new Search( 41 | key: new Key(SearchType.online.toString()), 42 | searchType: SearchType.online, 43 | ); 44 | })); 45 | }, 46 | ) 47 | ], 48 | ), 49 | body: new Center( 50 | child: new Text('Explore'), 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/view/home.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | 5 | import 'package:light/src/view/explore/explore.dart'; 6 | import 'package:light/src/view/shelf/shelf.dart'; 7 | import 'package:light/src/view/profile/profile.dart'; 8 | 9 | class Home extends StatefulWidget { 10 | const Home( 11 | {Key key, 12 | @required this.useLightTheme, 13 | @required this.prefs, 14 | @required this.onThemeChanged}) 15 | : assert(useLightTheme != null), 16 | assert(onThemeChanged != null), 17 | super(key: key); 18 | 19 | final bool useLightTheme; 20 | final SharedPreferences prefs; 21 | final ValueChanged onThemeChanged; 22 | 23 | @override 24 | _HomeState createState() => new _HomeState(); 25 | } 26 | 27 | class _HomeState extends State { 28 | List<_NavigationItem> navigationItems; //导航元素 29 | int currentIndex = 1; //当前导航索引 30 | List pages = []; //页面列表 31 | bool hideBottom = false; // 隐藏底部栏 32 | 33 | /// 隐藏底部导航栏 34 | void handleHideBottom(bool hide) { 35 | setState(() { 36 | hideBottom = hide; 37 | }); 38 | } 39 | 40 | Widget _buildNavigations(BuildContext context) { 41 | if (navigationItems == null || navigationItems.length == 0) { 42 | navigationItems = <_NavigationItem>[ 43 | new _NavigationItem( 44 | icon: Icons.explore, 45 | prefs: widget.prefs, 46 | title: '发现', 47 | name: _NavigationName.explore, 48 | useLightTheme: widget.useLightTheme, 49 | onThemeChanged: widget.onThemeChanged, 50 | hideBottom: handleHideBottom), 51 | new _NavigationItem( 52 | icon: Icons.import_contacts, 53 | prefs: widget.prefs, 54 | title: '收藏', 55 | name: _NavigationName.shelf, 56 | useLightTheme: widget.useLightTheme, 57 | onThemeChanged: widget.onThemeChanged, 58 | hideBottom: handleHideBottom), 59 | new _NavigationItem( 60 | icon: Icons.explore, 61 | prefs: widget.prefs, 62 | title: '我的', 63 | name: _NavigationName.profile, 64 | useLightTheme: widget.useLightTheme, 65 | onThemeChanged: widget.onThemeChanged, 66 | hideBottom: handleHideBottom), 67 | ]; 68 | } 69 | return new BottomNavigationBar( 70 | items: navigationItems.map((_NavigationItem navItem) { 71 | return navItem.item; 72 | }).toList(), 73 | onTap: (int index) { 74 | setState(() { 75 | currentIndex = index; 76 | }); 77 | }, 78 | currentIndex: currentIndex, 79 | ); 80 | } 81 | 82 | Widget _buildPage(BuildContext context, int index) { 83 | navigationItems[index].build(context); 84 | navigationItems.forEach((_NavigationItem item) { 85 | if (!(pages.indexOf(item.page) >= 0) && item.page != null) { 86 | pages.add(item.page); 87 | } 88 | }); 89 | pages 90 | ..removeWhere((Widget page) => page == navigationItems[index].page) 91 | ..add(navigationItems[index].page); 92 | return new Stack( 93 | children: pages, 94 | ); 95 | } 96 | 97 | Widget build(BuildContext context) { 98 | Widget bottomNavigationBar = _buildNavigations(context); 99 | return new Scaffold( 100 | body: _buildPage(context, currentIndex), 101 | bottomNavigationBar: hideBottom ? null : bottomNavigationBar); 102 | } 103 | } 104 | 105 | enum _NavigationName { explore, shelf, profile } 106 | 107 | class _NavigationItem { 108 | _NavigationItem( 109 | {@required IconData icon, 110 | @required this.title, 111 | @required this.name, 112 | @required this.useLightTheme, 113 | @required this.prefs, 114 | @required this.onThemeChanged, 115 | @required this.hideBottom}) 116 | : this.item = new BottomNavigationBarItem( 117 | icon: new Icon(icon), title: new Text(title)); 118 | final useLightTheme; 119 | final SharedPreferences prefs; 120 | final ValueChanged onThemeChanged; 121 | final ValueChanged hideBottom; 122 | final String title; 123 | final _NavigationName name; 124 | final BottomNavigationBarItem item; 125 | Widget page; 126 | 127 | Widget build(BuildContext context) { 128 | if (page != null) { 129 | return page; 130 | } 131 | switch (name) { 132 | case _NavigationName.explore: 133 | page = new Explore( 134 | key: new Key(name.toString()), 135 | useLightTheme: useLightTheme, 136 | onThemeChanged: onThemeChanged, 137 | ); 138 | break; 139 | case _NavigationName.shelf: 140 | page = new Shelf( 141 | key: new Key(name.toString()), 142 | useLightTheme: useLightTheme, 143 | prefs: prefs, 144 | onThemeChanged: onThemeChanged, 145 | showReadProgress: true, 146 | hideBottom: hideBottom, 147 | ); 148 | break; 149 | case _NavigationName.profile: 150 | page = new Profile( 151 | key: new Key(name.toString()), 152 | useLightTheme: useLightTheme, 153 | onThemeChanged: onThemeChanged, 154 | ); 155 | break; 156 | default: 157 | //error 158 | } 159 | return page; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /lib/src/view/profile/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class Profile extends StatefulWidget { 5 | Profile( 6 | {@required Key key, 7 | @required this.useLightTheme, 8 | @required this.onThemeChanged}) 9 | : super(key: key); 10 | final bool useLightTheme; 11 | final ValueChanged onThemeChanged; 12 | 13 | @override 14 | _ProfileState createState() => new _ProfileState(); 15 | } 16 | 17 | class _ProfileState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return new Scaffold( 21 | body: new Center( 22 | child: new Text('Profile'), 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/view/reader/mask.dart: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creatint/light-old/68d5a8cef1a1090ce4d051df156c4d720a3f973c/lib/src/view/reader/mask.dart -------------------------------------------------------------------------------- /lib/src/view/reader/menu.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'setting_pannel.dart'; 6 | import 'package:light/src/widgets/item_button.dart'; 7 | import 'setting_list.dart'; 8 | import 'package:light/src/model/read_mode.dart'; 9 | import 'package:light/src/service/book_service.dart'; 10 | import 'package:light/src/model/book.dart'; 11 | import 'package:light/src/service/app_service.dart'; 12 | import 'package:light/src/widgets/custom_slider.dart'; 13 | 14 | enum Actions { list, mode, night, more } 15 | 16 | class Menu extends StatefulWidget { 17 | Menu( 18 | {Key key, 19 | @required this.book, 20 | @required this.msgStream, 21 | @required this.handleSettings, 22 | @required this.readModeList, 23 | @required this.currentReadModeId}); 24 | 25 | final Book book; 26 | final Stream msgStream; 27 | final HandleSettings handleSettings; 28 | final List readModeList; 29 | final int currentReadModeId; 30 | 31 | @override 32 | MenuState createState() => new MenuState(); 33 | } 34 | 35 | class MenuState extends State { 36 | /// 服务 37 | AppService appService = new AppService(); 38 | 39 | /// 订阅者,用于取消订阅 40 | StreamSubscription subscription; 41 | 42 | /// 消息流 43 | Stream bookDecoderStream; 44 | 45 | /// 分页实例 46 | Record record; 47 | 48 | /// 是否处于计算 49 | bool isCalculating = true; 50 | 51 | /// 分页是否计算完成 52 | bool isDone = false; 53 | 54 | /// 进度 0.0 - 100.0 55 | double process = 0.0; 56 | 57 | /// 是否显示底部默认导航栏 58 | bool showBottom = true; 59 | bool showHeader = true; 60 | 61 | /// 中部空白区域点击 62 | void onTapMiddle() { 63 | print('onTap@Menu'); 64 | Navigator.pop(context); 65 | } 66 | 67 | /// 68 | void onTapMenu(int index) { 69 | print('onTapMenu $index'); 70 | } 71 | 72 | /// 主题切换 73 | void handleModeChange() { 74 | print('handleModeChange'); 75 | } 76 | 77 | /// 刷新进度 78 | void refreshProcess() { 79 | print('process = ${record.process}'); 80 | setState(() { 81 | isCalculating = false; 82 | isDone = true; 83 | process = record.process; 84 | }); 85 | } 86 | 87 | /// 处理进度改变 88 | void handleProcessChange(value) { 89 | print('process=$value index=${record.pageIndexFromProcess(value)}'); 90 | appService.add(['record/jump', record.pageIndexFromProcess(value)]); 91 | } 92 | 93 | /// 上一章 94 | void prevChapter() {} 95 | 96 | /// 下一章 97 | void nextChapter() {} 98 | 99 | /// 处理分页计算进度 100 | void handleCalculateProcess() { 101 | if (null != Record.records) { 102 | refreshProcess(); 103 | } 104 | bookDecoderStream = Record.receiveStream; 105 | if (null == bookDecoderStream) { 106 | print('没msgStream,退出'); 107 | return; 108 | } 109 | subscription = bookDecoderStream.listen((value) { 110 | print('menu msg: $value'); 111 | 112 | // 计算的进度消息 113 | if (value is Map && 'active' == value['state']) { 114 | // print('进度:${value['process']}%,' 115 | // '页码:${value['number']},' 116 | // '长度:${value['length']},' 117 | // '平均长度:${value['eveLength']},' 118 | // '计算:${value['times']} 次,' 119 | // '总计算:${value['totalTimes']} 次,' 120 | // '平均计算:${value['aveTimes']} 次,' 121 | // '用时:${value['time']} ms,' 122 | // '平均用时 ${value['aveTime']} ms'); 123 | setState(() { 124 | process = value['process']; 125 | isCalculating = true; 126 | }); 127 | } else if (value is Map && 'done' == value['state']) { 128 | // 计算完成 129 | // print('计算完成,共 ${value['length']}字符,共 ${value['records'].length} ' 130 | // '页,循环:${value['loopTimes']}次\n' 131 | // '总用时 ${value['time']} s ' 132 | // '平均用时 ${value['aveTime']} ms\n' 133 | // '总计算 ${value['times']} 次 ' 134 | // '平均计算 ${value['aveTimes']} 次'); 135 | refreshProcess(); 136 | } 137 | }); 138 | } 139 | 140 | void handleMenu(Actions action) { 141 | switch (action) { 142 | case Actions.list: 143 | showHeader = false; 144 | showBottom = false; 145 | SystemChrome.setEnabledSystemUIOverlays([]); 146 | Navigator 147 | .push( 148 | context, 149 | new PageRouteBuilder( 150 | opaque: false, 151 | transitionDuration: const Duration(seconds: 0), 152 | pageBuilder: (BuildContext context, _, __) { 153 | return new Container( 154 | child: new Row( 155 | children: [ 156 | new Expanded( 157 | child: new Container( 158 | color: Colors.white, 159 | child: new Center( 160 | child: new Text('目录'), 161 | ), 162 | )), 163 | new GestureDetector( 164 | onTap: () { 165 | Navigator.pop(context, true); 166 | }, 167 | child: new Container( 168 | color: Colors.black26, 169 | width: 50.0, 170 | ), 171 | ) 172 | ], 173 | )); 174 | })) 175 | .then((value) { 176 | // Navigator.pop(context); 177 | showHeader = true; 178 | showBottom = true; 179 | SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); 180 | }); 181 | break; 182 | case Actions.mode: 183 | Navigator 184 | .push( 185 | context, 186 | new PageRouteBuilder( 187 | opaque: false, 188 | transitionDuration: const Duration(seconds: 0), 189 | pageBuilder: (BuildContext context, _, __) => 190 | new SettingPannel( 191 | readModeList: widget.readModeList, 192 | currentReadModeId: widget.currentReadModeId, 193 | handleSettings: widget.handleSettings, 194 | ))) 195 | .then((bool value) { 196 | if (value == true) { 197 | Navigator.pop(context); 198 | } else { 199 | setState(() { 200 | showBottom = true; 201 | }); 202 | } 203 | }); 204 | setState(() { 205 | showBottom = false; 206 | }); 207 | break; 208 | case Actions.night: 209 | break; 210 | case Actions.more: 211 | Navigator 212 | .push( 213 | context, 214 | new PageRouteBuilder( 215 | opaque: false, 216 | transitionDuration: const Duration(seconds: 0), 217 | pageBuilder: (BuildContext context, _, __) => 218 | new SettingList())) 219 | .then((value) {}); 220 | break; 221 | } 222 | } 223 | 224 | @override 225 | void initState() { 226 | super.initState(); 227 | print('initState@Menu'); 228 | SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); 229 | handleCalculateProcess(); 230 | record = new Record(); 231 | // appService.stream.listen((value) { 232 | // if (value[0] == 'book/record') { 233 | // 234 | // } 235 | // }); 236 | } 237 | 238 | @override 239 | void dispose() { 240 | super.dispose(); 241 | print('dispose@Menu'); 242 | SystemChrome.setEnabledSystemUIOverlays([]); 243 | subscription?.cancel(); 244 | } 245 | 246 | @override 247 | Widget build(BuildContext context) { 248 | TextStyle _ktextStyle = new TextStyle(color: Colors.white70); 249 | return new Scaffold( 250 | backgroundColor: Colors.transparent, 251 | body: Column( 252 | children: [ 253 | new Offstage( 254 | offstage: !showHeader, 255 | child: new AppBar( 256 | elevation: 0.0, 257 | backgroundColor: Colors.black87, 258 | leading: new IconButton( 259 | icon: const Icon(Icons.arrow_back), 260 | onPressed: () { 261 | Navigator.pop(context, true); 262 | }), 263 | ), 264 | ), 265 | new Expanded( 266 | child: new GestureDetector( 267 | onTap: onTapMiddle, 268 | child: new Container( 269 | color: Colors.black26, 270 | ))), 271 | new Offstage( 272 | offstage: !showBottom, 273 | child: new Column( 274 | children: [ 275 | new Container( 276 | padding: 277 | const EdgeInsets.only(top: 24.0, left: 18.0, right: 18.0), 278 | color: Colors.black87, 279 | child: new Row( 280 | children: [ 281 | new Container( 282 | width: 54.0, 283 | child: new FlatButton( 284 | padding: EdgeInsets.zero, 285 | onPressed: isCalculating ? null : prevChapter, 286 | child: new Text( 287 | '上一章', 288 | style: _ktextStyle, 289 | ))), 290 | new Expanded( 291 | child: new CustomSlider( 292 | value: process, 293 | min: 0.0, 294 | max: 100.0, 295 | onChanged: isCalculating 296 | ? null 297 | : (double value) { 298 | setState(() { 299 | process = value; 300 | }); 301 | }, 302 | onEnded: (double value) { 303 | print('onEnded value=$value'); 304 | handleProcessChange(process); 305 | }, 306 | ), 307 | ), 308 | new Container( 309 | width: 54.0, 310 | child: new FlatButton( 311 | padding: EdgeInsets.zero, 312 | onPressed: isCalculating ? null : nextChapter, 313 | child: new Text( 314 | '下一章', 315 | style: _ktextStyle, 316 | )), 317 | ), 318 | ], 319 | ), 320 | ), 321 | new Container( 322 | padding: 323 | const EdgeInsets.only(top: 5.0, left: 14.0, right: 14.0), 324 | height: 65.0, 325 | color: Colors.black87, 326 | child: new Row( 327 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 328 | children: [ 329 | new ItemButton( 330 | width: 48.0, 331 | icon: const Icon( 332 | Icons.list, 333 | color: Colors.white70, 334 | ), 335 | title: const Text( 336 | '目录', 337 | style: const TextStyle(color: Colors.white70), 338 | ), 339 | onTap: () { 340 | print('目录'); 341 | handleMenu(Actions.list); 342 | }, 343 | ), 344 | new ItemButton( 345 | width: 48.0, 346 | icon: const Icon(Icons.text_fields, 347 | color: Colors.white70), 348 | title: const Text('设置', 349 | style: const TextStyle(color: Colors.white70)), 350 | onTap: () { 351 | handleMenu(Actions.mode); 352 | }, 353 | ), 354 | new ItemButton( 355 | width: 48.0, 356 | icon: const Icon(Icons.brightness_2, 357 | color: Colors.white70), 358 | title: const Text('夜间', 359 | style: const TextStyle(color: Colors.white70)), 360 | onTap: () { 361 | handleMenu(Actions.night); 362 | }, 363 | ), 364 | new ItemButton( 365 | width: 48.0, 366 | icon: 367 | const Icon(Icons.more_horiz, color: Colors.white70), 368 | title: const Text('更多', 369 | style: const TextStyle(color: Colors.white70)), 370 | onTap: () { 371 | handleMenu(Actions.more); 372 | }, 373 | ), 374 | ], 375 | // type: BottomNavigationBarType.fixed, 376 | // onTap: onTapMenu, 377 | ), 378 | ), 379 | ], 380 | ), 381 | ) 382 | ], 383 | ), 384 | ); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /lib/src/view/reader/reader.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'package:light/src/model/book.dart'; 7 | import 'page.dart'; 8 | import 'menu.dart'; 9 | import 'setting_pannel.dart'; 10 | import 'package:light/src/model/read_mode.dart'; 11 | import 'package:light/src/service/book_service.dart'; 12 | 13 | class Reader extends StatefulWidget { 14 | Reader({Key key, @required this.book, this.prefs}); 15 | 16 | final Book book; 17 | final SharedPreferences prefs; 18 | 19 | @override 20 | ReaderState createState() => new ReaderState(); 21 | } 22 | 23 | class ReaderState extends State { 24 | final GlobalKey menuKey = new GlobalKey(); 25 | final GlobalKey pageKey = new GlobalKey(); 26 | 27 | /// 28 | static final BookService bookService = new BookService(); 29 | 30 | ///资源服务实例 31 | Future bookDecoderFuture; 32 | 33 | ///阅读主题 34 | List readModeList; 35 | 36 | ///当前阅读主题ID 37 | int currentReadModeId = 20; 38 | 39 | /// 消息流 40 | Stream msgStream; 41 | 42 | ///内容显示格式 43 | TextAlign textAlign = TextAlign.justify; 44 | TextDirection textDirection = TextDirection.ltr; 45 | double fontSize = 20.0; 46 | double lineHeight = 1.2; 47 | 48 | TextStyle get textStyle { 49 | return new TextStyle( 50 | color: readModeList[currentReadModeId].fontColor, 51 | fontSize: fontSize, 52 | height: lineHeight, 53 | fontStyle: FontStyle.normal, 54 | fontWeight: FontWeight.normal, 55 | fontFamily: 'HYQH', 56 | textBaseline: TextBaseline.ideographic 57 | ); 58 | } 59 | 60 | ///显示菜单 61 | Future showMenu() { 62 | return Navigator 63 | .push( 64 | context, 65 | new PageRouteBuilder( 66 | opaque: false, 67 | transitionDuration: const Duration(seconds: 0), 68 | pageBuilder: (BuildContext context, _, __) { 69 | return new Menu( 70 | key: new Key(currentReadModeId.toString()), 71 | book: widget.book, 72 | msgStream: msgStream, 73 | readModeList: readModeList, 74 | currentReadModeId: currentReadModeId, 75 | handleSettings: handleSettings, 76 | ); 77 | })) 78 | .then((value) { 79 | if (true == value) { 80 | Navigator.pop(context); 81 | return false; 82 | } else { 83 | return true; 84 | } 85 | }); 86 | } 87 | 88 | ///处理设置 89 | void handleSettings(Settings setting, dynamic value) { 90 | print('handleSettings@Reader value=$value'); 91 | switch(setting) { 92 | case Settings.mode: 93 | if (value >= 0) 94 | setState(() { 95 | currentReadModeId = value; 96 | }); 97 | break; 98 | case Settings.lineHeight: 99 | setState(() { 100 | lineHeight = value; 101 | }); 102 | break; 103 | case Settings.fontSize: 104 | setState(() { 105 | fontSize = value; 106 | }); 107 | break; 108 | default: 109 | } 110 | } 111 | 112 | @override 113 | Widget build(BuildContext context) { 114 | // print('build@Reader'); 115 | return new Container( 116 | child: new Stack( 117 | children: [ 118 | // new Text('hwllo ') 119 | new Page( 120 | key: pageKey, 121 | prefs: widget.prefs, 122 | showMenu: showMenu, 123 | bookDecoderFuture: bookDecoderFuture, 124 | bookService: bookService, 125 | readModeList: readModeList, 126 | currentReadModeId: currentReadModeId, 127 | textStyle: textStyle, 128 | textAlign: textAlign, 129 | textDirection: textDirection, 130 | ) 131 | ], 132 | ), 133 | ); 134 | } 135 | 136 | @override 137 | void initState() { 138 | super.initState(); 139 | // print('initState@Reader'); 140 | SystemChrome.setEnabledSystemUIOverlays([]); 141 | readModeList = bookService.getReadModes(); 142 | bookDecoderFuture = BookDecoder.init(book: widget.book); 143 | } 144 | 145 | @override 146 | void dispose() { 147 | super.dispose(); 148 | print('dispose@Reader'); 149 | SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/view/reader/setting_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SettingList extends StatefulWidget { 4 | @override 5 | _SettingListState createState() => _SettingListState(); 6 | } 7 | 8 | class _SettingListState extends State { 9 | @override 10 | Widget build(BuildContext context) { 11 | return new Scaffold( 12 | appBar: new AppBar( 13 | title: new Text('阅读设置'), 14 | ), 15 | body: new Container( 16 | child: new Text('settings'), 17 | ), 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/view/reader/setting_pannel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:light/src/widgets/label.dart'; 4 | import 'package:light/src/widgets/custom_button.dart'; 5 | import 'package:light/src/model/read_mode.dart'; 6 | 7 | enum Settings { fontSize, lineHeight, mode } 8 | 9 | typedef void HandleSettings(Settings setting, dynamic); 10 | 11 | class SettingPannel extends StatefulWidget { 12 | SettingPannel( 13 | {Key key, 14 | @required this.handleSettings, 15 | @required this.readModeList, 16 | @required this.currentReadModeId}); 17 | 18 | final HandleSettings handleSettings; 19 | final List readModeList; 20 | final int currentReadModeId; 21 | 22 | @override 23 | _SettingPannelState createState() => 24 | new _SettingPannelState(currentReadModeId: currentReadModeId); 25 | } 26 | 27 | class _SettingPannelState extends State { 28 | _SettingPannelState({Key key, this.currentReadModeId}); 29 | 30 | ///是否显示更多主题设置 31 | bool showMoreMode = false; 32 | int currentReadModeId; 33 | 34 | IndexedWidgetBuilder modeButtonBuilder(ReadModeType type) { 35 | List list = 36 | widget.readModeList.where((mode) => mode.type == type).toList(); 37 | return (BuildContext context, int index) { 38 | print(list[index]?.id); 39 | return buildModeButton(list[index]); 40 | }; 41 | } 42 | 43 | Widget buildModeButton(ReadMode mode) { 44 | print('id=${mode.id} currentId=$currentReadModeId ${mode.id == 45 | currentReadModeId}'); 46 | print(mode.backgroundColor); 47 | return new Container( 48 | padding: const EdgeInsets.only(right: 8.0, top: 4.0, bottom: 4.0), 49 | height: 48.0, 50 | width: 48.0, 51 | child: new CustomButton( 52 | active: mode.id == currentReadModeId, 53 | shape: const CircleBorder(), 54 | child: new Container( 55 | padding: const EdgeInsets.all(0.0), 56 | constraints: new BoxConstraints(maxHeight: 40.0, maxWidth: 40.0), 57 | margin: mode.id == currentReadModeId 58 | ? const EdgeInsets.all(3.0) 59 | // ? const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0) 60 | // ? const EdgeInsets.symmetric(vertical: 8.0, horizontal: 0.0) 61 | : null, 62 | decoration: new BoxDecoration( 63 | shape: BoxShape.circle, 64 | // borderRadius: mode.id == widget.currentReadModeId 65 | // ? new BorderRadius.all(new Radius.circular(15.0)) 66 | // : null, 67 | color: mode.backgroundColor, 68 | image: mode.buttonImage), 69 | ), 70 | onPressed: () { 71 | handleSettings(Settings.mode, mode.id); 72 | }, 73 | ), 74 | ); 75 | } 76 | 77 | ///处理设置 78 | void handleSettings(Settings setting, dynamic value) { 79 | print('handleSettings@SettingPannel value=$value'); 80 | switch (setting) { 81 | case Settings.mode: 82 | if (value >= 0) { 83 | setState(() { 84 | currentReadModeId = value; 85 | }); 86 | widget.handleSettings(setting, value); 87 | } 88 | break; 89 | default: 90 | } 91 | } 92 | 93 | void handleShowMoreMode() { 94 | setState(() { 95 | showMoreMode = !showMoreMode; 96 | }); 97 | } 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | return showMoreMode 102 | ? new Column( 103 | mainAxisAlignment: MainAxisAlignment.end, 104 | children: [ 105 | new Expanded( 106 | child: new GestureDetector( 107 | onTap: () { 108 | handleShowMoreMode(); 109 | }, 110 | child: new Container( 111 | color: Colors.transparent, 112 | )), 113 | ), 114 | new Container( 115 | padding: const EdgeInsets.only( 116 | left: 16.0, right: 16.0, bottom: 16.0), 117 | color: Colors.black87, 118 | child: new Column( 119 | crossAxisAlignment: CrossAxisAlignment.start, 120 | children: [ 121 | new Label( 122 | title: '纯色', 123 | ), 124 | new Container( 125 | constraints: new BoxConstraints(maxHeight: 60.0), 126 | child: new GridView.builder( 127 | key: new Key(currentReadModeId.toString()), 128 | padding: const EdgeInsets.all(0.0), 129 | gridDelegate: 130 | new SliverGridDelegateWithMaxCrossAxisExtent( 131 | maxCrossAxisExtent: 48.0), 132 | itemCount: widget.readModeList 133 | .where( 134 | (mode) => mode.type == ReadModeType.color) 135 | .toList() 136 | .length, 137 | itemBuilder: 138 | modeButtonBuilder(ReadModeType.color)), 139 | ), 140 | new Label( 141 | title: '纹理', 142 | ), 143 | new Container( 144 | constraints: new BoxConstraints(maxHeight: 50.0), 145 | child: new GridView.builder( 146 | key: new Key(currentReadModeId.toString()), 147 | padding: const EdgeInsets.all(0.0), 148 | gridDelegate: 149 | new SliverGridDelegateWithMaxCrossAxisExtent( 150 | maxCrossAxisExtent: 48.0), 151 | itemCount: widget.readModeList 152 | .where((mode) => 153 | mode.type == ReadModeType.texture) 154 | .toList() 155 | .length, 156 | itemBuilder: 157 | modeButtonBuilder(ReadModeType.texture)), 158 | ), 159 | new Label( 160 | title: '图片', 161 | ), 162 | new Container( 163 | constraints: new BoxConstraints(maxHeight: 50.0), 164 | child: new GridView.builder( 165 | key: new Key(currentReadModeId.toString()), 166 | padding: const EdgeInsets.all(0.0), 167 | gridDelegate: 168 | new SliverGridDelegateWithMaxCrossAxisExtent( 169 | maxCrossAxisExtent: 48.0), 170 | itemCount: widget.readModeList 171 | .where( 172 | (mode) => mode.type == ReadModeType.image) 173 | .toList() 174 | .length, 175 | itemBuilder: 176 | modeButtonBuilder(ReadModeType.image)), 177 | ) 178 | ])), 179 | ], 180 | ) 181 | : new Column( 182 | mainAxisAlignment: MainAxisAlignment.end, 183 | children: [ 184 | new Expanded( 185 | child: new GestureDetector( 186 | onTap: () { 187 | Navigator.pop(context, true); 188 | }, 189 | child: new Container( 190 | color: Colors.transparent, 191 | ))), 192 | new Container( 193 | padding: const EdgeInsets.only( 194 | top: 16.0, left: 8.0, right: 16.0, bottom: 16.0), 195 | color: Colors.black87, 196 | child: new Column( 197 | children: [ 198 | new Row( 199 | children: [ 200 | new Label(title: '字号'), 201 | new Expanded( 202 | child: new Row( 203 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 204 | children: [ 205 | new Expanded( 206 | child: new CustomButton( 207 | width: double.infinity, 208 | title: '小', 209 | onPressed: () { 210 | widget.handleSettings( 211 | Settings.fontSize, -2); 212 | }, 213 | ), 214 | ), 215 | new Expanded( 216 | child: new CustomButton( 217 | width: double.infinity, 218 | title: '大', 219 | onPressed: () { 220 | widget.handleSettings(Settings.fontSize, 2); 221 | }, 222 | ), 223 | ), 224 | ], 225 | ), 226 | ), 227 | ], 228 | ), 229 | new Row( 230 | children: [ 231 | new Label(title: '行距'), 232 | new Expanded( 233 | child: new Row( 234 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 235 | children: [ 236 | new CustomButton( 237 | active: true, 238 | shape: const CircleBorder(), 239 | iconData: Icons.format_align_justify, 240 | onPressed: () { 241 | widget.handleSettings(Settings.lineHeight, 1.0); 242 | }, 243 | ), 244 | new CustomButton( 245 | shape: const CircleBorder(), 246 | iconData: Icons.view_headline, 247 | onPressed: () { 248 | widget.handleSettings(Settings.lineHeight, 1.2); 249 | }, 250 | ), 251 | new CustomButton( 252 | shape: const CircleBorder(), 253 | iconData: Icons.menu, 254 | onPressed: () { 255 | widget.handleSettings(Settings.lineHeight, 1.4); 256 | }, 257 | ), 258 | new CustomButton( 259 | shape: const CircleBorder(), 260 | iconData: Icons.drag_handle, 261 | onPressed: () { 262 | widget.handleSettings(Settings.lineHeight, 1.6); 263 | }, 264 | ), 265 | new CustomButton( 266 | shape: const CircleBorder(), 267 | iconData: Icons.remove, 268 | onPressed: () { 269 | widget.handleSettings(Settings.lineHeight, 1.8); 270 | }, 271 | ), 272 | ], 273 | ), 274 | ), 275 | ], 276 | ), 277 | new Row( 278 | children: [ 279 | new Label(title: '背景'), 280 | new Expanded( 281 | child: new Row( 282 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 283 | children: widget.readModeList 284 | .sublist(0, 4) 285 | .map((mode) => buildModeButton(mode)) 286 | .toList() 287 | ..add(new CustomButton( 288 | shape: const CircleBorder(), 289 | borderColor: Colors.transparent, 290 | iconData: Icons.more_horiz, 291 | onPressed: handleShowMoreMode, 292 | ))), 293 | ), 294 | ], 295 | ), 296 | ], 297 | ), 298 | ) 299 | ], 300 | ); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /lib/src/view/search/search.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | import 'package:light/src/widgets/custom_page_route.dart'; 8 | import 'package:light/src/model/book.dart'; 9 | import 'package:light/src/view/search/tieba/search_tieba.dart'; 10 | import 'package:light/src/service/search.dart'; 11 | import 'package:light/src/widgets/custom_indicator.dart'; 12 | import 'package:light/src/view/search/search_item.dart'; 13 | 14 | ///搜索类型 online:在线搜索 local:本地搜索 15 | enum SearchType { online, local } 16 | 17 | class Search extends StatefulWidget { 18 | Search( 19 | {@required Key key, 20 | this.searchType: SearchType.online, 21 | this.isSearched: false, 22 | this.word}) 23 | : super(key: key); 24 | 25 | final SearchType searchType; 26 | final bool isSearched; //搜索过的页面搜索时不再跳转路由 27 | final String word; //word不为null,则为结果页面 28 | 29 | @override 30 | SearchState createState() => 31 | new SearchState(isSearched: isSearched, word: word); 32 | } 33 | 34 | class SearchState extends State with SingleTickerProviderStateMixin { 35 | SearchState({this.isSearched, this.word}); 36 | 37 | final TextEditingController textEditingController = 38 | new TextEditingController(); //输入框控件 39 | bool isSearched; //避免初始化时显示seggestions,初始化后置为false 40 | String word; //用于搜索的关键字 41 | bool isLoadding = false; 42 | List suggestions = []; //搜索建议 43 | List histories = []; //搜索历史 44 | List recommends = []; //资源推荐 45 | List results; //搜索结果 46 | AnimationController controller; 47 | Animation animation; 48 | Widget defaultCover = new CircleAvatar( 49 | child: new Text('书'), 50 | ); 51 | 52 | ///执行搜索,获取结果 53 | void doSearch(String text) { 54 | print('doSearch text=$text'); 55 | setState(() { 56 | isLoadding = true; 57 | word = text; 58 | textEditingController.text = text; 59 | }); 60 | print('widget.word=${widget.word}'); 61 | getOnlineBooksAll(text).then((res) { 62 | print('获取数据'); 63 | print(res); 64 | setState(() { 65 | isLoadding = false; 66 | suggestions.clear(); 67 | results = res; 68 | }); 69 | }); 70 | } 71 | 72 | ///处理搜索事件 73 | void handleSearch(BuildContext context, String text) { 74 | print('handleSearch text=$text'); 75 | if (text.isEmpty) { 76 | return; 77 | } 78 | print('handleSearch isSearch=${widget.isSearched.toString()}'); 79 | if (widget.isSearched) { 80 | print('不跳转路由'); 81 | //处于结果路由,不再跳转路由 82 | doSearch(text); 83 | } else { 84 | //处于开始页面,跳转路由 85 | print('跳转路由'); 86 | textEditingController?.clear(); 87 | suggestions?.clear(); 88 | Navigator 89 | .of(context) 90 | .push(new CustomPageRoute(builder: (BuildContext context) { 91 | return new Search( 92 | key: new Key(widget.searchType.toString() + text), 93 | searchType: widget.searchType, 94 | isSearched: true, 95 | word: text); 96 | })); 97 | } 98 | } 99 | 100 | void _searchTieba(BuildContext context) { 101 | print('_searchTieba'); 102 | Navigator 103 | .of(context) 104 | .push(new CustomPageRoute(builder: (BuildContext context) { 105 | return new SearchTieba( 106 | word: textEditingController.text, 107 | ); 108 | })); 109 | } 110 | 111 | ///处理输入框中字符改变事件 112 | void handleChanged(String text) { 113 | print('handleChanged text=$text text.isEmpty=${text.isEmpty}'); 114 | if (isSearched) { 115 | isSearched = false; 116 | return; 117 | } 118 | setState(() { 119 | isLoadding = true; 120 | suggestions.clear(); 121 | }); 122 | if (text.isEmpty) { 123 | return; 124 | } 125 | if (widget.searchType == SearchType.online) { 126 | ///在线查询 127 | String url = 'https://sp0.baidu.com/' 128 | '5a1Fazu8AA54nxGko9WTAnF6hhy/su?wd=%WORD%&json=1&csor=0&ie=utf-8' 129 | .replaceFirst(new RegExp('%WORD%'), text); 130 | RegExp reg = new RegExp(r'window\.baidu\.sug\((.*)\);'); 131 | 132 | http.get(url).then((var response) { 133 | Match matche = reg.firstMatch(response.body); 134 | if (matche != null) { 135 | String str = matche.group(1); 136 | Map res = json.decode(str); 137 | if (res['s'] != null) { 138 | if (textEditingController.text.isNotEmpty) { 139 | print('handleChanged2 text=$text isEmpty=${text.isEmpty}'); 140 | setState(() { 141 | suggestions = res['s']; 142 | isLoadding = false; 143 | }); 144 | } 145 | return; 146 | } 147 | } 148 | throw '查询失败,无法联网或API不可用'; 149 | }).catchError((error) { 150 | print('出错啦!!$error'); 151 | }); 152 | } else if (widget.searchType == SearchType.local) { 153 | ///本地查询 154 | 155 | } 156 | } 157 | 158 | ///构建搜索框 159 | Widget buildSearchField(BuildContext context) { 160 | return new TextField( 161 | style: new TextStyle(color: Colors.white, fontSize: 18.0), 162 | autofocus: !widget.isSearched, 163 | controller: textEditingController, 164 | onChanged: handleChanged, 165 | onSubmitted: (String text) { 166 | handleSearch(context, text); 167 | }, 168 | decoration: new InputDecoration( 169 | border: InputBorder.none, 170 | hintText: widget.isSearched ? word : '作品、作者', 171 | hintStyle: new TextStyle(color: Colors.white30, fontSize: 18.0), 172 | suffixIcon: new Offstage( 173 | offstage: textEditingController.text == null || 174 | textEditingController.text.isEmpty, 175 | child: new IconButton( 176 | icon: new Icon( 177 | Icons.clear, 178 | color: Colors.white70, 179 | ), 180 | onPressed: () { 181 | //重置列表 182 | setState(() { 183 | word = null; 184 | textEditingController.clear(); 185 | suggestions.clear(); 186 | }); 187 | }), 188 | ), 189 | ), 190 | ); 191 | } 192 | 193 | ///构建AppBar 194 | AppBar buildAppBar(BuildContext context) { 195 | return new AppBar( 196 | title: buildSearchField(context), 197 | actions: [ 198 | new IconButton( 199 | icon: new Icon(Icons.search), 200 | onPressed: () { 201 | handleSearch(context, textEditingController.text); 202 | }, 203 | ), 204 | ], 205 | ); 206 | } 207 | 208 | ///构建初始页,输入框为空时显示,包括搜索历史、推荐资源等 209 | Widget buildStart(BuildContext context) { 210 | return new Offstage( 211 | offstage: (null != suggestions && suggestions.isNotEmpty) || 212 | textEditingController.text.isNotEmpty, 213 | child: new Center( 214 | child: new Text('无历史'), 215 | ), 216 | ); 217 | } 218 | 219 | ///构建搜索建议,输入框中有文字时获取搜索建议 220 | Widget buildSuggestions(BuildContext context) { 221 | List items = []; 222 | if (suggestions == null || suggestions.isEmpty) { 223 | return new Offstage( 224 | offstage: null == suggestions || suggestions.isEmpty, 225 | child: new Container()); 226 | } 227 | items.add(new ListTile( 228 | leading: new Icon(Icons.search), 229 | title: new Text('搜索贴吧?'), 230 | onTap: () { 231 | _searchTieba(context); 232 | }, 233 | )); 234 | for (String title in suggestions) { 235 | if (title.isNotEmpty) { 236 | items.add(new SearchItem( 237 | title: title, 238 | style: new TextStyle(color: Colors.black26), 239 | onTap: () { 240 | handleSearch(context, title); 241 | }, 242 | )); 243 | } 244 | } 245 | return new Offstage( 246 | offstage: null == suggestions || suggestions.isEmpty, 247 | child: new Container( 248 | // color: Colors.white, 249 | // color: Theme.of(context).backgroundColor, 250 | child: new ListView( 251 | children: ListTile 252 | .divideTiles( 253 | context: context, 254 | tiles: items, 255 | ) 256 | .toList(), 257 | ), 258 | ), 259 | ); 260 | } 261 | 262 | Widget buildCover(BuildContext context, Book book) { 263 | RegExp reg = new RegExp('^http'); 264 | if (null == book.coverUri || book.coverUri.isEmpty) { 265 | return defaultCover; 266 | } 267 | if (reg.hasMatch(book.coverUri)) { 268 | return new Image.network(book.coverUri); 269 | } 270 | return new Image.file(new File(book.coverUri)); 271 | } 272 | 273 | ///构建搜索结果 274 | Widget buildResults(BuildContext context) { 275 | print('buildResults'); 276 | List items = []; 277 | if (results == null || results.isEmpty) { 278 | items.add(new Container( 279 | alignment: Alignment.center, 280 | height: 50.0, 281 | child: new Text( 282 | '无结果', 283 | textAlign: TextAlign.center, 284 | ), 285 | )); 286 | items.add(new ListTile( 287 | leading: new Icon(Icons.search), 288 | title: new Text('搜索贴吧?'), 289 | onTap: () { 290 | _searchTieba(context); 291 | }, 292 | )); 293 | } else { 294 | for (Book book in results) { 295 | if (book != null) { 296 | items.add(new SearchItem( 297 | cover: buildCover(context, book), 298 | title: book.title, 299 | subtitle: book.description, 300 | style: new TextStyle(color: Colors.black26), 301 | )); 302 | } 303 | } 304 | } 305 | return new Offstage( 306 | offstage: !widget.isSearched || textEditingController.text.isEmpty, 307 | child: new Container( 308 | // color: Colors.white, 309 | child: new ListView( 310 | children: ListTile 311 | .divideTiles( 312 | context: context, 313 | tiles: items, 314 | ) 315 | .toList(), 316 | ), 317 | ), 318 | ); 319 | } 320 | 321 | Widget buildIndicator(BuildContext context) { 322 | return new Offstage(offstage: !isLoadding, child: new CustomIndicator()); 323 | } 324 | 325 | ///构建页面 326 | Widget buildBody(BuildContext context) { 327 | print('buildBody text=${textEditingController 328 | .text} suggestion.isEmpty=${suggestions.isEmpty}'); 329 | List pages = []; 330 | //初始页面 331 | pages.add(buildStart(context)); 332 | 333 | ///有结果则显示结果 334 | pages.add(buildResults(context)); 335 | 336 | ///有搜索建议则显示搜索建议 337 | pages.add(buildSuggestions(context)); 338 | 339 | ///是否显示加载动画 340 | pages.add(buildIndicator(context)); 341 | return new Stack(children: pages); 342 | } 343 | 344 | @override 345 | void initState() { 346 | super.initState(); 347 | print('initState'); 348 | if (widget.word != null && widget.word.isNotEmpty) { 349 | //存在word,进行搜索 350 | textEditingController.text = widget.word; 351 | doSearch(widget.word); 352 | } 353 | } 354 | 355 | @override 356 | void dispose() { 357 | super.dispose(); 358 | print('dispose'); 359 | suggestions?.clear(); 360 | results?.clear(); 361 | textEditingController?.clear(); 362 | textEditingController?.dispose(); 363 | controller?.stop(); 364 | } 365 | 366 | @override 367 | Widget build(BuildContext context) { 368 | return new Scaffold(appBar: buildAppBar(context), body: buildBody(context)); 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /lib/src/view/search/search_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | ///列表元素 5 | class SearchItem extends StatelessWidget{ 6 | SearchItem({ 7 | @required this.title, 8 | this.style, 9 | this.subtitle, 10 | this.value, 11 | this.onTap, 12 | this.cover, 13 | }); 14 | final String title; 15 | final TextStyle style; 16 | final String subtitle; 17 | final cover; 18 | final String value; 19 | final VoidCallback onTap; 20 | @override 21 | Widget build(BuildContext context) { 22 | return new ListTile( 23 | leading: cover, 24 | title: new Text(title, style: style,), 25 | subtitle: subtitle != null ? new Text(subtitle) : null, 26 | onTap: onTap, 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/view/search/tieba/search_tieba.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:http/http.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'package:light/src/widgets/custom_page_route.dart'; 6 | import 'package:light/src/view/search/search_item.dart'; 7 | import 'package:light/src/widgets/custom_indicator.dart'; 8 | import 'package:light/src/view/search/tieba/tieba.dart'; 9 | 10 | class SearchTieba extends StatefulWidget { 11 | SearchTieba({ 12 | this.word 13 | }); 14 | final String word; 15 | @override 16 | SearchTiebaState createState() => new SearchTiebaState(word: word); 17 | } 18 | 19 | class SearchTiebaState extends State { 20 | SearchTiebaState({ 21 | this.word 22 | }); 23 | String word; 24 | TextEditingController textEditingController = new TextEditingController(); 25 | List suggestions = []; 26 | List results = []; 27 | bool isLoadding = false; 28 | 29 | ///跳转到贴吧 30 | void jump(BuildContext context, Map tieba) { 31 | print('tap on tieba=${tieba['fname']}'); 32 | Navigator.of(context).push(new CustomPageRoute(builder: 33 | (BuildContext context) => new Tieba(fname: tieba['fname']) 34 | )); 35 | } 36 | 37 | ///执行搜索 38 | void handleSearch(String text) { 39 | print('handleSearch word=$text'); 40 | if (text == null || text.isEmpty) { 41 | return null; 42 | } 43 | setState((){ 44 | word = text; 45 | isLoadding = true; 46 | suggestions?.clear(); 47 | results?.clear(); 48 | }); 49 | //获取results 50 | int time = new DateTime.now().millisecondsSinceEpoch; 51 | print('time=$time'); 52 | String url = 'http://tieba.baidu.com/suggestion?query=%WORD%&ie=utf-8&_=%TIME%' 53 | .replaceFirst(new RegExp(r'%WORD%'), text) 54 | .replaceFirst(new RegExp(r'%TIME%'), time.toString()); 55 | print(url); 56 | get(url).then((response){ 57 | Map res = json.decode(response.body); 58 | print(res); 59 | if (res == null && res.isEmpty) { 60 | return; 61 | }else if (res['error'] != 0 || res['query_match'] == null 62 | || res['query_match']['search_data'] == null) { 63 | //TODO:贴吧API ERROR,记录日志 64 | return; 65 | } 66 | res['query_match']['search_data'].forEach((tieba){ 67 | print(tieba); 68 | setState((){ 69 | results.add(tieba); 70 | }); 71 | }); 72 | setState((){ 73 | isLoadding = false; 74 | }); 75 | }); 76 | 77 | } 78 | 79 | ///处理搜索框文字改变事件 80 | void handleChanged(String text) { 81 | print('handleChanged text=$text'); 82 | setState((){ 83 | suggestions.clear(); 84 | }); 85 | if (text == null || text.isEmpty) { 86 | return; 87 | } 88 | //获取suggestion 89 | int time = new DateTime.now().millisecondsSinceEpoch; 90 | print('time=$time'); 91 | String url = 'http://tieba.baidu.com/suggestion?query=%WORD%&ie=utf-8&_=%TIME%' 92 | .replaceFirst(new RegExp(r'%WORD%'), text) 93 | .replaceFirst(new RegExp(r'%TIME%'), time.toString()); 94 | print(url); 95 | get(url).then((response){ 96 | Map result = json.decode(response.body); 97 | print(result); 98 | if (result == null && result.isEmpty) { 99 | return; 100 | }else if (result['error'] != 0 || result['query_match'] == null 101 | || result['query_match']['search_data'] == null) { 102 | //TODO:贴吧API ERROR,记录日志 103 | return; 104 | } 105 | result['query_match']['search_data'].forEach((Map tieba){ 106 | print(tieba); 107 | setState((){ 108 | suggestions.add(tieba['fname']); 109 | }); 110 | }); 111 | }); 112 | // new http.HttpClient(); 113 | 114 | } 115 | 116 | 117 | ///构建搜索框 118 | Widget buildSearchField(BuildContext context) { 119 | return new TextField( 120 | onChanged: handleChanged, 121 | style: new TextStyle( 122 | color: Colors.white, 123 | fontSize: 18.0 124 | ), 125 | controller: textEditingController, 126 | decoration: new InputDecoration( 127 | hintText: word ?? '贴吧', 128 | border: InputBorder.none, 129 | suffixIcon: new Offstage( 130 | offstage: textEditingController.text == null || 131 | textEditingController.text.isEmpty, 132 | child: new IconButton( 133 | icon: new Icon( 134 | Icons.clear, 135 | color: Colors.white70, 136 | ), 137 | onPressed: () { 138 | //重置列表 139 | setState(() { 140 | word = null; 141 | textEditingController.clear(); 142 | suggestions.clear(); 143 | }); 144 | }), 145 | ), 146 | ), 147 | ); 148 | } 149 | 150 | ///构建AppBar 151 | Widget buildAppBar(BuildContext context) { 152 | return new AppBar( 153 | title: buildSearchField(context), 154 | actions: [ 155 | new IconButton(icon: new Icon(Icons.search), onPressed: (){ 156 | handleSearch(textEditingController.text); 157 | }) 158 | ], 159 | ); 160 | } 161 | 162 | ///构建suggestions 163 | Widget buildSuggestions(BuildContext context) { 164 | List items = []; 165 | if (suggestions == null || suggestions.isEmpty) { 166 | return null; 167 | } 168 | for(String title in suggestions) { 169 | if (title.isNotEmpty) { 170 | items.add(new SearchItem( 171 | title: title, 172 | style: new TextStyle(color: Colors.black26), 173 | onTap: (){ 174 | handleSearch(title); 175 | }, 176 | )); 177 | } 178 | } 179 | return new Container( 180 | color: Colors.white, 181 | child: new ListView( 182 | children: ListTile.divideTiles( 183 | context: context, 184 | tiles: items, 185 | ).toList(), 186 | ), 187 | ); 188 | } 189 | 190 | ///构建results 191 | Widget buildResults(BuildContext context) { 192 | List items = []; 193 | if (results == null || results.isEmpty) { 194 | return null; 195 | } 196 | results.forEach((Map tieba){ 197 | if (tieba != null && tieba.isNotEmpty) { 198 | items.add(new ListTile( 199 | leading: new Image.network(tieba['fpic']), 200 | title: new Text(tieba['fname'] + '吧'), 201 | onTap: (){ 202 | jump(context, tieba); 203 | }, 204 | )); 205 | } 206 | }); 207 | return new Container( 208 | child: new ListView( 209 | children: ListTile.divideTiles( 210 | context: context, 211 | tiles: items 212 | ).toList(), 213 | ), 214 | ); 215 | } 216 | 217 | ///构建加载动画页面 218 | Widget buildIndicator(BuildContext context) { 219 | return new CustomIndicator(); 220 | } 221 | 222 | ///构建页面 223 | Widget buildBody(BuildContext context) { 224 | List pages = []; 225 | if (results != null && results.isNotEmpty) { 226 | pages.add(buildResults(context)); 227 | } 228 | if (suggestions != null && suggestions.isNotEmpty) { 229 | pages.add(buildSuggestions(context)); 230 | } 231 | if (isLoadding) { 232 | pages.add(buildIndicator(context)); 233 | } 234 | return new Stack(children: pages,); 235 | } 236 | 237 | ///初始化 238 | @override 239 | void initState() { 240 | super.initState(); 241 | print('tieba init word=${widget.word}'); 242 | if (word != null && word.isNotEmpty) { 243 | //存在word,进行搜索 244 | textEditingController.text = word; 245 | handleSearch(word); 246 | } 247 | } 248 | 249 | @override 250 | Widget build(BuildContext context) { 251 | return new Scaffold( 252 | appBar: buildAppBar(context), 253 | body: buildBody(context), 254 | ); 255 | } 256 | } -------------------------------------------------------------------------------- /lib/src/view/shelf/book_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:light/src/model/book.dart'; 6 | import 'package:light/src/model/selected_list_model.dart'; 7 | 8 | class BookItem extends StatelessWidget { 9 | BookItem( 10 | {@required this.book, 11 | @required this.showReadProgress, 12 | @required this.onTap, 13 | @required this.onLongPress, 14 | @required this.inSelect, 15 | @required this.selectedBooks}); 16 | 17 | final ValueChanged onTap; 18 | final ValueChanged onLongPress; 19 | final Book book; 20 | final bool inSelect; 21 | final bool showReadProgress; 22 | final SelectedListModel selectedBooks; 23 | 24 | final RegExp regHttp = new RegExp(r'^http'); 25 | 26 | ///检查是否选中 27 | bool checkSelected() { 28 | if (null != selectedBooks && selectedBooks.indexOf(book) >= 0) return true; 29 | return false; 30 | } 31 | 32 | ///295/405 33 | List buildCover(BuildContext context) { 34 | Image image; 35 | if (null == book.coverUri || book.coverUri.isEmpty) { 36 | image = new Image.asset( 37 | 'assets/default_cover.jpg', 38 | fit: BoxFit.cover, 39 | ); 40 | } else if (regHttp.hasMatch(book.coverUri)) { 41 | image = new Image.network( 42 | book.coverUri, 43 | fit: BoxFit.cover, 44 | ); 45 | } else { 46 | image = new Image.file( 47 | new File(book.coverUri), 48 | fit: BoxFit.cover, 49 | ); 50 | } 51 | return [ 52 | new Positioned.fill(child: image), 53 | // new Positioned( 54 | // top: -5.0, 55 | // right: -5.0, 56 | // child: new Offstage( 57 | // offstage: !inSelect, 58 | // child: new SizedBox( 59 | // height: 21.0, 60 | // width: 21.0, 61 | // child: new CircleAvatar( 62 | // backgroundColor: Colors.white, 63 | // ), 64 | // ), 65 | // ), 66 | // ), 67 | new Positioned( 68 | top: -5.0, 69 | right: -5.0, 70 | child: new Offstage( 71 | offstage: true, 72 | child: new SizedBox( 73 | height: 21.0, 74 | width: 21.0, 75 | child: new CircleAvatar( 76 | backgroundColor: Colors.red, 77 | child: new Text( 78 | '2', 79 | style: new TextStyle(fontSize: 12.0), 80 | ), 81 | ), 82 | ), 83 | ), 84 | ), 85 | // new Offstage( 86 | // offstage: , 87 | // ) 88 | ]..addAll([ 89 | new Positioned( 90 | top: -5.0, 91 | right: -5.0, 92 | child: new Offstage( 93 | offstage: !inSelect, 94 | child: new SizedBox( 95 | height: 21.0, 96 | width: 21.0, 97 | child: new Container( 98 | padding: EdgeInsets.zero, 99 | margin: const EdgeInsets.only(bottom: 1.0), 100 | decoration: new BoxDecoration( 101 | color: Colors.white, 102 | shape: BoxShape.circle, 103 | border: new Border.all(color: Colors.red, width: 1.0)), 104 | ), 105 | ), 106 | ), 107 | ), 108 | new Positioned( 109 | top: -7.0, 110 | right: -4.0, 111 | child: new Offstage( 112 | offstage: !inSelect || !checkSelected(), 113 | child: new SizedBox( 114 | height: 21.0, 115 | width: 21.0, 116 | child: new Container( 117 | decoration: new BoxDecoration( 118 | color: Colors.white, shape: BoxShape.circle), 119 | child: new Icon( 120 | Icons.check_circle, 121 | color: Colors.red, 122 | size: 24.0, 123 | ), 124 | ), 125 | ), 126 | ), 127 | ), 128 | ]); 129 | } 130 | 131 | @override 132 | Widget build(BuildContext context) { 133 | return new GestureDetector( 134 | onTap: () => onTap(book), 135 | onLongPress: () => onLongPress(book), 136 | child: new Container( 137 | child: new Column( 138 | crossAxisAlignment: CrossAxisAlignment.start, 139 | children: [ 140 | new Flexible( 141 | child: new Stack( 142 | children: buildCover(context), 143 | overflow: Overflow.visible, 144 | ), 145 | ), 146 | new Container( 147 | height: showReadProgress ? 68.0 : 48.0, 148 | child: new Column( 149 | crossAxisAlignment: CrossAxisAlignment.start, 150 | children: [ 151 | new Container( 152 | margin: const EdgeInsets.only(top: 8.0), 153 | child: new Text( 154 | book.title, 155 | maxLines: 2, 156 | textAlign: TextAlign.start, 157 | overflow: TextOverflow.ellipsis, 158 | ), 159 | ), 160 | new Offstage( 161 | offstage: !showReadProgress, 162 | child: new Text( 163 | '已阅读3%', 164 | style: Theme 165 | .of(context) 166 | .textTheme 167 | .body2 168 | .copyWith(color: Colors.grey), 169 | )), 170 | ], 171 | ), 172 | ) 173 | ], 174 | ), 175 | ), 176 | ); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /lib/src/view/shelf/entity_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'dart:io'; 4 | //import 'dart:async'; 5 | 6 | import 'package:light/src/service/file_service.dart'; 7 | import 'package:light/src/model/selected_list_model.dart'; 8 | 9 | typedef void LongPress({FileSystemEntity entity, FileType type}); 10 | 11 | class EntityItem extends StatefulWidget { 12 | EntityItem( 13 | {Key key, 14 | @required this.entity, 15 | @required this.onTap, 16 | @required this.onLongPress, 17 | @required this.selectedEntities, 18 | @required this.inSelect}) 19 | : super(key: key); 20 | 21 | final FileSystemEntity entity; 22 | final ValueChanged onTap; 23 | 24 | // final ValueChanged onLongPress; 25 | final LongPress onLongPress; 26 | final SelectedListModel selectedEntities; 27 | final inSelect; 28 | 29 | @override 30 | EntityItemState createState() => new EntityItemState(); 31 | } 32 | 33 | class EntityItemState extends State { 34 | FileType type; 35 | 36 | ///检查是否选中 37 | bool checkSelected() { 38 | if (widget.selectedEntities.indexOf(widget.entity) >= 0) return true; 39 | return false; 40 | } 41 | 42 | ///获取选中图标高亮颜色 43 | Color getColor() { 44 | if (checkSelected()) { 45 | return Theme.of(context).accentColor; 46 | } 47 | return Theme.of(context).disabledColor; 48 | } 49 | 50 | ///构建头部图标 51 | Icon buildLeadingIcon() { 52 | type = getType(widget.entity); 53 | IconData data; 54 | switch (type) { 55 | case FileType.TEXT: 56 | case FileType.PDF: 57 | case FileType.EPUB: 58 | data = Icons.book; 59 | break; 60 | case FileType.OTHER: 61 | data = Icons.insert_drive_file; 62 | break; 63 | case FileType.DIRECTORY: 64 | data = Icons.folder; 65 | break; 66 | case FileType.NOT_FOUND: 67 | default: 68 | data = Icons.do_not_disturb; 69 | } 70 | return new Icon(data); 71 | } 72 | 73 | ///构建尾部图标 74 | Icon buildTailIcon() { 75 | if (checkSelected()) { 76 | return new Icon( 77 | Icons.check_circle, 78 | color: getColor(), 79 | ); 80 | } 81 | return new Icon( 82 | Icons.radio_button_unchecked, 83 | color: getColor(), 84 | ); 85 | } 86 | 87 | @override 88 | void initState() { 89 | super.initState(); 90 | } 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return new DecoratedBox( 95 | decoration: new BoxDecoration( 96 | border: new Border( 97 | bottom: new BorderSide(color: Colors.grey[200], width: 1.0))), 98 | child: new ListTile( 99 | onTap: () { 100 | widget.onTap(widget.entity); 101 | }, 102 | onLongPress: () { 103 | widget.onLongPress( 104 | entity: widget.entity, type: getType(widget.entity)); 105 | }, 106 | leading: buildLeadingIcon(), 107 | title: new Text( 108 | getBasename(widget.entity.path), 109 | overflow: TextOverflow.ellipsis, 110 | ), 111 | trailing: new Offstage( 112 | offstage: !widget.inSelect, 113 | child: buildTailIcon(), 114 | ))); 115 | } 116 | } 117 | 118 | class CustomIcon extends StatelessWidget { 119 | @override 120 | Widget build(BuildContext context) { 121 | final IconThemeData iconTheme = IconTheme.of(context); 122 | return new Container( 123 | margin: const EdgeInsets.all(11.0), 124 | width: 1.0, 125 | height: 1.0, 126 | color: iconTheme.color, 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/src/view/shelf/shelf.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | import 'package:light/src/service/book_service.dart'; 7 | import 'package:light/src/model/book.dart'; 8 | import 'package:light/src/view/search/search.dart'; 9 | import 'package:light/src/widgets/custom_page_route.dart'; 10 | import 'package:light/src/view/shelf/import_book.dart'; 11 | import 'package:light/src/view/shelf/book_item.dart'; 12 | import 'package:light/src/view/reader/reader.dart'; 13 | import 'package:light/src/model/selected_list_model.dart'; 14 | 15 | class Shelf extends StatefulWidget { 16 | Shelf( 17 | {@required Key key, 18 | @required this.useLightTheme, 19 | @required this.prefs, 20 | @required this.onThemeChanged, 21 | @required this.showReadProgress, 22 | @required this.hideBottom}) 23 | : super(key: key); 24 | final bool useLightTheme; 25 | final SharedPreferences prefs; 26 | final bool showReadProgress; 27 | final ValueChanged onThemeChanged; 28 | final ValueChanged hideBottom; 29 | 30 | @override 31 | _ShelfState createState() => new _ShelfState(); 32 | } 33 | 34 | enum Actions { import, changeTheme, edit } 35 | 36 | class _ShelfState extends State { 37 | /// 当前Book列表 38 | List books = []; 39 | 40 | /// 服务 41 | BookService bookService = new BookService(); 42 | 43 | /// 选中的Book列表 44 | SelectedListModel selectedBooks; 45 | 46 | //是否是选择模式 47 | bool inSelect = false; 48 | 49 | getBooks() { 50 | bookService.getBooks().then((value) { 51 | // print(value); 52 | setState(() { 53 | books = value; 54 | }); 55 | }); 56 | } 57 | 58 | void handleSearch() { 59 | Navigator 60 | .of(context) 61 | .push(new CustomPageRoute(builder: (BuildContext context) { 62 | return new Search( 63 | key: new Key(SearchType.local.toString()), 64 | searchType: SearchType.local, 65 | ); 66 | })); 67 | } 68 | 69 | /// 点击跳转相应书籍 70 | void handleOnTap(Book book) { 71 | if (inSelect) { 72 | setState(() { 73 | if (selectedBooks.indexOf(book) >= 0) { 74 | selectedBooks.remove(book); 75 | } else { 76 | selectedBooks.add(book); 77 | } 78 | }); 79 | } else { 80 | Navigator.push( 81 | context, 82 | new CustomPageRoute( 83 | builder: (context) => new Reader( 84 | book: book, 85 | prefs: widget.prefs, 86 | ))); 87 | } 88 | } 89 | 90 | /// 进入编辑模式 91 | void handleInEdit([Book book]) { 92 | print('handleInEdit book=$book'); 93 | if (null == selectedBooks) { 94 | selectedBooks = new SelectedListModel( 95 | handleRemove: handleRemove, handleIndexOf: handleIndexOf); 96 | } 97 | if (inSelect) { 98 | if (null != book) { 99 | selectedBooks.add(book); 100 | } 101 | } else { 102 | inSelect = true; 103 | selectedBooks.add(book); 104 | // 隐藏底部导航栏 105 | widget.hideBottom(true); 106 | } 107 | setState(() {}); 108 | } 109 | 110 | /// 退出编辑模式 111 | void handleOutEdit() { 112 | setState(() { 113 | inSelect = false; 114 | selectedBooks.clear(); 115 | }); 116 | // 显示底部导航栏 117 | widget.hideBottom(false); 118 | } 119 | 120 | ///SelectedListModel所用移除元素方法 121 | void handleRemove(Book book, List list) { 122 | if (list.length == 0) return; 123 | list.removeWhere((Book tmp) { 124 | return tmp == book; 125 | }); 126 | } 127 | 128 | ///SelectedListModel所用查询索引方法 129 | int handleIndexOf(Book book, List list) { 130 | if (list.length == 0) return -1; 131 | return list.indexWhere((Book tmp) { 132 | return tmp == book; 133 | }); 134 | } 135 | 136 | /// 全选 137 | void handleSelectAll() { 138 | if (selectedBooks.length >= books.length) { 139 | setState(() { 140 | selectedBooks.clear(); 141 | }); 142 | } else { 143 | setState(() { 144 | selectedBooks.clear(); 145 | selectedBooks.addAll(books); 146 | }); 147 | } 148 | } 149 | 150 | /// 从数据库删除选中书籍 151 | handleDelete() { 152 | print('handleDelete'); 153 | if (!(selectedBooks.list.length > 0)) { 154 | return; 155 | } 156 | final ThemeData theme = Theme.of(context); 157 | final TextStyle dialogTextStyle = 158 | theme.textTheme.subhead.copyWith(color: theme.textTheme.caption.color); 159 | showDialog( 160 | context: context, 161 | builder: (BuildContext context) { 162 | return new AlertDialog( 163 | content: new Text('确定要删除吗?', style: dialogTextStyle), 164 | actions: [ 165 | new FlatButton( 166 | child: new Text( 167 | '取消', 168 | style: new TextStyle(color: Colors.black87), 169 | ), 170 | onPressed: () { 171 | Navigator.pop(context, false); 172 | }), 173 | new FlatButton( 174 | child: const Text('确定'), 175 | onPressed: () { 176 | Navigator.pop(context, true); 177 | }) 178 | ]); 179 | }).then((bool value) async { 180 | if (value) { 181 | print('执行删除'); 182 | int count = await bookService.deleteBooks(selectedBooks.list); 183 | print('删除 $count 本书'); 184 | setState(() { 185 | selectedBooks.clear(); 186 | inSelect = false; 187 | }); 188 | getBooks(); 189 | if (null != count && count > 0) { 190 | showDialog( 191 | context: context, 192 | builder: (BuildContext context) { 193 | new Timer( 194 | const Duration(seconds: 2), () => Navigator.pop(context)); 195 | return new AlertDialog( 196 | content: new Text('成功删除$count个资源'), 197 | ); 198 | }); 199 | } else if (null != count) { 200 | showDialog( 201 | context: context, 202 | builder: (BuildContext context) { 203 | new Timer( 204 | const Duration(seconds: 1), () => Navigator.pop(context)); 205 | return new AlertDialog( 206 | content: new Text('删除失败'), 207 | ); 208 | }); 209 | } 210 | } else { 211 | print('取消'); 212 | } 213 | }); 214 | } 215 | 216 | void handleAction(Actions action) { 217 | switch (action) { 218 | case Actions.import: 219 | Navigator 220 | .of(context) 221 | .push(new CustomPageRoute( 222 | builder: (BuildContext context) => new ImportBook( 223 | key: new Key('start'), 224 | prefs: widget.prefs, 225 | isRoot: true, 226 | // path: '/storage/emulated/0/DuoKan/Downloads/MiCloudBooks', 227 | // path: '/storage/emulated/0/DuoKan', 228 | ))) 229 | .then((_) { 230 | getBooks(); 231 | }); 232 | break; 233 | case Actions.changeTheme: 234 | print('widget.useLightTheme: ${widget.useLightTheme}'); 235 | widget.onThemeChanged(!widget.useLightTheme); 236 | break; 237 | case Actions.edit: 238 | handleInEdit(); 239 | break; 240 | } 241 | } 242 | 243 | Widget buildAppBar(BuildContext context) { 244 | return inSelect 245 | ? new AppBar( 246 | leading: new Container( 247 | width: 48.0, 248 | child: new FlatButton( 249 | onPressed: handleOutEdit, 250 | child: new Text( 251 | '取消', 252 | style: Theme.of(context).primaryTextTheme.button, 253 | ), 254 | padding: EdgeInsets.zero, 255 | ), 256 | ), 257 | title: new Text(selectedBooks.length > 0 258 | ? '已选择${selectedBooks.length}本图书' 259 | : '请选择图书'), 260 | brightness: Brightness.light, 261 | actions: [ 262 | new Container( 263 | width: 52.0, 264 | child: new FlatButton( 265 | onPressed: handleSelectAll, 266 | child: new Text( 267 | selectedBooks.length >= books.length ? '全不选' : '全选', 268 | style: Theme.of(context).primaryTextTheme.button, 269 | ), 270 | padding: EdgeInsets.zero, 271 | ), 272 | ) 273 | ], 274 | centerTitle: true, 275 | ) 276 | : new AppBar( 277 | title: new Text('收藏'), 278 | actions: [ 279 | new IconButton( 280 | icon: new Icon(Icons.search), onPressed: handleSearch), 281 | new PopupMenuButton( 282 | onSelected: handleAction, 283 | itemBuilder: (BuildContext context) => >[ 284 | new PopupMenuItem( 285 | child: new Text('编辑'), value: Actions.edit), 286 | new PopupMenuItem( 287 | child: new Text('导入'), value: Actions.import), 288 | new PopupMenuItem( 289 | child: 290 | new Text(widget.useLightTheme ? '夜间模式' : '白天模式'), 291 | value: Actions.changeTheme), 292 | ], 293 | ) 294 | ], 295 | ); 296 | } 297 | 298 | ///构建选择模式底部组件 299 | Widget buildBottomBar() { 300 | return new Container( 301 | height: 48.0, 302 | child: new Row( 303 | mainAxisAlignment: MainAxisAlignment.center, 304 | crossAxisAlignment: CrossAxisAlignment.stretch, 305 | children: [ 306 | new Expanded( 307 | child: new FlatButton( 308 | padding: const EdgeInsets.symmetric(horizontal: 0.0), 309 | onPressed: handleDelete, 310 | child: new Text('删除')), 311 | ), 312 | ])); 313 | } 314 | 315 | Widget buildBody(BuildContext context) { 316 | return new Stack( 317 | children: [ 318 | new Offstage( 319 | offstage: books.isNotEmpty, 320 | child: new Center( 321 | child: new Text('空空如也!'), 322 | ), 323 | ), 324 | new Offstage( 325 | offstage: books.isEmpty, 326 | child: new Column( 327 | children: [ 328 | new Expanded( 329 | child: new GridView.extent( 330 | maxCrossAxisExtent: 130.0, 331 | mainAxisSpacing: 8.0, 332 | crossAxisSpacing: 24.0, 333 | padding: const EdgeInsets.all(24.0), 334 | childAspectRatio: 0.46, 335 | children: books 336 | .map((Book book) => new BookItem( 337 | book: book, 338 | inSelect: inSelect, 339 | selectedBooks: selectedBooks, 340 | showReadProgress: widget.showReadProgress, 341 | onTap: handleOnTap, 342 | onLongPress: handleInEdit, 343 | )) 344 | .toList(), 345 | ), 346 | ), 347 | new Offstage( 348 | offstage: !inSelect, 349 | child: new Column(children: [ 350 | new Divider(height: 1.0), 351 | buildBottomBar() 352 | ]), 353 | ) 354 | ], 355 | ), 356 | ), 357 | ], 358 | ); 359 | } 360 | 361 | @override 362 | void initState() { 363 | super.initState(); 364 | getBooks(); 365 | } 366 | 367 | @override 368 | Widget build(BuildContext context) { 369 | return new Scaffold(appBar: buildAppBar(context), body: buildBody(context)); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /lib/src/view/web/page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Page extends StatefulWidget { 4 | @override 5 | _PageState createState() => new _PageState(); 6 | } 7 | 8 | class _PageState extends State { 9 | @override 10 | Widget build(BuildContext context) { 11 | return new Scaffold( 12 | body: new Center( 13 | child: new Text('Pages'), 14 | ), 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /lib/src/view/web/select_items.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SelectItem extends StatefulWidget { 4 | @override 5 | _SelectItem createState() => new _SelectItem(); 6 | } 7 | 8 | class _SelectItem extends State { 9 | @override 10 | Widget build(BuildContext context) { 11 | return new Scaffold( 12 | body: new Center( 13 | child: new Text('Select Items'), 14 | ), 15 | ); 16 | } 17 | } -------------------------------------------------------------------------------- /lib/src/widgets/custom_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomButton extends StatelessWidget { 4 | CustomButton( 5 | {Key key, 6 | this.title, 7 | this.shape, 8 | this.onPressed, 9 | this.borderColor, 10 | this.splashColor, 11 | this.color, 12 | this.child, 13 | this.width, 14 | this.iconData, 15 | this.active: false}); 16 | 17 | final String title; 18 | final ShapeBorder shape; 19 | final VoidCallback onPressed; 20 | final Color borderColor; 21 | final Color splashColor; 22 | final Color color; 23 | final Widget child; 24 | final double width; 25 | final IconData iconData; 26 | final bool active; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return new Container( 31 | constraints: new BoxConstraints(maxWidth: width ?? 40.0), 32 | // margin: const EdgeInsets.symmetric(horizontal: 8.0), 33 | // margin: const EdgeInsets.symmetric(horizontal: 0.0), 34 | // margin: const EdgeInsets.all(8.0), 35 | child: new OutlineButton( 36 | padding: const EdgeInsets.all(0.0), 37 | shape: shape ?? const StadiumBorder(), 38 | color: color ?? Colors.transparent, 39 | splashColor: splashColor ?? Colors.transparent, 40 | borderSide: new BorderSide( 41 | color: active ? Colors.orange : borderColor ?? Colors.white30, 42 | width: 2.0), 43 | onPressed: onPressed, 44 | child: iconData != null 45 | ? new Icon( 46 | iconData, 47 | color: Theme.of(context).accentIconTheme.color, 48 | ) 49 | : (child ?? 50 | new Text( 51 | title, 52 | style: Theme.of(context).accentTextTheme.button, 53 | )), 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/widgets/custom_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CustomIndicator extends StatelessWidget { 4 | CustomIndicator({Key key}): super(key: key); 5 | @override 6 | Widget build(BuildContext context) { 7 | return new Container( 8 | color: Colors.white, 9 | child: new Center( 10 | child: const CircularProgressIndicator() 11 | ), 12 | ); 13 | } 14 | } -------------------------------------------------------------------------------- /lib/src/widgets/custom_page_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | // Fractional offset from 1/4 screen below the top to fully on screen. 6 | final Tween _kBottomUpTween = new Tween( 7 | begin: const Offset(0.0, 0.25), 8 | end: Offset.zero, 9 | ); 10 | 11 | // Used for Android and Fuchsia. 12 | class _MountainViewPageTransition extends StatelessWidget { 13 | _MountainViewPageTransition({ 14 | Key key, 15 | @required bool fade, 16 | @required Animation routeAnimation, 17 | @required this.child, 18 | }) : _positionAnimation = _kBottomUpTween.animate(new CurvedAnimation( 19 | parent: routeAnimation, // The route's linear 0.0 - 1.0 animation. 20 | curve: Curves.fastOutSlowIn, 21 | )), 22 | _opacityAnimation = fade ? new CurvedAnimation( 23 | parent: routeAnimation, 24 | curve: Curves.easeIn, // Eyeballed from other Material apps. 25 | ) : const AlwaysStoppedAnimation(1.0), 26 | super(key: key); 27 | 28 | final Animation _positionAnimation; 29 | final Animation _opacityAnimation; 30 | final Widget child; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | // TODO(ianh): tell the transform to be un-transformed for hit testing 35 | return new FadeTransition( 36 | opacity: _opacityAnimation, 37 | child: child, 38 | ); 39 | } 40 | } 41 | 42 | class CustomPageRoute extends PageRoute { 43 | CustomPageRoute({ 44 | @required this.builder, 45 | RouteSettings settings: const RouteSettings(), 46 | this.maintainState: true, 47 | bool fullscreenDialog: false, 48 | }) : assert(builder != null), 49 | super(settings: settings, fullscreenDialog: fullscreenDialog) { 50 | // ignore: prefer_asserts_in_initializer_lists , https://github.com/dart-lang/sdk/issues/31223 51 | assert(opaque); 52 | } 53 | 54 | /// Turns on the fading of routes during page transitions. 55 | /// 56 | /// This is currently disabled by default because of performance issues on 57 | /// low-end phones. Eventually these issues will be resolved and this flag 58 | /// will be removed. 59 | @Deprecated('This flag will eventually be removed once the performance issues are resolved. See: https://github.com/flutter/flutter/issues/13736') 60 | static bool debugEnableFadingRoutes = false; 61 | 62 | /// Builds the primary contents of the route. 63 | final WidgetBuilder builder; 64 | 65 | @override 66 | final bool maintainState; 67 | 68 | /// A delegate PageRoute to which iOS themed page operations are delegated to. 69 | /// It's lazily created on first use. 70 | CupertinoPageRoute get _cupertinoPageRoute { 71 | assert(_useCupertinoTransitions); 72 | _internalCupertinoPageRoute ??= new CupertinoPageRoute( 73 | builder: builder, // Not used. 74 | fullscreenDialog: fullscreenDialog, 75 | hostRoute: this, 76 | ); 77 | return _internalCupertinoPageRoute; 78 | } 79 | CupertinoPageRoute _internalCupertinoPageRoute; 80 | 81 | /// Whether we should currently be using Cupertino transitions. This is true 82 | /// if the theme says we're on iOS, or if we're in an active gesture. 83 | bool get _useCupertinoTransitions { 84 | return _internalCupertinoPageRoute?.popGestureInProgress == true 85 | || Theme.of(navigator.context).platform == TargetPlatform.iOS; 86 | } 87 | 88 | @override 89 | Duration get transitionDuration => const Duration(milliseconds: 300); 90 | 91 | @override 92 | Color get barrierColor => null; 93 | 94 | @override 95 | String get barrierLabel => null; 96 | 97 | @override 98 | bool canTransitionFrom(TransitionRoute previousRoute) { 99 | return previousRoute is MaterialPageRoute || previousRoute is CupertinoPageRoute; 100 | } 101 | 102 | @override 103 | bool canTransitionTo(TransitionRoute nextRoute) { 104 | // Don't perform outgoing animation if the next route is a fullscreen dialog. 105 | return (nextRoute is MaterialPageRoute && !nextRoute.fullscreenDialog) 106 | || (nextRoute is CupertinoPageRoute && !nextRoute.fullscreenDialog); 107 | } 108 | 109 | @override 110 | void dispose() { 111 | _internalCupertinoPageRoute?.dispose(); 112 | super.dispose(); 113 | } 114 | 115 | @override 116 | Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { 117 | final Widget result = builder(context); 118 | assert(() { 119 | if (result == null) { 120 | throw new FlutterError( 121 | 'The builder for route "${settings.name}" returned null.\n' 122 | 'Route builders must never return null.' 123 | ); 124 | } 125 | return true; 126 | }()); 127 | return result; 128 | } 129 | 130 | @override 131 | Widget buildTransitions(BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { 132 | if (_useCupertinoTransitions) { 133 | return _cupertinoPageRoute.buildTransitions(context, animation, secondaryAnimation, child); 134 | } else { 135 | return new _MountainViewPageTransition( 136 | routeAnimation: animation, 137 | child: child, 138 | fade: debugEnableFadingRoutes, // ignore: deprecated_member_use 139 | ); 140 | } 141 | } 142 | 143 | @override 144 | String get debugLabel => '${super.debugLabel}(${settings.name})'; 145 | } -------------------------------------------------------------------------------- /lib/src/widgets/dialog_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | 4 | class DialogItem extends StatelessWidget { 5 | const DialogItem({ Key key, this.icon, this.color, this.text, this.onPressed }) : super(key: key); 6 | 7 | final IconData icon; 8 | final Color color; 9 | final String text; 10 | final VoidCallback onPressed; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return new SimpleDialogOption( 15 | onPressed: onPressed, 16 | child: new Row( 17 | mainAxisAlignment: MainAxisAlignment.start, 18 | crossAxisAlignment: CrossAxisAlignment.center, 19 | children: [ 20 | new Icon(icon, size: 36.0, color: color), 21 | new Padding( 22 | padding: const EdgeInsets.only(left: 16.0), 23 | child: new Text(text), 24 | ), 25 | ], 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/widgets/image_view.dart: -------------------------------------------------------------------------------- 1 | //import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class ImageView extends StatefulWidget { 5 | ImageView({this.image}); 6 | 7 | final Image image; 8 | 9 | @override 10 | ImageViewState createState() => new ImageViewState(); 11 | } 12 | 13 | class ImageViewState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | return new Container( 17 | child: widget.image, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/widgets/item_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ItemButton extends StatelessWidget { 4 | ItemButton({ 5 | Key key, 6 | this.onTap, 7 | this.icon, 8 | this.title, 9 | this.iconSize, 10 | this.width 11 | }); 12 | 13 | final VoidCallback onTap; 14 | final Icon icon; 15 | final Widget title; 16 | final double iconSize; 17 | final double width; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return new IconTheme( 22 | data: Theme.of(context).iconTheme.copyWith(size: iconSize ?? 24.0), 23 | child: new InkResponse( 24 | onTap: onTap, 25 | child: new Container( 26 | width: width ?? (iconSize ?? 24.0) * 3, 27 | child: new Column( 28 | crossAxisAlignment: CrossAxisAlignment.center, 29 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 30 | mainAxisSize: MainAxisSize.min, 31 | children: [ 32 | new Align( 33 | alignment: Alignment.topCenter, 34 | heightFactor: 1.0, 35 | child: icon, 36 | ), 37 | new Align( 38 | alignment: Alignment.bottomCenter, 39 | heightFactor: 1.0, 40 | child: title, 41 | ), 42 | ], 43 | ), 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /lib/src/widgets/label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Label extends StatelessWidget { 4 | Label({Key key, this.title, this.style, this.padding}); 5 | 6 | final String title; 7 | final TextStyle style; 8 | final EdgeInsets padding; 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return new Container( 13 | padding: padding ?? 14 | const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0), 15 | child: title != null 16 | ? new Text( 17 | title, 18 | style: style ?? 19 | Theme 20 | .of(context) 21 | .textTheme 22 | .body1 23 | .copyWith(color: Colors.white70, fontSize: 16.0), 24 | ) 25 | : null); 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /lib/src/widgets/select_bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:light/src/model/selected_list_model.dart'; 4 | 5 | class SelectBottomBar extends StatelessWidget { 6 | SelectBottomBar( 7 | {Key key, 8 | @required this.selectedList, 9 | @required this.list, 10 | this.handleSelectAll, 11 | this.handleCancel, 12 | this.handleEnter, 13 | this.buttonText}); 14 | 15 | final VoidCallback handleSelectAll; 16 | final VoidCallback handleCancel; 17 | final VoidCallback handleEnter; 18 | final SelectedListModel selectedList; 19 | final List list; 20 | final String buttonText; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | Color getButtonColor() { 25 | if (selectedList.isEmpty) 26 | return Theme.of(context).disabledColor; 27 | else 28 | return Theme.of(context).accentColor; 29 | } 30 | 31 | TextStyle getButtonTextStyle() { 32 | if (selectedList.isEmpty) { 33 | return Theme.of(context).textTheme.button; 34 | } else { 35 | return Theme.of(context).primaryTextTheme.button; 36 | } 37 | } 38 | 39 | return new Container( 40 | height: 48.0, 41 | child: new Row( 42 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 43 | crossAxisAlignment: CrossAxisAlignment.stretch, 44 | children: [ 45 | new Row( 46 | children: [ 47 | new Container( 48 | padding: const EdgeInsets.only(left: 16.0), 49 | child: new Text('已选${list.length}项')), 50 | new FlatButton( 51 | padding: const EdgeInsets.symmetric(horizontal: 0.0), 52 | onPressed: handleSelectAll, 53 | child: new Text( 54 | selectedList.length >= list.length ? '全不选' : '全选', 55 | )), 56 | new FlatButton( 57 | padding: const EdgeInsets.symmetric(horizontal: 0.0), 58 | onPressed: handleCancel, 59 | child: new Text('取消')), 60 | ], 61 | ), 62 | new FlatButton( 63 | color: getButtonColor(), 64 | onPressed: handleEnter, 65 | child: new Text( 66 | buttonText, 67 | style: getButtonTextStyle(), 68 | )) 69 | ], 70 | ), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /light.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /light_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: light 2 | description: Light novel reader. 3 | 4 | dependencies: 5 | flutter: 6 | sdk: flutter 7 | 8 | # The following adds the Cupertino Icons font to your application. 9 | # Use with the CupertinoIcons class for iOS style icons. 10 | cupertino_icons: ^0.1.0 11 | path_provider: any 12 | file: ^2.3.7 13 | html: any 14 | flutter_webview_plugin: ^0.1.3 15 | url_launcher: ^3.0.0 16 | sqflite: ^0.8.4 17 | shared_preferences: ^0.4.0 18 | options_file: any 19 | crypto: "^2.0.2+1" 20 | epub: 21 | git: https://github.com/creatint/dart-epub-cn 22 | html2md: ^0.1.7 23 | # path: d:/tmp/html2md/ 24 | # mimic: 25 | # path: d:/tmp/mimic/ 26 | 27 | dev_dependencies: 28 | flutter_test: 29 | sdk: flutter 30 | 31 | 32 | # For information on the generic Dart part of this file, see the 33 | # following page: https://www.dartlang.org/tools/pub/pubspec 34 | 35 | # The following section is specific to Flutter. 36 | flutter: 37 | 38 | # The following line ensures that the Material Icons font is 39 | # included with your application, so that you can use the icons in 40 | # the material Icons class. 41 | uses-material-design: true 42 | 43 | # To add assets to your application, add an assets section, like this: 44 | # assets: 45 | # - images/a_dot_burr.jpeg 46 | # - images/a_dot_ham.jpeg 47 | assets: 48 | - assets/light.db 49 | - assets/config.ini 50 | - assets/default_cover.jpg 51 | - assets/background/bg1.png 52 | - assets/background/bg2.png 53 | - assets/background/bg3.png 54 | - assets/background/bg4.png 55 | - assets/background/bg5.png 56 | - assets/background/bg6.png 57 | - assets/background/bg7.jpg 58 | - assets/background/bg8.png 59 | - assets/background/bg9.png 60 | - assets/background/bg10.png 61 | - assets/background/bg11.png 62 | 63 | fonts: 64 | - family: Lora 65 | fonts: 66 | - asset: assets/font/Lora-Regular.ttf 67 | - family: HYQH 68 | fonts: 69 | - asset: assets/font/HYQH.ttf 70 | - family: FZYouH 71 | fonts: 72 | - asset: assets/font/FZYouH.ttf 73 | - family: Avenir 74 | fonts: 75 | - asset: assets/font/Avenir.ttf 76 | - family: Noto 77 | fonts: 78 | - asset: assets/font/Noto Serif.ttf 79 | # An image asset can refer to one or more resolution-specific "variants", see 80 | # https://flutter.io/assets-and-images/#resolution-aware. 81 | 82 | # For details regarding adding assets from package dependencies, see 83 | # https://flutter.io/assets-and-images/#from-packages 84 | 85 | # To add custom fonts to your application, add a fonts section here, 86 | # in this "flutter" section. Each entry in this list should have a 87 | # "family" key with the font family name, and a "fonts" key with a 88 | # list giving the asset and other descriptors for the font. For 89 | # example: 90 | # fonts: 91 | # - family: Schyler 92 | # fonts: 93 | # - asset: fonts/Schyler-Regular.ttf 94 | # - asset: fonts/Schyler-Italic.ttf 95 | # style: italic 96 | # - family: Trajan Pro 97 | # fonts: 98 | # - asset: fonts/TrajanPro.ttf 99 | # - asset: fonts/TrajanPro_Bold.ttf 100 | # weight: 700 101 | # 102 | # For details regarding fonts from package dependencies, 103 | # see https://flutter.io/custom-fonts/#from-packages 104 | --------------------------------------------------------------------------------