Merge pull request #1 from gedoor/master

update fork
pull/349/head
Gladtbam 5 years ago committed by GitHub
commit 0aa6ee9803
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 62
      app/src/main/assets/httpTTS.json
  2. 8
      app/src/main/assets/readConfig.json
  3. 47
      app/src/main/assets/updateLog.md
  4. 5
      app/src/main/java/io/legado/app/App.kt
  5. 11
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  6. 5
      app/src/main/java/io/legado/app/base/VMBaseActivity.kt
  7. 10
      app/src/main/java/io/legado/app/constant/AppPattern.kt
  8. 2
      app/src/main/java/io/legado/app/constant/EventBus.kt
  9. 3
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  10. 9
      app/src/main/java/io/legado/app/constant/Theme.kt
  11. 25
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  12. 31
      app/src/main/java/io/legado/app/data/dao/HttpTTSDao.kt
  13. 14
      app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt
  14. 5
      app/src/main/java/io/legado/app/data/entities/Book.kt
  15. 12
      app/src/main/java/io/legado/app/data/entities/HttpTTS.kt
  16. 5
      app/src/main/java/io/legado/app/data/entities/ReadRecord.kt
  17. 6
      app/src/main/java/io/legado/app/data/entities/ReadRecordShow.kt
  18. 3
      app/src/main/java/io/legado/app/help/AppConfig.kt
  19. 52
      app/src/main/java/io/legado/app/help/BookHelp.kt
  20. 19
      app/src/main/java/io/legado/app/help/DefaultValueHelp.kt
  21. 3
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  22. 10
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  23. 13
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  24. 8
      app/src/main/java/io/legado/app/help/http/CookieStore.kt
  25. 69
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  26. 12
      app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt
  27. 8
      app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt
  28. 2
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  29. 25
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  30. 12
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  31. 11
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  32. 16
      app/src/main/java/io/legado/app/lib/theme/view/ATERadioNoButton.kt
  33. 2
      app/src/main/java/io/legado/app/model/Debug.kt
  34. 93
      app/src/main/java/io/legado/app/model/SearchBookModel.kt
  35. 2
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  36. 165
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  37. 2
      app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt
  38. 2
      app/src/main/java/io/legado/app/model/rss/Rss.kt
  39. 14
      app/src/main/java/io/legado/app/model/webBook/BookInfo.kt
  40. 5
      app/src/main/java/io/legado/app/model/webBook/BookList.kt
  41. 117
      app/src/main/java/io/legado/app/model/webBook/SearchBookModel.kt
  42. 12
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  43. 1
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  44. 2
      app/src/main/java/io/legado/app/service/DownloadService.kt
  45. 48
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  46. 2
      app/src/main/java/io/legado/app/service/help/AudioPlay.kt
  47. 2
      app/src/main/java/io/legado/app/service/help/CheckSource.kt
  48. 17
      app/src/main/java/io/legado/app/service/help/ReadAloud.kt
  49. 14
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  50. 5
      app/src/main/java/io/legado/app/ui/about/AboutFragment.kt
  51. 35
      app/src/main/java/io/legado/app/ui/about/ReadRecordActivity.kt
  52. 5
      app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt
  53. 5
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceActivity.kt
  54. 5
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleActivity.kt
  55. 5
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceActivity.kt
  56. 7
      app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt
  57. 2
      app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt
  58. 5
      app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt
  59. 11
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt
  60. 78
      app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverViewModel.kt
  61. 14
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  62. 85
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt
  63. 5
      app/src/main/java/io/legado/app/ui/book/chapterlist/BookmarkFragment.kt
  64. 28
      app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListAdapter.kt
  65. 8
      app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt
  66. 5
      app/src/main/java/io/legado/app/ui/book/download/DownloadActivity.kt
  67. 3
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowActivity.kt
  68. 2
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  69. 10
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  70. 10
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  71. 17
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  72. 10
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  73. 3
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  74. 3
      app/src/main/java/io/legado/app/ui/book/local/ImportBookActivity.kt
  75. 39
      app/src/main/java/io/legado/app/ui/book/read/Help.kt
  76. 17
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  77. 15
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  78. 8
      app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt
  79. 9
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  80. 7
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  81. 4
      app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt
  82. 38
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt
  83. 8
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  84. 12
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  85. 164
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineDialog.kt
  86. 34
      app/src/main/java/io/legado/app/ui/book/read/config/SpeakEngineViewModel.kt
  87. 12
      app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt
  88. 10
      app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt
  89. 6
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  90. 14
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  91. 10
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  92. 2
      app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt
  93. 16
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt
  94. 26
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt
  95. 13
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  96. 19
      app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt
  97. 7
      app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt
  98. 21
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  99. 5
      app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt
  100. 8
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,62 @@
[
{
"id": 1598233029304,
"name": "度小美",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=0&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029305,
"name": "度小宇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=1&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029306,
"name": "度逍遥",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=3&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029307,
"name": "度丫丫",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=4&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029308,
"name": "度小娇",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=5&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029309,
"name": "度米朵",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=103&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029310,
"name": "度博文",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=106&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029311,
"name": "度小童",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=110&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029312,
"name": "度小萌",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=111&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029313,
"name": "百度骚男",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=11&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029314,
"name": "百度评书",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=6&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
},
{
"id": 1598233029315,
"name": "百度主持",
"url": "http://tts.baidu.com/text2audio,{\n \"method\": \"POST\",\n \"body\": \"tex={{java.encodeURI(java.encodeURI(speakText))}}&spd={{String((speakSpeed + 5) / 10 + 4)}}&per=9&cuid=baidu_speech_demo&idx=1&cod=2&lan=zh&ctp=1&pdt=1&vol=5&pit=5&_res_tag_=audio\"\n}"
}
]

@ -1,9 +1,9 @@
[
{
"bgStr": "#EBD9BB",
"bgStrNight": "#1E2021",
"textColor": "#63543C",
"textColorNight": "#DCDFE1",
"bgStr": "#FFFFFF",
"bgStrNight": "#000000",
"textColor": "#000000",
"textColorNight": "#FFFFFF",
"bgType": 0,
"bgTypeNight": 0,
"darkStatusIcon": true,

@ -3,11 +3,56 @@
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
- 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/08/23**
**2020/08/29**
* 修复一个null引起的崩溃bug
* 修复我的界面滚动时图标消失的bug
* 修复从详情页目录打开章节内容不对的bug
* 书源选择增加滑动选择, 选择框区域滑动时进行选择 by [Mupceet](https://github.com/Mupceet)
* 请求头,支持http代理,socks4 socks5代理设置 by [10bits](https://github.com/10bits)
```
socks5代理
{
"proxy":"socks5://127.0.0.1:1080"
}
http代理
{
"proxy":"http://127.0.0.1:1080"
}
支持代理服务器验证
{
"proxy":"socks5://127.0.0.1:1080@用户名@密码"
}
注意:这些请求头是无意义的,会被忽略掉
```
**2020/08/28**
* 修复一些bug
* 换源不再改变书名和作者,防止换到一些错误的书源后不能再换源
**2020/08/27**
* 修复主题bug
* 修复封面bug
* 优化书籍更新,搜索,换源
* e-ink模式不再固定背景
**2020/08/26**
* js添加java.encodeURI(speakText),用于解决js编码时有~的语句朗读不出来
* 修复书名太长删除阅读记录按钮不显示的bug
* 完成本地书籍编码选择
**2020/08/25**
* 阅读记录可以删除了
* 修复翻页模式选择颜色问题
* 修复toolbar在一些情况下文字颜色不对的bug
* 多设备阅读记录叠加
* 封面链接支持修改headers
**2020/08/24**
* 应用被杀死时停止朗读
* 默认封面添加删除操作
* 备份阅读记录
* 书源添加移除分组支持多选,多个分组以逗号(中英均可)隔开
* 可以自定义在线朗读了
**2020/08/22**
* 添加阅读时间记录

@ -6,6 +6,7 @@ import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDexApplication
@ -34,14 +35,16 @@ class App : MultiDexApplication() {
@JvmStatic
lateinit var db: AppDatabase
private set
}
lateinit var androidId: String
var versionCode = 0
var versionName = ""
}
override fun onCreate() {
super.onCreate()
INSTANCE = this
androidId = Settings.System.getString(contentResolver, Settings.Secure.ANDROID_ID)
CrashHandler().init(this)
LanguageUtils.setConfigurationOld(this)
db = AppDatabase.createDatabase(INSTANCE)

@ -25,6 +25,7 @@ abstract class BaseActivity(
private val layoutID: Int,
private val fullScreen: Boolean = true,
private val theme: Theme = Theme.Auto,
private val toolBarTheme: Theme = Theme.Auto,
private val transparent: Boolean = false
) : AppCompatActivity(),
CoroutineScope by MainScope() {
@ -65,7 +66,7 @@ abstract class BaseActivity(
final override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return menu?.let {
val bool = onCompatCreateOptionsMenu(it)
it.applyTint(this, theme)
it.applyTint(this, toolBarTheme)
bool
} ?: super.onCreateOptionsMenu(menu)
}
@ -129,10 +130,10 @@ abstract class BaseActivity(
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
}
ATH.setStatusBarColorAuto(this, fullScreen)
if (theme == Theme.Dark) {
ATH.setLightStatusBar(this, false)
} else if (theme == Theme.Light) {
ATH.setLightStatusBar(this, true)
if (toolBarTheme == Theme.Dark) {
ATH.setLightStatusBar(window, false)
} else if (toolBarTheme == Theme.Light) {
ATH.setLightStatusBar(window, true)
}
upNavigationBarColor()
}

@ -6,8 +6,9 @@ import io.legado.app.constant.Theme
abstract class VMBaseActivity<VM : ViewModel>(
layoutID: Int,
fullScreen: Boolean = true,
theme: Theme = Theme.Auto
) : BaseActivity(layoutID, fullScreen, theme) {
theme: Theme = Theme.Auto,
toolBarTheme: Theme = Theme.Auto
) : BaseActivity(layoutID, fullScreen, theme, toolBarTheme) {
protected abstract val viewModel: VM

@ -3,8 +3,14 @@ package io.legado.app.constant
import java.util.regex.Pattern
object AppPattern {
val JS_PATTERN: Pattern = Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val JS_PATTERN: Pattern =
Pattern.compile("(<js>[\\w\\W]*?</js>|@js:[\\w\\W]*$)", Pattern.CASE_INSENSITIVE)
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
val imgPattern: Pattern =
Pattern.compile("<img .*?src.*?=.*?\"(.*?(?:,\\{.*\\})?)\".*?>", Pattern.CASE_INSENSITIVE)
val authorRegex = "\\s*者\\s*[::]".toRegex()
val nameRegex = Regex("\\s+作\\s*者.*")
val authorRegex = Regex(".*?作\\s*?者[::]")
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
val splitGroupRegex = Regex("[,;]")
}

@ -3,7 +3,7 @@ package io.legado.app.constant
object EventBus {
const val MEDIA_BUTTON = "mediaButton"
const val RECREATE = "RECREATE"
const val UP_BOOK = "sourceDebugLog"
const val UP_BOOK = "upBookToc"
const val ALOUD_STATE = "aloud_state"
const val TTS_PROGRESS = "ttsStart"
const val TTS_DS = "ttsDs"

@ -9,10 +9,9 @@ object PreferKey {
const val clickAllNext = "clickAllNext"
const val hideNavigationBar = "hideNavigationBar"
const val precisionSearch = "precisionSearch"
const val readAloudOnLine = "readAloudOnLine"
const val speakEngine = "speakEngine"
const val readAloudByPage = "readAloudByPage"
const val ttsSpeechRate = "ttsSpeechRate"
const val ttsSpeechPer = "ttsSpeechPer"
const val prevKey = "prevKeyCode"
const val nextKey = "nextKeyCode"
const val showRss = "showRss"

@ -1,6 +1,7 @@
package io.legado.app.constant
import io.legado.app.help.AppConfig
import io.legado.app.utils.ColorUtils
enum class Theme {
Dark, Light, Auto, Transparent;
@ -11,5 +12,13 @@ enum class Theme {
Dark
} else Light
}
fun getTheme(backgroundColor: Int): Theme {
return if (ColorUtils.isColorLight(backgroundColor)) {
Light
} else {
Dark
}
}
}
}

@ -6,6 +6,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.App
import io.legado.app.data.dao.*
import io.legado.app.data.entities.*
@ -14,8 +15,8 @@ import io.legado.app.data.entities.*
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class],
version = 17,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class],
version = 19,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -33,7 +34,9 @@ abstract class AppDatabase : RoomDatabase() {
migration_12_13,
migration_13_14,
migration_14_15,
migration_15_17
migration_15_17,
migration_17_18,
migration_18_19
)
.allowMainThreadQueries()
.build()
@ -94,6 +97,21 @@ abstract class AppDatabase : RoomDatabase() {
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
}
}
private val migration_17_18 = object : Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
private val migration_18_19 = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`androidId`, `bookName`))")
database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${App.androidId}' as androidId, bookName, readTime from readRecord")
database.execSQL("DROP TABLE readRecord")
database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
}
}
}
abstract fun bookDao(): BookDao
@ -110,4 +128,5 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun cookieDao(): CookieDao
abstract fun txtTocRule(): TxtTocRuleDao
abstract fun readRecordDao(): ReadRecordDao
abstract fun httpTTSDao(): HttpTTSDao
}

@ -0,0 +1,31 @@
package io.legado.app.data.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import io.legado.app.data.entities.HttpTTS
@Dao
interface HttpTTSDao {
@get:Query("select * from httpTTS order by name")
val all: List<HttpTTS>
@Query("select * from httpTTS order by name")
fun observeAll(): LiveData<List<HttpTTS>>
@get:Query("select count(*) from httpTTS")
val count: Int
@Query("select * from httpTTS where id = :id")
fun get(id: Long): HttpTTS?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg httpTTS: HttpTTS)
@Delete
fun delete(vararg httpTTS: HttpTTS)
@Update
fun update(vararg httpTTS: HttpTTS)
}

@ -2,6 +2,7 @@ package io.legado.app.data.dao
import androidx.room.*
import io.legado.app.data.entities.ReadRecord
import io.legado.app.data.entities.ReadRecordShow
@Dao
interface ReadRecordDao {
@ -9,16 +10,27 @@ interface ReadRecordDao {
@get:Query("select * from readRecord")
val all: List<ReadRecord>
@get:Query("select bookName, sum(readTime) as readTime from readRecord group by bookName order by bookName")
val allShow: List<ReadRecordShow>
@get:Query("select sum(readTime) from readRecord")
val allTime: Long
@Query("select readTime from readRecord where bookName = :bookName")
@Query("select sum(readTime) from readRecord where bookName = :bookName")
fun getReadTime(bookName: String): Long?
@Query("select readTime from readRecord where androidId = :androidId and bookName = :bookName")
fun getReadTime(androidId: String, bookName: String): Long?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg readRecord: ReadRecord)
@Update
fun update(vararg record: ReadRecord)
@Delete
fun delete(vararg record: ReadRecord)
@Query("delete from readRecord where bookName = :bookName")
fun deleteByName(bookName: String)
}

@ -11,6 +11,7 @@ import io.legado.app.constant.BookType
import io.legado.app.help.AppConfig
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.fromJsonObject
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
@ -115,6 +116,10 @@ data class Book(
return charset(charset ?: "UTF-8")
}
fun getFolderName(): String {
return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl)
}
fun toSearchBook(): SearchBook {
return SearchBook(
name = name,

@ -0,0 +1,12 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "httpTTS")
data class HttpTTS(
@PrimaryKey
val id: Long = System.currentTimeMillis(),
var name: String = "",
var url: String = ""
)

@ -1,11 +1,10 @@
package io.legado.app.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "readRecord")
@Entity(tableName = "readRecord", primaryKeys = ["androidId", "bookName"])
data class ReadRecord(
@PrimaryKey
var androidId: String = "",
var bookName: String = "",
var readTime: Long = 0L
)

@ -0,0 +1,6 @@
package io.legado.app.data.entities
data class ReadRecordShow(
var bookName: String = "",
var readTime: Long = 0L
)

@ -81,9 +81,6 @@ object AppConfig {
App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value)
}
val ttsSpeechPer: String
get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0"
val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false)
var chineseConverterType: Int

@ -2,6 +2,7 @@ package io.legado.app.help
import com.hankcs.hanlp.HanLP
import io.legado.app.App
import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@ -16,19 +17,12 @@ import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.JaccardSimilarity
import org.jetbrains.anko.toast
import java.io.File
import java.util.regex.Pattern
import kotlin.math.min
object BookHelp {
private const val cacheFolderName = "book_cache"
private const val cacheImageFolderName = "images"
private val downloadDir: File = App.INSTANCE.externalFilesDir
private val srcPattern =
Pattern.compile("<img .*?src.*?=.*?\"(.*?(?:,\\{.*\\})?)\".*?>", Pattern.CASE_INSENSITIVE)
fun bookFolderName(book: Book): String {
return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
}
fun formatChapterName(bookChapter: BookChapter): String {
return String.format(
@ -51,7 +45,7 @@ object BookHelp {
Coroutine.async {
val bookFolderNames = arrayListOf<String>()
App.db.bookDao().all.forEach {
bookFolderNames.add(bookFolderName(it))
bookFolderNames.add(it.getFolderName())
}
val file = FileUtils.getDirFile(downloadDir, cacheFolderName)
file.listFiles()?.forEach { bookFile ->
@ -62,18 +56,17 @@ object BookHelp {
}
}
@Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return
//保存文本
FileUtils.createFileIfNotExist(
downloadDir,
formatChapterName(bookChapter),
subDirs = arrayOf(cacheFolderName, bookFolderName(book))
subDirs = arrayOf(cacheFolderName, book.getFolderName())
).writeText(content)
//保存图片
content.split("\n").forEach {
val matcher = srcPattern.matcher(it)
val matcher = AppPattern.imgPattern.matcher(it)
if (matcher.find()) {
var src = matcher.group(1)
src = NetworkUtils.getAbsoluteURL(bookChapter.url, src)
@ -86,12 +79,12 @@ object BookHelp {
}
fun saveImage(book: Book, src:String) {
val analyzeUrl = AnalyzeUrl(src, null, null, null, null)
val analyzeUrl = AnalyzeUrl(src)
analyzeUrl.getImageBytes(book.origin)?.let {
FileUtils.createFileIfNotExist(
downloadDir,
"${MD5Utils.md5Encode16(src)}${src.substringAfterLast(".").substringBefore(",")}",
subDirs = arrayOf(cacheFolderName, bookFolderName(book), cacheImageFolderName)
"${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}",
subDirs = arrayOf(cacheFolderName, book.getFolderName(), cacheImageFolderName)
).writeBytes(it)
}
}
@ -99,16 +92,27 @@ object BookHelp {
fun getImage(book: Book, src: String): File {
return FileUtils.getFile(
downloadDir,
"${MD5Utils.md5Encode16(src)}${src.substringAfterLast(".").substringBefore(",")}",
subDirs = arrayOf(cacheFolderName, bookFolderName(book), cacheImageFolderName)
"${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}",
subDirs = arrayOf(cacheFolderName, book.getFolderName(), cacheImageFolderName)
)
}
private fun getImageSuffix(src: String): String {
var suffix = src.substringAfterLast(".").substringBefore(",")
if (suffix.length > 5) {
suffix = ".jpg"
}
return suffix
}
fun getChapterFiles(book: Book): List<String> {
val fileNameList = arrayListOf<String>()
if (book.isLocalBook()) {
return fileNameList
}
FileUtils.createFolderIfNotExist(
downloadDir,
subDirs = arrayOf(cacheFolderName, bookFolderName(book))
subDirs = arrayOf(cacheFolderName, book.getFolderName())
).list()?.let {
fileNameList.addAll(it)
}
@ -122,7 +126,7 @@ object BookHelp {
FileUtils.exists(
downloadDir,
formatChapterName(bookChapter),
subDirs = arrayOf(cacheFolderName, bookFolderName(book))
subDirs = arrayOf(cacheFolderName, book.getFolderName())
)
}
}
@ -134,7 +138,7 @@ object BookHelp {
val file = FileUtils.getFile(
downloadDir,
formatChapterName(bookChapter),
subDirs = arrayOf(cacheFolderName, bookFolderName(book))
subDirs = arrayOf(cacheFolderName, book.getFolderName())
)
if (file.exists()) {
return file.readText()
@ -150,24 +154,20 @@ object BookHelp {
FileUtils.createFileIfNotExist(
downloadDir,
formatChapterName(bookChapter),
subDirs = arrayOf(cacheFolderName, bookFolderName(book))
subDirs = arrayOf(cacheFolderName, book.getFolderName())
).delete()
}
}
private fun formatFolderName(folderName: String): String {
return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "")
}
fun formatBookName(name: String): String {
return name
.replace("\\s+作\\s*者.*".toRegex(), "")
.replace(AppPattern.nameRegex, "")
.trim { it <= ' ' }
}
fun formatBookAuthor(author: String): String {
return author
.replace(".*?作\\s*?者[::]".toRegex(), "")
.replace(AppPattern.authorRegex, "")
.trim { it <= ' ' }
}

@ -0,0 +1,19 @@
package io.legado.app.help
import io.legado.app.App
import io.legado.app.data.entities.HttpTTS
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
object DefaultValueHelp {
fun initHttpTTS() {
val json = String(App.INSTANCE.assets.open("httpTTS.json").readBytes())
GSON.fromJsonArray<HttpTTS>(json)?.let {
App.db.httpTTSDao().insert(*it.toTypedArray())
}
}
}

@ -9,6 +9,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isContentPath
import io.legado.app.model.analyzeRule.AnalyzeUrl
import java.io.File
object ImageLoader {
@ -19,7 +20,7 @@ object ImageLoader {
fun load(context: Context, path: String?): RequestBuilder<Drawable> {
return when {
path.isNullOrEmpty() -> Glide.with(context).load(path)
path.isAbsUrl() -> Glide.with(context).load(path)
path.isAbsUrl() -> Glide.with(context).load(AnalyzeUrl(path).getGlideUrl())
path.isContentPath() -> Glide.with(context).load(Uri.parse(path))
else -> try {
Glide.with(context).load(File(path))

@ -6,6 +6,7 @@ import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.MD5Utils
import java.net.URLEncoder
import java.util.*
@Keep
@ -52,10 +53,19 @@ interface JsExtensions {
fun timeFormat(time: Long): String {
return dateFormat.format(Date(time))
}
//utf8编码转gbk编码
fun utf8ToGbk(str: String): String {
val utf8 = String(str.toByteArray(charset("UTF-8")))
val unicode = String(utf8.toByteArray(), charset("UTF-8"))
return String(unicode.toByteArray(charset("GBK")))
}
fun encodeURI(str: String): String {
return try {
URLEncoder.encode(str, "UTF-8")
} catch (e: Exception) {
""
}
}
}

@ -29,13 +29,7 @@ object ReadBookConfig {
var bg: Drawable? = null
var bgMeanColor: Int = 0
val textColor: Int
get() =
if (AppConfig.isEInkMode && !AppConfig.isNightTheme) {
Color.BLACK
} else {
durConfig.textColor()
}
val textColor: Int get() = durConfig.textColor()
init {
upConfig()
@ -77,10 +71,6 @@ object ReadBookConfig {
val dm = resources.displayMetrics
val width = dm.widthPixels
val height = dm.heightPixels
if (AppConfig.isEInkMode && !AppConfig.isNightTheme) {
bg = ColorDrawable(Color.WHITE)
bgMeanColor = Color.WHITE
} else {
bg = durConfig.bgDrawable(width, height).apply {
if (this is BitmapDrawable) {
bgMeanColor = BitmapUtils.getMeanColor(bitmap)
@ -88,7 +78,6 @@ object ReadBookConfig {
bgMeanColor = color
}
}
}
isScroll = pageAnim == 3
}

@ -6,13 +6,13 @@ import com.franmontiel.persistentcookiejar.persistence.SerializableCookie
import io.legado.app.App
import io.legado.app.data.entities.Cookie
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.NetworkUtils
object CookieStore : CookiePersistor {
fun setCookie(url: String, cookie: String?) {
Coroutine.async {
val cookieBean = Cookie(url, cookie ?: "")
val cookieBean = Cookie(NetworkUtils.getSubDomain(url), cookie ?: "")
App.db.cookieDao().insert(cookieBean)
}
}
@ -33,12 +33,12 @@ object CookieStore : CookiePersistor {
}
fun getCookie(url: String): String {
val cookieBean = App.db.cookieDao().get(url)
val cookieBean = App.db.cookieDao().get(NetworkUtils.getSubDomain(url))
return cookieBean?.cookie ?: ""
}
fun removeCookie(url: String) {
App.db.cookieDao().delete(url)
App.db.cookieDao().delete(NetworkUtils.getSubDomain(url))
}
private fun cookieToMap(cookie: String): MutableMap<String, String> {

@ -4,11 +4,10 @@ import io.legado.app.constant.AppConst
import io.legado.app.help.http.api.HttpGetApi
import io.legado.app.utils.NetworkUtils
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ConnectionSpec
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.*
import retrofit2.Retrofit
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
@ -83,6 +82,18 @@ object HttpHelper {
return getRetrofit(baseUrl, encode).create(T::class.java)
}
inline fun <reified T> getApiServiceWithProxy(
baseUrl: String,
encode: String? = null,
proxy: String? = null
): T {
return getRetrofitWithProxy(baseUrl, encode, proxy).create(T::class.java)
}
inline fun <reified T> getBytesApiService(baseUrl: String): T {
return getByteRetrofit(baseUrl).create(T::class.java)
}
fun getRetrofit(baseUrl: String, encode: String? = null): Retrofit {
return Retrofit.Builder().baseUrl(baseUrl)
//增加返回值为字符串的支持(以实体类返回)
@ -91,6 +102,56 @@ object HttpHelper {
.build()
}
fun getRetrofitWithProxy(
baseUrl: String,
encode: String? = null,
proxy: String? = null
): Retrofit {
val r = Regex("(http|socks4|socks5)://(.*):(\\d{2,5})(@.*@.*)?")
val ms = proxy?.let { r.findAll(it) };
val group = ms?.first()
var type = "direct" //直接连接
var host = "127.0.0.1" //代理服务器hostname
var port = 1080 //代理服务器port
var username = "" //代理服务器验证用户名
var password = "" //代理服务器验证密码
if (group != null) {
type = if (group.groupValues[1] == "http") {
"http"
} else {
"socks"
}
host = group.groupValues[2]
port = group.groupValues[3].toInt()
if (group.groupValues[4] != "") {
username = group.groupValues[4].split("@")[1]
password = group.groupValues[4].split("@")[2]
}
}
val builder = client.newBuilder()
if (type != "direct" && host != "") {
if (type == "http") {
builder.proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)));
} else {
builder.proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)));
}
if (username != "" && password != "") {
builder.proxyAuthenticator { _, response -> //设置代理服务器账号密码
val credential: String = Credentials.basic(username, password)
response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build()
}
}
}
return Retrofit.Builder().baseUrl(baseUrl)
//增加返回值为字符串的支持(以实体类返回)
.addConverterFactory(EncodeConverter(encode))
.client(builder.build())
.build()
}
fun getByteRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder().baseUrl(baseUrl)
.addConverterFactory(ByteConverter())

@ -32,6 +32,12 @@ interface HttpGetApi {
@HeaderMap headers: Map<String, String>
): Call<String>
@GET
fun getByte(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
@GET
fun getMap(
@Url url: String,
@ -46,6 +52,12 @@ interface HttpGetApi {
@HeaderMap headers: Map<String, String>
): Call<ByteArray>
@GET
suspend fun getByteAsync(
@Url url: String,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
@GET
suspend fun getMapByteAsync(
@Url url: String,

@ -49,4 +49,12 @@ interface HttpPostApi {
@FieldMap(encoded = true) fieldMap: Map<String, String>,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
@POST
suspend fun postBodyByteAsync(
@Url url: String,
@Body body: RequestBody,
@HeaderMap headers: Map<String, String>
): Response<ByteArray>
}

@ -32,6 +32,7 @@ object Backup {
"replaceRule.json",
"txtTocRule.json",
"readRecord.json",
"httpTTS.json",
ReadBookConfig.readConfigFileName,
"config.xml"
)
@ -59,6 +60,7 @@ object Backup {
writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath)
writeListToJson(App.db.txtTocRule().all, "txtTocRule.json", backupPath)
writeListToJson(App.db.readRecordDao().all, "readRecord.json", backupPath)
writeListToJson(App.db.httpTTSDao().all, "httpTTS.json", backupPath)
GSON.toJson(ReadBookConfig.configList)?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it)

@ -39,7 +39,8 @@ object Restore {
"readConfig",
PreferKey.themeMode,
PreferKey.bookshelfLayout,
PreferKey.showRss
PreferKey.showRss,
PreferKey.threadCount
)
//忽略标题
@ -47,7 +48,8 @@ object Restore {
App.INSTANCE.getString(R.string.read_config),
App.INSTANCE.getString(R.string.theme_mode),
App.INSTANCE.getString(R.string.bookshelf_layout),
App.INSTANCE.getString(R.string.show_rss)
App.INSTANCE.getString(R.string.show_rss),
App.INSTANCE.getString(R.string.thread_count)
)
//默认忽略keys
@ -135,7 +137,21 @@ object Restore {
App.db.txtTocRule().insert(*it.toTypedArray())
}
fileToListT<ReadRecord>(path, "readRecord.json")?.let {
App.db.readRecordDao().insert(*it.toTypedArray())
it.forEach { readRecord ->
//判断是不是本机记录
if (readRecord.androidId != App.androidId) {
App.db.readRecordDao().insert(readRecord)
} else {
val time = App.db.readRecordDao()
.getReadTime(readRecord.androidId, readRecord.bookName)
if (time == null || time < readRecord.readTime) {
App.db.readRecordDao().insert(readRecord)
}
}
}
}
fileToListT<HttpTTS>(path, "httpTTS.json")?.let {
App.db.httpTTSDao().insert(*it.toTypedArray())
}
}
}
@ -205,6 +221,7 @@ object Restore {
PreferKey.themeMode == key && ignoreThemeMode -> false
PreferKey.bookshelfLayout == key && ignoreBookshelfLayout -> false
PreferKey.showRss == key && ignoreShowRss -> false
PreferKey.threadCount == key && ignoreThreadCount -> false
else -> true
}
}
@ -217,6 +234,8 @@ object Restore {
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
private val ignoreShowRss: Boolean
get() = ignoreConfig[PreferKey.showRss] == true
private val ignoreThreadCount: Boolean
get() = ignoreConfig[PreferKey.threadCount] == true
fun saveIgnoreConfig() {
val json = GSON.toJson(ignoreConfig)

@ -51,11 +51,11 @@ object WebDavHelp {
return false
}
@Throws(Exception::class)
private fun getWebDavFileNames(): ArrayList<String> {
val url = rootWebDavUrl
val names = arrayListOf<String>()
if (initWebDav()) {
try {
var files = WebDav(url).listFiles()
files = files.reversed()
for (index: Int in 0 until min(10, files.size)) {
@ -63,16 +63,13 @@ object WebDavHelp {
names.add(it)
}
}
} catch (e: Exception) {
return names
}
}
return names
}
suspend fun showRestoreDialog(context: Context): Boolean {
suspend fun showRestoreDialog(context: Context) {
val names = withContext(IO) { getWebDavFileNames() }
return if (names.isNotEmpty()) {
if (names.isNotEmpty()) {
withContext(Main) {
context.selector(
title = context.getString(R.string.select_restore_file),
@ -83,9 +80,8 @@ object WebDavHelp {
}
}
}
true
} else {
false
throw Exception("Web dav no back up file")
}
}

@ -9,6 +9,7 @@ import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.view.View
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
import android.view.Window
import android.widget.EdgeEffect
import android.widget.ScrollView
import androidx.annotation.ColorInt
@ -63,16 +64,16 @@ object ATH {
} else {
activity.window.statusBarColor = color
}
setLightStatusBarAuto(activity, color)
setLightStatusBarAuto(activity.window, color)
}
fun setLightStatusBarAuto(activity: Activity, bgColor: Int) {
setLightStatusBar(activity, ColorUtils.isColorLight(bgColor))
fun setLightStatusBarAuto(window: Window, bgColor: Int) {
setLightStatusBar(window, ColorUtils.isColorLight(bgColor))
}
fun setLightStatusBar(activity: Activity, enabled: Boolean) {
fun setLightStatusBar(window: Window, enabled: Boolean) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val decorView = activity.window.decorView
val decorView = window.decorView
val systemUiVisibility = decorView.systemUiVisibility
if (enabled) {
decorView.systemUiVisibility =

@ -34,7 +34,6 @@ class ATERadioNoButton(context: Context, attrs: AttributeSet) :
isBottomBackground -> {
val isLight = ColorUtils.isColorLight(context.bottomBackground)
val textColor = context.getPrimaryTextColor(isLight)
setTextColor(textColor)
background = Selector.shapeBuild()
.setCornerRadius(2.dp)
.setStrokeWidth(2.dp)
@ -42,15 +41,28 @@ class ATERadioNoButton(context: Context, attrs: AttributeSet) :
.setCheckedStrokeColor(context.accentColor)
.setDefaultStrokeColor(textColor)
.create()
setTextColor(
Selector.colorBuild()
.setDefaultColor(textColor)
.setCheckedColor(context.getPrimaryTextColor(ColorUtils.isColorLight(context.accentColor)))
.create()
)
}
else -> {
val textColor = context.getCompatColor(R.color.primaryText)
background = Selector.shapeBuild()
.setCornerRadius(2.dp)
.setStrokeWidth(2.dp)
.setCheckedBgColor(context.accentColor)
.setCheckedStrokeColor(context.accentColor)
.setDefaultStrokeColor(context.getCompatColor(R.color.primaryText))
.setDefaultStrokeColor(textColor)
.create()
setTextColor(
Selector.colorBuild()
.setDefaultColor(textColor)
.setCheckedColor(context.getPrimaryTextColor(ColorUtils.isColorLight(context.accentColor)))
.create()
)
}
}

@ -6,6 +6,8 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.rss.Rss
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.htmlFormat
import io.legado.app.utils.isAbsUrl
import java.text.SimpleDateFormat

@ -1,93 +0,0 @@
package io.legado.app.model
import io.legado.app.App
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var mSearchId = System.currentTimeMillis()
private var searchPage = 1
private var searchKey: String = ""
private var task: Coroutine<*>? = null
private fun initSearchPool() {
searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
}
fun search(searchId: Long, key: String) {
if (searchId != mSearchId) {
task?.cancel()
searchPool.close()
initSearchPool()
mSearchId = searchId
searchPage = 1
if (key.isEmpty()) {
return
} else {
this.searchKey = key
}
} else {
searchPage++
}
task = Coroutine.async(scope, searchPool) {
val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: ""
val bookSourceList = if (searchGroup.isBlank()) {
App.db.bookSourceDao().allEnabled
} else {
App.db.bookSourceDao().getEnabledByGroup(searchGroup)
}
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(
searchKey,
searchPage,
scope = this,
context = searchPool
)
.timeout(30000L)
.onSuccess(IO) {
if (searchId == mSearchId) {
callBack.onSearchSuccess(it)
}
}
}
}.onStart {
callBack.onSearchStart()
}
task?.invokeOnCompletion {
if (searchId == mSearchId) {
callBack.onSearchFinish()
}
}
}
fun cancelSearch() {
task?.cancel()
mSearchId = 0
callBack.onSearchCancel()
}
fun close() {
task?.cancel()
mSearchId = 0
searchPool.close()
}
interface CallBack {
fun onSearchStart()
fun onSearchSuccess(searchBooks: ArrayList<SearchBook>)
fun onSearchFinish()
fun onSearchCancel()
}
}

@ -609,7 +609,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions {
*/
override fun ajax(urlStr: String): String? {
return try {
val analyzeUrl = AnalyzeUrl(urlStr, null, null, null, baseUrl, book)
val analyzeUrl = AnalyzeUrl(urlStr, baseUrl = baseUrl, book = book)
val call = analyzeUrl.getResponse(urlStr)
val response = call.execute()
response.body()

@ -3,6 +3,8 @@ package io.legado.app.model.analyzeRule
import android.annotation.SuppressLint
import android.text.TextUtils
import androidx.annotation.Keep
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.LazyHeaders
import io.legado.app.constant.AppConst.SCRIPT_ENGINE
import io.legado.app.constant.AppConst.UA_NAME
import io.legado.app.constant.AppConst.userAgent
@ -23,7 +25,6 @@ import java.util.*
import java.util.regex.Pattern
import javax.script.SimpleBindings
/**
* Created by GKF on 2018/1/24.
* 搜索URL规则解析
@ -34,10 +35,12 @@ class AnalyzeUrl(
var ruleUrl: String,
key: String? = null,
page: Int? = null,
speakText: String? = null,
speakSpeed: Int? = null,
headerMapF: Map<String, String>? = null,
baseUrl: String? = null,
book: BaseBook? = null,
var useWebView: Boolean = false
var useWebView: Boolean = false,
) : JsExtensions {
companion object {
private val pagePattern = Pattern.compile("<(.*?)>")
@ -47,8 +50,7 @@ class AnalyzeUrl(
private var baseUrl: String = ""
lateinit var url: String
private set
var path: String? = null
private set
private lateinit var urlHasQuery: String
val headerMap = HashMap<String, String>()
private var queryStr: String? = null
private val fieldMap = LinkedHashMap<String, String>()
@ -56,20 +58,34 @@ class AnalyzeUrl(
private var body: String? = null
private var requestBody: RequestBody? = null
private var method = RequestMethod.GET
private val splitUrlRegex = Regex(",\\s*(?=\\{)")
private var proxy: String? = null
init {
baseUrl?.let {
this.baseUrl = it.split(",[^\\{]*".toRegex(), 1)[0]
this.baseUrl = it.split(splitUrlRegex, 1)[0]
}
headerMapF?.let {
headerMap.putAll(it)
if (it.containsKey("proxy")) {
proxy = it["proxy"]
headerMap.remove("proxy")
}
}
headerMapF?.let { headerMap.putAll(it) }
//替换参数
analyzeJs(key, page, book)
replaceKeyPageJs(key, page, book)
analyzeJs(key, page, speakText, speakSpeed, book)
replaceKeyPageJs(key, page, speakText, speakSpeed, book)
//处理URL
initUrl()
}
private fun analyzeJs(key: String?, page: Int?, book: BaseBook?) {
private fun analyzeJs(
key: String?,
page: Int?,
speakText: String?,
speakSpeed: Int?,
book: BaseBook?,
) {
val ruleList = arrayListOf<String>()
var start = 0
var tmp: String
@ -96,11 +112,13 @@ class AnalyzeUrl(
when {
ruleStr.startsWith("<js>") -> {
ruleStr = ruleStr.substring(4, ruleStr.lastIndexOf("<"))
ruleUrl = evalJS(ruleStr, ruleUrl, page, key, book) as String
ruleUrl =
evalJS(ruleStr, ruleUrl, page, key, speakText, speakSpeed, book) as String
}
ruleStr.startsWith("@js", true) -> {
ruleStr = ruleStr.substring(4)
ruleUrl = evalJS(ruleStr, ruleUrl, page, key, book) as String
ruleUrl =
evalJS(ruleStr, ruleUrl, page, key, speakText, speakSpeed, book) as String
}
else -> ruleUrl = ruleStr.replace("@result", ruleUrl)
}
@ -110,7 +128,13 @@ class AnalyzeUrl(
/**
* 替换关键字,页数,JS
*/
private fun replaceKeyPageJs(key: String?, page: Int?, book: BaseBook?) {
private fun replaceKeyPageJs(
key: String?,
page: Int?,
speakText: String?,
speakSpeed: Int?,
book: BaseBook?,
) {
//page
page?.let {
val matcher = pagePattern.matcher(ruleUrl)
@ -132,10 +156,14 @@ class AnalyzeUrl(
simpleBindings["baseUrl"] = baseUrl
simpleBindings["page"] = page
simpleBindings["key"] = key
simpleBindings["speakText"] = speakText
simpleBindings["speakSpeed"] = speakSpeed
simpleBindings["book"] = book
val expMatcher = EXP_PATTERN.matcher(ruleUrl)
while (expMatcher.find()) {
jsEval = SCRIPT_ENGINE.eval(expMatcher.group(1), simpleBindings)
jsEval = expMatcher.group(1)?.let {
SCRIPT_ENGINE.eval(it, simpleBindings)
} ?: ""
if (jsEval is String) {
expMatcher.appendReplacement(sb, jsEval)
} else if (jsEval is Double && jsEval % 1.0 == 0.0) {
@ -153,8 +181,9 @@ class AnalyzeUrl(
* 处理URL
*/
private fun initUrl() {
var urlArray = ruleUrl.split(",[^\\{]*".toRegex(), 2)
var urlArray = ruleUrl.split(splitUrlRegex, 2)
url = urlArray[0]
urlHasQuery = urlArray[0]
NetworkUtils.getBaseUrl(url)?.let {
baseUrl = it
}
@ -172,6 +201,7 @@ class AnalyzeUrl(
?.let { headerMap.putAll(it) }
}
}
headerMap[UA_NAME] = headerMap[UA_NAME] ?: userAgent
charset = option.charset
body = if (option.body is String) {
option.body
@ -242,12 +272,16 @@ class AnalyzeUrl(
result: Any?,
page: Int?,
key: String?,
book: BaseBook?
speakText: String?,
speakSpeed: Int?,
book: BaseBook?,
): Any {
val bindings = SimpleBindings()
bindings["java"] = this
bindings["page"] = page
bindings["key"] = key
bindings["speakText"] = speakText
bindings["speakSpeed"] = speakSpeed
bindings["book"] = book
bindings["result"] = result
bindings["baseUrl"] = baseUrl
@ -285,7 +319,7 @@ class AnalyzeUrl(
suspend fun getResponseAwait(
tag: String,
jsStr: String? = null,
sourceRegex: String? = null
sourceRegex: String? = null,
): Res {
if (useWebView) {
val params = AjaxWebView.AjaxParams(url)
@ -304,51 +338,110 @@ class AnalyzeUrl(
val res = when {
method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
if (proxy == null) {
HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset)
.postMapAsync(url, fieldMap, headerMap)
} else {
HttpHelper
.getApiServiceWithProxy<HttpPostApi>(baseUrl, charset, proxy)
.postMapAsync(url, fieldMap, headerMap)
}
} else {
if (proxy == null) {
HttpHelper
.getApiService<HttpPostApi>(baseUrl, charset)
.postBodyAsync(url, requestBody!!, headerMap)
} else {
HttpHelper
.getApiServiceWithProxy<HttpPostApi>(baseUrl, charset, proxy)
.postBodyAsync(url, requestBody!!, headerMap)
}
}
fieldMap.isEmpty() -> HttpHelper
}
fieldMap.isEmpty() -> {
if (proxy == null) {
HttpHelper
.getApiService<HttpGetApi>(baseUrl, charset)
.getAsync(url, headerMap)
else -> HttpHelper
} else {
HttpHelper
.getApiServiceWithProxy<HttpGetApi>(baseUrl, charset, proxy)
.getAsync(url, headerMap)
}
}
else -> {
if (proxy == null) {
HttpHelper
.getApiService<HttpGetApi>(baseUrl, charset)
.getMapAsync(url, fieldMap, headerMap)
} else {
HttpHelper
.getApiServiceWithProxy<HttpGetApi>(baseUrl, charset, proxy)
.getMapAsync(url, fieldMap, headerMap)
}
}
}
return Res(NetworkUtils.getUrl(res), res.body())
}
@Throws(Exception::class)
fun getImageBytes(
tag: String
): ByteArray? {
//资源为本站的资源,保留cookie
//图片盗链的不保留当前的cookie,可由js生成图片源站的cookie
fun getImageBytes(tag: String): ByteArray? {
val cookie = CookieStore.getCookie(tag)
NetworkUtils.getBaseUrl(url)?.let {
val regex: Any
if(it.lastIndexOf(".") != it.indexOf(".")) {
regex = it.substring(it.indexOf(".")+1).toRegex()
} else {
regex = it.substring(it.lastIndexOf("/")+1).toRegex()
}
if(regex.containsMatchIn(tag)) {
if (cookie.isNotEmpty()) {
headerMap["Cookie"] += cookie
}
return if (fieldMap.isEmpty()) {
HttpHelper.getBytes(url, mapOf(), headerMap)
} else {
HttpHelper.getBytes(url, fieldMap, headerMap)
}
}
headerMap[UA_NAME] = headerMap[UA_NAME] ?:userAgent
if(fieldMap.isEmpty()) {
return HttpHelper.getBytes(url, mapOf(), headerMap)
@Throws(Exception::class)
suspend fun getResponseBytes(tag: String? = null): ByteArray? {
if (tag != null) {
val cookie = CookieStore.getCookie(tag)
if (cookie.isNotEmpty()) {
headerMap["Cookie"] = cookie
}
}
val response = when {
method == RequestMethod.POST -> {
if (fieldMap.isNotEmpty()) {
HttpHelper
.getBytesApiService<HttpPostApi>(baseUrl)
.postMapByteAsync(url, fieldMap, headerMap)
} else {
return HttpHelper.getBytes(url, fieldMap, headerMap)
HttpHelper
.getBytesApiService<HttpPostApi>(baseUrl)
.postBodyByteAsync(url, requestBody!!, headerMap)
}
}
fieldMap.isEmpty() -> HttpHelper
.getBytesApiService<HttpGetApi>(baseUrl)
.getByteAsync(url, headerMap)
else -> HttpHelper
.getBytesApiService<HttpGetApi>(baseUrl)
.getMapByteAsync(url, fieldMap, headerMap)
}
return response.body()
}
@Throws(Exception::class)
fun getGlideUrl(): Any? {
var glideUrl: Any = urlHasQuery
if (headerMap.isNotEmpty()) {
val headers = LazyHeaders.Builder()
headerMap.forEach { (key, value) ->
headers.addHeader(key, value)
}
glideUrl = GlideUrl(urlHasQuery, headers.build())
}
return glideUrl
}
data class UrlOption(
@ -356,7 +449,7 @@ class AnalyzeUrl(
val charset: String?,
val webView: Any?,
val headers: Any?,
val body: Any?
val body: Any?,
)
}

@ -20,7 +20,9 @@ class AnalyzeTxtFile {
@Throws(Exception::class)
fun analyze(book: Book): ArrayList<BookChapter> {
val bookFile = getBookFile(book)
if (book.charset == null) {
book.charset = EncodingDetect.getEncode(bookFile)
}
charset = book.fileCharset()
val rulePattern = if (book.tocUrl.isNotEmpty()) {
Pattern.compile(book.tocUrl, Pattern.MULTILINE)

@ -1,4 +1,4 @@
package io.legado.app.model
package io.legado.app.model.rss
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource

@ -2,7 +2,6 @@ package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.R
import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.help.BookHelp
@ -19,7 +18,8 @@ object BookInfo {
book: Book,
body: String?,
bookSource: BookSource,
baseUrl: String
baseUrl: String,
canReName: Boolean,
) {
body ?: throw Exception(
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl)
@ -35,13 +35,17 @@ object BookInfo {
}
}
Debug.log(bookSource.bookSourceUrl, "┌获取书名")
BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).trim { it <= ' ' }.let {
if (it.isNotEmpty()) book.name = it
BookHelp.formatBookName(analyzeRule.getString(infoRule.name)).let {
if (it.isNotEmpty() && (canReName || book.name.isEmpty())) {
book.name = it
}
}
Debug.log(bookSource.bookSourceUrl, "${book.name}")
Debug.log(bookSource.bookSourceUrl, "┌获取作者")
BookHelp.formatBookAuthor(analyzeRule.getString(infoRule.author)).let {
if (it.isNotEmpty()) book.author = it.replace(AppPattern.authorRegex, "")
if (it.isNotEmpty() && (canReName || book.name.isEmpty())) {
book.author = it
}
}
Debug.log(bookSource.bookSourceUrl, "${book.author}")
Debug.log(bookSource.bookSourceUrl, "┌获取分类")

@ -11,6 +11,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils.wordCountFormat
import io.legado.app.utils.htmlFormat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
@ -161,7 +162,7 @@ object BookList {
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}")
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介")
searchBook.intro = analyzeRule.getString(intro)
searchBook.intro = analyzeRule.getString(intro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接")
@ -221,7 +222,7 @@ object BookList {
Debug.log(bookSource.bookSourceUrl, "${searchBook.latestChapterTitle}", log)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取简介", log)
searchBook.intro = analyzeRule.getString(ruleIntro)
searchBook.intro = analyzeRule.getString(ruleIntro).htmlFormat()
Debug.log(bookSource.bookSourceUrl, "${searchBook.intro}", log, true)
if (!scope.isActive) throw CancellationException()
Debug.log(bookSource.bookSourceUrl, "┌获取封面链接", log)

@ -0,0 +1,117 @@
package io.legado.app.model.webBook
import io.legado.app.App
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.utils.getPrefString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import kotlin.math.min
class SearchBookModel(private val scope: CoroutineScope, private val callBack: CallBack) {
val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
private var mSearchId = 0L
private var searchPage = 1
private var searchKey: String = ""
private var tasks = CompositeCoroutine()
private var bookSourceList = arrayListOf<BookSource>()
@Volatile
private var searchIndex = -1
private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
}
fun search(searchId: Long, key: String) {
callBack.onSearchStart()
if (searchId != mSearchId) {
if (key.isEmpty()) {
callBack.onSearchCancel()
return
} else {
this.searchKey = key
}
if (mSearchId != 0L) {
close()
}
initSearchPool()
mSearchId = searchId
searchPage = 1
val searchGroup = App.INSTANCE.getPrefString("searchGroup") ?: ""
bookSourceList.clear()
if (searchGroup.isBlank()) {
bookSourceList.addAll(App.db.bookSourceDao().allEnabled)
} else {
bookSourceList.addAll(App.db.bookSourceDao().getEnabledByGroup(searchGroup))
}
} else {
searchPage++
}
searchIndex = -1
for (i in 0 until threadCount) {
search(searchId)
}
}
private fun search(searchId: Long) {
synchronized(this) {
if (searchIndex >= bookSourceList.lastIndex) {
return
}
searchIndex++
val source = bookSourceList[searchIndex]
val task = WebBook(source).searchBook(
searchKey,
searchPage,
scope = scope,
context = searchPool!!
).timeout(30000L)
.onSuccess(IO) {
if (searchId == mSearchId) {
callBack.onSearchSuccess(it)
}
}
.onFinally {
synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) {
search(searchId)
} else {
searchIndex++
}
if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size,
threadCount)
) {
callBack.onSearchFinish()
}
}
}
tasks.add(task)
}
}
fun cancelSearch() {
close()
callBack.onSearchCancel()
}
fun close() {
tasks.clear()
searchPool?.close()
mSearchId = 0L
}
interface CallBack {
fun onSearchStart()
fun onSearchSuccess(searchBooks: ArrayList<SearchBook>)
fun onSearchFinish()
fun onSearchCancel()
}
}

@ -1,15 +1,12 @@
package io.legado.app.model
package io.legado.app.model.webBook
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.webBook.BookChapterList
import io.legado.app.model.webBook.BookContent
import io.legado.app.model.webBook.BookInfo
import io.legado.app.model.webBook.BookList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
@ -93,7 +90,8 @@ class WebBook(val bookSource: BookSource) {
fun getBookInfo(
book: Book,
scope: CoroutineScope = Coroutine.DEFAULT,
context: CoroutineContext = Dispatchers.IO
context: CoroutineContext = Dispatchers.IO,
canReName: Boolean = true,
): Coroutine<Book> {
return Coroutine.async(scope, context) {
book.type = bookSource.bookSourceType
@ -109,7 +107,7 @@ class WebBook(val bookSource: BookSource) {
)
analyzeUrl.getResponseAwait(bookSource.bookSourceUrl).body
}
BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl)
BookInfo.analyzeBookInfo(book, body, bookSource, book.bookUrl, canReName)
book
}
}

@ -336,6 +336,7 @@ abstract class BaseReadAloudService : BaseService(),
abstract fun aloudServicePendingIntent(actionStr: String): PendingIntent?
open fun nextChapter() {
ReadBook.upReadStartTime()
if (!ReadBook.moveToNextChapter(true)) {
stopSelf()
}

@ -16,7 +16,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.Download
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO

@ -7,8 +7,8 @@ import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
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.api.HttpPostApi
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.FileUtils
import io.legado.app.utils.LogUtils
@ -17,7 +17,6 @@ import kotlinx.coroutines.isActive
import java.io.File
import java.io.FileDescriptor
import java.io.FileInputStream
import java.net.URLEncoder
class HttpReadAloudService : BaseReadAloudService(),
MediaPlayer.OnPreparedListener,
@ -31,7 +30,7 @@ class HttpReadAloudService : BaseReadAloudService(),
override fun onCreate() {
super.onCreate()
ttsFolder = cacheDir.absolutePath + File.separator + "bdTts"
ttsFolder = externalCacheDir!!.absolutePath + File.separator + "httpTTS"
mediaPlayer.setOnErrorListener(this)
mediaPlayer.setOnPreparedListener(this)
mediaPlayer.setOnCompletionListener(this)
@ -72,13 +71,13 @@ class HttpReadAloudService : BaseReadAloudService(),
FileUtils.deleteFile(ttsFolder)
for (index in 0 until contentList.size) {
if (isActive) {
val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com")
.create(HttpPostApi::class.java)
.postMapByteAsync(
"http://tts.baidu.com/text2audio",
getAudioBody(contentList[index]), mapOf()
).body()
if (bytes != null && isActive) {
ReadAloud.httpTTS?.let {
AnalyzeUrl(
it.url,
speakText = contentList[index],
speakSpeed = AppConfig.ttsSpeechRate
).getResponseBytes()?.let { bytes ->
if (isActive) {
val file = getSpeakFile(index)
file.writeBytes(bytes)
if (index == nowSpeak) {
@ -87,6 +86,8 @@ class HttpReadAloudService : BaseReadAloudService(),
playAudio(fis.fd)
}
}
}
}
} else {
break
}
@ -113,31 +114,6 @@ class HttpReadAloudService : BaseReadAloudService(),
return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3")
}
private fun getAudioBody(content: String): Map<String, String> {
return mapOf(
Pair("tex", encodeTwo(content)),
Pair("spd", ((AppConfig.ttsSpeechRate + 5) / 10 + 4).toString()),
Pair("per", AppConfig.ttsSpeechPer),
Pair("cuid", "baidu_speech_demo"),
Pair("idx", "1"),
Pair("cod", "2"),
Pair("lan", "zh"),
Pair("ctp", "1"),
Pair("pdt", "1"),
Pair("vol", "5"),
Pair("pit", "5"),
Pair("_res_tag_", "audio")
)
}
private fun encodeTwo(content: String): String {
return try {
URLEncoder.encode(URLEncoder.encode(content, "UTF-8"), "UTF-8")
} catch (e: Exception) {
" "
}
}
override fun pauseReadAloud(pause: Boolean) {
super.pauseReadAloud(pause)
mediaPlayer.pause()

@ -6,7 +6,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.constant.IntentAction
import io.legado.app.constant.Status
import io.legado.app.data.entities.Book
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.AudioPlayService
object AudioPlay {

@ -7,7 +7,7 @@ import io.legado.app.R
import io.legado.app.constant.IntentAction
import io.legado.app.data.entities.BookSource
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CheckSourceService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

@ -5,22 +5,31 @@ import android.content.Intent
import io.legado.app.App
import io.legado.app.constant.IntentAction
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.HttpTTS
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.HttpReadAloudService
import io.legado.app.service.TTSReadAloudService
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefLong
object ReadAloud {
var aloudClass: Class<*> = getReadAloudClass()
private var aloudClass: Class<*> = getReadAloudClass()
var httpTTS: HttpTTS? = null
fun getReadAloudClass(): Class<*> {
return if (App.INSTANCE.getPrefBoolean(PreferKey.readAloudOnLine)) {
private fun getReadAloudClass(): Class<*> {
val spId = App.INSTANCE.getPrefLong(PreferKey.speakEngine)
httpTTS = App.db.httpTTSDao().get(spId)
stop(App.INSTANCE)
return if (httpTTS != null) {
HttpReadAloudService::class.java
} else {
TTSReadAloudService::class.java
}
}
fun upReadAloudClass() {
aloudClass = getReadAloudClass()
}
fun play(
context: Context,
title: String,

@ -13,7 +13,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.provider.ChapterProvider
@ -22,6 +22,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.anko.getStackTraceString
import org.jetbrains.anko.toast
@ -370,12 +371,20 @@ object ReadBook {
}
}.onError {
it.printStackTrace()
App.INSTANCE.toast(it.localizedMessage ?: "ChapterProvider ERROR")
App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}")
}
}
private val imageStyle get() = webBook?.bookSource?.ruleContent?.imageStyle
fun setCharset(charset: String) {
book?.let {
it.charset = charset
callBack?.loadChapterList(it)
}
saveRead()
}
fun saveRead() {
Coroutine.async {
book?.let { book ->
@ -392,6 +401,7 @@ object ReadBook {
}
interface CallBack {
fun loadChapterList(book: Book)
fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true)
fun upView()
fun pageChanged()

@ -11,6 +11,7 @@ import io.legado.app.App
import io.legado.app.R
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.applyTint
import io.legado.app.utils.openUrl
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toast
@ -34,7 +35,7 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.about)
findPreference<Preference>("check_update")?.summary =
"${getString(R.string.version)} ${App.INSTANCE.versionName}"
"${getString(R.string.version)} ${App.versionName}"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -92,7 +93,7 @@ class AboutFragment : PreferenceFragmentCompat() {
}
}
}
}.show()
}.show().applyTint()
}
private fun joinQQGroup(key: String): Boolean {

@ -8,7 +8,12 @@ import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.ReadRecord
import io.legado.app.data.entities.ReadRecordShow
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.utils.applyTint
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.activity_read_record.*
import kotlinx.android.synthetic.main.item_read_record.*
import kotlinx.android.synthetic.main.item_read_record.view.*
@ -16,6 +21,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.anko.sdk27.listeners.onClick
class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) {
@ -27,7 +33,7 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) {
}
private fun initView() {
tv_book_name.text = "总阅读时间"
tv_book_name.setText(R.string.all_read_time)
recycler_view.layoutManager = LinearLayoutManager(this)
adapter = RecordAdapter(this)
recycler_view.adapter = adapter
@ -39,7 +45,7 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) {
withContext(Main) {
tv_read_time.text = formatDuring(allTime)
}
val readRecords = App.db.readRecordDao().all
val readRecords = App.db.readRecordDao().allShow
withContext(Main) {
adapter.setItems(readRecords)
}
@ -47,17 +53,34 @@ class ReadRecordActivity : BaseActivity(R.layout.activity_read_record) {
}
inner class RecordAdapter(context: Context) :
SimpleRecyclerAdapter<ReadRecord>(context, R.layout.item_read_record) {
SimpleRecyclerAdapter<ReadRecordShow>(context, R.layout.item_read_record) {
override fun convert(holder: ItemViewHolder, item: ReadRecord, payloads: MutableList<Any>) {
override fun convert(
holder: ItemViewHolder,
item: ReadRecordShow,
payloads: MutableList<Any>
) {
holder.itemView.apply {
tv_book_name.text = item.bookName
tv_read_time.text = formatDuring(item.readTime)
iv_remove.visible()
}
}
override fun registerListener(holder: ItemViewHolder) {
holder.itemView.apply {
iv_remove.onClick {
alert(R.string.delete, R.string.sure_del) {
okButton {
getItem(holder.layoutPosition)?.let {
App.db.readRecordDao().deleteByName(it.bookName)
initData()
}
}
noButton()
}.show().applyTint()
}
}
}
}

@ -2,7 +2,6 @@ package io.legado.app.ui.association
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.Observer
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Theme
@ -23,12 +22,12 @@ class FileAssociationActivity : VMBaseActivity<FileAssociationViewModel>(
override fun onActivityCreated(savedInstanceState: Bundle?) {
rotate_loading.show()
viewModel.errorLiveData.observe(this, Observer {
viewModel.errorLiveData.observe(this, {
rotate_loading.hide()
toast(it)
finish()
})
viewModel.successLiveData.observe(this, Observer {
viewModel.successLiveData.observe(this, {
rotate_loading.hide()
startActivity(it)
finish()

@ -9,7 +9,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
@ -41,11 +40,11 @@ class ImportBookSourceActivity : VMBaseActivity<ImportBookSourceViewModel>(
override fun onActivityCreated(savedInstanceState: Bundle?) {
rotate_loading.show()
viewModel.errorLiveData.observe(this, Observer {
viewModel.errorLiveData.observe(this, {
rotate_loading.hide()
errorDialog(it)
})
viewModel.successLiveData.observe(this, Observer {
viewModel.successLiveData.observe(this, {
rotate_loading.hide()
if (it > 0) {
successDialog()

@ -1,7 +1,6 @@
package io.legado.app.ui.association
import android.os.Bundle
import androidx.lifecycle.Observer
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
@ -26,11 +25,11 @@ class ImportReplaceRuleActivity : VMBaseActivity<ImportReplaceRuleViewModel>(
override fun onActivityCreated(savedInstanceState: Bundle?) {
rotate_loading.show()
viewModel.errorLiveData.observe(this, Observer {
viewModel.errorLiveData.observe(this, {
rotate_loading.hide()
errorDialog(it)
})
viewModel.successLiveData.observe(this, Observer {
viewModel.successLiveData.observe(this, {
rotate_loading.hide()
if (it.size > 0) {
successDialog(it)

@ -9,7 +9,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
@ -41,11 +40,11 @@ class ImportRssSourceActivity : VMBaseActivity<ImportRssSourceViewModel>(
override fun onActivityCreated(savedInstanceState: Bundle?) {
rotate_loading.show()
viewModel.errorLiveData.observe(this, Observer {
viewModel.errorLiveData.observe(this, {
rotate_loading.hide()
errorDialog(it)
})
viewModel.successLiveData.observe(this, Observer {
viewModel.successLiveData.observe(this, {
rotate_loading.hide()
if (it > 0) {
successDialog()

@ -8,7 +8,6 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.SeekBar
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions.bitmapTransform
@ -37,7 +36,7 @@ import org.jetbrains.anko.startActivityForResult
class AudioPlayActivity :
VMBaseActivity<AudioPlayViewModel>(R.layout.activity_audio_play, theme = Theme.Dark),
VMBaseActivity<AudioPlayViewModel>(R.layout.activity_audio_play, toolBarTheme = Theme.Dark),
ChangeSourceDialog.CallBack {
override val viewModel: AudioPlayViewModel
@ -48,8 +47,8 @@ class AudioPlayActivity :
override fun onActivityCreated(savedInstanceState: Bundle?) {
title_bar.transparent()
AudioPlay.titleData.observe(this, Observer { title_bar.title = it })
AudioPlay.coverData.observe(this, Observer { upCover(it) })
AudioPlay.titleData.observe(this, { title_bar.title = it })
AudioPlay.coverData.observe(this, { upCover(it) })
viewModel.initData(intent)
initView()
}

@ -8,7 +8,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.AudioPlay
import kotlinx.coroutines.Dispatchers

@ -5,7 +5,6 @@ import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
@ -94,7 +93,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
private fun initGroupData() {
groupLiveData?.removeObservers(this)
groupLiveData = App.db.bookGroupDao().liveDataAll()
groupLiveData?.observe(this, Observer {
groupLiveData?.observe(this, {
groupList.clear()
groupList.addAll(it)
adapter.notifyDataSetChanged()
@ -112,7 +111,7 @@ class ArrangeBookActivity : VMBaseActivity<ArrangeBookViewModel>(R.layout.activi
AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer { list ->
booksLiveData?.observe(this, { list ->
val books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { it.latestChapterTime }
2 -> list.sortedBy { it.name }

@ -8,13 +8,12 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.GridLayoutManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
import kotlinx.android.synthetic.main.dialog_change_source.*
@ -61,7 +60,7 @@ class ChangeCoverDialog : BaseDialogFragment(),
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tool_bar.setBackgroundColor(bottomBackground)
tool_bar.setBackgroundColor(primaryColor)
tool_bar.setTitle(R.string.change_cover_source)
viewModel.initData(arguments)
initMenu()
@ -70,7 +69,7 @@ class ChangeCoverDialog : BaseDialogFragment(),
private fun initMenu() {
tool_bar.inflateMenu(R.menu.change_cover)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
}
@ -83,7 +82,7 @@ class ChangeCoverDialog : BaseDialogFragment(),
override fun observeLiveBus() {
super.observeLiveBus()
viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
viewModel.searchStateData.observe(viewLifecycleOwner, {
refresh_progress_bar.isAutoLoading = it
if (it) {
stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp)
@ -92,7 +91,7 @@ class ChangeCoverDialog : BaseDialogFragment(),
}
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
})
viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer {
viewModel.searchBooksLiveData.observe(viewLifecycleOwner, {
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it))
adapter.setItems(it)
diffResult.dispatchUpdatesTo(adapter)

@ -6,24 +6,31 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import kotlin.math.min
class ChangeCoverViewModel(application: Application) : BaseViewModel(application) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
var name: String = ""
var author: String = ""
private var task: Coroutine<*>? = null
private var tasks = CompositeCoroutine()
private var bookSourceList = arrayListOf<BookSource>()
val searchStateData = MutableLiveData<Boolean>()
val searchBooksLiveData = MutableLiveData<List<SearchBook>>()
private val searchBooks = ArrayList<SearchBook>()
@Volatile
private var searchIndex = -1
fun initData(arguments: Bundle?) {
arguments?.let { bundle ->
bundle.getString("name")?.let {
@ -35,13 +42,18 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
searchIndex = -1
}
fun loadDbSearchBook() {
execute {
App.db.searchBookDao().getEnableHasCover(name, author).let {
searchBooks.addAll(it)
if (it.size <= 1) {
searchBooksLiveData.postValue(searchBooks)
search()
startSearch()
} else {
searchBooksLiveData.postValue(searchBooks)
}
@ -49,13 +61,27 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
fun search() {
task = execute {
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L)
private fun startSearch() {
execute {
bookSourceList.clear()
bookSourceList.addAll(App.db.bookSourceDao().allEnabled)
searchStateData.postValue(true)
initSearchPool()
for (i in 0 until threadCount) {
search()
}
}
}
private fun search() {
synchronized(this) {
if (searchIndex >= bookSourceList.lastIndex) {
return
}
searchIndex++
val source = bookSourceList[searchIndex]
val task = WebBook(source).searchBook(name, scope = this, context = searchPool!!)
.timeout(60000L)
.onSuccess(Dispatchers.IO) {
if (it.isNotEmpty()) {
val searchBook = it[0]
@ -70,29 +96,35 @@ class ChangeCoverViewModel(application: Application) : BaseViewModel(application
}
}
}
.onFinally {
synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) {
search()
} else {
searchIndex++
}
}.onStart {
searchStateData.postValue(true)
}.onCancel {
if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size,
threadCount)
) {
searchStateData.postValue(false)
}
task?.invokeOnCompletion {
searchStateData.postValue(false)
}
}
tasks.add(task)
}
}
fun stopSearch() {
if (task?.isActive == true) {
task?.cancel()
if (tasks.isEmpty) {
startSearch()
} else {
search()
tasks.clear()
}
}
override fun onCleared() {
super.onCleared()
searchPool.close()
searchPool?.close()
}
}

@ -9,17 +9,15 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.PreferKey
import io.legado.app.constant.Theme
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getPrefBoolean
@ -69,7 +67,7 @@ class ChangeSourceDialog : BaseDialogFragment(),
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tool_bar.setBackgroundColor(bottomBackground)
tool_bar.setBackgroundColor(primaryColor)
viewModel.initData(arguments)
showTitle()
initMenu()
@ -86,7 +84,7 @@ class ChangeSourceDialog : BaseDialogFragment(),
private fun initMenu() {
tool_bar.inflateMenu(R.menu.change_source)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.findItem(R.id.menu_load_toc)?.isChecked =
getPrefBoolean(PreferKey.changeSourceLoadToc)
@ -136,16 +134,16 @@ class ChangeSourceDialog : BaseDialogFragment(),
}
private fun initLiveData() {
viewModel.searchStateData.observe(viewLifecycleOwner, Observer {
viewModel.searchStateData.observe(viewLifecycleOwner, {
refresh_progress_bar.isAutoLoading = it
if (it) {
stopMenuItem?.setIcon(R.drawable.ic_stop_black_24dp)
} else {
stopMenuItem?.setIcon(R.drawable.ic_refresh_black_24dp)
}
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
})
viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer {
viewModel.searchBooksLiveData.observe(viewLifecycleOwner, {
val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it))
adapter.setItems(it)
diffResult.dispatchUpdatesTo(adapter)

@ -10,30 +10,36 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.WebBook
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.getPrefBoolean
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExecutorCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import org.jetbrains.anko.debug
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.Executors
import kotlin.math.min
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private var searchPool =
Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private val threadCount = AppConfig.threadCount
private var searchPool: ExecutorCoroutineDispatcher? = null
val handler = Handler()
val searchStateData = MutableLiveData<Boolean>()
val searchBooksLiveData = MutableLiveData<List<SearchBook>>()
var name: String = ""
var author: String = ""
private var task: Coroutine<*>? = null
private var tasks = CompositeCoroutine()
private var screenKey: String = ""
private val searchBooks = hashSetOf<SearchBook>()
private var bookSourceList = arrayListOf<BookSource>()
private val searchBooks = CopyOnWriteArraySet<SearchBook>()
private var postTime = 0L
private val sendRunnable = Runnable { upAdapter() }
@Volatile
private var searchIndex = -1
fun initData(arguments: Bundle?) {
arguments?.let { bundle ->
@ -46,13 +52,18 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
}
private fun initSearchPool() {
searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher()
searchIndex = -1
}
fun loadDbSearchBook() {
execute {
App.db.searchBookDao().getByNameAuthorEnable(name, author).let {
searchBooks.addAll(it)
if (it.size <= 1) {
upAdapter()
search()
startSearch()
} else {
upAdapter()
}
@ -83,13 +94,27 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
upAdapter()
}
fun search() {
task = execute {
val bookSourceList = App.db.bookSourceDao().allEnabled
for (item in bookSourceList) {
//task取消时自动取消 by (scope = this@execute)
WebBook(item).searchBook(name, scope = this@execute, context = searchPool)
.timeout(30000L)
private fun startSearch() {
execute {
bookSourceList.clear()
bookSourceList.addAll(App.db.bookSourceDao().allEnabled)
searchStateData.postValue(true)
initSearchPool()
for (i in 0 until threadCount) {
search()
}
}
}
private fun search() {
synchronized(this) {
if (searchIndex >= bookSourceList.lastIndex) {
return
}
searchIndex++
val source = bookSourceList[searchIndex]
val task = WebBook(source).searchBook(name, scope = this, context = searchPool!!)
.timeout(60000L)
.onSuccess(IO) {
it.forEach { searchBook ->
if (searchBook.name == name && searchBook.author == author) {
@ -102,19 +127,25 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
} else {
searchFinish(searchBook)
}
return@onSuccess
return@forEach
}
}
}
.onFinally {
synchronized(this) {
if (searchIndex < bookSourceList.lastIndex) {
search()
} else {
searchIndex++
}
}.onStart {
searchStateData.postValue(true)
}.onCancel {
if (searchIndex >= bookSourceList.lastIndex + min(bookSourceList.size,
threadCount)
) {
searchStateData.postValue(false)
}
task?.invokeOnCompletion {
searchStateData.postValue(false)
}
}
tasks.add(task)
}
}
@ -166,16 +197,18 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio
}
fun stopSearch() {
if (task?.isActive == true) {
task?.cancel()
if (tasks.isEmpty) {
startSearch()
} else {
search()
tasks.clear()
searchPool?.close()
searchStateData.postValue(false)
}
}
override fun onCleared() {
super.onCleared()
searchPool.close()
searchPool?.close()
}
fun disableSource(searchBook: SearchBook) {

@ -8,7 +8,6 @@ import android.os.Bundle
import android.view.View
import android.widget.EditText
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.recyclerview.widget.LinearLayoutManager
@ -59,7 +58,7 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
LivePagedListBuilder(
App.db.bookmarkDao().observeByBook(book.bookUrl, book.name, book.author), 20
).build()
bookmarkLiveData?.observe(viewLifecycleOwner, Observer { adapter.submitList(it) })
bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) })
}
}
@ -74,7 +73,7 @@ class BookmarkFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragment_
newText
), 20
).build()
bookmarkLiveData?.observe(viewLifecycleOwner, Observer { adapter.submitList(it) })
bookmarkLiveData?.observe(viewLifecycleOwner, { adapter.submitList(it) })
}
}

@ -1,7 +1,7 @@
package io.legado.app.ui.book.chapterlist
import android.content.Context
import android.widget.TextView
import android.view.View
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
@ -21,8 +21,11 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
override fun convert(holder: ItemViewHolder, item: BookChapter, payloads: MutableList<Any>) {
with(holder.itemView) {
val isDur = callback.durChapterIndex() == item.index
val cached = callback.isLocalBook
|| cacheFileNames.contains(BookHelp.formatChapterName(item))
if (payloads.isEmpty()) {
if (callback.durChapterIndex() == item.index) {
if (isDur) {
tv_chapter_name.setTextColor(context.accentColor)
} else {
tv_chapter_name.setTextColor(context.getCompatColor(R.color.primaryText))
@ -32,15 +35,9 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
tv_tag.text = item.tag
tv_tag.visible()
}
upHasCache(
tv_chapter_name,
cacheFileNames.contains(BookHelp.formatChapterName(item))
)
upHasCache(this, isDur, cached)
} else {
upHasCache(
tv_chapter_name,
cacheFileNames.contains(BookHelp.formatChapterName(item))
)
upHasCache(this, isDur, cached)
}
}
}
@ -53,11 +50,18 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
}
}
private fun upHasCache(textView: TextView, contains: Boolean) {
textView.paint.isFakeBoldText = contains
private fun upHasCache(itemView: View, isDur: Boolean, cached: Boolean) = itemView.apply {
tv_chapter_name.paint.isFakeBoldText = cached
iv_checked.setImageResource(R.drawable.ic_outline_cloud_24)
iv_checked.visible(!cached)
if (isDur) {
iv_checked.setImageResource(R.drawable.ic_check)
iv_checked.visible()
}
}
interface Callback {
val isLocalBook: Boolean
fun openChapter(bookChapter: BookChapter)
fun durChapterIndex(): Int
}

@ -6,7 +6,6 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseFragment
@ -89,7 +88,7 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
private fun initDoc() {
tocLiveData?.removeObservers(this@ChapterListFragment)
tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl)
tocLiveData?.observe(viewLifecycleOwner, Observer {
tocLiveData?.observe(viewLifecycleOwner, {
adapter.setItems(it)
if (!scrollToDurChapter) {
mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0)
@ -124,12 +123,15 @@ class ChapterListFragment : VMBaseFragment<ChapterListViewModel>(R.layout.fragme
} else {
tocLiveData?.removeObservers(this)
tocLiveData = App.db.bookChapterDao().liveDataSearch(viewModel.bookUrl, newText)
tocLiveData?.observe(viewLifecycleOwner, Observer {
tocLiveData?.observe(viewLifecycleOwner, {
adapter.setItems(it)
})
}
}
override val isLocalBook: Boolean
get() = viewModel.book?.isLocalBook() == true
override fun durChapterIndex(): Int {
return durChapterIndex
}

@ -6,7 +6,6 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import io.legado.app.App
@ -129,7 +128,7 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup()
else -> App.db.bookDao().observeByGroup(groupId)
}
booksLiveData?.observe(this, Observer { list ->
booksLiveData?.observe(this, { list ->
val booksDownload = list.filter {
it.isOnLineTxt()
}
@ -147,7 +146,7 @@ class DownloadActivity : VMBaseActivity<DownloadViewModel>(R.layout.activity_dow
private fun initGroupData() {
groupLiveData?.removeObservers(this)
groupLiveData = App.db.bookGroupDao().liveDataAll()
groupLiveData?.observe(this, Observer {
groupLiveData?.observe(this, {
groupList.clear()
groupList.addAll(it)
adapter.notifyDataSetChanged()

@ -1,7 +1,6 @@
package io.legado.app.ui.book.explore
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
@ -27,7 +26,7 @@ class ExploreShowActivity : VMBaseActivity<ExploreShowViewModel>(R.layout.activi
override fun onActivityCreated(savedInstanceState: Bundle?) {
title_bar.title = intent.getStringExtra("exploreName")
initRecyclerView()
viewModel.booksData.observe(this, Observer { upData(it) })
viewModel.booksData.observe(this, { upData(it) })
viewModel.initData(intent)
}

@ -7,7 +7,7 @@ import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
import kotlinx.coroutines.Dispatchers.IO
class ExploreShowViewModel(application: Application) : BaseViewModel(application) {

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -20,7 +19,6 @@ import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.AppConfig
import io.legado.app.help.ItemTouchCallback
@ -30,7 +28,7 @@ import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
@ -65,7 +63,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tool_bar.setBackgroundColor(bottomBackground)
tool_bar.setBackgroundColor(primaryColor)
tool_bar.title = getString(R.string.group_manage)
initData()
initMenu()
@ -79,7 +77,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
tv_ok.setTextColor(requireContext().accentColor)
tv_ok.visible()
tv_ok.onClick { dismiss() }
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, {
val diffResult =
DiffUtil.calculateDiff(GroupDiffCallBack(ArrayList(adapter.getItems()), it))
adapter.setItems(it, diffResult)
@ -94,7 +92,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
tool_bar.setOnMenuItemClickListener(this)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.let {
it.applyTint(requireContext(), Theme.getTheme())
it.applyTint(requireContext())
it.findItem(R.id.menu_group_all)
.isChecked = AppConfig.bookGroupAllShow
it.findItem(R.id.menu_group_local)

@ -11,7 +11,6 @@ import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -20,7 +19,6 @@ import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
@ -29,7 +27,7 @@ import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton
import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModel
@ -81,7 +79,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tool_bar.setBackgroundColor(bottomBackground)
tool_bar.setBackgroundColor(primaryColor)
callBack = activity as? CallBack
arguments?.let {
groupId = it.getInt("groupId")
@ -94,7 +92,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
private fun initView() {
tool_bar.title = getString(R.string.group_select)
tool_bar.inflateMenu(R.menu.book_group_manage)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
tool_bar.menu.setGroupVisible(R.id.menu_groups, false)
adapter = GroupAdapter(requireContext())
@ -114,7 +112,7 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
}
private fun initData() {
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer {
App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, {
adapter.setItems(it)
})
}

@ -9,7 +9,6 @@ import android.view.Menu
import android.view.MenuItem
import android.widget.CheckBox
import android.widget.LinearLayout
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions.bitmapTransform
@ -25,6 +24,7 @@ import io.legado.app.help.IntentDataHelp
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.ui.audio.AudioPlayActivity
import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.ui.book.changesource.ChangeSourceDialog
@ -35,10 +35,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.dp
import io.legado.app.utils.getViewModel
import io.legado.app.utils.gone
import io.legado.app.utils.visible
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.activity_book_info.*
import org.jetbrains.anko.sdk27.listeners.onClick
import org.jetbrains.anko.startActivity
@ -47,7 +44,7 @@ import org.jetbrains.anko.toast
class BookInfoActivity :
VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info, theme = Theme.Dark),
VMBaseActivity<BookInfoViewModel>(R.layout.activity_book_info, toolBarTheme = Theme.Dark),
GroupSelectDialog.CallBack,
ChapterListAdapter.CallBack,
ChangeSourceDialog.CallBack,
@ -60,14 +57,16 @@ class BookInfoActivity :
override val viewModel: BookInfoViewModel
get() = getViewModel(BookInfoViewModel::class.java)
@SuppressLint("PrivateResource")
override fun onActivityCreated(savedInstanceState: Bundle?) {
title_bar.transparent()
arc_view.setBgColor(backgroundColor)
ll_info.setBackgroundColor(backgroundColor)
scroll_view.setBackgroundColor(backgroundColor)
fl_action.setBackgroundColor(bottomBackground)
viewModel.bookData.observe(this, Observer { showBook(it) })
viewModel.chapterListData.observe(this, Observer { upLoading(false, it) })
tv_shelf.setTextColor(getPrimaryTextColor(ColorUtils.isColorLight(bottomBackground)))
viewModel.bookData.observe(this, { showBook(it) })
viewModel.chapterListData.observe(this, { upLoading(false, it) })
viewModel.initData(intent)
initOnClick()
}
@ -97,7 +96,7 @@ class BookInfoActivity :
if (it.isLocalBook()) {
it.tocUrl = ""
}
viewModel.loadBookInfo(it)
viewModel.loadBookInfo(it, false)
}
}
R.id.menu_can_update -> {

@ -9,8 +9,8 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp
import io.legado.app.model.WebBook
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.ReadBook
import kotlinx.coroutines.Dispatchers.IO
@ -49,15 +49,15 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
fun loadBookInfo(
book: Book,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null
book: Book, canReName: Boolean = true,
changeDruChapterIndex: ((chapters: List<BookChapter>) -> Unit)? = null,
) {
execute {
if (book.isLocalBook()) {
loadChapter(book, changeDruChapterIndex)
} else {
App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource ->
WebBook(bookSource).getBookInfo(book, this)
WebBook(bookSource).getBookInfo(book, this, canReName = canReName)
.onSuccess(IO) {
bookData.postValue(book)
if (inBookshelf) {
@ -138,7 +138,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
}
bookData.postValue(newBook)
if (newBook.tocUrl.isEmpty()) {
loadBookInfo(newBook) { upChangeDurChapterIndex(newBook, it) }
loadBookInfo(newBook, false) { upChangeDurChapterIndex(newBook, it) }
} else {
loadChapter(newBook) { upChangeDurChapterIndex(newBook, it) }
}

@ -7,7 +7,6 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.Observer
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
@ -30,7 +29,7 @@ class BookInfoEditActivity :
get() = getViewModel(BookInfoEditViewModel::class.java)
override fun onActivityCreated(savedInstanceState: Bundle?) {
viewModel.bookData.observe(this, Observer { upView(it) })
viewModel.bookData.observe(this, { upView(it) })
if (viewModel.bookData.value == null) {
intent.getStringExtra("bookUrl")?.let {
viewModel.loadBook(it)

@ -12,7 +12,6 @@ import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
@ -114,7 +113,7 @@ class ImportBookActivity : VMBaseActivity<ImportBookViewModel>(R.layout.activity
private fun initData() {
localUriLiveData?.removeObservers(this)
localUriLiveData = App.db.bookDao().observeLocalUri()
localUriLiveData?.observe(this, Observer {
localUriLiveData?.observe(this, {
adapter.upBookHas(it)
})
}

@ -14,15 +14,13 @@ import io.legado.app.R
import io.legado.app.data.entities.Bookmark
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
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.yesButton
import io.legado.app.lib.dialogs.*
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.service.help.Download
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.applyTint
import io.legado.app.utils.requestInputMethod
import kotlinx.android.synthetic.main.dialog_download_choice.view.*
@ -37,7 +35,7 @@ object Help {
/**
* 更新状态栏,导航栏
*/
fun upSystemUiVisibility(activity: Activity, toolBarHide: Boolean = true) {
fun upSystemUiVisibility(window: Window, toolBarHide: Boolean = true) {
var flag = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_IMMERSIVE
@ -53,13 +51,13 @@ object Help {
flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
}
}
activity.window.decorView.systemUiVisibility = flag
window.decorView.systemUiVisibility = flag
if (toolBarHide) {
ATH.setLightStatusBar(activity, ReadBookConfig.durConfig.statusIconDark())
ATH.setLightStatusBar(window, ReadBookConfig.durConfig.statusIconDark())
} else {
ATH.setLightStatusBarAuto(
activity,
ThemeStore.statusBarColor(activity, AppConfig.isTransparentStatusBar)
window,
ThemeStore.statusBarColor(App.INSTANCE, AppConfig.isTransparentStatusBar)
)
}
}
@ -181,4 +179,27 @@ object Help {
noButton()
}.show().applyTint().requestInputMethod()
}
@SuppressLint("InflateParams")
fun showCharsetConfig(context: Context) = with(context) {
val charsets =
arrayListOf("UTF-8", "GB2312", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII")
alert(R.string.set_charset) {
var editText: AutoCompleteTextView? = null
customView {
layoutInflater.inflate(R.layout.dialog_edit_text, null).apply {
editText = edit_view
edit_view.setFilterValues(charsets)
edit_view.setText(ReadBook.book?.charset)
}
}
okButton {
val text = editText?.text?.toString()
text?.let {
ReadBook.setCharset(it)
}
}
cancelButton()
}.show().applyTint()
}
}

@ -14,7 +14,6 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.core.view.size
import androidx.lifecycle.Observer
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import io.legado.app.BuildConfig
import io.legado.app.R
@ -108,11 +107,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
initView()
upScreenTimeOut()
ReadBook.callBack = this
ReadBook.titleDate.observe(this, Observer {
ReadBook.titleDate.observe(this) {
title_bar.title = it
upMenu()
upView()
})
}
viewModel.initData(intent)
}
@ -128,6 +127,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
override fun onResume() {
super.onResume()
ReadBook.readStartTime = System.currentTimeMillis()
upSystemUiVisibility()
timeBatteryReceiver = TimeBatteryReceiver.register(this)
page_view.upTime()
@ -251,8 +251,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_copy_text ->
TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent())
R.id.menu_update_toc -> ReadBook.book?.let {
ReadBook.upMsg(getString(R.string.toc_updateing))
viewModel.loadChapterList(it)
loadChapterList(it)
}
R.id.menu_enable_replace -> ReadBook.book?.let {
it.useReplaceRule = !it.useReplaceRule
@ -275,6 +274,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
Pair("loginUrl", it.loginUrl)
)
}
R.id.menu_set_charset -> Help.showCharsetConfig(this)
}
return super.onCompatOptionsItemSelected(item)
}
@ -515,6 +515,11 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
return false
}
override fun loadChapterList(book: Book) {
ReadBook.upMsg(getString(R.string.toc_updateing))
viewModel.loadChapterList(book)
}
/**
* 内容加载完成
*/
@ -686,7 +691,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
* 更新状态栏,导航栏
*/
override fun upSystemUiVisibility() {
Help.upSystemUiVisibility(this, !read_menu.isVisible)
Help.upSystemUiVisibility(window, !read_menu.isVisible)
upNavigationBarColor()
}

@ -10,8 +10,8 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentDataHelp
import io.legado.app.model.WebBook
import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
@ -64,10 +64,17 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
ReadBook.loadContent(resetPageOffset = true)
}
} else {
isInitFinish = true
if (ReadBook.durChapterIndex != book.durChapterIndex) {
ReadBook.durChapterIndex = book.durChapterIndex
ReadBook.durPageIndex = book.durChapterPos
ReadBook.prevTextChapter = null
ReadBook.curTextChapter = null
ReadBook.nextTextChapter = null
}
ReadBook.book!!.group = book.group
ReadBook.titleDate.postValue(book.name)
ReadBook.upWebBook(book)
isInitFinish = true
if (!book.isLocalBook() && ReadBook.webBook == null) {
autoChangeSource(book.name, book.author)
return
@ -82,6 +89,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} else {
if (ReadBook.curTextChapter != null) {
ReadBook.callBack?.upContent(resetPageOffset = false)
} else {
ReadBook.loadContent(resetPageOffset = true)
}
}
}
@ -95,7 +104,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (book.isLocalBook()) {
loadChapterList(book, changeDruChapterIndex)
} else {
ReadBook.webBook?.getBookInfo(book, this)
ReadBook.webBook?.getBookInfo(book, this, canReName = false)
?.onSuccess {
loadChapterList(book, changeDruChapterIndex)
}

@ -2,10 +2,7 @@ package io.legado.app.ui.book.read.config
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.SeekBar
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
@ -14,7 +11,6 @@ import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.ColorUtils
import kotlinx.android.synthetic.main.dialog_auto_read.*
import org.jetbrains.anko.sdk27.listeners.onClick
@ -26,10 +22,10 @@ class AutoReadDialog : BaseDialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes

@ -9,10 +9,7 @@ import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.documentfile.provider.DocumentFile
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -47,10 +44,10 @@ class BgTextConfigDialog : BaseDialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 5, 0, 0)
val attr = it.attributes
@ -102,7 +99,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
sw_dark_status_icon.onCheckedChange { buttonView, isChecked ->
if (buttonView?.isPressed == true) {
setStatusIconDark(isChecked)
activity?.let {
activity?.window?.let {
Help.upSystemUiVisibility(it)
}
}

@ -4,10 +4,7 @@ import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.LinearLayout
import androidx.fragment.app.DialogFragment
import androidx.preference.Preference
@ -30,10 +27,10 @@ class MoreConfigDialog : DialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes

@ -6,11 +6,11 @@ import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.help.ReadBookConfig
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.postEvent
import kotlinx.android.synthetic.main.dialog_read_padding.*
@ -20,10 +20,10 @@ class PaddingConfigDialog : BaseDialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
val attr = it.attributes
attr.dimAmount = 0.0f
it.attributes = attr

@ -10,16 +10,16 @@ import android.widget.LinearLayout
import androidx.fragment.app.DialogFragment
import androidx.preference.ListPreference
import androidx.preference.Preference
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BasePreferenceFragment
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.getPrefLong
import io.legado.app.utils.postEvent
class ReadAloudConfigDialog : DialogFragment() {
@ -29,10 +29,10 @@ class ReadAloudConfigDialog : DialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.setBackgroundDrawableResource(R.color.transparent)
it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT)
}
@ -62,11 +62,18 @@ class ReadAloudConfigDialog : DialogFragment() {
class ReadAloudPreferenceFragment : BasePreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val speakEngineSummary: String
get() {
val eid = App.INSTANCE.getPrefLong(PreferKey.speakEngine)
val ht = App.db.httpTTSDao().get(eid)
return ht?.name ?: getString(R.string.local_tts)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_aloud)
upPreferenceSummary(
findPreference<ListPreference>(PreferKey.ttsSpeechPer),
AppConfig.ttsSpeechPer
findPreference(PreferKey.speakEngine),
speakEngineSummary
)
}
@ -85,6 +92,14 @@ class ReadAloudConfigDialog : DialogFragment() {
super.onPause()
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
when (preference?.key) {
PreferKey.speakEngine ->
SpeakEngineDialog().show(childFragmentManager, "speakEngine")
}
return super.onPreferenceTreeClick(preference)
}
override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?
@ -95,16 +110,9 @@ class ReadAloudConfigDialog : DialogFragment() {
postEvent(EventBus.MEDIA_BUTTON, false)
}
}
PreferKey.readAloudOnLine -> {
ReadAloud.stop(requireContext())
ReadAloud.aloudClass = ReadAloud.getReadAloudClass()
}
PreferKey.ttsSpeechPer -> {
upPreferenceSummary(
findPreference<ListPreference>(PreferKey.ttsSpeechPer),
AppConfig.ttsSpeechPer
)
ReadAloud.upTtsSpeechRate(requireContext())
PreferKey.speakEngine -> {
upPreferenceSummary(findPreference(key), speakEngineSummary)
ReadAloud.upReadAloudClass()
}
}
}

@ -2,10 +2,7 @@ package io.legado.app.ui.book.read.config
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import android.widget.SeekBar
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
@ -16,7 +13,6 @@ import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.Help
import io.legado.app.utils.ColorUtils
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.observeEvent
@ -31,10 +27,10 @@ class ReadAloudDialog : BaseDialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes

@ -4,16 +4,12 @@ import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.*
import androidx.core.view.get
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
@ -21,7 +17,6 @@ import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.book.read.Help
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.*
@ -40,10 +35,10 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.let {
it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
it.setBackgroundDrawableResource(R.color.background)
it.decorView.setPadding(0, 0, 0, 0)
val attr = it.attributes
@ -209,9 +204,6 @@ class ReadStyleDialog : BaseDialogFragment(), FontSelectDialog.CallBack {
upBg()
postEvent(EventBus.UP_CONFIG, true)
}
if (AppConfig.isEInkMode) {
toast(R.string.e_ink_change_bg)
}
}
private fun showBgTextConfig(index: Int): Boolean {

@ -0,0 +1,164 @@
package io.legado.app.ui.book.read.config
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.HttpTTS
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.cancelButton
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.primaryColor
import io.legado.app.service.help.ReadAloud
import io.legado.app.utils.*
import kotlinx.android.synthetic.main.dialog_http_tts_edit.view.*
import kotlinx.android.synthetic.main.dialog_recycler_view.*
import kotlinx.android.synthetic.main.item_http_tts.view.*
import org.jetbrains.anko.sdk27.listeners.onClick
class SpeakEngineDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onStart() {
super.onStart()
val dm = DisplayMetrics()
activity?.windowManager?.defaultDisplay?.getMetrics(dm)
dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt())
}
lateinit var adapter: Adapter
lateinit var viewModel: SpeakEngineViewModel
private var httpTTSData: LiveData<List<HttpTTS>>? = null
var engineId = App.INSTANCE.getPrefLong(PreferKey.speakEngine)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = getViewModel(SpeakEngineViewModel::class.java)
return inflater.inflate(R.layout.dialog_recycler_view, container)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
initView()
initMenu()
initData()
}
private fun initView() {
tool_bar.setBackgroundColor(primaryColor)
tool_bar.setTitle(R.string.speak_engine)
recycler_view.layoutManager = LinearLayoutManager(requireContext())
adapter = Adapter(requireContext())
recycler_view.adapter = adapter
tv_footer_left.setText(R.string.local_tts)
tv_footer_left.visible()
tv_footer_left.onClick {
removePref(PreferKey.speakEngine)
dismiss()
}
tv_ok.visible()
tv_ok.onClick {
putPrefLong(PreferKey.speakEngine, engineId)
dismiss()
}
tv_cancel.visible()
tv_cancel.onClick {
dismiss()
}
}
private fun initMenu() {
tool_bar.inflateMenu(R.menu.speak_engine)
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
}
private fun initData() {
httpTTSData?.removeObservers(this)
httpTTSData = App.db.httpTTSDao().observeAll()
httpTTSData?.observe(this, {
adapter.setItems(it)
})
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_add -> editHttpTTS()
R.id.menu_default -> viewModel.importDefault()
}
return true
}
@SuppressLint("InflateParams")
private fun editHttpTTS(v: HttpTTS? = null) {
val httpTTS = v?.copy() ?: HttpTTS()
requireContext().alert(titleResource = R.string.speak_engine) {
var rootView: View? = null
customView {
LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_http_tts_edit, null).apply {
rootView = this
tv_name.setText(httpTTS.name)
tv_url.setText(httpTTS.url)
}
}
cancelButton()
okButton {
rootView?.apply {
httpTTS.name = tv_name.text.toString()
httpTTS.url = tv_url.text.toString()
App.db.httpTTSDao().insert(httpTTS)
ReadAloud.upReadAloudClass()
}
}
}.show().applyTint()
}
inner class Adapter(context: Context) :
SimpleRecyclerAdapter<HttpTTS>(context, R.layout.item_http_tts) {
override fun convert(holder: ItemViewHolder, item: HttpTTS, payloads: MutableList<Any>) {
holder.itemView.apply {
cb_name.text = item.name
cb_name.isChecked = item.id == engineId
}
}
override fun registerListener(holder: ItemViewHolder) {
holder.itemView.apply {
cb_name.onClick {
getItem(holder.layoutPosition)?.let { httpTTS ->
engineId = httpTTS.id
notifyItemRangeChanged(0, getActualItemCount())
}
}
iv_edit.onClick {
editHttpTTS(getItem(holder.layoutPosition))
}
iv_menu_delete.onClick {
getItem(holder.layoutPosition)?.let { httpTTS ->
App.db.httpTTSDao().delete(httpTTS)
}
}
}
}
}
}

@ -0,0 +1,34 @@
package io.legado.app.ui.book.read.config
import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.help.DefaultValueHelp
import io.legado.app.help.http.HttpHelper
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
class SpeakEngineViewModel(application: Application) : BaseViewModel(application) {
fun importDefault() {
execute {
DefaultValueHelp.initHttpTTS()
}
}
fun importOnLine(url: String, finally: (msg: String) -> Unit) {
execute {
HttpHelper.simpleGetAsync(url)?.let { json ->
GSON.fromJsonArray<TxtTocRule>(json)?.let {
App.db.txtTocRule().insert(*it.toTypedArray())
}
}
}.onSuccess {
finally("导入成功")
}.onError {
finally("导入失败")
}
}
}

@ -22,19 +22,19 @@ class TipConfigDialog : BaseDialogFragment() {
super.onStart()
val dm = DisplayMetrics()
activity?.let {
Help.upSystemUiVisibility(it)
it.windowManager?.defaultDisplay?.getMetrics(dm)
}
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
dialog?.window?.let {
Help.upSystemUiVisibility(it)
it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.dialog_tip_config, container)
}

@ -11,7 +11,6 @@ import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -21,7 +20,6 @@ import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.help.ItemTouchCallback
import io.legado.app.lib.dialogs.alert
@ -29,7 +27,7 @@ import io.legado.app.lib.dialogs.cancelButton
import io.legado.app.lib.dialogs.customView
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
@ -68,11 +66,11 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
tool_bar.setBackgroundColor(bottomBackground)
tool_bar.setBackgroundColor(primaryColor)
durRegex = arguments?.getString("tocRegex")
tool_bar.setTitle(R.string.txt_toc_regex)
tool_bar.inflateMenu(R.menu.txt_toc_regex)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
initView()
initData()
@ -105,7 +103,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
private fun initData() {
tocRegexLiveData?.removeObservers(viewLifecycleOwner)
tocRegexLiveData = App.db.txtTocRule().observeAll()
tocRegexLiveData?.observe(viewLifecycleOwner, Observer { tocRules ->
tocRegexLiveData?.observe(viewLifecycleOwner, { tocRules ->
initSelectedName(tocRules)
adapter.setItems(tocRules)
})

@ -7,6 +7,7 @@ import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import io.legado.app.App
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@ -18,7 +19,6 @@ import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.utils.*
import java.util.*
import java.util.regex.Pattern
@Suppress("DEPRECATION")
@ -38,8 +38,6 @@ object ChapterProvider {
var typeface: Typeface = Typeface.SANS_SERIF
lateinit var titlePaint: TextPaint
lateinit var contentPaint: TextPaint
private val srcPattern =
Pattern.compile("<img .*?src.*?=.*?\"(.*?(?:,\\{.*\\})?)\".*?>", Pattern.CASE_INSENSITIVE)
init {
upStyle()
@ -62,7 +60,7 @@ object ChapterProvider {
var durY = 0f
textPages.add(TextPage())
contents.forEachIndexed { index, text ->
val matcher = srcPattern.matcher(text)
val matcher = AppPattern.imgPattern.matcher(text)
if (matcher.find()) {
var src = matcher.group(1)
if (!book.isEpub()) {

@ -7,13 +7,13 @@ import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook
@ -176,17 +176,17 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
}
private fun initLiveData() {
App.db.bookSourceDao().liveGroupEnabled().observe(this, Observer {
App.db.bookSourceDao().liveGroupEnabled().observe(this, {
groups.clear()
it.map { group ->
groups.addAll(group.splitNotBlank(",", ";"))
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
upGroupMenu()
})
viewModel.searchBookLiveData.observe(this, Observer {
viewModel.searchBookLiveData.observe(this, {
upSearchItems(it)
})
viewModel.isSearchLiveData.observe(this, Observer {
viewModel.isSearchLiveData.observe(this, {
if (it) {
startSearch()
} else {
@ -254,7 +254,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
rv_bookshelf_search.gone()
} else {
bookData = App.db.bookDao().liveDataSearch(key)
bookData?.observe(this, Observer {
bookData?.observe(this, {
if (it.isEmpty()) {
tv_book_show.gone()
rv_bookshelf_search.gone()
@ -272,7 +272,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
} else {
App.db.searchKeywordDao().liveDataSearch(key)
}
historyData?.observe(this, Observer {
historyData?.observe(this, {
historyKeyAdapter.setItems(it)
if (it.isEmpty()) {
tv_clear_history.invisible()

@ -8,7 +8,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword
import io.legado.app.model.SearchBookModel
import io.legado.app.model.webBook.SearchBookModel
import io.legado.app.utils.getPrefBoolean
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
@ -30,13 +30,16 @@ class SearchViewModel(application: Application) : BaseViewModel(application),
* 开始搜索
*/
fun search(key: String) {
if (searchKey != key && key.isNotEmpty()) {
if ((searchKey == key) || key.isNotEmpty()) {
searchBookModel.cancelSearch()
searchBooks.clear()
searchBookLiveData.postValue(searchBooks)
searchID = System.currentTimeMillis()
searchKey = key
}
if (searchKey.isEmpty()) {
return
}
searchBookModel.search(searchID, searchKey)
}
@ -54,6 +57,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application),
override fun onSearchStart() {
isSearchLiveData.postValue(true)
isLoading = true
}
override fun onSearchSuccess(searchBooks: ArrayList<SearchBook>) {
@ -110,7 +114,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application),
&& item.author == searchBook.author
) {
hasSame = true
searchBook.addOrigin(item.bookUrl)
searchBook.addOrigin(item.origin)
break
}
}

@ -4,7 +4,7 @@ import android.app.Application
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.model.Debug
import io.legado.app.model.WebBook
import io.legado.app.model.webBook.WebBook
class BookSourceDebugModel(application: Application) : BaseViewModel(application),
Debug.Callback {

@ -11,13 +11,13 @@ import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.BookSource
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.ItemTouchCallback
@ -31,6 +31,7 @@ import io.legado.app.ui.filechooser.FileChooserDialog
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.ui.qrcode.QrCodeActivity
import io.legado.app.ui.widget.SelectActionBar
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.ui.widget.text.AutoCompleteTextView
import io.legado.app.utils.*
@ -137,6 +138,13 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
val itemTouchCallback = ItemTouchCallback()
itemTouchCallback.onItemTouchCallbackListener = adapter
itemTouchCallback.isCanDrag = true
val dragSelectTouchHelper: DragSelectTouchHelper =
DragSelectTouchHelper(adapter.initDragSelectTouchHelperCallback()).setSlideArea(16, 50)
dragSelectTouchHelper.attachToRecyclerView(recycler_view)
// When this page is opened, it is in selection mode
dragSelectTouchHelper.activeSlideSelect()
// Note: need judge selection first, so add ItemTouchHelper after it.
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view)
}
@ -164,7 +172,7 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
App.db.bookSourceDao().liveDataSearch("%$searchKey%")
}
}
bookSourceLiveDate?.observe(this, Observer { data ->
bookSourceLiveDate?.observe(this, { data ->
val sourceList = when (sort) {
1 -> data.sortedBy { it.weight }
2 -> data.sortedBy { it.bookSourceName }
@ -179,10 +187,10 @@ class BookSourceActivity : VMBaseActivity<BookSourceViewModel>(R.layout.activity
}
private fun initLiveDataGroup() {
App.db.bookSourceDao().liveGroup().observe(this, Observer {
App.db.bookSourceDao().liveGroup().observe(this, {
groups.clear()
it.map { group ->
groups.addAll(group.splitNotBlank(",", ";"))
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
upGroupMenu()
})

@ -14,6 +14,7 @@ import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.data.entities.BookSource
import io.legado.app.help.ItemTouchCallback.OnItemTouchCallbackListener
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import kotlinx.android.synthetic.main.item_book_source.view.*
@ -193,6 +194,31 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
}
}
fun initDragSelectTouchHelperCallback(): DragSelectTouchHelper.Callback {
return object : DragSelectTouchHelper.AdvanceCallback<BookSource>(Mode.ToggleAndReverse) {
override fun currentSelectedId(): MutableSet<BookSource> {
return selected
}
override fun getItemId(position: Int): BookSource {
return getItem(position)!!
}
override fun updateSelectState(position: Int, isSelected: Boolean): Boolean {
getItem(position)?.let {
if (isSelected) {
selected.add(it)
} else {
selected.remove(it)
}
notifyItemChanged(position, bundleOf(Pair("selected", null)))
return true
}
return false
}
}
}
interface CallBack {
fun del(bookSource: BookSource)
fun edit(bookSource: BookSource)

@ -5,6 +5,7 @@ import android.text.TextUtils
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.entities.BookSource
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON
@ -96,9 +97,9 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
fun selectionAddToGroups(sources: List<BookSource>, groups: String) {
execute {
val list = arrayListOf<BookSource>()
sources.forEach {
sources.forEach { source ->
val newGroupList = arrayListOf<String>()
it.bookSourceGroup?.splitNotBlank(",", ";")?.forEach {
source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.forEach {
newGroupList.add(it)
}
groups.splitNotBlank(",", ";", "").forEach {
@ -106,7 +107,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}
val lh = LinkedHashSet(newGroupList)
val newGroup = ArrayList(lh).joinToString(separator = ",")
list.add(it.copy(bookSourceGroup = newGroup))
list.add(source.copy(bookSourceGroup = newGroup))
}
App.db.bookSourceDao().update(*list.toTypedArray())
}
@ -115,9 +116,9 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
fun selectionRemoveFromGroups(sources: List<BookSource>, groups: String) {
execute {
val list = arrayListOf<BookSource>()
sources.forEach {
sources.forEach { source ->
val newGroupList = arrayListOf<String>()
it.bookSourceGroup?.splitNotBlank(",", ";")?.forEach {
source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.forEach {
newGroupList.add(it)
}
groups.splitNotBlank(",", ";", "").forEach {
@ -125,7 +126,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}
val lh = LinkedHashSet(newGroupList)
val newGroup = ArrayList(lh).joinToString(separator = ",")
list.add(it.copy(bookSourceGroup = newGroup))
list.add(source.copy(bookSourceGroup = newGroup))
}
App.db.bookSourceDao().update(*list.toTypedArray())
}

@ -11,18 +11,18 @@ import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Theme
import io.legado.app.constant.AppPattern
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.yesButton
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.getViewModelOfActivity
@ -56,22 +56,23 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(backgroundColor)
initData()
}
private fun initData() {
tool_bar.setBackgroundColor(primaryColor)
tool_bar.title = getString(R.string.group_manage)
tool_bar.inflateMenu(R.menu.group_manage)
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
adapter = GroupAdapter(requireContext())
recycler_view.layoutManager = LinearLayoutManager(requireContext())
recycler_view.addItemDecoration(VerticalDivider(requireContext()))
recycler_view.adapter = adapter
App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, Observer {
initData()
}
private fun initData() {
App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, {
val groups = linkedSetOf<String>()
it.map { group ->
groups.addAll(group.splitNotBlank(",", ";"))
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
adapter.setItems(groups.toList())
})

@ -19,6 +19,7 @@ import io.legado.app.help.storage.WebDavHelp
import io.legado.app.ui.filechooser.FilePicker
import io.legado.app.utils.getPrefString
import io.legado.app.utils.isContentPath
import io.legado.app.utils.longToast
import io.legado.app.utils.toast
import kotlinx.coroutines.Dispatchers.Main
import org.jetbrains.anko.toast
@ -77,8 +78,9 @@ object BackupRestoreUi {
fun restore(fragment: Fragment) {
Coroutine.async(context = Main) {
val restoreFromWebDav = WebDavHelp.showRestoreDialog(fragment.requireContext())
if (!restoreFromWebDav) {
WebDavHelp.showRestoreDialog(fragment.requireContext())
}.onError {
fragment.longToast("WebDavError:${it.localizedMessage},将从本地备份恢复。")
val backupPath = fragment.getPrefString(PreferKey.backupPath)
if (backupPath?.isNotEmpty() == true) {
if (backupPath.isContentPath()) {
@ -97,7 +99,6 @@ object BackupRestoreUi {
}
}
}
}
fun restoreByFolder(fragment: Fragment) {
selectBackupFolder(fragment, restoreSelectRequestCode)

@ -20,6 +20,9 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.okButton
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH
import io.legado.app.receiver.SharedReceiverActivity
@ -79,11 +82,7 @@ class OtherConfigFragment : BasePreferenceFragment(),
.show {
putPrefInt(PreferKey.webPort, it)
}
PreferKey.cleanCache -> {
BookHelp.clearCache()
FileUtils.deleteFile(requireActivity().cacheDir.absolutePath)
toast(R.string.clear_cache_success)
}
PreferKey.cleanCache -> clearCache()
PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) {
selectDefaultCover()
} else {
@ -145,6 +144,18 @@ class OtherConfigFragment : BasePreferenceFragment(),
}
}
private fun clearCache() {
requireContext().alert(titleResource = R.string.clear_cache,
messageResource = R.string.sure_del) {
okButton {
BookHelp.clearCache()
FileUtils.deleteFile(requireActivity().cacheDir.absolutePath)
toast(R.string.clear_cache_success)
}
noButton()
}.show().applyTint()
}
private fun selectDefaultCover() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)

@ -12,7 +12,7 @@ import androidx.fragment.app.FragmentManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R
import io.legado.app.constant.Theme
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.filechooser.adapter.FileAdapter
import io.legado.app.ui.filechooser.adapter.PathAdapter
import io.legado.app.ui.widget.recycler.VerticalDivider
@ -93,6 +93,7 @@ class FileChooserDialog : DialogFragment(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tool_bar.setBackgroundColor(primaryColor)
view.setBackgroundResource(R.color.background_card)
arguments?.let {
requestCode = it.getInt("requestCode")
@ -129,7 +130,7 @@ class FileChooserDialog : DialogFragment(),
tool_bar.menu.add(menuTitle)
}
}
tool_bar.menu.applyTint(requireContext(), Theme.getTheme())
tool_bar.menu.applyTint(requireContext())
tool_bar.setOnMenuItemClickListener(this)
}

@ -62,7 +62,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
//自动更新书籍
if (AppConfig.autoRefreshBook) {
view_pager_main.postDelayed({
viewModel.upChapterList()
viewModel.upAllBookToc()
}, 1000)
}
view_pager_main.postDelayed({
@ -93,8 +93,8 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
}
private fun upVersion() {
if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) {
putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode)
if (getPrefInt(PreferKey.versionCode) != App.versionCode) {
putPrefInt(PreferKey.versionCode, App.versionCode)
if (!BuildConfig.DEBUG) {
val log = String(assets.open("updateLog.md").readBytes())
TextDialog.show(supportFragmentManager, log, TextDialog.MD, 5000, true)
@ -168,7 +168,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
}
}
private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) :
private inner class TabFragmentPageAdapter(fm: FragmentManager) :
FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
override fun getItemPosition(`object`: Any): Int {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save