Merge pull request #19 from gedoor/master

up
pull/379/head
口口吕 5 years ago committed by GitHub
commit b6dd225818
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/build.gradle
  2. 29
      app/google-services.json
  3. 69
      app/src/debug/google-services.json
  4. 38
      app/src/main/AndroidManifest.xml
  5. 126
      app/src/main/assets/txtTocRule.json
  6. 22
      app/src/main/assets/updateLog.md
  7. 9
      app/src/main/java/io/legado/app/App.kt
  8. 9
      app/src/main/java/io/legado/app/base/BaseDialogFragment.kt
  9. 2
      app/src/main/java/io/legado/app/constant/AppConst.kt
  10. 3
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  11. 1
      app/src/main/java/io/legado/app/data/entities/BaseBook.kt
  12. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  13. 12
      app/src/main/java/io/legado/app/data/entities/SearchBook.kt
  14. 6
      app/src/main/java/io/legado/app/help/AppConfig.kt
  15. 7
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  16. 14
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  17. 16
      app/src/main/java/io/legado/app/help/storage/OldRule.kt
  18. 4
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  19. 77
      app/src/main/java/io/legado/app/model/WebBook.kt
  20. 22
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  21. 45
      app/src/main/java/io/legado/app/model/webBook/BookContent.kt
  22. 28
      app/src/main/java/io/legado/app/service/CheckSourceService.kt
  23. 17
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  24. 8
      app/src/main/java/io/legado/app/ui/about/AboutActivity.kt
  25. 10
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  26. 4
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  27. 32
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  28. 33
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  29. 205
      app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt
  30. 39
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  31. 20
      app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt
  32. 10
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  33. 15
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  34. 50
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt
  35. 8
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  36. 83
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt
  37. 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt
  38. 19
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  39. 24
      app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt
  40. 8
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  41. 33
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  42. 10
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  43. 2
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt
  44. 56
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  45. 46
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt
  46. 12
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  47. 9
      app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt
  48. 17
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt
  49. 23
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  50. 64
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt
  51. 12
      app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt
  52. 19
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt
  53. 4
      app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt
  54. 111
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  55. 40
      app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt
  56. 10
      app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt
  57. 7
      app/src/main/java/io/legado/app/utils/BitmapUtils.kt
  58. 20
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  59. 12
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  60. 24
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  61. 105
      app/src/main/res/layout/dialog_bookshelf_config.xml
  62. 3
      app/src/main/res/layout/dialog_number_picker.xml
  63. 5
      app/src/main/res/layout/dialog_read_book_style.xml
  64. 27
      app/src/main/res/layout/popup_action_menu.xml
  65. 3
      app/src/main/res/menu/content_select_action.xml
  66. 25
      app/src/main/res/values/arrays.xml
  67. 22
      app/src/main/res/values/strings.xml
  68. 6
      app/src/main/res/xml/pref_config_read.xml

@ -54,6 +54,7 @@ android {
if (project.hasProperty("RELEASE_STORE_FILE")) { if (project.hasProperty("RELEASE_STORE_FILE")) {
signingConfig signingConfigs.myConfig signingConfig signingConfigs.myConfig
} }
applicationIdSuffix '.release'
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
@ -123,7 +124,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
//room //room
def room_version = '2.2.3' def room_version = '2.2.4'
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
@ -139,7 +140,7 @@ dependencies {
implementation 'com.jeremyliao:live-event-bus-x:1.4.5' implementation 'com.jeremyliao:live-event-bus-x:1.4.5'
// //
def coroutines_version = '1.2.2' def coroutines_version = '1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
@ -175,7 +176,7 @@ dependencies {
//MarkDown //MarkDown
implementation 'ru.noties.markwon:core:3.0.2' implementation 'ru.noties.markwon:core:3.0.2'
// //
implementation 'com.github.houbb:opencc4j:1.4.0' implementation 'com.github.houbb:opencc4j:1.4.0'
} }

@ -63,6 +63,35 @@
] ]
} }
} }
},
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:c1481c1c3d3f51eff624a7",
"android_client_info": {
"package_name": "io.legado.app.release"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
} }
], ],
"configuration_version": "1" "configuration_version": "1"

@ -1,69 +0,0 @@
{
"project_info": {
"project_number": "453392274790",
"firebase_url": "https://legado-fca69.firebaseio.com",
"project_id": "legado-fca69",
"storage_bucket": "legado-fca69.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:1d2b1eefbe0e78cff624a7",
"android_client_info": {
"package_name": "io.legado.app"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:453392274790:android:c4eac14b1410eec5f624a7",
"android_client_info": {
"package_name": "io.legado.app.debug"
}
},
"oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyD90mfNLhA7cAzzI9SonpSz5mrF5BnmyJA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "453392274790-hnbpatpce9hbjiggj76hgo7queu86atq.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

@ -123,9 +123,6 @@
<activity <activity
android:name=".ui.config.ConfigActivity" android:name=".ui.config.ConfigActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTask" />
<activity <activity
android:name="io.legado.app.ui.book.search.SearchActivity" android:name="io.legado.app.ui.book.search.SearchActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
@ -135,24 +132,51 @@
<activity <activity
android:name=".ui.about.DonateActivity" android:name=".ui.about.DonateActivity"
android:launchMode="singleTask" /> android:launchMode="singleTask" />
<activity android:name=".ui.book.arrange.ArrangeBookActivity" /> <!--书源管理-->
<activity android:name=".ui.book.source.debug.BookSourceDebugActivity" />
<activity android:name=".ui.book.source.manage.BookSourceActivity"> <activity android:name=".ui.book.source.manage.BookSourceActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data <data
android:host="booksource" android:host="booksource"
android:scheme="yuedu" /> android:scheme="yuedu" />
</intent-filter> </intent-filter>
</activity> </activity>
<!--订阅源管理-->
<activity android:name=".ui.rss.source.manage.RssSourceActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="rsssource"
android:scheme="yuedu" />
</intent-filter>
</activity>
<!--替换规则界面-->
<activity
android:name=".ui.replacerule.ReplaceRuleActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="replace"
android:scheme="yuedu" />
</intent-filter>
</activity>
<activity android:name=".ui.book.arrange.ArrangeBookActivity" />
<activity android:name=".ui.book.source.debug.BookSourceDebugActivity" />
<activity android:name=".ui.chapterlist.ChapterListActivity" /> <activity android:name=".ui.chapterlist.ChapterListActivity" />
<activity android:name=".ui.rss.read.ReadRssActivity" /> <activity android:name=".ui.rss.read.ReadRssActivity" />
<activity android:name=".ui.importbook.ImportBookActivity" /> <activity android:name=".ui.importbook.ImportBookActivity" />
<activity android:name=".ui.explore.ExploreShowActivity" /> <activity android:name=".ui.explore.ExploreShowActivity" />
<activity android:name=".ui.rss.source.manage.RssSourceActivity" />
<activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" /> <activity android:name=".ui.rss.source.debug.RssSourceDebugActivity" />
<activity android:name=".ui.rss.article.RssArticlesActivity" /> <activity android:name=".ui.rss.article.RssArticlesActivity" />
<activity android:name=".ui.rss.favorites.RssFavoritesActivity" /> <activity android:name=".ui.rss.favorites.RssFavoritesActivity" />

@ -1,62 +1,68 @@
[ [
{ {
"enable": true, "enable": true,
"name": "数字 分隔符 标题名称", "name": "目录",
"rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", "rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 0 "serialNumber": 0
}, },
{ {
"enable": true, "enable": false,
"name": "目录", "name": "目录(不匹配行前空白)",
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "rule": "^(?<=\\s)(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 1 "serialNumber": 1
}, },
{ {
"enable": false, "enable": false,
"name": "目录(不匹配行前空白)", "name": "目录(去简介)",
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$",
"serialNumber": 2 "serialNumber": 2
}, },
{ {
"enable": false, "enable": false,
"name": "目录(去简介)", "name": "目录(古典、轻小说备用)",
"rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", "rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$",
"serialNumber": 3 "serialNumber": 3
}, },
{ {
"enable": false, "enable": true,
"name": "目录(古典小说备用)", "name": "数字 分隔符 标题名称",
"rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$", "rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$",
"serialNumber": 4 "serialNumber": 4
}, },
{ {
"enable": true, "enable": true,
"name": "Chapter/Section/Part 序号 标题", "name": "正文 标题/序号",
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$", "rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$",
"serialNumber": 5 "serialNumber": 5
}, },
{ {
"enable": true, "enable": true,
"name": "正文 标题/序号", "name": "Chapter/Section/Part 序号 标题",
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$", "rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$",
"serialNumber": 6 "serialNumber": 6
}, },
{ {
"enable": true, "enable": true,
"name": "特殊符号 序号 标题", "name": "特殊符号 序号 标题",
"rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$", "rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$",
"serialNumber": 7 "serialNumber": 7
}, },
{ {
"enable": true, "enable": true,
"name": "特殊符号 标题", "name": "特殊符号 标题",
"rule": "^[ \\t]{0,4}[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", "rule": "^[ \\t]{0,4}[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$",
"serialNumber": 8 "serialNumber": 8
}, },
{ {
"enable":false, "enable":false,
"name": "特殊符号 标题(不匹配空白字符)", "name": "特殊符号 标题(不匹配空白字符)",
"rule": "(?<=\\s)[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", "rule": "^(?<=\\s)[\\[〈「『〖〔《(【\\(☆★].{1,30}[\\)】)》〕〗』」〉\\]]?\\s{0,4}$",
"serialNumber": 9 "serialNumber": 9
} },
{
"enable":false,
"name": "顶格标题",
"rule": "^\\S.{1,20}$",
"serialNumber": 10
}
] ]

@ -2,6 +2,28 @@
* 旧版数据导入教程: * 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/02/29**
* 添加书源一键导入
* 修复主题模式跟随系统
* 修复书源校验
* 添加书架排序
* 添加点击翻页开关
* 修复共用布局没有记住配置的bug
**2020/02/28**
* 解决阅读界面部分字体超出范围的问题
* 修复背景切换有时空白的bug
* 修复滚动翻页问题
**2020/02/27**
* 修复bug,边距调节,换源等一些bug,记不清了
* 修复默认字体问题
* 改了下包名,好上架应用市场
**2020/02/26**
* 修复仿真翻页
* 功能添加: 选择默认字体时, 可选择字体默认字体(非衬线), 系统衬线字体, 系统等宽字体by hingbong
**2020/02/25** **2020/02/25**
* 优化文本选择和滚动,感觉很完美了 * 优化文本选择和滚动,感觉很完美了

@ -5,6 +5,7 @@ import android.app.Application
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -62,6 +63,13 @@ class App : Application() {
registerActivityLife() registerActivityLife()
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES, Configuration.UI_MODE_NIGHT_NO -> applyDayNight()
}
}
/** /**
* 更新主题 * 更新主题
*/ */
@ -91,7 +99,6 @@ class App : Application() {
) )
.apply() .apply()
} }
// ChapterProvider.upReadAloudSpan()
} }
fun applyDayNight() { fun applyDayNight() {

@ -2,6 +2,7 @@ package io.legado.app.base
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -21,4 +22,12 @@ abstract class BaseDialogFragment : DialogFragment(), CoroutineScope {
super.onDestroy() super.onDestroy()
job.cancel() job.cancel()
} }
fun <T> execute(
scope: CoroutineScope = this,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context) { block() }
}
} }

@ -22,7 +22,7 @@ object AppConst {
const val UA_NAME = "User-Agent" const val UA_NAME = "User-Agent"
val userAgent: String by lazy { val userAgent: String by lazy {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"
} }
val SCRIPT_ENGINE: ScriptEngine by lazy { val SCRIPT_ENGINE: ScriptEngine by lazy {

@ -5,6 +5,7 @@ object PreferKey {
const val themeMode = "themeMode" const val themeMode = "themeMode"
const val downloadPath = "downloadPath" const val downloadPath = "downloadPath"
const val hideStatusBar = "hideStatusBar" const val hideStatusBar = "hideStatusBar"
const val clickTurnPage = "clickTurnPage"
const val clickAllNext = "clickAllNext" const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar" const val hideNavigationBar = "hideNavigationBar"
const val precisionSearch = "precisionSearch" const val precisionSearch = "precisionSearch"
@ -16,6 +17,7 @@ object PreferKey {
const val nextKey = "nextKeyCode" const val nextKey = "nextKeyCode"
const val showRss = "showRss" const val showRss = "showRss"
const val bookshelfLayout = "bookshelfLayout" const val bookshelfLayout = "bookshelfLayout"
const val bookshelfSort = "bookshelfSort"
const val recordLog = "recordLog" const val recordLog = "recordLog"
const val processText = "process_text" const val processText = "process_text"
const val cleanCache = "cleanCache" const val cleanCache = "cleanCache"
@ -38,4 +40,5 @@ object PreferKey {
const val bodyIndent = "textIndent" const val bodyIndent = "textIndent"
const val shareLayout = "shareLayout" const val shareLayout = "shareLayout"
const val readStyleSelect = "readStyleSelect" const val readStyleSelect = "readStyleSelect"
const val systemTypefaces = "system_typefaces"
} }

@ -3,6 +3,7 @@ package io.legado.app.data.entities
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
interface BaseBook { interface BaseBook {
var bookUrl: String
var variableMap: HashMap<String, String>? var variableMap: HashMap<String, String>?
var kind: String? var kind: String?
var wordCount: String? var wordCount: String?

@ -17,7 +17,7 @@ import kotlin.math.max
@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))]) @Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))])
data class Book( data class Book(
@PrimaryKey @PrimaryKey
var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
var tocUrl: String = "", // 目录页Url (toc=table of Contents) var tocUrl: String = "", // 目录页Url (toc=table of Contents)
var origin: String = BookType.local, // 书源URL(默认BookType.local) var origin: String = BookType.local, // 书源URL(默认BookType.local)
var originName: String = "", //书源名称 or 本地书籍文件名 var originName: String = "", //书源名称 or 本地书籍文件名

@ -21,7 +21,7 @@ import kotlinx.android.parcel.Parcelize
) )
data class SearchBook( data class SearchBook(
@PrimaryKey @PrimaryKey
var bookUrl: String = "", override var bookUrl: String = "",
var origin: String = "", // 书源规则 var origin: String = "", // 书源规则
var originName: String = "", var originName: String = "",
var type: Int = 0, // @BookType var type: Int = 0, // @BookType
@ -78,16 +78,12 @@ data class SearchBook(
variable = GSON.toJson(variableMap) variable = GSON.toJson(variableMap)
} }
@Ignore @delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel
var origins: LinkedHashSet<String>? = null val origins: LinkedHashSet<String> by lazy { linkedSetOf(origin) }
private set
fun addOrigin(origin: String) { fun addOrigin(origin: String) {
if (origins == null) { origins.add(origin)
origins = linkedSetOf(this.origin)
}
origins?.add(origin)
} }
fun getDisplayLastChapterTitle(): String { fun getDisplayLastChapterTitle(): String {

@ -86,4 +86,10 @@ object AppConfig {
set(value) { set(value) {
App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value) App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value)
} }
var systemTypefaces: Int
get() = App.INSTANCE.getPrefInt(PreferKey.systemTypefaces)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.systemTypefaces, value)
}
} }

@ -34,7 +34,7 @@ object ReadBookConfig {
var shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout) var shareLayout = App.INSTANCE.getPrefBoolean(PreferKey.shareLayout)
set(value) { set(value) {
field = value field = value
App.INSTANCE.putPrefBoolean(PreferKey.shareLayout) App.INSTANCE.putPrefBoolean(PreferKey.shareLayout, value)
} }
var pageAnim = App.INSTANCE.getPrefInt(PreferKey.pageAnim) var pageAnim = App.INSTANCE.getPrefInt(PreferKey.pageAnim)
set(value) { set(value) {
@ -43,6 +43,7 @@ object ReadBookConfig {
App.INSTANCE.putPrefInt(PreferKey.pageAnim, value) App.INSTANCE.putPrefInt(PreferKey.pageAnim, value)
} }
var isScroll = pageAnim == 3 var isScroll = pageAnim == 3
val clickTurnPage get() = App.INSTANCE.getPrefBoolean(PreferKey.clickTurnPage, true)
var bg: Drawable? = null var bg: Drawable? = null
init { init {
@ -215,7 +216,7 @@ object ReadBookConfig {
class Config( class Config(
private var bgStr: String = "#EEEEEE",//白天背景 private var bgStr: String = "#EEEEEE",//白天背景
private var bgStrNight: String = "#000000",//夜间背景 private var bgStrNight: String = "#000000",//夜间背景
private var bgType: Int = 0,//白天背景类型 private var bgType: Int = 0,//白天背景类型 0:颜色, 1:assets图片, 2其它图片
private var bgTypeNight: Int = 0,//夜间背景类型 private var bgTypeNight: Int = 0,//夜间背景类型
private var darkStatusIcon: Boolean = true,//白天是否暗色状态栏 private var darkStatusIcon: Boolean = true,//白天是否暗色状态栏
private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 private var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏
@ -299,7 +300,7 @@ object ReadBookConfig {
1 -> { 1 -> {
BitmapDrawable( BitmapDrawable(
resources, resources,
BitmapUtils.decodeBitmap( BitmapUtils.decodeAssetsBitmap(
App.INSTANCE, App.INSTANCE,
"bg" + File.separator + bgStr(), "bg" + File.separator + bgStr(),
width, width,

@ -100,17 +100,9 @@ object Backup {
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
val file = File(backupPath + File.separator + fileName) val file = File(backupPath + File.separator + fileName)
if (file.exists()) { if (file.exists()) {
var doc = treeDoc.findFile(fileName) treeDoc.findFile(fileName)?.delete()
if (null != doc && doc.exists()) { treeDoc.createFile("", fileName)?.let {
doc.delete() DocumentUtils.writeBytes(context, file.readBytes(), it.uri)
}
doc = treeDoc.createFile("", fileName)
doc?.let {
DocumentUtils.writeText(
context,
file.readText(),
it.uri
)
} }
} }
} }

@ -100,15 +100,22 @@ object OldRule {
if (oldRule.isNullOrBlank()) return null if (oldRule.isNullOrBlank()) return null
var newRule = oldRule var newRule = oldRule
var reverse = false var reverse = false
var allinone = false
if (oldRule.startsWith("-")) { if (oldRule.startsWith("-")) {
reverse = true reverse = true
newRule = oldRule.substring(1) newRule = oldRule.substring(1)
} }
if (newRule.startsWith("+")) {
allinone = true
newRule = newRule.substring(1)
}
if (!newRule.startsWith("@CSS:", true) && if (!newRule.startsWith("@CSS:", true) &&
!newRule.startsWith("@XPath:", true) && !newRule.startsWith("@XPath:", true) &&
!newRule.startsWith("//") && !newRule.startsWith("//") &&
!newRule.startsWith("##") && !newRule.startsWith("##") &&
!newRule.startsWith(":") !newRule.startsWith(":") &&
!newRule.contains("@js:",true) &&
!newRule.contains("<js>",true)
) { ) {
if (newRule.contains("#") && !newRule.contains("##")) { if (newRule.contains("#") && !newRule.contains("##")) {
newRule = oldRule.replace("#", "##") newRule = oldRule.replace("#", "##")
@ -134,8 +141,11 @@ object OldRule {
newRule = newRule.replace("&", "&&") newRule = newRule.replace("&", "&&")
} }
} }
if (allinone) {
newRule = "+" + newRule
}
if (reverse) { if (reverse) {
newRule += "-" newRule = "-" + newRule
} }
return newRule return newRule
} }
@ -145,7 +155,7 @@ object OldRule {
if (!oldUrls.contains("\n") && !oldUrls.contains("&&")) if (!oldUrls.contains("\n") && !oldUrls.contains("&&"))
return toNewUrl(oldUrls) return toNewUrl(oldUrls)
val urls = oldUrls.split("(&&|\n)+".toRegex()) val urls = oldUrls.split("(&&|\r?\n)+".toRegex())
return urls.map { return urls.map {
toNewUrl(it)?.replace("\n\\s*".toRegex(), "") toNewUrl(it)?.replace("\n\\s*".toRegex(), "")
}.joinToString("\n") }.joinToString("\n")

@ -12,6 +12,8 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.LauncherIconHelp import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -113,6 +115,8 @@ object Restore {
hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar)
hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar)
} }
ChapterProvider.upStyle()
ReadBook.loadContent()
} }
LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon)) LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon))
} }

@ -86,17 +86,18 @@ class WebBook(val bookSource: BookSource) {
): Coroutine<Book> { ): Coroutine<Book> {
book.type = bookSource.bookSourceType book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = if (!book.infoHtml.isNullOrEmpty()) { val body =
book.infoHtml if (!book.infoHtml.isNullOrEmpty()) {
} else { book.infoHtml
val analyzeUrl = AnalyzeUrl( } else {
book = book, val analyzeUrl = AnalyzeUrl(
ruleUrl = book.bookUrl, book = book,
baseUrl = sourceUrl, ruleUrl = book.bookUrl,
headerMapF = bookSource.getHeaderMap() baseUrl = sourceUrl,
) headerMapF = bookSource.getHeaderMap()
analyzeUrl.getResponseAwait().body )
} analyzeUrl.getResponseAwait().body
}
BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl) BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl)
book book
} }
@ -112,16 +113,17 @@ class WebBook(val bookSource: BookSource) {
): Coroutine<List<BookChapter>> { ): Coroutine<List<BookChapter>> {
book.type = bookSource.bookSourceType book.type = bookSource.bookSourceType
return Coroutine.async(scope, context) { return Coroutine.async(scope, context) {
val body = if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) { val body =
book.tocHtml if (book.bookUrl == book.tocUrl && !book.tocHtml.isNullOrEmpty()) {
} else { book.tocHtml
AnalyzeUrl( } else {
book = book, AnalyzeUrl(
ruleUrl = book.tocUrl, book = book,
baseUrl = book.bookUrl, ruleUrl = book.tocUrl,
headerMapF = bookSource.getHeaderMap() baseUrl = book.bookUrl,
).getResponseAwait().body headerMapF = bookSource.getHeaderMap()
} ).getResponseAwait().body
}
BookChapterList.analyzeChapterList(this, book, body, bookSource, book.tocUrl) BookChapterList.analyzeChapterList(this, book, body, bookSource, book.tocUrl)
} }
} }
@ -141,22 +143,23 @@ class WebBook(val bookSource: BookSource) {
Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}") Debug.log(sourceUrl, "⇒正文规则为空,使用章节链接:${bookChapter.url}")
return@async bookChapter.url return@async bookChapter.url
} }
val body = if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) { val body =
book.tocHtml if (bookChapter.url == book.bookUrl && !book.tocHtml.isNullOrEmpty()) {
} else { book.tocHtml
val analyzeUrl = } else {
AnalyzeUrl( val analyzeUrl =
book = book, AnalyzeUrl(
ruleUrl = bookChapter.url, book = book,
baseUrl = book.tocUrl, ruleUrl = bookChapter.url,
headerMapF = bookSource.getHeaderMap() baseUrl = book.tocUrl,
) headerMapF = bookSource.getHeaderMap()
analyzeUrl.getResponseAwait( )
bookSource.bookSourceUrl, analyzeUrl.getResponseAwait(
jsStr = bookSource.getContentRule().webJs, bookSource.bookSourceUrl,
sourceRegex = bookSource.getContentRule().sourceRegex jsStr = bookSource.getContentRule().webJs,
).body sourceRegex = bookSource.getContentRule().sourceRegex
} ).body
}
BookContent.analyzeContent( BookContent.analyzeContent(
this, this,
body, body,

@ -31,6 +31,7 @@ object BookChapterList {
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val analyzeRule = AnalyzeRule(book)
val tocRule = bookSource.getTocRule() val tocRule = bookSource.getTocRule()
val nextUrlList = arrayListOf(baseUrl) val nextUrlList = arrayListOf(baseUrl)
var reverse = false var reverse = false
@ -43,7 +44,10 @@ object BookChapterList {
listRule = listRule.substring(1) listRule = listRule.substring(1)
} }
var chapterData = var chapterData =
analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true) analyzeChapterList(
analyzeRule.setContent(body, baseUrl),
book.bookUrl, baseUrl, tocRule, listRule, bookSource, log = true
)
chapterData.chapterList?.let { chapterData.chapterList?.let {
chapterList.addAll(it) chapterList.addAll(it)
} }
@ -63,8 +67,8 @@ object BookChapterList {
).getResponseAwait() ).getResponseAwait()
.body?.let { nextBody -> .body?.let { nextBody ->
chapterData = analyzeChapterList( chapterData = analyzeChapterList(
nextBody, nextUrl, tocRule, listRule, analyzeRule.setContent(nextBody, nextUrl),
book, bookSource, log = false book.bookUrl, nextUrl, tocRule, listRule, bookSource
) )
nextUrl = if (chapterData.nextUrl.isNotEmpty()) { nextUrl = if (chapterData.nextUrl.isNotEmpty()) {
chapterData.nextUrl[0] chapterData.nextUrl[0]
@ -98,7 +102,9 @@ object BookChapterList {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getResponseAwait().body ).getResponseAwait().body
val nextChapterData = analyzeChapterList( val nextChapterData = analyzeChapterList(
nextBody, item.nextUrl, tocRule, listRule, book, bookSource analyzeRule.setContent(nextBody, item.nextUrl),
book.bookUrl, item.nextUrl, tocRule, listRule, bookSource,
false
) )
synchronized(chapterDataList) { synchronized(chapterDataList) {
val isFinished = addChapterListIsFinish( val isFinished = addChapterListIsFinish(
@ -167,19 +173,17 @@ object BookChapterList {
} }
private fun analyzeChapterList( private fun analyzeChapterList(
body: String?, analyzeRule: AnalyzeRule,
bookUrl: String,
baseUrl: String, baseUrl: String,
tocRule: TocRule, tocRule: TocRule,
listRule: String, listRule: String,
book: Book,
bookSource: BookSource, bookSource: BookSource,
getNextUrl: Boolean = true, getNextUrl: Boolean = true,
log: Boolean = false log: Boolean = false
): ChapterData<List<String>> { ): ChapterData<List<String>> {
val chapterList = arrayListOf<BookChapter>() val chapterList = arrayListOf<BookChapter>()
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
val nextTocRule = tocRule.nextTocUrl val nextTocRule = tocRule.nextTocUrl
if (getNextUrl && !nextTocRule.isNullOrEmpty()) { if (getNextUrl && !nextTocRule.isNullOrEmpty()) {
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log) Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log)
@ -208,7 +212,7 @@ object BookChapterList {
var isVip: String? var isVip: String?
for (item in elements) { for (item in elements) {
analyzeRule.setContent(item) analyzeRule.setContent(item)
val bookChapter = BookChapter(bookUrl = book.bookUrl) val bookChapter = BookChapter(bookUrl = bookUrl)
analyzeRule.chapter = bookChapter analyzeRule.chapter = bookChapter
bookChapter.title = analyzeRule.getString(nameRule) bookChapter.title = analyzeRule.getString(nameRule)
bookChapter.url = analyzeRule.getString(urlRule, true) bookChapter.url = analyzeRule.getString(urlRule, true)

@ -33,10 +33,14 @@ object BookContent {
) )
) )
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}")
val analyzeRule = AnalyzeRule(book)
val content = StringBuilder() val content = StringBuilder()
val nextUrlList = arrayListOf(baseUrl) val nextUrlList = arrayListOf(baseUrl)
val contentRule = bookSource.getContentRule() val contentRule = bookSource.getContentRule()
var contentData = analyzeContent(body, contentRule, book, bookChapter, bookSource, baseUrl) var contentData = analyzeContent(
analyzeRule.setContent(body, baseUrl),
contentRule, bookChapter, bookSource
)
content.append(contentData.content.replace(bookChapter.title, "")) content.append(contentData.content.replace(bookChapter.title, ""))
if (contentData.nextUrl.size == 1) { if (contentData.nextUrl.size == 1) {
var nextUrl = contentData.nextUrl[0] var nextUrl = contentData.nextUrl[0]
@ -56,15 +60,15 @@ object BookContent {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getResponseAwait() ).getResponseAwait()
.body?.let { nextBody -> .body?.let { nextBody ->
contentData = contentData =
analyzeContent( analyzeContent(
nextBody, contentRule, book, analyzeRule.setContent(nextBody, nextUrl),
bookChapter, bookSource, baseUrl, false contentRule, bookChapter, bookSource, false
) )
nextUrl = nextUrl =
if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else "" if (contentData.nextUrl.isNotEmpty()) contentData.nextUrl[0] else ""
content.append(contentData.content) content.append(contentData.content)
} }
} }
Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}") Debug.log(bookSource.bookSourceUrl, "◇本章总页数:${nextUrlList.size}")
} else if (contentData.nextUrl.size > 1) { } else if (contentData.nextUrl.size > 1) {
@ -81,19 +85,20 @@ object BookContent {
headerMapF = bookSource.getHeaderMap() headerMapF = bookSource.getHeaderMap()
).getResponseAwait() ).getResponseAwait()
.body?.let { .body?.let {
contentData = contentData =
analyzeContent( analyzeContent(
it, contentRule, book, bookChapter, analyzeRule.setContent(it, item.nextUrl),
bookSource, item.nextUrl, false contentRule, bookChapter, bookSource, false
) )
item.content = contentData.content item.content = contentData.content
} }
} }
} }
for (item in contentDataList) { for (item in contentDataList) {
content.append(item.content) content.append(item.content)
} }
} }
Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "┌获取章节名称")
Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "${bookChapter.title}")
Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") Debug.log(bookSource.bookSourceUrl, "┌获取正文内容")
@ -103,17 +108,13 @@ object BookContent {
@Throws(Exception::class) @Throws(Exception::class)
private fun analyzeContent( private fun analyzeContent(
body: String, analyzeRule: AnalyzeRule,
contentRule: ContentRule, contentRule: ContentRule,
book: Book,
chapter: BookChapter, chapter: BookChapter,
bookSource: BookSource, bookSource: BookSource,
baseUrl: String,
printLog: Boolean = true printLog: Boolean = true
): ContentData<List<String>> { ): ContentData<List<String>> {
val nextUrlList = arrayListOf<String>() val nextUrlList = arrayListOf<String>()
val analyzeRule = AnalyzeRule(book)
analyzeRule.setContent(body, baseUrl)
analyzeRule.chapter = chapter analyzeRule.chapter = chapter
val nextUrlRule = contentRule.nextContentUrl val nextUrlRule = contentRule.nextContentUrl
if (!nextUrlRule.isNullOrEmpty()) { if (!nextUrlRule.isNullOrEmpty()) {

@ -14,14 +14,15 @@ import io.legado.app.model.WebBook
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.toast
import java.util.concurrent.Executors import java.util.concurrent.Executors
class CheckSourceService : BaseService() { class CheckSourceService : BaseService() {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null private var task: Coroutine<*>? = null
private var idsCount = 0 private val allIds = LinkedHashSet<String>()
private val unCheckIds = LinkedHashSet<String>() private val checkedIds = LinkedHashSet<String>()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -46,12 +47,12 @@ class CheckSourceService : BaseService() {
private fun check(ids: List<String>) { private fun check(ids: List<String>) {
task?.cancel() task?.cancel()
unCheckIds.clear() allIds.clear()
idsCount = ids.size checkedIds.clear()
unCheckIds.addAll(ids) allIds.addAll(ids)
updateNotification(0, getString(R.string.progress_show, 0, idsCount)) updateNotification(0, getString(R.string.progress_show, 0, allIds.size))
task = execute { task = execute(context = searchPool) {
unCheckIds.forEach { sourceUrl -> allIds.forEach { sourceUrl ->
App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source ->
val webBook = WebBook(source) val webBook = WebBook(source)
webBook.searchBook("我的", scope = this, context = searchPool) webBook.searchBook("我的", scope = this, context = searchPool)
@ -59,15 +60,16 @@ class CheckSourceService : BaseService() {
source.addGroup("失效") source.addGroup("失效")
App.db.bookSourceDao().update(source) App.db.bookSourceDao().update(source)
}.onFinally { }.onFinally {
unCheckIds.remove(sourceUrl) checkedIds.add(sourceUrl)
val checkedCount = idsCount - unCheckIds.size
updateNotification( updateNotification(
checkedCount, checkedIds.size,
getString(R.string.progress_show, checkedCount, idsCount) getString(R.string.progress_show, checkedIds.size, allIds.size)
) )
} }
} }
} }
}.onError {
toast("校验书源出错:${it.localizedMessage}")
} }
task?.invokeOnCompletion { task?.invokeOnCompletion {
@ -92,7 +94,7 @@ class CheckSourceService : BaseService() {
getString(R.string.cancel), getString(R.string.cancel),
IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop) IntentHelp.servicePendingIntent<CheckSourceService>(this, IntentAction.stop)
) )
builder.setProgress(idsCount, state, false) builder.setProgress(allIds.size, state, false)
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
val notification = builder.build() val notification = builder.build()
startForeground(112202, notification) startForeground(112202, notification)

@ -5,16 +5,15 @@ import android.media.MediaPlayer
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.http.api.HttpPostApi import io.legado.app.help.http.api.HttpPostApi
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils import io.legado.app.utils.LogUtils
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import org.jetbrains.anko.toast
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.FileInputStream import java.io.FileInputStream
@ -27,7 +26,7 @@ class HttpReadAloudService : BaseReadAloudService(),
private val mediaPlayer = MediaPlayer() private val mediaPlayer = MediaPlayer()
private lateinit var ttsFolder: String private lateinit var ttsFolder: String
private var job: Job? = null private var task: Coroutine<*>? = null
private var playingIndex = -1 private var playingIndex = -1
override fun onCreate() { override fun onCreate() {
@ -40,13 +39,12 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
job?.cancel() task?.cancel()
mediaPlayer.release() mediaPlayer.release()
} }
override fun newReadAloud(dataKey: String?, play: Boolean) { override fun newReadAloud(dataKey: String?, play: Boolean) {
mediaPlayer.reset() mediaPlayer.reset()
job?.cancel()
playingIndex = -1 playingIndex = -1
super.newReadAloud(dataKey, play) super.newReadAloud(dataKey, play)
} }
@ -64,7 +62,8 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun downloadAudio() { private fun downloadAudio() {
job = launch(IO) { task?.cancel()
task = execute {
FileUtils.deleteFile(ttsFolder) FileUtils.deleteFile(ttsFolder)
for (index in 0 until contentList.size) { for (index in 0 until contentList.size) {
if (isActive) { if (isActive) {
@ -85,6 +84,8 @@ class HttpReadAloudService : BaseReadAloudService(),
break break
} }
} }
}.onError {
toast("下载朗读文件出错:${it.localizedMessage}")
} }
} }
@ -150,7 +151,7 @@ class HttpReadAloudService : BaseReadAloudService(),
* 更新朗读速度 * 更新朗读速度
*/ */
override fun upSpeechRate(reset: Boolean) { override fun upSpeechRate(reset: Boolean) {
job?.cancel() task?.cancel()
mediaPlayer.stop() mediaPlayer.stop()
playingIndex = -1 playingIndex = -1
downloadAudio() downloadAudio()

@ -6,7 +6,7 @@ import android.view.MenuItem
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.shareText import org.jetbrains.anko.share
class AboutActivity : BaseActivity(R.layout.activity_about) { class AboutActivity : BaseActivity(R.layout.activity_about) {
@ -27,9 +27,9 @@ class AboutActivity : BaseActivity(R.layout.activity_about) {
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_scoring -> openUrl("market://details?id=$packageName") R.id.menu_scoring -> openUrl("market://details?id=$packageName")
R.id.menu_share_it -> shareText( R.id.menu_share_it -> share(
"App Share", getString(R.string.app_share_description),
getString(R.string.app_share_description) getString(R.string.app_name)
) )
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)

@ -95,12 +95,14 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initBookData() { private fun initBookData() {
booksLiveData?.removeObservers(this) booksLiveData?.removeObservers(this)
booksLiveData = booksLiveData =
if (groupId == -1) { when (groupId) {
App.db.bookDao().observeAll() -1 -> App.db.bookDao().observeAll()
} else { -2 -> App.db.bookDao().observeLocal()
App.db.bookDao().observeByGroup(groupId) -3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId)
} }
booksLiveData?.observe(this, Observer { booksLiveData?.observe(this, Observer {
adapter.selectedBooks.clear()
adapter.setItems(it) adapter.setItems(it)
upSelectCount() upSelectCount()
}) })

@ -33,7 +33,6 @@ import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.read.config.* import io.legado.app.ui.book.read.config.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.ui.book.read.page.TextPageFactory import io.legado.app.ui.book.read.page.TextPageFactory
@ -656,7 +655,6 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upSystemUiVisibility() upSystemUiVisibility()
page_view.upBg() page_view.upBg()
page_view.upStyle() page_view.upStyle()
ChapterProvider.upStyle()
if (it) { if (it) {
ReadBook.loadContent() ReadBook.loadContent()
} else { } else {
@ -693,7 +691,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
upScreenTimeOut() upScreenTimeOut()
} }
observeEvent<Boolean>(PreferKey.textSelectAble) { observeEvent<Boolean>(PreferKey.textSelectAble) {
page_view.upSelectAble(it) page_view.curPage.upSelectAble(it)
} }
} }

@ -15,6 +15,8 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.view.SupportMenuInflater import androidx.appcompat.view.SupportMenuInflater
import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.size
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
@ -22,9 +24,11 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_fillet_text.view.* import kotlinx.android.synthetic.main.item_fillet_text.view.*
import kotlinx.android.synthetic.main.popup_action_menu.view.* import kotlinx.android.synthetic.main.popup_action_menu.view.*
import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.share
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@ -48,10 +52,27 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
recycler_view.adapter = adapter recycler_view.adapter = adapter
val menu = MenuBuilder(context) val menu = MenuBuilder(context)
SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) SupportMenuInflater(context).inflate(R.menu.content_select_action, menu)
adapter.setItems(menu.visibleItems)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
onInitializeMenu(menu) val popupMenu = PopupMenu(context, iv_menu_more)
onInitializeMenu(popupMenu.menu)
if (popupMenu.menu.size > 0) {
iv_menu_more.visible()
popupMenu.setOnMenuItemClickListener { item ->
item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
this@TextActionMenu.dismiss()
true
}
}
iv_menu_more.onClick {
popupMenu.show()
}
} }
adapter.setItems(menu.visibleItems)
} }
inner class Adapter(context: Context) : inner class Adapter(context: Context) :
@ -81,6 +102,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
private fun onMenuItemSelected(item: MenuItemImpl) { private fun onMenuItemSelected(item: MenuItemImpl) {
when (item.itemId) { when (item.itemId) {
R.id.menu_copy -> context.sendToClip(callBack.selectedText) R.id.menu_copy -> context.sendToClip(callBack.selectedText)
R.id.menu_share_str -> context.share(callBack.selectedText)
R.id.menu_browser -> { R.id.menu_browser -> {
try { try {
val intent = if (callBack.selectedText.isAbsUrl()) { val intent = if (callBack.selectedText.isAbsUrl()) {
@ -98,12 +120,6 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
context.toast(e.localizedMessage ?: "ERROR") context.toast(e.localizedMessage ?: "ERROR")
} }
} }
else -> item.intent?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
context.startActivity(it)
}
}
} }
callBack.onMenuActionFinally() callBack.onMenuActionFinally()
} }

@ -12,7 +12,6 @@ import io.legado.app.R
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ImageLoader
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
@ -20,6 +19,7 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.book.read.Help import io.legado.app.ui.book.read.Help
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.getIndexById
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.putPrefString import io.legado.app.utils.putPrefString
@ -31,6 +31,8 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick
class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
val callBack get() = activity as? ReadBookActivity
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val dm = DisplayMetrics() val dm = DisplayMetrics()
@ -121,11 +123,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
} }
} }
tv_padding.onClick { tv_padding.onClick {
val activity = activity
dismiss() dismiss()
if (activity is ReadBookActivity) { callBack?.showPaddingConfig()
activity.showPaddingConfig()
}
} }
dsb_text_size.onChanged = { dsb_text_size.onChanged = {
ReadBookConfig.textSize = it + 5 ReadBookConfig.textSize = it + 5
@ -144,15 +143,9 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
postEvent(EventBus.UP_CONFIG, true) postEvent(EventBus.UP_CONFIG, true)
} }
rg_page_anim.onCheckedChange { _, checkedId -> rg_page_anim.onCheckedChange { _, checkedId ->
for (i in 0 until rg_page_anim.childCount) { rg_page_anim.getIndexById(checkedId).let {
if (checkedId == rg_page_anim[i].id) { ReadBookConfig.pageAnim = it
ReadBookConfig.pageAnim = i callBack?.page_view?.upPageAnim()
val activity = activity
if (activity is ReadBookActivity) {
activity.page_view.upPageAnim()
}
break
}
} }
} }
cb_share_layout.onCheckedChangeListener = { checkBox, isChecked -> cb_share_layout.onCheckedChangeListener = { checkBox, isChecked ->
@ -187,10 +180,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
private fun showBgTextConfig(index: Int): Boolean { private fun showBgTextConfig(index: Int): Boolean {
dismiss() dismiss()
changeBg(index) changeBg(index)
val activity = activity callBack?.showBgTextConfig()
if (activity is ReadBookActivity) {
activity.showBgTextConfig()
}
return true return true
} }
@ -219,12 +209,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack {
4 -> bg4 4 -> bg4
else -> bg0 else -> bg0
} }
ReadBookConfig.getConfig(i).apply { iv.setImageDrawable(ReadBookConfig.getConfig(i).bgDrawable(100, 150))
when (bgType()) {
2 -> ImageLoader.load(requireContext(), bgStr()).centerCrop().into(iv)
else -> iv.setImageDrawable(bgDrawable(100, 150))
}
}
} }
} }

@ -8,10 +8,10 @@ import android.text.TextUtils
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.dp import io.legado.app.utils.dp
@ -78,7 +78,7 @@ object ChapterProvider {
durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder) durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder)
} }
} }
textPages.last().height = durY textPages.last().height = durY + 20.dp
textPages.last().text = stringBuilder.toString() textPages.last().text = stringBuilder.toString()
if (pageLines.size < textPages.size) { if (pageLines.size < textPages.size) {
pageLines.add(textPages.last().textLines.size) pageLines.add(textPages.last().textLines.size)
@ -145,42 +145,15 @@ object ChapterProvider {
title.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) title.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words) stringBuilder.append(words)
textLine.text = words textLine.text = words
val desiredWidth = layout.getLineMax(lineIndex) val desiredWidth = layout.getLineWidth(lineIndex)
if (lineIndex != layout.lineCount - 1) { if (lineIndex != layout.lineCount - 1) {
val gapCount: Int = words.length - 1 addCharsToLineMiddle(textLine, words, titlePaint, desiredWidth, 0f)
val d = (visibleWidth - desiredWidth) / gapCount
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, titlePaint)
val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw)
val textChar = TextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
}
} else { } else {
//最后一行 //最后一行
textLine.text = "$words\n" val x = if (ReadBookConfig.titleCenter)
stringBuilder.append("\n") (visibleWidth - layout.getLineWidth(lineIndex)) / 2
var x = if (ReadBookConfig.titleCenter)
(visibleWidth - layout.getLineMax(lineIndex)) / 2
else 0f else 0f
for (i in words.indices) { addCharsToLineLast(textLine, words, stringBuilder, titlePaint, x)
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, titlePaint)
val x1 = x + cw
val textChar = TextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
}
} }
} }
durY += paragraphSpacing durY += paragraphSpacing
@ -224,80 +197,120 @@ object ChapterProvider {
textLine.lineBase = (paddingTop + durY - textLine.lineBase = (paddingTop + durY -
(layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat() (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat()
textLine.lineBottom = textLine.lineBase + contentPaint.fontMetrics.descent textLine.lineBottom = textLine.lineBase + contentPaint.fontMetrics.descent
var words = val words =
text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex))
stringBuilder.append(words) stringBuilder.append(words)
textLine.text = words textLine.text = words
val desiredWidth = layout.getLineMax(lineIndex) val desiredWidth = layout.getLineWidth(lineIndex)
if (lineIndex == 0 && layout.lineCount > 1) { if (lineIndex == 0 && layout.lineCount > 1) {
//第一行 //第一行
var x = 0f addCharsToLineFirst(textLine, words, contentPaint, desiredWidth)
val icw = StaticLayout.getDesiredWidth(bodyIndent, contentPaint) / bodyIndent.length
for (i in 0..bodyIndent.lastIndex) {
val x1 = x + icw
val textChar = TextChar(
charData = bodyIndent[i].toString(),
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
}
words = words.replaceFirst(bodyIndent, "")
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
val x1 = if (i != words.lastIndex) x + cw + d else x + cw
val textChar1 = TextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar1)
x = x1
}
} else if (lineIndex == layout.lineCount - 1) { } else if (lineIndex == layout.lineCount - 1) {
//最后一行 //最后一行
stringBuilder.append("\n") addCharsToLineLast(textLine, words, stringBuilder, contentPaint, 0f)
textLine.text = "$words\n"
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
val x1 = x + cw
val textChar = TextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
}
} else { } else {
//中间行 //中间行
val gapCount: Int = words.length - 1 addCharsToLineMiddle(textLine, words, contentPaint, desiredWidth, 0f)
val d = (visibleWidth - desiredWidth) / gapCount
var x = 0f
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, contentPaint)
val x1 = if (i != words.lastIndex) x + cw + d else x + cw
val textChar = TextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
textLine.textChars.add(textChar)
x = x1
}
} }
} }
durY += paragraphSpacing durY += paragraphSpacing
return durY return durY
} }
/**
* 有缩进,两端对齐
*/
private fun addCharsToLineFirst(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float
) {
var x = 0f
val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
for (i in 0..bodyIndent.lastIndex) {
val x1 = x + icw
textLine.addTextChar(
charData = bodyIndent[i].toString(),
start = paddingLeft + x,
end = paddingLeft + x1
)
x = x1
}
val words1 = words.replaceFirst(bodyIndent, "")
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x)
}
/**
* 无缩进,两端对齐
*/
private fun addCharsToLineMiddle(
textLine: TextLine,
words: String,
textPaint: TextPaint,
desiredWidth: Float,
startX: Float
) {
val gapCount: Int = words.length - 1
val d = (visibleWidth - desiredWidth) / gapCount
var x = startX
for (i in words.indices) {
val char = words[i]
val cw = StaticLayout.getDesiredWidth(char.toString(), textPaint)
val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw)
textLine.addTextChar(
charData = char.toString(),
start = paddingLeft + x,
end = paddingLeft + x1
)
x = x1
}
exceed(textLine, words)
}
/**
* 最后一行,自然排列
*/
private fun addCharsToLineLast(
textLine: TextLine,
words: String,
stringBuilder: StringBuilder,
textPaint: TextPaint,
startX: Float
) {
stringBuilder.append("\n")
textLine.text = "$words\n"
var x = startX
for (i in words.indices) {
val char = words[i].toString()
val cw = StaticLayout.getDesiredWidth(char, textPaint)
val x1 = x + cw
textLine.addTextChar(
charData = char,
start = paddingLeft + x,
end = paddingLeft + x1
)
x = x1
}
exceed(textLine, words)
}
/**
* 超出边界处理
*/
private fun exceed(textLine: TextLine, words: String) {
val endX = textLine.textChars.last().end
if (endX > visibleRight) {
val cc = (endX - visibleRight) / words.length
for (i in 0..words.lastIndex) {
textLine.getTextCharReverseAt(i).let {
val py = cc * (words.length - i)
it.start = it.start - py
it.end = it.end - py
}
}
}
}
/** /**
* 更新样式 * 更新样式
@ -308,7 +321,11 @@ object ChapterProvider {
if (!TextUtils.isEmpty(fontPath)) { if (!TextUtils.isEmpty(fontPath)) {
Typeface.createFromFile(fontPath) Typeface.createFromFile(fontPath)
} else { } else {
Typeface.SANS_SERIF when (AppConfig.systemTypefaces) {
1 -> Typeface.SERIF
2 -> Typeface.MONOSPACE
else -> Typeface.SANS_SERIF
}
} }
} catch (e: Exception) { } catch (e: Exception) {
App.INSTANCE.removePref(PreferKey.readBookFont) App.INSTANCE.removePref(PreferKey.readBookFont)

@ -47,17 +47,21 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
invalidate() invalidate()
} }
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { fun upVisibleRect() {
super.onSizeChanged(w, h, oldw, oldh)
ChapterProvider.viewWidth = w
ChapterProvider.viewHeight = h
ChapterProvider.upSize()
visibleRect.set( visibleRect.set(
ChapterProvider.paddingLeft.toFloat(), ChapterProvider.paddingLeft.toFloat(),
ChapterProvider.paddingTop.toFloat(), ChapterProvider.paddingTop.toFloat(),
ChapterProvider.visibleRight.toFloat(), ChapterProvider.visibleRight.toFloat(),
ChapterProvider.visibleBottom.toFloat() ChapterProvider.visibleBottom.toFloat()
) )
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
ChapterProvider.viewWidth = w
ChapterProvider.viewHeight = h
ChapterProvider.upSize()
upVisibleRect()
textPage.format() textPage.format()
} }
@ -160,15 +164,23 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
pageOffset += offset pageOffset += offset
if (pageOffset > 0) { if (pageOffset > 0) {
pageFactory.moveToPrev() if (!pageFactory.hasPrev()) {
textPage = pageFactory.currentPage pageOffset = 0f
pageOffset -= textPage.height } else {
upView?.invoke(textPage) pageFactory.moveToPrev()
textPage = pageFactory.currentPage
pageOffset -= textPage.height
upView?.invoke(textPage)
}
} else if (pageOffset < -textPage.height) { } else if (pageOffset < -textPage.height) {
pageOffset += textPage.height if (!pageFactory.hasNext()) {
pageFactory.moveToNext() pageOffset = -textPage.height.toFloat()
textPage = pageFactory.currentPage } else {
upView?.invoke(textPage) pageOffset += textPage.height
pageFactory.moveToNext()
textPage = pageFactory.currentPage
upView?.invoke(textPage)
}
} }
invalidate() invalidate()
} }
@ -185,6 +197,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
y: Float, y: Float,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit
) { ) {
if (!selectAble) return
if (!visibleRect.contains(x, y)) return if (!visibleRect.contains(x, y)) return
var relativeOffset = relativeOffset(0) var relativeOffset = relativeOffset(0)
for ((lineIndex, textLine) in textPage.textLines.withIndex()) { for ((lineIndex, textLine) in textPage.textLines.withIndex()) {

@ -5,8 +5,10 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst.TIME_FORMAT import io.legado.app.constant.AppConst.TIME_FORMAT
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.* import io.legado.app.utils.*
@ -37,8 +39,9 @@ class ContentView(context: Context) : FrameLayout(context) {
tv_bottom_right.typeface = ChapterProvider.typeface tv_bottom_right.typeface = ChapterProvider.typeface
//显示状态栏时隐藏header //显示状态栏时隐藏header
if (hideStatusBar) { if (hideStatusBar) {
ll_header.layoutParams = ll_header.layoutParams = ll_header.layoutParams.apply {
ll_header.layoutParams.apply { height = context.statusBarHeight } height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp
}
ll_header.setPadding( ll_header.setPadding(
headerPaddingLeft.dp, headerPaddingLeft.dp,
headerPaddingTop.dp, headerPaddingTop.dp,
@ -51,18 +54,13 @@ class ContentView(context: Context) : FrameLayout(context) {
ll_header.gone() ll_header.gone()
page_panel.setPadding(0, context.statusBarHeight, 0, 0) page_panel.setPadding(0, context.statusBarHeight, 0, 0)
} }
content_text_view.setPadding(
paddingLeft.dp,
paddingTop.dp,
paddingRight.dp,
paddingBottom.dp
)
ll_footer.setPadding( ll_footer.setPadding(
footerPaddingLeft.dp, footerPaddingLeft.dp,
footerPaddingTop.dp, footerPaddingTop.dp,
footerPaddingRight.dp, footerPaddingRight.dp,
footerPaddingBottom.dp footerPaddingBottom.dp
) )
content_text_view.upVisibleRect()
durConfig.textColor().let { durConfig.textColor().let {
tv_top_left.setTextColor(it) tv_top_left.setTextColor(it)
tv_top_right.setTextColor(it) tv_top_right.setTextColor(it)
@ -94,7 +92,11 @@ class ContentView(context: Context) : FrameLayout(context) {
} }
fun setContent(textPage: TextPage) { fun setContent(textPage: TextPage) {
tv_bottom_left.text = textPage.title tv_bottom_left.text = when (AppConfig.chineseConverterType) {
1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title)
2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title)
else -> textPage.title
}
setPageIndex(textPage.index, textPage.pageSize) setPageIndex(textPage.index, textPage.pageSize)
content_text_view.resetPageOffset() content_text_view.resetPageOffset()
content_text_view.setContent(textPage) content_text_view.setContent(textPage)

@ -120,7 +120,7 @@ class PageView(context: Context, attrs: AttributeSet) :
if (noAnim) { if (noAnim) {
fillPage(PageDelegate.Direction.PREV) fillPage(PageDelegate.Direction.PREV)
} else { } else {
pageDelegate?.start(PageDelegate.Direction.PREV) pageDelegate?.prevPageByAnim()
} }
} }
@ -128,16 +128,12 @@ class PageView(context: Context, attrs: AttributeSet) :
if (noAnim) { if (noAnim) {
fillPage(PageDelegate.Direction.NEXT) fillPage(PageDelegate.Direction.NEXT)
} else { } else {
pageDelegate?.start(PageDelegate.Direction.NEXT) pageDelegate?.nextPageByAnim()
} }
} }
fun upSelectAble(selectAble: Boolean) {
pageDelegate?.upSelectAble(selectAble)
curPage.upSelectAble(selectAble)
}
fun upStyle() { fun upStyle() {
ChapterProvider.upStyle()
curPage.upStyle() curPage.upStyle()
prevPage.upStyle() prevPage.upStyle()
nextPage.upStyle() nextPage.upStyle()

@ -52,4 +52,19 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie
} }
} }
override fun nextPageByAnim() {
if (!hasNext()) return
setDirection(Direction.NEXT)
setBitmap()
setTouchPoint(viewWidth.toFloat(), 0f)
onAnimStart()
}
override fun prevPageByAnim() {
if (!hasPrev()) return
setDirection(Direction.PREV)
setBitmap()
setTouchPoint(0f, 0f)
onAnimStart()
}
} }

@ -12,6 +12,7 @@ import android.widget.Scroller
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.ContentView
import io.legado.app.ui.book.read.page.PageView import io.legado.app.ui.book.read.page.PageView
import io.legado.app.utils.screenshot import io.legado.app.utils.screenshot
@ -101,10 +102,6 @@ abstract class PageDelegate(protected val pageView: PageView) :
onScroll() onScroll()
} }
fun upSelectAble(selectAble: Boolean) {
detector.setIsLongpressEnabled(selectAble)
}
open fun fling( open fun fling(
startX: Int, startY: Int, velocityX: Int, velocityY: Int, startX: Int, startY: Int, velocityX: Int, velocityY: Int,
minX: Int, maxX: Int, minY: Int, maxY: Int minX: Int, maxX: Int, minY: Int, maxY: Int
@ -161,34 +158,6 @@ abstract class PageDelegate(protected val pageView: PageView) :
} }
} }
fun start(direction: Direction) {
if (isStarted) return
if (direction === Direction.NEXT) {
val x = viewWidth.toFloat()
val y = viewHeight.toFloat()
//初始化动画
setStartPoint(x, y, false)
//设置点击点
setTouchPoint(x, y, false)
//设置方向
if (!hasNext()) {
return
}
} else {
val x = 0.toFloat()
val y = viewHeight.toFloat()
//初始化动画
setStartPoint(x, y, false)
//设置点击点
setTouchPoint(x, y, false)
//设置方向方向
if (!hasPrev()) {
return
}
}
onAnimStart()
}
open fun onAnimStart() {}//scroller start open fun onAnimStart() {}//scroller start
open fun onDraw(canvas: Canvas) {}//绘制 open fun onDraw(canvas: Canvas) {}//绘制
@ -197,6 +166,10 @@ abstract class PageDelegate(protected val pageView: PageView) :
open fun onScroll() {}//移动contentView, slidePage open fun onScroll() {}//移动contentView, slidePage
abstract fun nextPageByAnim()
abstract fun prevPageByAnim()
@CallSuper @CallSuper
open fun setDirection(direction: Direction) { open fun setDirection(direction: Direction) {
mDirection = direction mDirection = direction
@ -264,21 +237,14 @@ abstract class PageDelegate(protected val pageView: PageView) :
if (centerRectF.contains(x, y)) { if (centerRectF.contains(x, y)) {
pageView.callBack.clickCenter() pageView.callBack.clickCenter()
setTouchPoint(x, y) setTouchPoint(x, y)
} else { } else if (ReadBookConfig.clickTurnPage) {
if (x > viewWidth / 2 || if (x > viewWidth / 2 ||
AppConfig.clickAllNext AppConfig.clickAllNext
) { ) {
//设置动画方向 nextPageByAnim()
if (!hasNext()) return true
setDirection(Direction.NEXT)
setBitmap()
} else { } else {
if (!hasPrev()) return true prevPageByAnim()
setDirection(Direction.PREV)
setBitmap()
} }
setTouchPoint(x, y)
onAnimStart()
} }
return true return true
} }

@ -58,4 +58,12 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) {
super.onDestroy() super.onDestroy()
mVelocity.recycle() mVelocity.recycle()
} }
override fun nextPageByAnim() {
}
override fun prevPageByAnim() {
}
} }

@ -40,7 +40,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private var mMiddleY = 0f private var mMiddleY = 0f
private var mDegrees = 0f private var mDegrees = 0f
private var mTouchToCornerDis = 0f private var mTouchToCornerDis = 0f
private var mColorMatrixFilter: ColorMatrixColorFilter = ColorMatrixColorFilter( private var mColorMatrixFilter = ColorMatrixColorFilter(
ColorMatrix( ColorMatrix(
floatArrayOf( floatArrayOf(
1f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, 0f,
@ -55,7 +55,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
// 是否属于右上左下 // 是否属于右上左下
private var mIsRtOrLb = false private var mIsRtOrLb = false
private var mMaxLength = 0f private var mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
// 背面颜色组 // 背面颜色组
private var mBackShadowColors: IntArray private var mBackShadowColors: IntArray
// 前面颜色组 // 前面颜色组
@ -71,15 +71,13 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
private var mFrontShadowDrawableVLR: GradientDrawable private var mFrontShadowDrawableVLR: GradientDrawable
private var mFrontShadowDrawableVRL: GradientDrawable private var mFrontShadowDrawableVRL: GradientDrawable
private val mPaint: Paint = Paint() private val mPaint: Paint = Paint().apply { style = Paint.Style.FILL }
private var curBitmap: Bitmap? = null private var curBitmap: Bitmap? = null
private var prevBitmap: Bitmap? = null private var prevBitmap: Bitmap? = null
private var nextBitmap: Bitmap? = null private var nextBitmap: Bitmap? = null
init { init {
mMaxLength = hypot(viewWidth.toDouble(), viewWidth.toDouble()).toFloat()
mPaint.style = Paint.Style.FILL
//设置颜色数组 //设置颜色数组
val color = intArrayOf(0x333333, -0x4fcccccd) val color = intArrayOf(0x333333, -0x4fcccccd)
mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color) mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color)
@ -117,7 +115,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
override fun setViewSize(width: Int, height: Int) { override fun setViewSize(width: Int, height: Int) {
super.setViewSize(width, height) super.setViewSize(width, height)
mMaxLength = hypot(viewWidth.toDouble(), viewWidth.toDouble()).toFloat() mMaxLength = hypot(viewWidth.toDouble(), viewHeight.toDouble()).toFloat()
} }
override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) { override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) {
@ -126,7 +124,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} }
override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) { override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) {
super.setTouchPoint(x, y, invalidate) super.setTouchPoint(x, y, false)
//触摸y中间位置吧y变成屏幕高度 //触摸y中间位置吧y变成屏幕高度
if ((startY > viewHeight * 0.33 && startY < viewHeight * 0.66) if ((startY > viewHeight * 0.33 && startY < viewHeight * 0.66)
|| mDirection == Direction.PREV || mDirection == Direction.PREV
@ -139,6 +137,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
) { ) {
touchY = 1f touchY = 1f
} }
pageView.invalidate()
} }
override fun setDirection(direction: Direction) { override fun setDirection(direction: Direction) {
@ -219,18 +218,22 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
if (mDirection === Direction.NEXT) { when (mDirection) {
calcPoints() Direction.NEXT -> {
drawCurrentPageArea(canvas, curBitmap, mPath0) calcPoints()
drawNextPageAreaAndShadow(canvas, nextBitmap) drawCurrentPageArea(canvas, curBitmap)
drawCurrentPageShadow(canvas) drawNextPageAreaAndShadow(canvas, nextBitmap)
drawCurrentBackArea(canvas, curBitmap) drawCurrentPageShadow(canvas)
} else { drawCurrentBackArea(canvas, curBitmap)
calcPoints() }
drawCurrentPageArea(canvas, prevBitmap, mPath0) Direction.PREV -> {
drawNextPageAreaAndShadow(canvas, curBitmap) calcPoints()
drawCurrentPageShadow(canvas) drawCurrentPageArea(canvas, prevBitmap)
drawCurrentBackArea(canvas, prevBitmap) drawNextPageAreaAndShadow(canvas, curBitmap)
drawCurrentPageShadow(canvas)
drawCurrentBackArea(canvas, prevBitmap)
}
else -> return
} }
} }
@ -258,7 +261,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
val left: Int val left: Int
val right: Int val right: Int
if (mIsRtOrLb) { if (mIsRtOrLb) {
left = mBezierStart1.x.toInt() - 1 left = (mBezierStart1.x - 1).toInt()
right = (mBezierStart1.x + f3 + 1).toInt() right = (mBezierStart1.x + f3 + 1).toInt()
mFolderShadowDrawable = mFolderShadowDrawableLR mFolderShadowDrawable = mFolderShadowDrawableLR
} else { } else {
@ -337,17 +340,16 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
var mCurrentPageShadow: GradientDrawable var mCurrentPageShadow: GradientDrawable
if (mIsRtOrLb) { if (mIsRtOrLb) {
leftX = mBezierControl1.x.toInt() leftX = mBezierControl1.x.toInt()
rightX = mBezierControl1.x.toInt() + 25 rightX = (mBezierControl1.x + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableVLR mCurrentPageShadow = mFrontShadowDrawableVLR
} else { } else {
leftX = mBezierControl1.x.toInt() - 25 leftX = (mBezierControl1.x - 25).toInt()
rightX = mBezierControl1.x.toInt() + 1 rightX = (mBezierControl1.x + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableVRL mCurrentPageShadow = mFrontShadowDrawableVRL
} }
var rotateDegrees: Float = var rotateDegrees = Math.toDegrees(
Math.toDegrees( atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble()
atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble() ).toFloat()
).toFloat()
canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y)
mCurrentPageShadow.setBounds( mCurrentPageShadow.setBounds(
leftX, (mBezierControl1.y - mMaxLength).toInt(), leftX, (mBezierControl1.y - mMaxLength).toInt(),
@ -372,24 +374,25 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
if (mIsRtOrLb) { if (mIsRtOrLb) {
leftX = mBezierControl2.y.toInt() leftX = mBezierControl2.y.toInt()
rightX = mBezierControl2.y.toInt() + 25 rightX = (mBezierControl2.y + 25).toInt()
mCurrentPageShadow = mFrontShadowDrawableHTB mCurrentPageShadow = mFrontShadowDrawableHTB
} else { } else {
leftX = mBezierControl2.y.toInt() - 25 leftX = (mBezierControl2.y - 25).toInt()
rightX = mBezierControl2.y.toInt() + 1 rightX = (mBezierControl2.y + 1).toInt()
mCurrentPageShadow = mFrontShadowDrawableHBT mCurrentPageShadow = mFrontShadowDrawableHBT
} }
rotateDegrees = Math.toDegrees( rotateDegrees = Math.toDegrees(
atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble() atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble()
).toFloat() ).toFloat()
canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y)
val temp: Float = val temp =
if (mBezierControl2.y < 0) mBezierControl2.y - viewHeight else mBezierControl2.y if (mBezierControl2.y < 0) (mBezierControl2.y - viewHeight).toDouble()
val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt() else mBezierControl2.y.toDouble()
val hmg = hypot(mBezierControl2.x.toDouble(), temp)
if (hmg > mMaxLength) if (hmg > mMaxLength)
mCurrentPageShadow.setBounds( mCurrentPageShadow.setBounds(
(mBezierControl2.x - 25).toInt() - hmg, leftX, (mBezierControl2.x - 25 - hmg).toInt(), leftX,
(mBezierControl2.x + mMaxLength).toInt() - hmg, rightX (mBezierControl2.x + mMaxLength - hmg).toInt(), rightX
) )
else else
mCurrentPageShadow.setBounds( mCurrentPageShadow.setBounds(
@ -400,6 +403,7 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
canvas.restore() canvas.restore()
} }
//
private fun drawNextPageAreaAndShadow( private fun drawNextPageAreaAndShadow(
canvas: Canvas, canvas: Canvas,
bitmap: Bitmap? bitmap: Bitmap?
@ -447,10 +451,10 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
canvas.restore() canvas.restore()
} }
//
private fun drawCurrentPageArea( private fun drawCurrentPageArea(
canvas: Canvas, canvas: Canvas,
bitmap: Bitmap?, bitmap: Bitmap?
path: Path
) { ) {
bitmap ?: return bitmap ?: return
mPath0.reset() mPath0.reset()
@ -461,11 +465,12 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi
mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y) mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y)
mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat())
mPath0.close() mPath0.close()
canvas.save() canvas.save()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
canvas.clipOutPath(path) canvas.clipOutPath(mPath0)
} else { } else {
canvas.clipPath(path, Region.Op.XOR) canvas.clipPath(mPath0, Region.Op.XOR)
} }
canvas.drawBitmap(bitmap, 0f, 0f, null) canvas.drawBitmap(bitmap, 0f, 0f, null)
canvas.restore() canvas.restore()

@ -2,7 +2,7 @@ package io.legado.app.ui.book.read.page.entities
data class TextChar( data class TextChar(
val charData: String, val charData: String,
var selected: Boolean = false, var start: Float,
val start: Float, var end: Float,
val end: Float var selected: Boolean = false
) )

@ -8,4 +8,21 @@ data class TextLine(
var lineBottom: Float = 0f, var lineBottom: Float = 0f,
val isTitle: Boolean = false, val isTitle: Boolean = false,
var isReadAloud: Boolean = false var isReadAloud: Boolean = false
) ) {
fun addTextChar(charData: String, start: Float, end: Float) {
textChars.add(TextChar(charData, start = start, end = end))
}
fun getTextCharAt(index: Int): TextChar {
return textChars[index]
}
fun getTextCharReverseAt(index: Int): TextChar {
return textChars[textChars.lastIndex - index]
}
fun getTextCharsCount(): Int {
return textChars.size
}
}

@ -16,10 +16,6 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
} }
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return true
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition] val newItem = newItems[newItemPosition]
if (oldItem.name != newItem.name) { if (oldItem.name != newItem.name) {
@ -28,7 +24,13 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.author != newItem.author) { if (oldItem.author != newItem.author) {
return false return false
} }
if (oldItem.origins?.size != newItem.origins?.size) { return true
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition]
val newItem = newItems[newItemPosition]
if (oldItem.origins.size != newItem.origins.size) {
return false return false
} }
if (oldItem.coverUrl != newItem.coverUrl) { if (oldItem.coverUrl != newItem.coverUrl) {
@ -56,20 +58,20 @@ class DiffCallBack(private val oldItems: List<SearchBook>, private val newItems:
if (oldItem.author != newItem.author) { if (oldItem.author != newItem.author) {
payload.putString("author", newItem.author) payload.putString("author", newItem.author)
} }
if (oldItem.origins?.size != newItem.origins?.size) { if (oldItem.origins.size != newItem.origins.size) {
payload.putInt("origins", newItem.origins?.size ?: 1) payload.putInt("origins", newItem.origins.size)
} }
if (oldItem.coverUrl != newItem.coverUrl) { if (oldItem.coverUrl != newItem.coverUrl) {
payload.putString("group", newItem.coverUrl) payload.putString("cover", newItem.coverUrl)
} }
if (oldItem.kind != newItem.kind) { if (oldItem.kind != newItem.kind) {
payload.putString("enabled", newItem.kind) payload.putString("kind", newItem.kind)
} }
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
payload.putString("enabled", newItem.latestChapterTitle) payload.putString("last", newItem.latestChapterTitle)
} }
if (oldItem.intro != newItem.intro) { if (oldItem.intro != newItem.intro) {
payload.putString("enabled", newItem.intro) payload.putString("intro", newItem.intro)
} }
return payload return payload
} }

@ -40,7 +40,7 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
with(itemView) { with(itemView) {
tv_name.text = searchBook.name tv_name.text = searchBook.name
tv_author.text = context.getString(R.string.author_show, searchBook.author) tv_author.text = context.getString(R.string.author_show, searchBook.author)
bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1) bv_originCount.setBadgeCount(searchBook.origins.size)
upLasted(itemView, searchBook.latestChapterTitle) upLasted(itemView, searchBook.latestChapterTitle)
tv_introduce.text = context.getString(R.string.intro_show, searchBook.intro) tv_introduce.text = context.getString(R.string.intro_show, searchBook.intro)
upKind(itemView, searchBook.getKindList()) upKind(itemView, searchBook.getKindList())
@ -56,9 +56,9 @@ class SearchAdapter(context: Context, val callBack: CallBack) :
"name" -> tv_name.text = searchBook.name "name" -> tv_name.text = searchBook.name
"author" -> tv_author.text = "author" -> tv_author.text =
context.getString(R.string.author_show, searchBook.author) context.getString(R.string.author_show, searchBook.author)
"originCount" -> bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1) "origins" -> bv_originCount.setBadgeCount(searchBook.origins.size)
"lasted" -> upLasted(itemView, searchBook.latestChapterTitle) "last" -> upLasted(itemView, searchBook.latestChapterTitle)
"introduce" -> tv_introduce.text = "intro" -> tv_introduce.text =
context.getString(R.string.intro_show, searchBook.intro) context.getString(R.string.intro_show, searchBook.intro)
"kind" -> upKind(itemView, searchBook.getKindList()) "kind" -> upKind(itemView, searchBook.getKindList())
"cover" -> iv_cover.load( "cover" -> iv_cover.load(

@ -85,8 +85,8 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
private fun precisionSearch(searchBooks: List<SearchBook>) { private fun precisionSearch(searchBooks: List<SearchBook>) {
val books = arrayListOf<SearchBook>() val books = arrayListOf<SearchBook>()
searchBooks.forEach { searchBook -> searchBooks.forEach { searchBook ->
if (searchBook.name.equals(searchKey, true) if (searchBook.name.contains(searchKey, true)
|| searchBook.author.equals(searchKey, true) || searchBook.author.contains(searchKey, true)
) books.add(searchBook) ) books.add(searchBook)
} }
App.db.searchBookDao().insert(*books.toTypedArray()) App.db.searchBookDao().insert(*books.toTypedArray())
@ -130,9 +130,9 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} else if (searchKey == item.author) { } else if (searchKey == item.author) {
for ((i, searchBook) in copyDataS.withIndex()) { for ((index, searchBook) in copyDataS.withIndex()) {
if (searchKey != searchBook.name && searchKey == searchBook.author) { if (searchKey != searchBook.name && searchKey == searchBook.author) {
copyDataS.add(i, item) copyDataS.add(index, item)
break break
} }
} }
@ -141,6 +141,31 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
searchBooks.sortWith(Comparator { o1, o2 ->
if (o1.name == searchKey && o2.name != searchKey) {
1
} else if (o1.name != searchKey && o2.name == searchKey) {
-1
} else if (o1.author == searchKey && o2.author != searchKey) {
1
} else if (o1.author != searchKey && o2.author == searchKey) {
-1
} else if (o1.name == o2.name) {
when {
o1.origins.size > o2.origins.size -> {
1
}
o1.origins.size < o2.origins.size -> {
-1
}
else -> {
0
}
}
} else {
0
}
})
searchBooks = copyDataS searchBooks = copyDataS
searchBookLiveData.postValue(copyDataS) searchBookLiveData.postValue(copyDataS)
} }

@ -25,9 +25,13 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.* import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareWithQr
import kotlinx.android.synthetic.main.activity_book_source_edit.* import kotlinx.android.synthetic.main.activity_book_source_edit.*
import org.jetbrains.anko.displayMetrics import org.jetbrains.anko.displayMetrics
import org.jetbrains.anko.share
import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
import kotlin.math.abs import kotlin.math.abs
@ -84,9 +88,7 @@ class BookSourceEditActivity :
} }
} }
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getSource())?.let { sourceStr -> R.id.menu_share_str -> GSON.toJson(getSource())?.let { share(it) }
shareText(getString(R.string.share_book_source), sourceStr)
}
R.id.menu_share_qr -> GSON.toJson(getSource())?.let { sourceStr -> R.id.menu_share_qr -> GSON.toJson(getSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_book_source), sourceStr) shareWithQr(getString(R.string.share_book_source), sourceStr)
} }

@ -58,7 +58,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
App.db.searchBookDao().insert(searchBook) App.db.searchBookDao().insert(searchBook)
if (screenKey.isEmpty()) { if (screenKey.isEmpty()) {
searchBooks.add(searchBook) searchBooks.add(searchBook)
} else if (searchBook.originName.contains(screenKey)) { } else if (searchBook.name.contains(screenKey)) {
searchBooks.add(searchBook) searchBooks.add(searchBook)
} }
upAdapter() upAdapter()

@ -26,7 +26,7 @@ import kotlinx.coroutines.Dispatchers.Main
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
object BackupRestoreUi { object BackupRestoreUi {
private const val selectFolderRequestCode = 21
private const val backupSelectRequestCode = 22 private const val backupSelectRequestCode = 22
private const val restoreSelectRequestCode = 33 private const val restoreSelectRequestCode = 33
private const val oldDataRequestCode = 11 private const val oldDataRequestCode = 11
@ -34,7 +34,7 @@ object BackupRestoreUi {
fun backup(fragment: Fragment) { fun backup(fragment: Fragment) {
val backupPath = AppConfig.backupPath val backupPath = AppConfig.backupPath
if (backupPath.isNullOrEmpty()) { if (backupPath.isNullOrEmpty()) {
selectBackupFolder(fragment) selectBackupFolder(fragment, backupSelectRequestCode)
} else { } else {
if (backupPath.isContentPath()) { if (backupPath.isContentPath()) {
val uri = Uri.parse(backupPath) val uri = Uri.parse(backupPath)
@ -46,40 +46,49 @@ object BackupRestoreUi {
fragment.toast(R.string.backup_success) fragment.toast(R.string.backup_success)
} }
} else { } else {
selectBackupFolder(fragment) selectBackupFolder(fragment, backupSelectRequestCode)
} }
} else { } else {
backupUsePermission(fragment) backupUsePermission(fragment, requestCode = backupSelectRequestCode)
} }
} }
} }
private fun backupUsePermission(fragment: Fragment, path: String = Backup.legadoPath) { private fun backupUsePermission(
fragment: Fragment,
path: String = Backup.legadoPath,
requestCode: Int = selectFolderRequestCode
) {
PermissionsCompat.Builder(fragment) PermissionsCompat.Builder(fragment)
.addPermissions(*Permissions.Group.STORAGE) .addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage) .rationale(R.string.tip_perm_request_storage)
.onGranted { .onGranted {
Coroutine.async { when (requestCode) {
AppConfig.backupPath = Backup.legadoPath selectFolderRequestCode -> AppConfig.backupPath = Backup.legadoPath
Backup.backup(fragment.requireContext(), path) else -> {
}.onSuccess { Coroutine.async {
fragment.toast(R.string.backup_success) AppConfig.backupPath = Backup.legadoPath
Backup.backup(fragment.requireContext(), path)
}.onSuccess {
fragment.toast(R.string.backup_success)
}
}
} }
} }
.request() .request()
} }
fun selectBackupFolder(fragment: Fragment) { fun selectBackupFolder(fragment: Fragment, requestCode: Int = selectFolderRequestCode) {
fragment.alert { fragment.alert {
titleResource = R.string.select_folder titleResource = R.string.select_folder
items(fragment.resources.getStringArray(R.array.select_folder).toList()) { _, index -> items(fragment.resources.getStringArray(R.array.select_folder).toList()) { _, index ->
when (index) { when (index) {
0 -> backupUsePermission(fragment) 0 -> backupUsePermission(fragment, requestCode = requestCode)
1 -> { 1 -> {
try { try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
fragment.startActivityForResult(intent, backupSelectRequestCode) fragment.startActivityForResult(intent, requestCode)
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
e.printStackTrace() e.printStackTrace()
fragment.toast(e.localizedMessage ?: "ERROR") fragment.toast(e.localizedMessage ?: "ERROR")
@ -88,7 +97,7 @@ object BackupRestoreUi {
2 -> { 2 -> {
FileChooserDialog.show( FileChooserDialog.show(
fragment.childFragmentManager, fragment.childFragmentManager,
backupSelectRequestCode, requestCode,
mode = FileChooserDialog.DIRECTORY mode = FileChooserDialog.DIRECTORY
) )
} }
@ -231,6 +240,9 @@ object BackupRestoreUi {
App.INSTANCE.toast(R.string.restore_success) App.INSTANCE.toast(R.string.restore_success)
} }
} }
selectFolderRequestCode -> {
AppConfig.backupPath = currentPath
}
} }
} }
@ -240,7 +252,8 @@ object BackupRestoreUi {
data?.data?.let { uri -> data?.data?.let { uri ->
App.INSTANCE.contentResolver.takePersistableUriPermission( App.INSTANCE.contentResolver.takePersistableUriPermission(
uri, uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
AppConfig.backupPath = uri.toString() AppConfig.backupPath = uri.toString()
Coroutine.async { Coroutine.async {
@ -254,7 +267,8 @@ object BackupRestoreUi {
data?.data?.let { uri -> data?.data?.let { uri ->
App.INSTANCE.contentResolver.takePersistableUriPermission( App.INSTANCE.contentResolver.takePersistableUriPermission(
uri, uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
AppConfig.backupPath = uri.toString() AppConfig.backupPath = uri.toString()
Coroutine.async { Coroutine.async {
@ -264,6 +278,16 @@ object BackupRestoreUi {
} }
} }
} }
selectFolderRequestCode -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
App.INSTANCE.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
AppConfig.backupPath = uri.toString()
}
}
oldDataRequestCode -> oldDataRequestCode ->
if (resultCode == RESULT_OK) data?.data?.let { uri -> if (resultCode == RESULT_OK) data?.data?.let { uri ->
ImportOldData.importUri(uri) ImportOldData.importUri(uri)

@ -2,6 +2,7 @@ package io.legado.app.ui.main.bookshelf
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -17,7 +18,10 @@ import io.legado.app.constant.AppConst
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.* import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.arrange.ArrangeBookActivity import io.legado.app.ui.book.arrange.ArrangeBookActivity
@ -27,6 +31,7 @@ import io.legado.app.ui.download.DownloadActivity
import io.legado.app.ui.importbook.ImportBookActivity import io.legado.app.ui.importbook.ImportBookActivity
import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_bookshelf_config.view.*
import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_edit_text.view.*
import kotlinx.android.synthetic.main.fragment_bookshelf.* import kotlinx.android.synthetic.main.fragment_bookshelf.*
import kotlinx.android.synthetic.main.view_tab_layout.* import kotlinx.android.synthetic.main.view_tab_layout.*
@ -61,7 +66,7 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
super.onCompatOptionsItemSelected(item) super.onCompatOptionsItemSelected(item)
when (item.itemId) { when (item.itemId) {
R.id.menu_search -> startActivity<SearchActivity>() R.id.menu_search -> startActivity<SearchActivity>()
R.id.menu_bookshelf_layout -> selectBookshelfLayout() R.id.menu_bookshelf_layout -> configBookshelf()
R.id.menu_group_manage -> GroupManageDialog() R.id.menu_group_manage -> GroupManageDialog()
.show(childFragmentManager, "groupManageDialog") .show(childFragmentManager, "groupManageDialog")
R.id.menu_add_local -> startActivity<ImportBookActivity>() R.id.menu_add_local -> startActivity<ImportBookActivity>()
@ -145,14 +150,35 @@ class BookshelfFragment : VMBaseFragment<BookshelfViewModel>(R.layout.fragment_b
} }
} }
private fun selectBookshelfLayout() { @SuppressLint("InflateParams")
selector( private fun configBookshelf() {
title = "选择书架布局", requireContext().alert(titleResource = R.string.bookshelf_layout) {
items = resources.getStringArray(R.array.bookshelf_layout).toList() val bookshelfLayout = getPrefInt(PreferKey.bookshelfLayout)
) { _, index -> val bookshelfSort = getPrefInt(PreferKey.bookshelfSort)
putPrefInt(PreferKey.bookshelfLayout, index) val root = LayoutInflater.from(requireContext())
activity?.recreate() .inflate(R.layout.dialog_bookshelf_config, null).apply {
} rg_layout.checkByIndex(bookshelfLayout)
rg_sort.checkByIndex(bookshelfSort)
}
customView = root
okButton {
root.apply {
var changed = false
if (bookshelfLayout != rg_layout.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfLayout, rg_layout.getCheckedIndex())
changed = true
}
if (bookshelfSort != rg_sort.getCheckedIndex()) {
putPrefInt(PreferKey.bookshelfSort, rg_sort.getCheckedIndex())
changed = true
}
if (changed) {
activity?.recreate()
}
}
}
noButton()
}.show().applyTint()
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")

@ -103,10 +103,16 @@ class BooksFragment : BaseFragment(R.layout.fragment_books),
-3 -> App.db.bookDao().observeAudio() -3 -> App.db.bookDao().observeAudio()
else -> App.db.bookDao().observeByGroup(groupId) else -> App.db.bookDao().observeByGroup(groupId)
} }
bookshelfLiveData?.observe(this, Observer { bookshelfLiveData?.observe(this, Observer { list ->
val books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }
3 -> list.sortedBy { it.order }
else -> list.sortedByDescending { it.durChapterTime }
}
val diffResult = DiffUtil val diffResult = DiffUtil
.calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), it)) .calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), books))
booksAdapter.setItems(it, diffResult) booksAdapter.setItems(books, diffResult)
}) })
} }

@ -2,6 +2,7 @@ package io.legado.app.ui.qrcode
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -11,7 +12,7 @@ import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.utils.RealPathUtil import io.legado.app.utils.readBytes
import kotlinx.android.synthetic.main.activity_qrcode_capture.* import kotlinx.android.synthetic.main.activity_qrcode_capture.*
import kotlinx.android.synthetic.main.view_title_bar.* import kotlinx.android.synthetic.main.view_title_bar.*
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -98,9 +99,11 @@ class QrCodeActivity : BaseActivity(R.layout.activity_qrcode_capture), QRCodeVie
zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别 zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别
if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) { if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) {
val picturePath = RealPathUtil.getPath(this, it)
// 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调 // 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调
zxingview.decodeQRCode(picturePath) it.readBytes(this)?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
zxingview.decodeQRCode(bitmap)
}
} }
} }
} }

@ -55,6 +55,7 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
private var dataInit = false private var dataInit = false
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme()
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initSelectActionView() initSelectActionView()
@ -73,6 +74,22 @@ class ReplaceRuleActivity : VMBaseActivity<ReplaceRuleViewModel>(R.layout.activi
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
} }
private fun initUriScheme() {
intent.data?.let {
when (it.path) {
"/importonline" -> it.getQueryParameter("src")?.let { url ->
Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
viewModel.importSource(url) { msg ->
title_bar.snackbar(msg)
}
}
else -> {
toast("格式不对")
}
}
}
}
private fun initRecyclerView() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)

@ -5,7 +5,9 @@ import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.webkit.WebResourceRequest
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import io.legado.app.R import io.legado.app.R
@ -14,10 +16,11 @@ import io.legado.app.lib.theme.DrawableUtils
import io.legado.app.lib.theme.primaryTextColor import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareText import io.legado.app.utils.openUrl
import kotlinx.android.synthetic.main.activity_rss_read.* import kotlinx.android.synthetic.main.activity_rss_read.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
import org.jetbrains.anko.share
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.jsoup.safety.Whitelist import org.jsoup.safety.Whitelist
@ -54,7 +57,7 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
when (item.itemId) { when (item.itemId) {
R.id.menu_rss_star -> viewModel.favorite() R.id.menu_rss_star -> viewModel.favorite()
R.id.menu_share_it -> viewModel.rssArticle?.let { R.id.menu_share_it -> viewModel.rssArticle?.let {
shareText("链接分享", it.link) share(it.link)
} }
R.id.menu_aloud -> readAloud() R.id.menu_aloud -> readAloud()
} }
@ -62,10 +65,24 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
} }
private fun initWebView() { private fun initWebView() {
webView.webViewClient = WebViewClient() webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
if (request?.url?.scheme == "http" || request?.url?.scheme == "https") {
return false
}
request?.url?.let {
openUrl(it)
}
return true
}
}
webView.settings.apply { webView.settings.apply {
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
domStorageEnabled = true domStorageEnabled = true
allowContentAccess = true
} }
} }

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
@ -19,22 +20,25 @@ import io.legado.app.constant.AppConst
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.utils.* import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import io.legado.app.utils.shareWithQr
import kotlinx.android.synthetic.main.activity_rss_source_edit.* import kotlinx.android.synthetic.main.activity_rss_source_edit.*
import org.jetbrains.anko.displayMetrics import org.jetbrains.anko.*
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast
import kotlin.math.abs import kotlin.math.abs
class RssSourceEditActivity : class RssSourceEditActivity :
VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false), VMBaseActivity<RssSourceEditViewModel>(R.layout.activity_rss_source_edit, false),
ViewTreeObserver.OnGlobalLayoutListener,
KeyboardToolPop.CallBack { KeyboardToolPop.CallBack {
private var mSoftKeyboardTool: PopupWindow? = null private var mSoftKeyboardTool: PopupWindow? = null
private var mIsSoftKeyBoardShowing = false private var mIsSoftKeyBoardShowing = false
private val qrRequestCode = 101
private val adapter = RssSourceEditAdapter() private val adapter = RssSourceEditAdapter()
private val sourceEntities: ArrayList<EditEntity> = ArrayList() private val sourceEntities: ArrayList<EditEntity> = ArrayList()
@ -99,9 +103,10 @@ class RssSourceEditActivity :
clipboard?.setPrimaryClip(ClipData.newPlainText(null, sourceStr)) clipboard?.setPrimaryClip(ClipData.newPlainText(null, sourceStr))
} }
} }
R.id.menu_qr_code_camera -> startActivityForResult<QrCodeActivity>(qrRequestCode)
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) } R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
R.id.menu_share_str -> GSON.toJson(getRssSource())?.let { sourceStr -> R.id.menu_share_str -> GSON.toJson(getRssSource())?.let { sourceStr ->
shareText(getString(R.string.share_rss_source), sourceStr) share(sourceStr)
} }
R.id.menu_share_qr -> GSON.toJson(getRssSource())?.let { sourceStr -> R.id.menu_share_qr -> GSON.toJson(getRssSource())?.let { sourceStr ->
shareWithQr(getString(R.string.share_rss_source), sourceStr) shareWithQr(getString(R.string.share_rss_source), sourceStr)
@ -113,7 +118,7 @@ class RssSourceEditActivity :
private fun initView() { private fun initView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this) mSoftKeyboardTool = KeyboardToolPop(this, AppConst.keyboardToolChars, this)
window.decorView.viewTreeObserver.addOnGlobalLayoutListener(KeyboardOnGlobalChangeListener()) window.decorView.viewTreeObserver.addOnGlobalLayoutListener(this)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)
recycler_view.adapter = adapter recycler_view.adapter = adapter
} }
@ -204,23 +209,34 @@ class RssSourceEditActivity :
mSoftKeyboardTool?.dismiss() mSoftKeyboardTool?.dismiss()
} }
private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() {
override fun onGlobalLayout() { val rect = Rect()
val rect = Rect() // 获取当前页面窗口的显示范围
// 获取当前页面窗口的显示范围 window.decorView.getWindowVisibleDisplayFrame(rect)
window.decorView.getWindowVisibleDisplayFrame(rect) val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels
val screenHeight = this@RssSourceEditActivity.displayMetrics.heightPixels val keyboardHeight = screenHeight - rect.bottom // 输入法的高度
val keyboardHeight = screenHeight - rect.bottom // 输入法的高度 val preShowing = mIsSoftKeyBoardShowing
val preShowing = mIsSoftKeyBoardShowing if (abs(keyboardHeight) > screenHeight / 5) {
if (abs(keyboardHeight) > screenHeight / 5) { mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法
mIsSoftKeyBoardShowing = true // 超过屏幕五分之一则表示弹出了输入法 recycler_view.setPadding(0, 0, 0, 100)
recycler_view.setPadding(0, 0, 0, 100) showKeyboardTopPopupWindow()
showKeyboardTopPopupWindow() } else {
} else { mIsSoftKeyBoardShowing = false
mIsSoftKeyBoardShowing = false recycler_view.setPadding(0, 0, 0, 0)
recycler_view.setPadding(0, 0, 0, 0) if (preShowing) {
if (preShowing) { closePopupWindow()
closePopupWindow() }
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
qrRequestCode -> if (resultCode == RESULT_OK) {
data?.getStringExtra("result")?.let {
viewModel.importSource(it) { source: RssSource ->
upRecyclerView(source)
}
} }
} }
} }

@ -71,4 +71,16 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
} }
} }
} }
fun importSource(text: String, finally: (source: RssSource) -> Unit) {
execute {
val text1 = text.trim()
GSON.fromJsonObject<RssSource>(text1)?.let {
finally.invoke(it)
}
}.onError {
toast(it.localizedMessage ?: "Error")
}
}
} }

@ -50,13 +50,14 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
get() = getViewModel(RssSourceViewModel::class.java) get() = getViewModel(RssSourceViewModel::class.java)
private val importRecordKey = "rssSourceRecordKey" private val importRecordKey = "rssSourceRecordKey"
private val qrRequestCode = 101 private val qrRequestCode = 101
private val importSource = 13141 private val importSource = 124
private lateinit var adapter: RssSourceAdapter private lateinit var adapter: RssSourceAdapter
private var sourceLiveData: LiveData<List<RssSource>>? = null private var sourceLiveData: LiveData<List<RssSource>>? = null
private var groups = hashSetOf<String>() private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
override fun onActivityCreated(savedInstanceState: Bundle?) { override fun onActivityCreated(savedInstanceState: Bundle?) {
initUriScheme()
initRecyclerView() initRecyclerView()
initSearchView() initSearchView()
initLiveDataGroup() initLiveDataGroup()
@ -102,6 +103,22 @@ class RssSourceActivity : VMBaseActivity<RssSourceViewModel>(R.layout.activity_r
return true return true
} }
private fun initUriScheme() {
intent.data?.let {
when (it.path) {
"/importonline" -> it.getQueryParameter("src")?.let { url ->
Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show()
viewModel.importSource(url) { msg ->
title_bar.snackbar(msg)
}
}
else -> {
toast("格式不对")
}
}
}
}
private fun initRecyclerView() { private fun initRecyclerView() {
ATH.applyEdgeEffectColor(recycler_view) ATH.applyEdgeEffectColor(recycler_view)
recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.layoutManager = LinearLayoutManager(this)

@ -20,7 +20,7 @@ class FontAdapter(context: Context, val callBack: CallBack) :
tv_font.typeface = typeface tv_font.typeface = typeface
tv_font.text = item.name tv_font.text = item.name
this.onClick { callBack.onClick(item) } this.onClick { callBack.onClick(item) }
if (item.name == callBack.curFilePath().substringAfterLast(File.separator)) { if (item.name == callBack.curFilePath.substringAfterLast(File.separator)) {
iv_checked.visible() iv_checked.visible()
} else { } else {
iv_checked.invisible() iv_checked.invisible()
@ -38,6 +38,6 @@ class FontAdapter(context: Context, val callBack: CallBack) :
interface CallBack { interface CallBack {
fun onClick(file: File) fun onClick(file: File)
fun curFilePath(): String val curFilePath: String
} }
} }

@ -16,7 +16,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
@ -87,13 +89,14 @@ class FontSelectDialog : BaseDialogFragment(),
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_default -> { R.id.menu_default -> {
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack) val requireContext = requireContext()
cb?.let { requireContext.alert(titleResource = R.string.system_typeface) {
if (it.curFontPath != "") { items(requireContext.resources.getStringArray(R.array.system_typefaces).toList()) { _, i ->
it.selectFile("") AppConfig.systemTypefaces = i
onDefaultFontChange()
dismiss()
} }
} }.show()
dismiss()
} }
R.id.menu_other -> { R.id.menu_other -> {
openFolder() openFolder()
@ -103,46 +106,48 @@ class FontSelectDialog : BaseDialogFragment(),
} }
private fun openFolder() { private fun openFolder() {
alert { launch(Main) {
titleResource = R.string.select_folder alert {
items(resources.getStringArray(R.array.select_folder).toList()) { _, index -> titleResource = R.string.select_folder
when (index) { items(resources.getStringArray(R.array.select_folder).toList()) { _, index ->
0 -> { when (index) {
val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts" 0 -> {
putPrefString(PreferKey.fontFolder, path) val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts"
getFontFilesByPermission(path) putPrefString(PreferKey.fontFolder, path)
} getFontFilesByPermission(path)
1 -> {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, fontFolderRequestCode)
} catch (e: java.lang.Exception) {
e.printStackTrace()
requireContext().toast(e.localizedMessage ?: "ERROR")
} }
} 1 -> {
2 -> { try {
PermissionsCompat.Builder(this@FontSelectDialog) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.addPermissions(*Permissions.Group.STORAGE) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.rationale(R.string.tip_perm_request_storage) startActivityForResult(intent, fontFolderRequestCode)
.onGranted { } catch (e: java.lang.Exception) {
FileChooserDialog.show( e.printStackTrace()
childFragmentManager, requireContext().toast(e.localizedMessage ?: "ERROR")
fontFolderRequestCode,
mode = FileChooserDialog.DIRECTORY
)
} }
.request() }
2 -> {
PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
FileChooserDialog.show(
childFragmentManager,
fontFolderRequestCode,
mode = FileChooserDialog.DIRECTORY
)
}
.request()
}
} }
} }
} }.show()
}.show() }
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun getFontFiles(doc: DocumentFile) { private fun getFontFiles(doc: DocumentFile) {
launch(IO) { execute {
val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri) val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri)
fontCacheFolder.listFiles()?.forEach { fontFile -> fontCacheFolder.listFiles()?.forEach { fontFile ->
var contain = false var contain = false
@ -177,6 +182,8 @@ class FontSelectDialog : BaseDialogFragment(),
} catch (e: Exception) { } catch (e: Exception) {
toast(e.localizedMessage ?: "") toast(e.localizedMessage ?: "")
} }
}.onError {
toast("getFontFiles:${it.localizedMessage}")
} }
} }
@ -204,12 +211,9 @@ class FontSelectDialog : BaseDialogFragment(),
launch(IO) { launch(IO) {
file.copyTo(FileUtils.createFileIfNotExist(fontFolder, file.name), true) file.copyTo(FileUtils.createFileIfNotExist(fontFolder, file.name), true)
.absolutePath.let { path -> .absolutePath.let { path ->
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack) if (curFilePath != path) {
cb?.let { withContext(Main) {
if (it.curFontPath != path) { callBack?.selectFile(path)
withContext(Main) {
it.selectFile(path)
}
} }
} }
} }
@ -217,12 +221,6 @@ class FontSelectDialog : BaseDialogFragment(),
} }
} }
override fun curFilePath(): String {
return (parentFragment as? CallBack)?.curFontPath
?: (activity as? CallBack)?.curFontPath
?: ""
}
override fun onFilePicked(requestCode: Int, currentPath: String) { override fun onFilePicked(requestCode: Int, currentPath: String) {
when (requestCode) { when (requestCode) {
fontFolderRequestCode -> { fontFolderRequestCode -> {
@ -255,6 +253,19 @@ class FontSelectDialog : BaseDialogFragment(),
} }
} }
private fun onDefaultFontChange() {
if (curFilePath == "") {
postEvent(EventBus.UP_CONFIG, true)
} else {
callBack?.selectFile("")
}
}
override val curFilePath: String get() = callBack?.curFontPath ?: ""
private val callBack: CallBack?
get() = (parentFragment as? CallBack) ?: (activity as? CallBack)
interface CallBack { interface CallBack {
fun selectFile(path: String) fun selectFile(path: String)
val curFontPath: String val curFontPath: String

@ -24,7 +24,11 @@ import io.legado.app.utils.sp
import kotlin.math.min import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
class CircleImageView : AppCompatImageView { class CircleImageView(context: Context, attrs: AttributeSet) :
AppCompatImageView(
context,
attrs
) {
private val mDrawableRect = RectF() private val mDrawableRect = RectF()
private val mBorderRect = RectF() private val mBorderRect = RectF()
@ -59,10 +63,9 @@ class CircleImageView : AppCompatImageView {
private var mBorderOverlay: Boolean = false private var mBorderOverlay: Boolean = false
var isDisableCircularTransformation: Boolean = false var isDisableCircularTransformation: Boolean = false
set(disableCircularTransformation) { set(disableCircularTransformation) {
if (isDisableCircularTransformation == disableCircularTransformation) { if (field == disableCircularTransformation) {
return return
} }
field = disableCircularTransformation field = disableCircularTransformation
initializeBitmap() initializeBitmap()
} }
@ -85,7 +88,6 @@ class CircleImageView : AppCompatImageView {
if (circleBackgroundColor == mCircleBackgroundColor) { if (circleBackgroundColor == mCircleBackgroundColor) {
return return
} }
mCircleBackgroundColor = circleBackgroundColor mCircleBackgroundColor = circleBackgroundColor
mCircleBackgroundPaint.color = circleBackgroundColor mCircleBackgroundPaint.color = circleBackgroundColor
invalidate() invalidate()
@ -117,23 +119,14 @@ class CircleImageView : AppCompatImageView {
private var textColor = context.getCompatColor(R.color.tv_text_default) private var textColor = context.getCompatColor(R.color.tv_text_default)
constructor(context: Context) : super(context) { init {
init() super.setScaleType(SCALE_TYPE)
} val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView)
mBorderWidth =
@JvmOverloads a.getDimensionPixelSize(
constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super( R.styleable.CircleImageView_civ_border_width,
context, DEFAULT_BORDER_WIDTH
attrs, )
defStyle
) {
val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0)
mBorderWidth = a.getDimensionPixelSize(
R.styleable.CircleImageView_civ_border_width,
DEFAULT_BORDER_WIDTH
)
mBorderColor = mBorderColor =
a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR) a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR)
mBorderOverlay = mBorderOverlay =
@ -152,11 +145,6 @@ class CircleImageView : AppCompatImageView {
} }
a.recycle() a.recycle()
init()
}
private fun init() {
super.setScaleType(SCALE_TYPE)
mReady = true mReady = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

@ -167,8 +167,14 @@ class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference
dialogIconNames?.let { dialogIconNames?.let {
val resId = context.resources val resId = context.resources
.getIdentifier(it[index].toString(), "mipmap", context.packageName) .getIdentifier(it[index].toString(), "mipmap", context.packageName)
val d = context.getCompatDrawable(resId) val d = try {
icon.setImageDrawable(d) context.getCompatDrawable(resId)
} catch (e: Exception) {
null
}
d?.let {
icon.setImageDrawable(d)
}
} }
label.isChecked = item.toString() == dialogValue label.isChecked = item.toString() == dialogValue
onClick { onClick {

@ -121,7 +121,12 @@ object BitmapUtils {
* @throws IOException * @throws IOException
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun decodeBitmap(context: Context, fileNameInAssets: String, width: Int, height: Int): Bitmap? { fun decodeAssetsBitmap(
context: Context,
fileNameInAssets: String,
width: Int,
height: Int
): Bitmap? {
var inputStream = context.assets.open(fileNameInAssets) var inputStream = context.assets.open(fileNameInAssets)
val op = BitmapFactory.Options() val op = BitmapFactory.Options()

@ -92,17 +92,6 @@ val Context.navigationBarHeight: Int
return resources.getDimensionPixelSize(resourceId) return resources.getDimensionPixelSize(resourceId)
} }
fun Context.shareText(title: String, text: String) {
try {
val textIntent = Intent(Intent.ACTION_SEND)
textIntent.type = "text/plain"
textIntent.putExtra(Intent.EXTRA_TEXT, text)
startActivity(Intent.createChooser(textIntent, title))
} catch (e: Exception) {
toast(R.string.can_not_share)
}
}
@SuppressLint("SetWorldReadable") @SuppressLint("SetWorldReadable")
fun Context.shareWithQr(title: String, text: String) { fun Context.shareWithQr(title: String, text: String) {
QRCodeEncoder.HINTS[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L QRCodeEncoder.HINTS[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.L
@ -144,6 +133,9 @@ fun Context.sendToClip(text: String) {
} }
} }
/**
* 系统是否暗色主题
*/
fun Context.sysIsDarkMode(): Boolean { fun Context.sysIsDarkMode(): Boolean {
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return mode == Configuration.UI_MODE_NIGHT_YES return mode == Configuration.UI_MODE_NIGHT_YES
@ -160,8 +152,12 @@ val Context.sysBattery: Int
} }
fun Context.openUrl(url: String) { fun Context.openUrl(url: String) {
openUrl(Uri.parse(url))
}
fun Context.openUrl(uri: Uri) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url) intent.data = uri
if (intent.resolveActivity(packageManager) != null) { if (intent.resolveActivity(packageManager) != null) {
try { try {
startActivity(intent) startActivity(intent)

@ -48,4 +48,14 @@ fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array<String> = run {
this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()
} }
fun Char?.isHAN(): Boolean {
this ?: return false
val ub: Character.UnicodeBlock = Character.UnicodeBlock.of(this) ?: return false
return ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C
|| ub === Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D
|| ub === Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub === Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT
}

@ -7,8 +7,10 @@ import android.os.Build
import android.view.View import android.view.View
import android.view.View.* import android.view.View.*
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.RadioGroup
import android.widget.SeekBar import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.get
import io.legado.app.App import io.legado.app.App
@ -69,4 +71,26 @@ fun View.screenshot(): Bitmap? {
fun SeekBar.progressAdd(int: Int) { fun SeekBar.progressAdd(int: Int) {
progress += int progress += int
}
fun RadioGroup.getIndexById(id: Int): Int {
for (i in 0 until this.childCount) {
if (id == get(i).id) {
return i
}
}
return 0
}
fun RadioGroup.getCheckedIndex(): Int {
for (i in 0 until this.childCount) {
if (checkedRadioButtonId == get(i).id) {
return i
}
}
return 0
}
fun RadioGroup.checkByIndex(index: Int) {
check(get(index).id)
} }

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/ll_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintRight_toLeftOf="@+id/ll_sort"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<io.legado.app.ui.widget.text.AccentTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
android:text="@string/view"
android:textSize="16sp" />
<RadioGroup
android:id="@+id/rg_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/layout_list" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/layout_grid3" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/layout_grid4" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/layout_grid5" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/layout_grid6" />
</RadioGroup>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_sort"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toRightOf="@+id/ll_layout"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent">
<io.legado.app.ui.widget.text.AccentTextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
android:text="@string/sort"
android:textSize="16sp" />
<RadioGroup
android:id="@+id/rg_sort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/bookshelf_px_0" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/bookshelf_px_1" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/bookshelf_px_2" />
<RadioButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/bookshelf_px_3" />
</RadioGroup>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -7,6 +7,7 @@
android:id="@+id/number_picker" android:id="@+id/number_picker"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center"
android:scrollbars="none" />
</FrameLayout> </FrameLayout>

@ -269,6 +269,7 @@
android:id="@+id/bg0" android:id="@+id/bg0"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text"
@ -281,6 +282,7 @@
android:id="@+id/bg1" android:id="@+id/bg1"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text"
@ -293,6 +295,7 @@
android:id="@+id/bg2" android:id="@+id/bg2"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text"
@ -305,6 +308,7 @@
android:id="@+id/bg3" android:id="@+id/bg3"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text"
@ -317,6 +321,7 @@
android:id="@+id/bg4" android:id="@+id/bg4"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:src="@drawable/image_cover_default"
app:civ_border_color="@color/tv_text_default" app:civ_border_color="@color/tv_text_default"
app:civ_border_width="1dp" app:civ_border_width="1dp"
app:text="@string/text" app:text="@string/text"

@ -1,8 +1,27 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" <androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/shape_card_view" android:background="@drawable/shape_card_view"
android:padding="5dp" /> android:padding="5dp"
android:orientation="horizontal">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_menu_more"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_more_vert"
android:tint="@color/tv_text_default"
android:visibility="gone"
android:contentDescription="@string/more_menu"
android:layout_gravity="center_vertical" />
</androidx.appcompat.widget.LinearLayoutCompat>

@ -13,4 +13,7 @@
android:id="@+id/menu_browser" android:id="@+id/menu_browser"
android:title="@string/browser" /> android:title="@string/browser" />
<item
android:id="@+id/menu_share_str"
android:title="@string/share" />
</menu> </menu>

@ -35,14 +35,6 @@
<item>9</item> <item>9</item>
</string-array> </string-array>
<string-array name="bookshelf_layout">
<item>@string/layout_list</item>
<item>@string/layout_grid3</item>
<item>@string/layout_grid4</item>
<item>@string/layout_grid5</item>
<item>@string/layout_grid6</item>
</string-array>
<string-array name="indent"> <string-array name="indent">
<item>@string/indent_0</item> <item>@string/indent_0</item>
<item>@string/indent_1</item> <item>@string/indent_1</item>
@ -111,18 +103,6 @@
<item>@string/screen_sensor</item> <item>@string/screen_sensor</item>
</string-array> </string-array>
<string-array name="bookshelf_px_title">
<item>@string/bookshelf_px_0</item>
<item>@string/bookshelf_px_1</item>
<item>@string/bookshelf_px_2</item>
</string-array>
<string-array name="bookshelf_px_value">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<declare-styleable name="Battery"> <declare-styleable name="Battery">
<attr name="batteryOrientation"> <attr name="batteryOrientation">
<enum name="horizontal" value="0" /> <enum name="horizontal" value="0" />
@ -152,4 +132,9 @@
<item>简体转繁体</item> <item>简体转繁体</item>
</string-array> </string-array>
<string-array name="system_typefaces">
<item>系统默认字体</item>
<item>系统衬线字体</item>
<item>系统等宽字体</item>
</string-array>
</resources> </resources>

@ -47,12 +47,13 @@
<string name="bookshelf_empty">书架还空着,先去添加吧!</string> <string name="bookshelf_empty">书架还空着,先去添加吧!</string>
<string name="action_search">搜索</string> <string name="action_search">搜索</string>
<string name="action_download">下载</string> <string name="action_download">下载</string>
<string name="layout_list">列表视图</string> <string name="layout_list">列表</string>
<string name="layout_grid3">网格视图三列</string> <string name="layout_grid3">网格三列</string>
<string name="layout_grid4">网格视图四列</string> <string name="layout_grid4">网格四列</string>
<string name="layout_grid5">网格视图五列</string> <string name="layout_grid5">网格五列</string>
<string name="layout_grid6">网格视图六列</string> <string name="layout_grid6">网格六列</string>
<string name="bookshelf_layout">书架布局</string> <string name="bookshelf_layout">书架布局</string>
<string name="view">视图</string>
<string name="book_library">书城</string> <string name="book_library">书城</string>
<string name="book_local">添加本地</string> <string name="book_local">添加本地</string>
<string name="book_source">书源</string> <string name="book_source">书源</string>
@ -192,7 +193,7 @@
<string name="img_cover">封面</string> <string name="img_cover">封面</string>
<string name="book"></string> <string name="book"></string>
<string name="volume_key_page">音量键翻页</string> <string name="volume_key_page">音量键翻页</string>
<string name="click_open_page">点击翻页</string> <string name="click_turn_page">点击翻页</string>
<string name="click_all_next_page">点击总是翻下一页</string> <string name="click_all_next_page">点击总是翻下一页</string>
<string name="page_anim">翻页动画</string> <string name="page_anim">翻页动画</string>
<string name="keep_light">屏幕超时</string> <string name="keep_light">屏幕超时</string>
@ -226,11 +227,11 @@
<string name="origin_show">来源:%s</string> <string name="origin_show">来源:%s</string>
<string name="import_replace_rule">本地导入</string> <string name="import_replace_rule">本地导入</string>
<string name="import_replace_rule_on_line">网络导入</string> <string name="import_replace_rule_on_line">网络导入</string>
<string name="bookshelf_px">书架排序</string>
<string name="check_update_interval">检查更新间隔</string> <string name="check_update_interval">检查更新间隔</string>
<string name="bookshelf_px_0">按阅读时间排序</string> <string name="bookshelf_px_0">按阅读时间</string>
<string name="bookshelf_px_1">按更新时间排序</string> <string name="bookshelf_px_1">按更新时间</string>
<string name="bookshelf_px_2">手动排序</string> <string name="bookshelf_px_2">按书名</string>
<string name="bookshelf_px_3">手动排序</string>
<string name="read_type">阅读方式</string> <string name="read_type">阅读方式</string>
<string name="del_select">删除所选</string> <string name="del_select">删除所选</string>
<string name="del_msg">是否确认删除?</string> <string name="del_msg">是否确认删除?</string>
@ -627,4 +628,5 @@
<string name="reduce"></string> <string name="reduce"></string>
<string name="plus"></string> <string name="plus"></string>
<string name="other_aloud_setting">其它朗读设置</string> <string name="other_aloud_setting">其它朗读设置</string>
<string name="system_typeface">系统内置字体样式</string>
</resources> </resources>

@ -28,6 +28,12 @@
android:key="volumeKeyPage" android:key="volumeKeyPage"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="true"
android:title="@string/click_turn_page"
android:key="clickTurnPage"
app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.SwitchPreference <io.legado.app.ui.widget.prefs.SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:title="@string/click_all_next_page" android:title="@string/click_all_next_page"

Loading…
Cancel
Save