Merge remote-tracking branch 'origin/master' into eink

# Conflicts:
#	app/src/main/java/io/legado/app/lib/theme/ATH.kt
#	app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
#	app/src/main/java/io/legado/app/utils/ContextExtensions.kt
pull/241/head
Modificator 5 years ago
commit cb374e548b
  1. 4
      app/src/main/assets/updateLog.md
  2. 13
      app/src/main/java/io/legado/app/App.kt
  3. 4
      app/src/main/java/io/legado/app/base/BaseActivity.kt
  4. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  5. 5
      app/src/main/java/io/legado/app/constant/Theme.kt
  6. 51
      app/src/main/java/io/legado/app/help/AppConfig.kt
  7. 135
      app/src/main/java/io/legado/app/help/BookHelp.kt
  8. 58
      app/src/main/java/io/legado/app/help/FileHelp.kt
  9. 13
      app/src/main/java/io/legado/app/help/PrefExtensions.kt
  10. 14
      app/src/main/java/io/legado/app/help/ReadBookConfig.kt
  11. 22
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  12. 21
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  13. 8
      app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt
  14. 15
      app/src/main/java/io/legado/app/lib/theme/ATH.kt
  15. 4
      app/src/main/java/io/legado/app/service/DownloadService.kt
  16. 10
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  17. 23
      app/src/main/java/io/legado/app/ui/README.md
  18. 4
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  19. 4
      app/src/main/java/io/legado/app/ui/book/read/Help.kt
  20. 5
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  21. 3
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  22. 4
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  23. 4
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  24. 3
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  25. 4
      app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt
  26. 61
      app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt
  27. 25
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  28. 16
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  29. 2
      app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt
  30. 4
      app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt
  31. 2
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt
  32. 15
      app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt
  33. 6
      app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt
  34. 15
      app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt
  35. 11
      app/src/main/java/io/legado/app/utils/ACache.kt
  36. 13
      app/src/main/java/io/legado/app/utils/ContextExtensions.kt
  37. 35
      app/src/main/java/io/legado/app/utils/DocumentUtils.kt
  38. 115
      app/src/main/java/io/legado/app/utils/FileUtils.kt
  39. 8
      app/src/main/java/io/legado/app/utils/FragmentExtensions.kt
  40. 5
      app/src/main/java/io/legado/app/utils/LogUtils.kt
  41. 39
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  42. 32
      app/src/main/java/io/legado/app/utils/StringUtils.kt
  43. 3
      app/src/main/java/io/legado/app/utils/UriExtensions.kt
  44. 2
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  45. 8
      app/src/main/java/io/legado/app/utils/ZipUtils.kt
  46. 12
      app/src/main/res/values/arrays.xml
  47. 3
      app/src/main/res/values/strings.xml
  48. 1
      app/src/main/res/xml/pref_config_theme.xml
  49. 12
      app/src/main/res/xml/pref_main.xml

@ -3,6 +3,10 @@
* 旧版数据导入教程: * 旧版数据导入教程:
* 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/01/27**
* 添加根据系统主题切换夜间模式
* 合并Modificator提交的代码
**2020/01/26** **2020/01/26**
* 修复bug * 修复bug
* 未加入书架可查看目录 * 未加入书架可查看目录

@ -15,13 +15,13 @@ import io.legado.app.constant.AppConst.channelIdReadAloud
import io.legado.app.constant.AppConst.channelIdWeb import io.legado.app.constant.AppConst.channelIdWeb
import io.legado.app.data.AppDatabase import io.legado.app.data.AppDatabase
import io.legado.app.help.ActivityHelp import io.legado.app.help.ActivityHelp
import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.ThemeStore
import io.legado.app.ui.book.read.page.ChapterProvider import io.legado.app.ui.book.read.page.ChapterProvider
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefInt
import io.legado.app.utils.isNightTheme
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class App : Application() { class App : Application() {
@ -50,7 +50,7 @@ class App : Application() {
} }
if (!ThemeStore.isConfigured(this, versionCode)) applyTheme() if (!ThemeStore.isConfigured(this, versionCode)) applyTheme()
initNightTheme() initNightMode()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId()
@ -67,7 +67,7 @@ class App : Application() {
* 更新主题 * 更新主题
*/ */
fun applyTheme() { fun applyTheme() {
if (isNightTheme) { if (AppConfig.isNightTheme) {
ThemeStore.editTheme(this) ThemeStore.editTheme(this)
.primaryColor( .primaryColor(
getPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color)) getPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color))
@ -98,11 +98,12 @@ class App : Application() {
fun applyDayNight() { fun applyDayNight() {
ReadBookConfig.upBg() ReadBookConfig.upBg()
applyTheme() applyTheme()
initNightTheme() initNightMode()
} }
private fun initNightTheme() { private fun initNightMode() {
val targetMode = if (isNightTheme) { val targetMode =
if (AppConfig.isNightTheme) {
AppCompatDelegate.MODE_NIGHT_YES AppCompatDelegate.MODE_NIGHT_YES
} else { } else {
AppCompatDelegate.MODE_NIGHT_NO AppCompatDelegate.MODE_NIGHT_NO

@ -52,8 +52,8 @@ abstract class BaseActivity(
} ?: super.onCreateOptionsMenu(menu) } ?: super.onCreateOptionsMenu(menu)
} }
override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean { override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
menu?.applyOpenTint(this) menu.applyOpenTint(this)
return super.onMenuOpened(featureId, menu) return super.onMenuOpened(featureId, menu)
} }

@ -21,4 +21,5 @@ object PreferKey {
const val backupPath = "backupUri" const val backupPath = "backupUri"
const val threadCount = "threadCount" const val threadCount = "threadCount"
const val keepLight = "keep_light" const val keepLight = "keep_light"
const val autoDarkMode = "autoDarkMode"
} }

@ -1,14 +1,13 @@
package io.legado.app.constant package io.legado.app.constant
import io.legado.app.App import io.legado.app.help.AppConfig
import io.legado.app.utils.isNightTheme
enum class Theme { enum class Theme {
Dark, Light, Auto; Dark, Light, Auto;
companion object { companion object {
fun getTheme(): Theme { fun getTheme(): Theme {
return if (App.INSTANCE.isNightTheme) { return if (AppConfig.isNightTheme) {
Dark Dark
} else Light } else Light
} }

@ -0,0 +1,51 @@
package io.legado.app.help
import io.legado.app.App
import io.legado.app.constant.PreferKey
import io.legado.app.utils.*
object AppConfig {
var isNightTheme: Boolean
get() {
return when (App.INSTANCE.getPrefString("themeMode", "0")) {
"1" -> false
"2" -> true
else -> App.INSTANCE.sysIsDarkMode()
}
}
set(value) {
if (value) {
App.INSTANCE.putPrefString("themeMode", "2")
} else {
App.INSTANCE.putPrefString("themeMode", "1")
}
}
var isTransparentStatusBar: Boolean
get() = App.INSTANCE.getPrefBoolean("transparentStatusBar")
set(value) {
App.INSTANCE.putPrefBoolean("transparentStatusBar", value)
}
var isShowRSS: Boolean
get() = App.INSTANCE.getPrefBoolean(PreferKey.showRss)
set(value) {
App.INSTANCE.putPrefBoolean(PreferKey.showRss, value)
}
var threadCount: Int
get() = App.INSTANCE.getPrefInt(PreferKey.threadCount)
set(value) {
App.INSTANCE.putPrefInt(PreferKey.threadCount, value)
}
var autoDarkMode: Boolean
get() = App.INSTANCE.getPrefBoolean(PreferKey.autoDarkMode)
set(value) {
App.INSTANCE.putPrefBoolean(PreferKey.autoDarkMode, value)
}
val isEInkMode: Boolean
get() = App.INSTANCE.getPrefBoolean("isEInkMode")
}

@ -1,19 +1,19 @@
package io.legado.app.help package io.legado.app.help
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.utils.MD5Utils import io.legado.app.utils.*
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import org.apache.commons.text.similarity.JaccardSimilarity import org.apache.commons.text.similarity.JaccardSimilarity
import java.io.File import java.io.File
import kotlin.math.min import kotlin.math.min
object BookHelp { object BookHelp {
private const val cacheFolderName = "book_cache"
private var downloadPath: String = private var downloadPath: String =
App.INSTANCE.getPrefString(PreferKey.downloadPath) App.INSTANCE.getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
@ -26,74 +26,125 @@ object BookHelp {
?: App.INSTANCE.cacheDir.absolutePath ?: App.INSTANCE.cacheDir.absolutePath
} }
private val downloadUri get() = Uri.parse(downloadPath)
private fun bookFolderName(book: Book): String {
return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl)
}
private fun bookChapterName(bookChapter: BookChapter): String {
return String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title))
}
private fun getBookCachePath(): String { private fun getBookCachePath(): String {
return "$downloadPath${File.separator}book_cache" return "$downloadPath${File.separator}$cacheFolderName"
} }
fun clearCache() { fun clearCache() {
FileHelp.deleteFile(getBookCachePath()) FileUtils.deleteFile(getBookCachePath())
FileHelp.getFolder(getBookCachePath()) FileUtils.getFolder(getBookCachePath())
} }
@Synchronized @Synchronized
fun saveContent(book: Book, bookChapter: BookChapter, content: String) { fun saveContent(book: Book, bookChapter: BookChapter, content: String) {
if (content.isEmpty()) return if (content.isEmpty()) return
FileHelp.getFolder(getBookFolder(book)).listFiles()?.forEach { if (downloadUri.isDocumentUri(App.INSTANCE)) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let {
DocumentUtils.createFileIfNotExist(
it,
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.uri?.writeText(App.INSTANCE, content)
}
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).listFiles()?.forEach {
if (it.name.startsWith(String.format("%05d", bookChapter.index))) { if (it.name.startsWith(String.format("%05d", bookChapter.index))) {
it.delete() it.delete()
return@forEach return@forEach
} }
} }
val filePath = getChapterPath(book, bookChapter) FileUtils.createFileIfNotExist(
val file = FileHelp.getFile(filePath) File(downloadPath),
file.writeText(content) "${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).writeText(content)
}
} }
fun getChapterCount(book: Book): Int { fun getChapterCount(book: Book): Int {
return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0 if (downloadUri.isDocumentUri(App.INSTANCE)) {
DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let {
return DocumentUtils.createFileIfNotExist(
it,
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.listFiles()?.size ?: 0
}
} else {
return FileUtils.createFileIfNotExist(
File(downloadPath),
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).list()?.size ?: 0
}
return 0
} }
fun hasContent(book: Book, bookChapter: BookChapter): Boolean { fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
val filePath = getChapterPath(book, bookChapter) if (downloadUri.isDocumentUri(App.INSTANCE)) {
runCatching { DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let {
val file = File(filePath) return DocumentUtils.exists(
if (file.exists()) { it,
return true "${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
} }
} else {
return FileUtils.exists(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)
} }
return false return false
} }
fun getContent(book: Book, bookChapter: BookChapter): String? { fun getContent(book: Book, bookChapter: BookChapter): String? {
val filePath = getChapterPath(book, bookChapter) if (downloadUri.isDocumentUri(App.INSTANCE)) {
runCatching { DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let {
val file = File(filePath) return DocumentUtils.createFileIfNotExist(
if (file.exists()) { it,
return file.readText() "${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.uri?.readText(App.INSTANCE)
} }
} else {
return FileUtils.createFileIfNotExist(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).readText()
} }
return null return null
} }
fun delContent(book: Book, bookChapter: BookChapter) { fun delContent(book: Book, bookChapter: BookChapter) {
val filePath = getChapterPath(book, bookChapter) if (downloadUri.isDocumentUri(App.INSTANCE)) {
kotlin.runCatching { DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let {
val file = File(filePath) DocumentUtils.createFileIfNotExist(
if (file.exists()) { it,
file.delete() "${bookChapterName(bookChapter)}.nb",
} subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
)?.delete()
} }
} else {
FileUtils.createFileIfNotExist(
File(downloadPath),
"${bookChapterName(bookChapter)}.nb",
subDirs = *arrayOf(cacheFolderName, bookFolderName(book))
).delete()
} }
private fun getBookFolder(book: Book): String {
val bookFolder = formatFolderName(book.name + MD5Utils.md5Encode16(book.bookUrl))
return "${getBookCachePath()}${File.separator}$bookFolder"
}
private fun getChapterPath(book: Book, bookChapter: BookChapter): String {
val chapterFile =
String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title))
return "${getBookFolder(book)}${File.separator}$chapterFile.nb"
} }
private fun formatFolderName(folderName: String): String { private fun formatFolderName(folderName: String): String {
@ -124,16 +175,16 @@ object BookHelp {
} }
var newIndex = 0 var newIndex = 0
val jaccardSimilarity = JaccardSimilarity() val jSimilarity = JaccardSimilarity()
var similarity = if (chapters.size > index) { var similarity = if (chapters.size > index) {
jaccardSimilarity.apply(title, chapters[index].title) jSimilarity.apply(title, chapters[index].title)
} else 0.0 } else 0.0
if (similarity == 1.0) { if (similarity == 1.0) {
return index return index
} else { } else {
for (i in 1..50) { for (i in 1..50) {
if (index - i in chapters.indices) { if (index - i in chapters.indices) {
jaccardSimilarity.apply(title, chapters[index - i].title).let { jSimilarity.apply(title, chapters[index - i].title).let {
if (it > similarity) { if (it > similarity) {
similarity = it similarity = it
newIndex = index - i newIndex = index - i
@ -144,7 +195,7 @@ object BookHelp {
} }
} }
if (index + i in chapters.indices) { if (index + i in chapters.indices) {
jaccardSimilarity.apply(title, chapters[index + i].title).let { jSimilarity.apply(title, chapters[index + i].title).let {
if (it > similarity) { if (it > similarity) {
similarity = it similarity = it
newIndex = index + i newIndex = index + i

@ -1,58 +0,0 @@
package io.legado.app.help
import io.legado.app.App
import java.io.File
import java.io.IOException
object FileHelp {
//获取文件夹
fun getFolder(filePath: String): File {
val file = File(filePath)
//如果文件夹不存在,就创建它
if (!file.exists()) {
file.mkdirs()
}
return file
}
//获取文件
@Synchronized
fun getFile(filePath: String): File {
val file = File(filePath)
try {
if (!file.exists()) {
//创建父类文件夹
getFolder(file.parent)
//创建文件
file.createNewFile()
}
} catch (e: IOException) {
e.printStackTrace()
}
return file
}
fun getCachePath(): String {
return App.INSTANCE.externalCacheDir?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
}
//递归删除文件夹下的数据
@Synchronized
fun deleteFile(filePath: String) {
val file = File(filePath)
if (!file.exists()) return
if (file.isDirectory) {
val files = file.listFiles()
files?.forEach { subFile ->
val path = subFile.path
deleteFile(path)
}
}
//删除文件
file.delete()
}
}

@ -1,13 +0,0 @@
package io.legado.app.help
import android.content.Context
import io.legado.app.constant.PreferKey
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefInt
val Context.isShowRSS: Boolean
get() = getPrefBoolean(PreferKey.showRss, true)
val Context.threadCount: Int
get() = getPrefInt(PreferKey.threadCount, 16)

@ -111,7 +111,7 @@ object ReadBookConfig {
var textSize: Int = 15 var textSize: Int = 15
) { ) {
fun setBg(bgType: Int, bg: String) { fun setBg(bgType: Int, bg: String) {
if (App.INSTANCE.isNightTheme) { if (AppConfig.isNightTheme) {
bgTypeNight = bgType bgTypeNight = bgType
bgStrNight = bg bgStrNight = bg
} else { } else {
@ -121,7 +121,7 @@ object ReadBookConfig {
} }
fun setTextColor(color: Int) { fun setTextColor(color: Int) {
if (App.INSTANCE.isNightTheme) { if (AppConfig.isNightTheme) {
textColorNight = "#${color.hexString}" textColorNight = "#${color.hexString}"
} else { } else {
textColor = "#${color.hexString}" textColor = "#${color.hexString}"
@ -129,7 +129,7 @@ object ReadBookConfig {
} }
fun setStatusIconDark(isDark: Boolean) { fun setStatusIconDark(isDark: Boolean) {
if (App.INSTANCE.isNightTheme) { if (AppConfig.isNightTheme) {
darkStatusIconNight = isDark darkStatusIconNight = isDark
} else { } else {
darkStatusIcon = isDark darkStatusIcon = isDark
@ -137,7 +137,7 @@ object ReadBookConfig {
} }
fun statusIconDark(): Boolean { fun statusIconDark(): Boolean {
return if (App.INSTANCE.isNightTheme) { return if (AppConfig.isNightTheme) {
darkStatusIconNight darkStatusIconNight
} else { } else {
darkStatusIcon darkStatusIcon
@ -145,17 +145,17 @@ object ReadBookConfig {
} }
fun textColor(): Int { fun textColor(): Int {
return if (App.INSTANCE.isNightTheme) Color.parseColor(textColorNight) return if (AppConfig.isNightTheme) Color.parseColor(textColorNight)
else Color.parseColor(textColor) else Color.parseColor(textColor)
} }
fun bgStr(): String { fun bgStr(): String {
return if (App.INSTANCE.isNightTheme) bgStrNight return if (AppConfig.isNightTheme) bgStrNight
else bgStr else bgStr
} }
fun bgType(): Int { fun bgType(): Int {
return if (App.INSTANCE.isNightTheme) bgTypeNight return if (AppConfig.isNightTheme) bgTypeNight
else bgType else bgType
} }

@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.App import io.legado.app.App
import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.DocumentUtils import io.legado.app.utils.DocumentUtils
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
@ -48,37 +47,40 @@ object Backup {
App.db.bookDao().allBooks.let { App.db.bookDao().allBooks.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val json = GSON.toJson(it) val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "bookshelf.json").writeText(json) FileUtils.getFile(backupPath + File.separator + "bookshelf.json")
.writeText(json)
} }
} }
App.db.bookGroupDao().all().let { App.db.bookGroupDao().all().let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val json = GSON.toJson(it) val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "bookGroup.json").writeText(json) FileUtils.getFile(backupPath + File.separator + "bookGroup.json")
.writeText(json)
} }
} }
App.db.bookSourceDao().all.let { App.db.bookSourceDao().all.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val json = GSON.toJson(it) val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "bookSource.json") FileUtils.getFile(backupPath + File.separator + "bookSource.json")
.writeText(json) .writeText(json)
} }
} }
App.db.rssSourceDao().all.let { App.db.rssSourceDao().all.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val json = GSON.toJson(it) val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "rssSource.json").writeText(json) FileUtils.getFile(backupPath + File.separator + "rssSource.json")
.writeText(json)
} }
} }
App.db.replaceRuleDao().all.let { App.db.replaceRuleDao().all.let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
val json = GSON.toJson(it) val json = GSON.toJson(it)
FileHelp.getFile(backupPath + File.separator + "replaceRule.json") FileUtils.getFile(backupPath + File.separator + "replaceRule.json")
.writeText(json) .writeText(json)
} }
} }
GSON.toJson(ReadBookConfig.configList)?.let { GSON.toJson(ReadBookConfig.configList)?.let {
FileHelp.getFile(backupPath + File.separator + ReadBookConfig.readConfigFileName) FileUtils.getFile(backupPath + File.separator + ReadBookConfig.readConfigFileName)
.writeText(it) .writeText(it)
} }
Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp ->
@ -111,7 +113,7 @@ object Backup {
doc?.let { doc?.let {
DocumentUtils.writeText( DocumentUtils.writeText(
context, context,
FileHelp.getFile(backupPath + File.separator + fileName).readText(), FileUtils.getFile(backupPath + File.separator + fileName).readText(),
doc.uri doc.uri
) )
} }
@ -122,8 +124,8 @@ object Backup {
private fun copyBackup() { private fun copyBackup() {
try { try {
for (fileName in backupFileNames) { for (fileName in backupFileNames) {
FileHelp.getFile(backupPath + File.separator + "bookshelf.json") FileUtils.getFile(backupPath + File.separator + "bookshelf.json")
.copyTo(FileHelp.getFile(legadoPath + File.separator + "bookshelf.json"), true) .copyTo(FileUtils.getFile(legadoPath + File.separator + "bookshelf.json"), true)
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()

@ -11,7 +11,6 @@ import com.jayway.jsonpath.ParseContext
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.FileHelp
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -38,7 +37,7 @@ object Restore {
for (fileName in Backup.backupFileNames) { for (fileName in Backup.backupFileNames) {
if (doc.name == fileName) { if (doc.name == fileName) {
DocumentUtils.readText(context, doc.uri)?.let { DocumentUtils.readText(context, doc.uri)?.let {
FileHelp.getFile(Backup.backupPath + File.separator + fileName) FileUtils.getFile(Backup.backupPath + File.separator + fileName)
.writeText(it) .writeText(it)
} }
} }
@ -51,7 +50,7 @@ object Restore {
suspend fun restore(path: String) { suspend fun restore(path: String) {
withContext(IO) { withContext(IO) {
try { try {
val file = FileHelp.getFile(path + File.separator + "bookshelf.json") val file = FileUtils.getFile(path + File.separator + "bookshelf.json")
val json = file.readText() val json = file.readText()
GSON.fromJsonArray<Book>(json)?.let { GSON.fromJsonArray<Book>(json)?.let {
App.db.bookDao().insert(*it.toTypedArray()) App.db.bookDao().insert(*it.toTypedArray())
@ -60,7 +59,7 @@ object Restore {
e.printStackTrace() e.printStackTrace()
} }
try { try {
val file = FileHelp.getFile(path + File.separator + "bookGroup.json") val file = FileUtils.getFile(path + File.separator + "bookGroup.json")
val json = file.readText() val json = file.readText()
GSON.fromJsonArray<BookGroup>(json)?.let { GSON.fromJsonArray<BookGroup>(json)?.let {
App.db.bookGroupDao().insert(*it.toTypedArray()) App.db.bookGroupDao().insert(*it.toTypedArray())
@ -69,7 +68,7 @@ object Restore {
e.printStackTrace() e.printStackTrace()
} }
try { try {
val file = FileHelp.getFile(path + File.separator + "bookSource.json") val file = FileUtils.getFile(path + File.separator + "bookSource.json")
val json = file.readText() val json = file.readText()
GSON.fromJsonArray<BookSource>(json)?.let { GSON.fromJsonArray<BookSource>(json)?.let {
App.db.bookSourceDao().insert(*it.toTypedArray()) App.db.bookSourceDao().insert(*it.toTypedArray())
@ -78,7 +77,7 @@ object Restore {
e.printStackTrace() e.printStackTrace()
} }
try { try {
val file = FileHelp.getFile(path + File.separator + "rssSource.json") val file = FileUtils.getFile(path + File.separator + "rssSource.json")
val json = file.readText() val json = file.readText()
GSON.fromJsonArray<RssSource>(json)?.let { GSON.fromJsonArray<RssSource>(json)?.let {
App.db.rssSourceDao().insert(*it.toTypedArray()) App.db.rssSourceDao().insert(*it.toTypedArray())
@ -87,7 +86,7 @@ object Restore {
e.printStackTrace() e.printStackTrace()
} }
try { try {
val file = FileHelp.getFile(path + File.separator + "replaceRule.json") val file = FileUtils.getFile(path + File.separator + "replaceRule.json")
val json = file.readText() val json = file.readText()
GSON.fromJsonArray<ReplaceRule>(json)?.let { GSON.fromJsonArray<ReplaceRule>(json)?.let {
App.db.replaceRuleDao().insert(*it.toTypedArray()) App.db.replaceRuleDao().insert(*it.toTypedArray())
@ -97,7 +96,7 @@ object Restore {
} }
try { try {
val file = val file =
FileHelp.getFile(path + File.separator + ReadBookConfig.readConfigFileName) FileUtils.getFile(path + File.separator + ReadBookConfig.readConfigFileName)
val configFile = val configFile =
File(App.INSTANCE.filesDir.absolutePath + File.separator + ReadBookConfig.readConfigFileName) File(App.INSTANCE.filesDir.absolutePath + File.separator + ReadBookConfig.readConfigFileName)
if (file.exists()) { if (file.exists()) {
@ -126,7 +125,7 @@ object Restore {
GlobalScope.launch(IO) { GlobalScope.launch(IO) {
try {// 导入书架 try {// 导入书架
val shelfFile = val shelfFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json") FileUtils.getFile(Backup.defaultPath + File.separator + "myBookShelf.json")
val json = shelfFile.readText() val json = shelfFile.readText()
val importCount = importOldBookshelf(json) val importCount = importOldBookshelf(json)
withContext(Main) { withContext(Main) {
@ -140,7 +139,7 @@ object Restore {
try {// Book source try {// Book source
val sourceFile = val sourceFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json") FileUtils.getFile(Backup.defaultPath + File.separator + "myBookSource.json")
val json = sourceFile.readText() val json = sourceFile.readText()
val importCount = importOldSource(json) val importCount = importOldSource(json)
withContext(Main) { withContext(Main) {
@ -154,7 +153,7 @@ object Restore {
try {// Replace rules try {// Replace rules
val ruleFile = val ruleFile =
FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json") FileUtils.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json")
val json = ruleFile.readText() val json = ruleFile.readText()
val importCount = importOldReplaceRule(json) val importCount = importOldReplaceRule(json)
withContext(Main) { withContext(Main) {

@ -2,10 +2,10 @@ package io.legado.app.help.storage
import android.content.Context import android.content.Context
import io.legado.app.App import io.legado.app.App
import io.legado.app.help.FileHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.http.HttpAuth import io.legado.app.lib.webdav.http.HttpAuth
import io.legado.app.utils.FileUtils
import io.legado.app.utils.ZipUtils import io.legado.app.utils.ZipUtils
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -17,9 +17,9 @@ import java.util.*
import kotlin.math.min import kotlin.math.min
object WebDavHelp { object WebDavHelp {
private val zipFilePath = FileHelp.getCachePath() + "/backup" + ".zip" private val zipFilePath = FileUtils.getCachePath() + "/backup" + ".zip"
private val unzipFilesPath by lazy { private val unzipFilesPath by lazy {
FileHelp.getCachePath() FileUtils.getCachePath()
} }
private fun getWebDavUrl(): String? { private fun getWebDavUrl(): String? {
@ -86,7 +86,7 @@ object WebDavHelp {
for (i in 0 until paths.size) { for (i in 0 until paths.size) {
paths[i] = path + File.separator + paths[i] paths[i] = path + File.separator + paths[i]
} }
FileHelp.deleteFile(zipFilePath) FileUtils.deleteFile(zipFilePath)
if (ZipUtils.zipFiles(paths, zipFilePath)) { if (ZipUtils.zipFiles(paths, zipFilePath)) {
WebDav(getWebDavUrl() + "legado").makeAsDir() WebDav(getWebDavUrl() + "legado").makeAsDir()
val putUrl = getWebDavUrl() + "legado/backup" + val putUrl = getWebDavUrl() + "legado/backup" +

@ -5,7 +5,6 @@ import android.app.Activity
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
@ -17,9 +16,8 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.AppConfig
import io.legado.app.utils.getCompatColor import io.legado.app.utils.getCompatColor
import io.legado.app.utils.isNightTheme
import io.legado.app.utils.isTransparentStatusBar
import kotlinx.android.synthetic.main.activity_main.view.* import kotlinx.android.synthetic.main.activity_main.view.*
import org.jetbrains.anko.backgroundColor import org.jetbrains.anko.backgroundColor
@ -38,7 +36,7 @@ object ATH {
} }
fun setStatusBarColorAuto(activity: Activity, fullScreen: Boolean) { fun setStatusBarColorAuto(activity: Activity, fullScreen: Boolean) {
val isTransparentStatusBar = activity.isTransparentStatusBar val isTransparentStatusBar = AppConfig.isTransparentStatusBar
setStatusBarColor( setStatusBarColor(
activity, activity,
ThemeStore.statusBarColor(activity, isTransparentStatusBar), ThemeStore.statusBarColor(activity, isTransparentStatusBar),
@ -132,14 +130,14 @@ object ATH {
fun setTint( fun setTint(
view: View, view: View,
@ColorInt color: Int, @ColorInt color: Int,
isDark: Boolean = view.context.isNightTheme isDark: Boolean = AppConfig.isNightTheme
) { ) {
TintHelper.setTintAuto(view, color, false, isDark) TintHelper.setTintAuto(view, color, false, isDark)
} }
fun setBackgroundTint( fun setBackgroundTint(
view: View, @ColorInt color: Int, view: View, @ColorInt color: Int,
isDark: Boolean = view.context.isNightTheme isDark: Boolean = AppConfig.isNightTheme
) { ) {
TintHelper.setTintAuto(view, color, true, isDark) TintHelper.setTintAuto(view, color, true, isDark)
} }
@ -207,11 +205,10 @@ object ATH {
.setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create() .setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create()
itemIconTintList = colorStateList itemIconTintList = colorStateList
itemTextColor = colorStateList itemTextColor = colorStateList
itemBackground = ColorDrawable(ThemeStore.primaryColor(bottomBar.context)) itemBackgroundResource = when (AppConfig.isNightTheme) {
/*itemBackgroundResource = when(context.isNightTheme) {
true -> R.drawable.item_bg_dark true -> R.drawable.item_bg_dark
false -> R.drawable.item_bg_light false -> R.drawable.item_bg_light
}*/ }
} }
} }

@ -9,10 +9,10 @@ import io.legado.app.base.BaseService
import io.legado.app.constant.Action import io.legado.app.constant.Action
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.threadCount
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -21,7 +21,7 @@ import java.util.concurrent.Executors
class DownloadService : BaseService() { class DownloadService : BaseService() {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(App.INSTANCE.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var tasks: ArrayList<Coroutine<*>> = arrayListOf() private var tasks: ArrayList<Coroutine<*>> = arrayListOf()
private val handler = Handler() private val handler = Handler()
private var runnable: Runnable = Runnable { upDownload() } private var runnable: Runnable = Runnable { upDownload() }

@ -4,14 +4,10 @@ import android.app.PendingIntent
import android.media.MediaPlayer import android.media.MediaPlayer
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.data.api.IHttpPostApi import io.legado.app.data.api.IHttpPostApi
import io.legado.app.help.FileHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.LogUtils import io.legado.app.utils.*
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import io.legado.app.utils.postEvent
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -65,7 +61,7 @@ class HttpReadAloudService : BaseReadAloudService(),
private fun downloadAudio() { private fun downloadAudio() {
job = launch(IO) { job = launch(IO) {
FileHelp.deleteFile(ttsFolder) FileUtils.deleteFile(ttsFolder)
for (index in 0 until contentList.size) { for (index in 0 until contentList.size) {
if (isActive) { if (isActive) {
val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com") val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com")
@ -104,7 +100,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun getSpeakFile(index: Int = nowSpeak): File { private fun getSpeakFile(index: Int = nowSpeak): File {
return FileHelp.getFile("${ttsFolder}${File.separator}${index}.mp3") return FileUtils.getFile("${ttsFolder}${File.separator}${index}.mp3")
} }
private fun getAudioBody(content: String): Map<String, String> { private fun getAudioBody(content: String): Map<String, String> {

@ -1 +1,24 @@
## 放置与界面有关的类 ## 放置与界面有关的类
* about 关于界面
* audio 音频播放界面
* book\info 书籍信息查看
* book\read 书籍阅读界面
* book\search 搜索书籍界面
* book\source 搜索书源界面
* changecover 封面换源界面
* changesource 换源界面
* chapterlist 目录界面
* config 配置界面
* download 下载界面
* explore 发现界面
* filechooser 文件选择界面
* importbook 书籍导入界面
* main 主界面
* qrcode 二维码扫描界面
* replacerule 替换净化界面
* rss\article 订阅条目界面
* rss\read 订阅阅读界面
* rss\source 订阅源界面
* welcome 欢迎界面
* widget 自定义插件

@ -98,8 +98,8 @@ class BookInfoActivity :
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean { override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
menu?.findItem(R.id.menu_can_update)?.isChecked = menu.findItem(R.id.menu_can_update)?.isChecked =
viewModel.bookData.value?.canUpdate ?: true viewModel.bookData.value?.canUpdate ?: true
return super.onMenuOpened(featureId, menu) return super.onMenuOpened(featureId, menu)
} }

@ -9,11 +9,11 @@ import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.ThemeStore
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.isTransparentStatusBar
object Help { object Help {
@ -43,7 +43,7 @@ object Help {
} else { } else {
ATH.setLightStatusBarAuto( ATH.setLightStatusBarAuto(
activity, activity,
ThemeStore.statusBarColor(activity, activity.isTransparentStatusBar) ThemeStore.statusBarColor(activity, AppConfig.isTransparentStatusBar)
) )
} }
} }

@ -11,6 +11,7 @@ import androidx.core.view.isVisible
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.buttonDisabledColor import io.legado.app.lib.theme.buttonDisabledColor
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
@ -41,7 +42,7 @@ class ReadMenu : FrameLayout {
init { init {
callBack = activity as? CallBack callBack = activity as? CallBack
inflate(context, R.layout.view_read_menu, this) inflate(context, R.layout.view_read_menu, this)
if (context.isNightTheme) { if (AppConfig.isNightTheme) {
fabNightTheme.setImageResource(R.drawable.ic_daytime) fabNightTheme.setImageResource(R.drawable.ic_daytime)
} else { } else {
fabNightTheme.setImageResource(R.drawable.ic_brightness) fabNightTheme.setImageResource(R.drawable.ic_brightness)
@ -144,7 +145,7 @@ class ReadMenu : FrameLayout {
//夜间模式 //夜间模式
fabNightTheme.onClick { fabNightTheme.onClick {
context.putPrefBoolean("isNightTheme", !context.isNightTheme) AppConfig.isNightTheme = !AppConfig.isNightTheme
App.INSTANCE.applyDayNight() App.INSTANCE.applyDayNight()
} }

@ -21,7 +21,6 @@ import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.base.adapter.SimpleRecyclerAdapter
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.help.FileHelp
import io.legado.app.help.ImageLoader import io.legado.app.help.ImageLoader
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.Permissions
@ -174,7 +173,7 @@ class BgTextConfigDialog : DialogFragment() {
var file = requireContext().getExternalFilesDir(null) var file = requireContext().getExternalFilesDir(null)
?: requireContext().filesDir ?: requireContext().filesDir
file = file =
FileHelp.getFile(file.absolutePath + File.separator + "bg" + File.separator + doc.name) FileUtils.getFile(file.absolutePath + File.separator + "bg" + File.separator + doc.name)
DocumentUtils.readBytes(requireContext(), uri)?.let { DocumentUtils.readBytes(requireContext(), uri)?.let {
file.writeBytes(it) file.writeBytes(it)
ReadBookConfig.getConfig().setBg(2, file.absolutePath) ReadBookConfig.getConfig().setBg(2, file.absolutePath)

@ -76,7 +76,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
!getPrefBoolean(PreferKey.precisionSearch) !getPrefBoolean(PreferKey.precisionSearch)
) )
precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch) precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch)
search_view.query.toString().trim()?.let { search_view.query?.toString()?.trim()?.let {
search_view.setQuery(it, true) search_view.setQuery(it, true)
} }
} }
@ -88,7 +88,7 @@ class SearchActivity : VMBaseActivity<SearchViewModel>(R.layout.activity_book_se
} else { } else {
putPrefString("searchGroup", item.title.toString()) putPrefString("searchGroup", item.title.toString())
} }
search_view.query.toString().trim()?.let { search_view.query?.toString()?.trim()?.let {
search_view.setQuery(it, true) search_view.setQuery(it, true)
} }
} }

@ -6,8 +6,8 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.threadCount
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
@ -18,7 +18,7 @@ import java.util.concurrent.Executors
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(context.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
private var task: Coroutine<*>? = null private var task: Coroutine<*>? = null
var callBack: CallBack? = null var callBack: CallBack? = null
var searchKey: String = "" var searchKey: String = ""

@ -7,7 +7,6 @@ import io.legado.app.App
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.api.IHttpGetApi import io.legado.app.data.api.IHttpGetApi
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.FileHelp
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.OldRule import io.legado.app.help.storage.OldRule
@ -92,7 +91,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}.let { }.let {
val json = GSON.toJson(it) val json = GSON.toJson(it)
val file = val file =
FileHelp.getFile(Backup.exportPath + File.separator + "exportBookSource.json") FileUtils.getFile(Backup.exportPath + File.separator + "exportBookSource.json")
file.writeText(json) file.writeText(json)
} }
}.onSuccess { }.onSuccess {

@ -8,8 +8,8 @@ import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.threadCount
import io.legado.app.model.WebBook import io.legado.app.model.WebBook
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -20,7 +20,7 @@ import java.util.concurrent.Executors
class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { class ChangeSourceViewModel(application: Application) : BaseViewModel(application) {
private var searchPool = private var searchPool =
Executors.newFixedThreadPool(context.threadCount).asCoroutineDispatcher() Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher()
var callBack: CallBack? = null var callBack: CallBack? = null
val searchStateData = MutableLiveData<Boolean>() val searchStateData = MutableLiveData<Boolean>()
var name: String = "" var name: String = ""

@ -1,6 +1,8 @@
package io.legado.app.ui.config package io.legado.app.ui.config
import android.app.Activity.RESULT_OK
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
@ -12,8 +14,10 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.threadCount import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.receiver.SharedReceiverActivity import io.legado.app.receiver.SharedReceiverActivity
import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.filechooser.FileChooserDialog
@ -26,7 +30,7 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
Preference.OnPreferenceChangeListener, Preference.OnPreferenceChangeListener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val downloadPath = 25324 private val requestCodeDownloadPath = 25324
private val packageManager = App.INSTANCE.packageManager private val packageManager = App.INSTANCE.packageManager
private val componentName = ComponentName( private val componentName = ComponentName(
App.INSTANCE, App.INSTANCE,
@ -61,18 +65,13 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
.setTitle(getString(R.string.threads_num_title)) .setTitle(getString(R.string.threads_num_title))
.setMaxValue(999) .setMaxValue(999)
.setMinValue(1) .setMinValue(1)
.setValue(requireContext().threadCount) .setValue(AppConfig.threadCount)
.show { .show {
requireContext().putPrefInt(PreferKey.threadCount, it) requireContext().putPrefInt(PreferKey.threadCount, it)
findPreference<Preference>(PreferKey.threadCount)?.summary = findPreference<Preference>(PreferKey.threadCount)?.summary =
getString(R.string.threads_num, it.toString()) getString(R.string.threads_num, it.toString())
} }
PreferKey.downloadPath -> FileChooserDialog.show( PreferKey.downloadPath -> selectDownloadPathSys()
childFragmentManager,
downloadPath,
mode = FileChooserDialog.DIRECTORY,
initPath = getPreferenceString(PreferKey.downloadPath).toString()
)
PreferKey.cleanCache -> { PreferKey.cleanCache -> {
BookHelp.clearCache() BookHelp.clearCache()
toast(R.string.clear_cache_success) toast(R.string.clear_cache_success)
@ -125,7 +124,7 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
PreferKey.downloadPath -> getPrefString(PreferKey.downloadPath) PreferKey.downloadPath -> getPrefString(PreferKey.downloadPath)
?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath ?: App.INSTANCE.cacheDir.absolutePath
PreferKey.threadCount -> requireContext().threadCount PreferKey.threadCount -> AppConfig.threadCount
else -> getPrefString(key) ?: "" else -> getPrefString(key) ?: ""
} }
} }
@ -148,7 +147,49 @@ class OtherConfigFragment : PreferenceFragmentCompat(),
} }
} }
private fun selectDownloadPathSys() {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityForResult(intent, requestCodeDownloadPath)
} catch (e: Exception) {
selectDownloadPath()
}
}
private fun selectDownloadPath() {
PermissionsCompat.Builder(this)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
FileChooserDialog.show(
childFragmentManager,
requestCodeDownloadPath,
mode = FileChooserDialog.DIRECTORY,
initPath = getPreferenceString(PreferKey.downloadPath).toString()
)
}
.request()
}
override fun onFilePicked(requestCode: Int, currentPath: String) { override fun onFilePicked(requestCode: Int, currentPath: String) {
if (requestCode == requestCodeDownloadPath) {
putPrefString(PreferKey.downloadPath, currentPath) putPrefString(PreferKey.downloadPath, currentPath)
} }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
requestCodeDownloadPath -> if (resultCode == RESULT_OK) {
data?.data?.let { uri ->
requireContext().contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
putPrefString(PreferKey.downloadPath, uri.toString())
}
}
}
}
} }

@ -10,6 +10,7 @@ import androidx.preference.PreferenceFragmentCompat
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.help.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.noButton
import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.dialogs.yesButton
@ -20,7 +21,7 @@ import io.legado.app.utils.*
class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
val items = arrayOf("极简","曜夜","经典","黑白","A屏黑") val items = arrayOf("极简", "曜夜", "经典", "黑白", "A屏黑")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_theme) addPreferencesFromResource(R.xml.pref_config_theme)
@ -110,31 +111,37 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
putPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100)) putPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100))
putPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color)) putPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color))
putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
putPrefBoolean("isNightTheme", false) AppConfig.isNightTheme = false
} }
1 -> { 1 -> {
putPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color)) putPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color))
putPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color)) putPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color))
putPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) putPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color))
putPrefBoolean("isNightTheme", true) AppConfig.isNightTheme = true
} }
2 -> { 2 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500)) putPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500))
putPrefInt("colorAccent", getCompatColor(R.color.md_pink_800)) putPrefInt("colorAccent", getCompatColor(R.color.md_pink_800))
putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100))
putPrefBoolean("isNightTheme", false) AppConfig.isNightTheme = false
} }
3 -> { 3 -> {
putPrefInt("colorPrimary", getCompatColor(R.color.white)) putPrefInt("colorPrimary", getCompatColor(R.color.white))
putPrefInt("colorAccent", getCompatColor(R.color.black)) putPrefInt("colorAccent", getCompatColor(R.color.black))
putPrefInt("colorBackground", getCompatColor(R.color.white)) putPrefInt("colorBackground", getCompatColor(R.color.white))
putPrefBoolean("isNightTheme", false) AppConfig.isNightTheme = false
} }
4 -> { 4 -> {
putPrefInt("colorPrimaryNight", getCompatColor(R.color.black)) putPrefInt("colorPrimaryNight", getCompatColor(R.color.black))
putPrefInt("colorAccentNight", getCompatColor(R.color.md_grey_600)) putPrefInt(
putPrefInt("colorBackgroundNight", getCompatColor(R.color.black)) "colorAccentNight",
putPrefBoolean("isNightTheme", true) getCompatColor(R.color.md_grey_600)
)
putPrefInt(
"colorBackgroundNight",
getCompatColor(R.color.black)
)
AppConfig.isNightTheme = true
} }
} }
App.INSTANCE.applyDayNight() App.INSTANCE.applyDayNight()
@ -166,7 +173,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar
} }
private fun upTheme(isNightTheme: Boolean) { private fun upTheme(isNightTheme: Boolean) {
if (this.isNightTheme == isNightTheme) { if (AppConfig.isNightTheme == isNightTheme) {
App.INSTANCE.applyTheme() App.INSTANCE.applyTheme()
recreateActivities() recreateActivities()
} }

@ -16,8 +16,8 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.Bus import io.legado.app.constant.Bus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.isShowRSS
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
@ -47,7 +47,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager) view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager)
view_pager_main.addOnPageChangeListener(this) view_pager_main.addOnPageChangeListener(this)
bottom_navigation_view.setOnNavigationItemSelectedListener(this) bottom_navigation_view.setOnNavigationItemSelectedListener(this)
bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = isShowRSS bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS
upVersion() upVersion()
} }
@ -68,10 +68,10 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
fragmentList.add(RssFragment()) fragmentList.add(RssFragment())
fragmentList.add(MyFragment()) fragmentList.add(MyFragment())
} }
if (isShowRSS && fragmentList.size < 4) { if (AppConfig.isShowRSS && fragmentList.size < 4) {
fragmentList.add(2, RssFragment()) fragmentList.add(2, RssFragment())
} }
if (!isShowRSS && fragmentList.size == 4) { if (!AppConfig.isShowRSS && fragmentList.size == 4) {
fragmentList.removeAt(2) fragmentList.removeAt(2)
} }
} }
@ -89,7 +89,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
pagePosition = position pagePosition = position
when (position) { when (position) {
0, 1, 3 -> bottom_navigation_view.menu.getItem(position).isChecked = true 0, 1, 3 -> bottom_navigation_view.menu.getItem(position).isChecked = true
2 -> if (isShowRSS) { 2 -> if (AppConfig.isShowRSS) {
bottom_navigation_view.menu.getItem(position).isChecked = true bottom_navigation_view.menu.getItem(position).isChecked = true
} else { } else {
bottom_navigation_view.menu.getItem(3).isChecked = true bottom_navigation_view.menu.getItem(3).isChecked = true
@ -147,10 +147,10 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
recreate() recreate()
} }
observeEvent<String>(Bus.SHOW_RSS) { observeEvent<String>(Bus.SHOW_RSS) {
bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = isShowRSS bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS
upFragmentList() upFragmentList()
view_pager_main.adapter?.notifyDataSetChanged() view_pager_main.adapter?.notifyDataSetChanged()
if (isShowRSS) { if (AppConfig.isShowRSS) {
view_pager_main.setCurrentItem(3, false) view_pager_main.setCurrentItem(3, false)
} }
} }
@ -168,7 +168,7 @@ class MainActivity : VMBaseActivity<MainViewModel>(R.layout.activity_main),
} }
override fun getCount(): Int { override fun getCount(): Int {
return if (isShowRSS) 4 else 3 return if (AppConfig.isShowRSS) 4 else 3
} }
} }

@ -200,7 +200,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
"isNightTheme" -> App.INSTANCE.applyDayNight() "themeMode" -> App.INSTANCE.applyDayNight()
"webService" -> { "webService" -> {
if (requireContext().getPrefBoolean("webService")) { if (requireContext().getPrefBoolean("webService")) {
WebService.start(requireContext()) WebService.start(requireContext())

@ -6,9 +6,9 @@ import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.FileHelp
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.Restore import io.legado.app.help.storage.Restore
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.splitNotBlank import io.legado.app.utils.splitNotBlank
import org.jetbrains.anko.toast import org.jetbrains.anko.toast
@ -80,7 +80,7 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
}.let { }.let {
val json = GSON.toJson(it) val json = GSON.toJson(it)
val file = val file =
FileHelp.getFile(Backup.exportPath + File.separator + "exportReplaceRule.json") FileUtils.getFile(Backup.exportPath + File.separator + "exportReplaceRule.json")
file.writeText(json) file.writeText(json)
} }
}.onSuccess { }.onSuccess {

@ -149,7 +149,7 @@ class ReadRssActivity : VMBaseActivity<ReadRssViewModel>(R.layout.activity_rss_r
webView.settings.javaScriptEnabled = true webView.settings.javaScriptEnabled = true
webView.evaluateJavascript("document.documentElement.outerHTML") { webView.evaluateJavascript("document.documentElement.outerHTML") {
val html = StringEscapeUtils.unescapeJson(it) val html = StringEscapeUtils.unescapeJson(it)
viewModel.readAloud(Jsoup.clean(html, Whitelist())) viewModel.readAloud(Jsoup.clean(html, Whitelist.none()))
} }
} }
} }

@ -85,17 +85,12 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application),
} }
fun clHtml(content: String): String { fun clHtml(content: String): String {
return if (content.contains("<style>|style=".toRegex())) { return """<style>
content img{max-width:100%; width:auto; height:auto;}
} else { video{object-fit:fill; max-width:100%; width:auto; height:auto;}
"""
<style>
img{max-width:100% !important; width:auto; height:auto;}
video{object-fit:fill; max-width:100% !important; width:auto; height:auto;}
body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;} body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}
</style>$content </style>
""" $content"""
}
} }
override fun onInit(status: Int) { override fun onInit(status: Int) {

@ -4,10 +4,10 @@ import android.app.Application
import android.text.TextUtils import android.text.TextUtils
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import io.legado.app.App import io.legado.app.App
import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.api.IHttpGetApi import io.legado.app.data.api.IHttpGetApi
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.FileHelp
import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.HttpHelper
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.help.storage.Restore.jsonPath
@ -65,7 +65,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let { App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let {
val json = GSON.toJson(it) val json = GSON.toJson(it)
val file = val file =
FileHelp.getFile(Backup.exportPath + File.separator + "exportRssSource.json") FileUtils.getFile(Backup.exportPath + File.separator + "exportRssSource.json")
file.writeText(json) file.writeText(json)
} }
}.onSuccess { }.onSuccess {
@ -123,6 +123,8 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
App.db.rssSourceDao().insert(*it.toTypedArray()) App.db.rssSourceDao().insert(*it.toTypedArray())
} }
} }
}.onSuccess {
finally.invoke(context.getString(R.string.success))
} }
} }

@ -5,7 +5,6 @@ import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -18,13 +17,9 @@ import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.App import io.legado.app.App
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.FileHelp
import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.Permissions
import io.legado.app.help.permission.PermissionsCompat import io.legado.app.help.permission.PermissionsCompat
import io.legado.app.utils.DocumentUtils import io.legado.app.utils.*
import io.legado.app.utils.getPrefString
import io.legado.app.utils.putPrefString
import io.legado.app.utils.toast
import kotlinx.android.synthetic.main.dialog_font_select.* import kotlinx.android.synthetic.main.dialog_font_select.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -127,11 +122,11 @@ class FontSelectDialog : DialogFragment(),
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun getFontFiles(uri: Uri) { private fun getFontFiles(uri: Uri) {
launch(IO) { launch(IO) {
FileHelp.deleteFile(fontCacheFolder) FileUtils.deleteFile(fontCacheFolder)
DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { file -> DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { file ->
if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) { if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) {
DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let { DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let {
FileHelp.getFile(fontCacheFolder + file.name).writeBytes(it) FileUtils.getFile(fontCacheFolder + file.name).writeBytes(it)
} }
} }
} }
@ -154,7 +149,7 @@ class FontSelectDialog : DialogFragment(),
private fun getFontFilesOld() { private fun getFontFilesOld() {
try { try {
val file = val file =
File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts") File(FileUtils.getSdCardPath() + File.separator + "Fonts")
file.listFiles { pathName -> file.listFiles { pathName ->
pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex())
}?.let { }?.let {
@ -167,7 +162,7 @@ class FontSelectDialog : DialogFragment(),
override fun onClick(file: File) { override fun onClick(file: File) {
launch(IO) { launch(IO) {
file.copyTo(FileHelp.getFile(fontFolder + file.name), true).absolutePath.let { path -> file.copyTo(FileUtils.getFile(fontFolder + file.name), true).absolutePath.let { path ->
val cb = (parentFragment as? CallBack) ?: (activity as? CallBack) val cb = (parentFragment as? CallBack) ?: (activity as? CallBack)
cb?.let { cb?.let {
if (it.curFontPath != path) { if (it.curFontPath != path) {

@ -171,7 +171,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* @return JSONObject数据 * @return JSONObject数据
*/ */
fun getAsJSONObject(key: String): JSONObject? { fun getAsJSONObject(key: String): JSONObject? {
val json = getAsString(key) val json = getAsString(key) ?: return null
return try { return try {
JSONObject(json) JSONObject(json)
} catch (e: Exception) { } catch (e: Exception) {
@ -311,17 +311,17 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
fun getAsObject(key: String): Any? { fun getAsObject(key: String): Any? {
val data = getAsBinary(key) val data = getAsBinary(key)
if (data != null) { if (data != null) {
var bais: ByteArrayInputStream? = null var bis: ByteArrayInputStream? = null
var ois: ObjectInputStream? = null var ois: ObjectInputStream? = null
try { try {
bais = ByteArrayInputStream(data) bis = ByteArrayInputStream(data)
ois = ObjectInputStream(bais) ois = ObjectInputStream(bis)
return ois.readObject() return ois.readObject()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} finally { } finally {
try { try {
bais?.close() bis?.close()
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} }
@ -597,6 +597,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
val w = drawable.intrinsicWidth val w = drawable.intrinsicWidth
val h = drawable.intrinsicHeight val h = drawable.intrinsicHeight
// 取 drawable 的颜色格式 // 取 drawable 的颜色格式
@Suppress("DEPRECATION")
val config = if (drawable.opacity != PixelFormat.OPAQUE) val config = if (drawable.opacity != PixelFormat.OPAQUE)
Bitmap.Config.ARGB_8888 Bitmap.Config.ARGB_8888
else else

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.provider.Settings import android.provider.Settings
@ -126,11 +127,7 @@ fun Context.shareWithQr(title: String, text: String) {
} }
} }
val Context.isNightTheme: Boolean fun Context.sysIsDarkMode(): Boolean {
get() = getPrefBoolean("isNightTheme") val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return mode == Configuration.UI_MODE_NIGHT_YES
val Context.isEInkMode: Boolean }
get() = getPrefBoolean("isEInkMode")
val Context.isTransparentStatusBar: Boolean
get() = getPrefBoolean("transparentStatusBar", true)

@ -2,9 +2,44 @@ package io.legado.app.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.documentfile.provider.DocumentFile
object DocumentUtils { object DocumentUtils {
fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean {
val parent = getDirDocument(root, *subDirs) ?: return false
return parent.findFile(fileName)?.exists() ?: false
}
fun createFileIfNotExist(
root: DocumentFile,
fileName: String,
mimeType: String = "",
vararg subDirs: String
): DocumentFile? {
val parent: DocumentFile? = createFileIfNotExist(root, *subDirs)
return parent?.createFile(mimeType, fileName)
}
fun createFileIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? {
var parent: DocumentFile? = root
for (subDirName in subDirs) {
val subDir = parent?.findFile(subDirName)
?: parent?.createDirectory(subDirName)
parent = subDir
}
return parent
}
fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? {
var parent = root
for (subDirName in subDirs) {
val subDir = parent.findFile(subDirName)
parent = subDir ?: return null
}
return parent
}
@JvmStatic @JvmStatic
@Throws(Exception::class) @Throws(Exception::class)
fun writeText(context: Context, data: String, fileUri: Uri): Boolean { fun writeText(context: Context, data: String, fileUri: Uri): Boolean {

@ -4,86 +4,92 @@ import android.content.ContentUris
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.os.storage.StorageManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import io.legado.app.App
import androidx.core.content.ContextCompat
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.lang.reflect.Array
import java.util.*
object FileUtils { object FileUtils {
fun getFileByPath(filePath: String): File? { fun exists(file: File, fileName: String, vararg subDirs: String): Boolean {
return if (filePath.isBlank()) null else File(filePath) val filePath =
file.absolutePath + File.separator + subDirs.joinToString(File.separator) + File.separator + fileName
return File(filePath).exists()
} }
fun getSdCardPath(): String { fun createFileIfNotExist(file: File, fileName: String, vararg subDirs: String): File {
var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath val filePath =
file.absolutePath + File.separator + subDirs.joinToString(File.separator) + File.separator + fileName
try { return getFile(filePath)
sdCardDirectory = File(sdCardDirectory).canonicalPath
} catch (ioe: IOException) {
ioe.printStackTrace()
} }
return sdCardDirectory fun createFileIfNotExist(file: File, vararg subDirs: String): File {
val filePath = file.absolutePath + File.separator + subDirs.joinToString(File.separator)
return getFolder(filePath)
} }
fun getStorageData(pContext: Context): ArrayList<String>? { fun getCachePath(): String {
return App.INSTANCE.externalCacheDir?.absolutePath
?: App.INSTANCE.cacheDir.absolutePath
}
val storageManager = pContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager //获取文件夹
fun getFolder(filePath: String): File {
val file = File(filePath)
//如果文件夹不存在,就创建它
if (!file.exists()) {
file.mkdirs()
}
return file
}
//获取文件
@Synchronized
fun getFile(filePath: String): File {
val file = File(filePath)
try { try {
val getVolumeList = storageManager.javaClass.getMethod("getVolumeList") if (!file.exists()) {
//创建父类文件夹
val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume") file.parent?.let {
val getPath = storageVolumeClazz.getMethod("getPath") getFolder(it)
val invokeVolumeList = getVolumeList.invoke(storageManager)
val length = Array.getLength(invokeVolumeList)
val list = ArrayList<String>()
for (i in 0 until length) {
val storageVolume = Array.get(invokeVolumeList, i)//得到StorageVolume对象
val path = getPath.invoke(storageVolume) as String
list.add(path)
} }
return list //创建文件
} catch (e: Exception) { file.createNewFile()
}
} catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} }
return file
return null
} }
//递归删除文件夹下的数据
@Synchronized
fun deleteFile(filePath: String) {
val file = File(filePath)
if (!file.exists()) return
fun getExtSdCardPaths(con: Context): ArrayList<String> { if (file.isDirectory) {
val paths = ArrayList<String>() val files = file.listFiles()
val files = ContextCompat.getExternalFilesDirs(con, "external") files?.forEach { subFile ->
val firstFile = files[0] val path = subFile.path
for (file in files) { deleteFile(path)
if (file != null && file != firstFile) {
val index = file.absolutePath.lastIndexOf("/Android/data")
if (index < 0) {
Log.w("", "Unexpected external file dir: " + file.absolutePath)
} else {
var path = file.absolutePath.substring(0, index)
try {
path = File(path).canonicalPath
} catch (e: IOException) {
// Keep non-canonical path.
} }
paths.add(path)
} }
//删除文件
file.delete()
} }
fun getSdCardPath(): String {
@Suppress("DEPRECATION")
var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath
try {
sdCardDirectory = File(sdCardDirectory).canonicalPath
} catch (ioe: IOException) {
ioe.printStackTrace()
} }
return paths return sdCardDirectory
} }
fun getPath(context: Context, uri: Uri): String? { fun getPath(context: Context, uri: Uri): String? {
@ -96,6 +102,7 @@ object FileUtils {
val type = split[0] val type = split[0]
if ("primary".equals(type, ignoreCase = true)) { if ("primary".equals(type, ignoreCase = true)) {
@Suppress("DEPRECATION")
return Environment.getExternalStorageDirectory().toString() + "/" + split[1] return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
} }
@ -145,7 +152,7 @@ object FileUtils {
private fun getDataColumn( private fun getDataColumn(
context: Context, uri: Uri, selection: String?, context: Context, uri: Uri, selection: String?,
selectionArgs: kotlin.Array<String>? selectionArgs: Array<String>?
): String? { ): String? {
val column = "_data" val column = "_data"

@ -54,17 +54,9 @@ fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? = requireContext
fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = requireContext().getCompatColorStateList(id) fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = requireContext().getCompatColorStateList(id)
val Fragment.isNightTheme: Boolean
get() = getPrefBoolean("isNightTheme")
val Fragment.isTransparentStatusBar: Boolean
get() = getPrefBoolean("transparentStatusBar")
inline fun <reified T : Activity> Fragment.startActivity(vararg params: Pair<String, Any?>) = inline fun <reified T : Activity> Fragment.startActivity(vararg params: Pair<String, Any?>) =
AnkoInternals.internalStartActivity(requireActivity(), T::class.java, params) AnkoInternals.internalStartActivity(requireActivity(), T::class.java, params)
inline fun <reified T : Activity> Fragment.startActivityForResult(requestCode: Int, vararg params: Pair<String, Any?>) = inline fun <reified T : Activity> Fragment.startActivityForResult(requestCode: Int, vararg params: Pair<String, Any?>) =
startActivityForResult(AnkoInternals.createIntent(requireActivity(), T::class.java, params), requestCode) startActivityForResult(AnkoInternals.createIntent(requireActivity(), T::class.java, params), requestCode)

@ -2,7 +2,6 @@ package io.legado.app.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import io.legado.app.App import io.legado.app.App
import io.legado.app.help.FileHelp
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -30,8 +29,8 @@ object LogUtils {
} }
private val fileHandler by lazy { private val fileHandler by lazy {
val logFolder = FileHelp.getCachePath() + File.separator + "logs" val logFolder = FileUtils.getCachePath() + File.separator + "logs"
FileHelp.getFolder(logFolder) FileUtils.getFolder(logFolder)
FileHandler( FileHandler(
logFolder + File.separator + "app.log", logFolder + File.separator + "app.log",
10240, 10240,

@ -4,34 +4,41 @@ package io.legado.app.utils
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
fun String?.isAbsUrl() = if (this.isNullOrBlank()) false else this.startsWith("http://", true) fun String?.isAbsUrl() =
|| this.startsWith("https://", true) this?.let {
it.startsWith("http://", true)
fun String?.isJson(): Boolean = this?.run { || it.startsWith("https://", true)
} ?: false
fun String?.isJson(): Boolean =
this?.run {
val str = this.trim() val str = this.trim()
when { when {
str.startsWith("{") && str.endsWith("}") -> true str.startsWith("{") && str.endsWith("}") -> true
str.startsWith("[") && str.endsWith("]") -> true str.startsWith("[") && str.endsWith("]") -> true
else -> false else -> false
} }
} ?: false } ?: false
fun String?.isJsonObject(): Boolean = this?.run { fun String?.isJsonObject(): Boolean =
this?.run {
val str = this.trim() val str = this.trim()
str.startsWith("{") && str.endsWith("}") str.startsWith("{") && str.endsWith("}")
} ?: false } ?: false
fun String?.isJsonArray(): Boolean = this?.run { fun String?.isJsonArray(): Boolean =
this?.run {
val str = this.trim() val str = this.trim()
str.startsWith("[") && str.endsWith("]") str.startsWith("[") && str.endsWith("]")
} ?: false } ?: false
fun String?.htmlFormat(): String = if (this.isNullOrBlank()) "" else fun String?.htmlFormat(): String =
this.replace("(?i)<(br[\\s/]*|/*p\\b.*?|/*div\\b.*?)>".toRegex(), "\n")// 替换特定标签为换行符 this?.replace("(?i)<(br[\\s/]*|/*p\\b.*?|/*div\\b.*?)>".toRegex(), "\n")
.replace("<[script>]*.*?>|&nbsp;".toRegex(), "")// 删除script标签对和空格转义符 ?.replace("<[script>]*.*?>|&nbsp;".toRegex(), "")
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字 ?.replace("\\s*\\n+\\s*".toRegex(), "\n  ")
.replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字 ?.replace("^[\\n\\s]+".toRegex(), "  ")
.replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 ?.replace("[\\n\\s]+$".toRegex(), "")
?: ""
fun String.splitNotBlank(vararg delimiter: String): Array<String> = run { fun String.splitNotBlank(vararg delimiter: String): Array<String> = run {
this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray()

@ -41,16 +41,18 @@ object StringUtils {
//将时间转换成日期 //将时间转换成日期
fun dateConvert(time: Long, pattern: String): String { fun dateConvert(time: Long, pattern: String): String {
val date = Date(time) val date = Date(time)
@SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) @SuppressLint("SimpleDateFormat")
val format = SimpleDateFormat(pattern)
return format.format(date) return format.format(date)
} }
//将日期转换成昨天、今天、明天 //将日期转换成昨天、今天、明天
fun dateConvert(source: String, pattern: String): String { fun dateConvert(source: String, pattern: String): String {
@SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) @SuppressLint("SimpleDateFormat")
val format = SimpleDateFormat(pattern)
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
try { try {
val date = format.parse(source) val date = format.parse(source) ?: return ""
val curTime = calendar.timeInMillis val curTime = calendar.timeInMillis
calendar.time = date calendar.time = date
//将MISC 转换成 sec //将MISC 转换成 sec
@ -62,13 +64,18 @@ object StringUtils {
//如果没有时间 //如果没有时间
if (oldHour == 0) { if (oldHour == 0) {
//比日期:昨天今天和明天 //比日期:昨天今天和明天
if (difDate == 0L) { return when {
return "今天" difDate == 0L -> {
} else if (difDate < DAY_OF_YESTERDAY) { "今天"
return "昨天" }
} else { difDate < DAY_OF_YESTERDAY -> {
@SuppressLint("SimpleDateFormat") val convertFormat = SimpleDateFormat("yyyy-MM-dd") "昨天"
return convertFormat.format(date) }
else -> {
@SuppressLint("SimpleDateFormat")
val convertFormat = SimpleDateFormat("yyyy-MM-dd")
convertFormat.format(date)
}
} }
} }
@ -78,7 +85,8 @@ object StringUtils {
difHour < HOUR_OF_DAY -> difHour.toString() + "小时前" difHour < HOUR_OF_DAY -> difHour.toString() + "小时前"
difDate < DAY_OF_YESTERDAY -> "昨天" difDate < DAY_OF_YESTERDAY -> "昨天"
else -> { else -> {
@SuppressLint("SimpleDateFormat") val convertFormat = SimpleDateFormat("yyyy-MM-dd") @SuppressLint("SimpleDateFormat")
val convertFormat = SimpleDateFormat("yyyy-MM-dd")
convertFormat.format(date) convertFormat.format(date)
} }
} }
@ -242,7 +250,7 @@ object StringUtils {
val m = p.matcher(data) val m = p.matcher(data)
val buf = StringBuffer(data.length) val buf = StringBuffer(data.length)
while (m.find()) { while (m.find()) {
val ch = Integer.parseInt(m.group(1), 16).toChar().toString() val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString()
m.appendReplacement(buf, Matcher.quoteReplacement(ch)) m.appendReplacement(buf, Matcher.quoteReplacement(ch))
} }
m.appendTail(buf) m.appendTail(buf)

@ -5,6 +5,9 @@ import android.net.Uri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import java.io.File import java.io.File
fun Uri.isDocumentUri(context: Context): Boolean {
return DocumentFile.isDocumentUri(context, this)
}
@Throws(Exception::class) @Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray? { fun Uri.readBytes(context: Context): ByteArray? {

@ -24,7 +24,7 @@ private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? {
val View.activity: AppCompatActivity? val View.activity: AppCompatActivity?
get() = getCompatActivity(context) get() = getCompatActivity(context)
inline fun View.hideSoftInput() = run { fun View.hideSoftInput() = run {
val imm = App.INSTANCE.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager val imm = App.INSTANCE.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.let { imm?.let {
imm.hideSoftInputFromWindow(this.windowToken, 0) imm.hideSoftInputFromWindow(this.windowToken, 0)

@ -57,7 +57,7 @@ object ZipUtils {
} }
return true return true
} finally { } finally {
if (zos != null) { zos?.let {
zos.finish() zos.finish()
zos.close() zos.close()
} }
@ -89,7 +89,7 @@ object ZipUtils {
} }
return true return true
} finally { } finally {
if (zos != null) { zos?.let {
zos.finish() zos.finish()
zos.close() zos.close()
} }
@ -261,7 +261,7 @@ object ZipUtils {
val files = ArrayList<File>() val files = ArrayList<File>()
val zip = ZipFile(zipFile) val zip = ZipFile(zipFile)
val entries = zip.entries() val entries = zip.entries()
try { zip.use {
if (isSpace(keyword)) { if (isSpace(keyword)) {
while (entries.hasMoreElements()) { while (entries.hasMoreElements()) {
val entry = entries.nextElement() as ZipEntry val entry = entries.nextElement() as ZipEntry
@ -285,8 +285,6 @@ object ZipUtils {
} }
} }
} }
} finally {
zip.close()
} }
return files return files
} }

@ -63,6 +63,18 @@
<item>@string/jf_convert_f</item> <item>@string/jf_convert_f</item>
</string-array> </string-array>
<string-array name="theme_mode">
<item>跟随系统</item>
<item>亮色主题</item>
<item>暗色主题</item>
</string-array>
<string-array name="theme_mode_v">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="NavBarColors"> <string-array name="NavBarColors">
<item>自动</item> <item>自动</item>
<item>黑色</item> <item>黑色</item>

@ -433,6 +433,7 @@
<string name="scan_qr_code">扫描二维码</string> <string name="scan_qr_code">扫描二维码</string>
<string name="click_on_selected_show_menu">选中时点击可弹出菜单</string> <string name="click_on_selected_show_menu">选中时点击可弹出菜单</string>
<string name="theme">主题</string> <string name="theme">主题</string>
<string name="theme_mode">主题模式</string>
<string name="default_theme">默认主题</string> <string name="default_theme">默认主题</string>
<string name="restore_default_theme">恢复主题为默认配色</string> <string name="restore_default_theme">恢复主题为默认配色</string>
<string name="join_qq_group">加入QQ群</string> <string name="join_qq_group">加入QQ群</string>
@ -583,5 +584,7 @@
<string name="text_too_long_qr_error">文字太多,生成二维码失败</string> <string name="text_too_long_qr_error">文字太多,生成二维码失败</string>
<string name="share_rss_source">分享RSS源</string> <string name="share_rss_source">分享RSS源</string>
<string name="share_book_source">分享书源</string> <string name="share_book_source">分享书源</string>
<string name="auto_dark_mode">自动切换夜间模式</string>
<string name="auto_dark_mode_s">夜间模式跟随系统</string>
</resources> </resources>

@ -2,7 +2,6 @@
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<io.legado.app.lib.theme.prefs.ATESwitchPreference <io.legado.app.lib.theme.prefs.ATESwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="transparentStatusBar" android:key="transparentStatusBar"

@ -12,10 +12,18 @@
android:title="@string/replace_purify" android:title="@string/replace_purify"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.lib.theme.prefs.ATEListPreference
android:defaultValue="0"
android:entries="@array/theme_mode"
android:entryValues="@array/theme_mode_v"
android:key="themeMode"
android:title="@string/theme_mode"
app:iconSpaceReserved="false" />
<io.legado.app.lib.theme.prefs.ATESwitchPreference <io.legado.app.lib.theme.prefs.ATESwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="isNightTheme" android:key="isEInkMode"
android:title="@string/night_theme" android:title="@string/eink_theme"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.lib.theme.prefs.ATESwitchPreference <io.legado.app.lib.theme.prefs.ATESwitchPreference

Loading…
Cancel
Save