Merge branch 'gedoor:master' into master

pull/2436/head
Xwite 3 years ago committed by GitHub
commit 3fbdfe682f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/src/main/AndroidManifest.xml
  2. 2
      app/src/main/assets/defaultData/directLinkUpload.json
  3. 12
      app/src/main/assets/updateLog.md
  4. 1
      app/src/main/java/io/legado/app/constant/AppConst.kt
  5. 8
      app/src/main/java/io/legado/app/constant/BookType.kt
  6. 7
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  7. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  8. 1
      app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
  9. 8
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  10. 2
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  11. 53
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  12. 2
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  13. 10
      app/src/main/java/io/legado/app/help/source/BookSourceExtensions.kt
  14. 6
      app/src/main/java/io/legado/app/help/storage/ImportOldData.kt
  15. 4
      app/src/main/java/io/legado/app/lib/webdav/WebDav.kt
  16. 8
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  17. 8
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  18. 9
      app/src/main/java/io/legado/app/service/WebService.kt
  19. 19
      app/src/main/java/io/legado/app/ui/association/FileAssociationViewModel.kt
  20. 67
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookActivity.kt
  21. 8
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt
  22. 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  23. 1
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageActivity.kt
  24. 3
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt
  25. 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  26. 11
      app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt
  27. 25
      app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt
  28. 9
      app/src/main/java/io/legado/app/ui/book/search/SearchScopeDialog.kt
  29. 2
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
  30. 34
      app/src/main/java/io/legado/app/ui/font/FontSelectDialog.kt
  31. 6
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  32. 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BooksFragment.kt
  33. 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
  34. 9
      app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt
  35. 225
      app/src/main/java/io/legado/app/utils/DocumentExtensions.kt
  36. 47
      app/src/main/java/io/legado/app/utils/DocumentUtils.kt
  37. 255
      app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
  38. 21
      app/src/main/java/io/legado/app/utils/FileExtensions.kt
  39. 3
      app/src/main/java/io/legado/app/utils/FileUtils.kt
  40. 67
      app/src/main/java/io/legado/app/utils/RegexExtensions.kt
  41. 4
      app/src/main/java/io/legado/app/utils/StringExtensions.kt
  42. 22
      app/src/main/java/io/legado/app/utils/UriExtensions.kt
  43. 2
      gradle.properties

@ -4,6 +4,7 @@
package="io.legado.app">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

@ -1,5 +1,5 @@
{
"uploadUrl": "http://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}",
"uploadUrl": "https://sy.mgz6.cc/shuyuan,{\"method\":\"POST\",\"body\": {\"file\": \"fileRequest\"},\"type\": \"multipart/form-data\"}",
"downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.cc/shuyuan/' + result",
"summary": "有效期2天"
}

@ -11,13 +11,21 @@
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
**2022/10/13**
**2022/10/16**
* 添加更新失败分组
* web服务和朗读服务添加唤醒锁
* 修复搜索禁用书源的bug
* 修复本地书籍很慢的问题
**2022/10/14**
* 更新cronet: 106.0.5249.126
* 替换规则支持js,可以用js判断匹配到的内容决定替换为什么,匹配到的内容变量为result,替换为@js:开头则自动采用js判断替换内容
* 书架管理添加筛选功能
* 搜索支持搜索范围多分组和单书源设置
* 书源管理界面书源菜单添加单书源搜索数据功能
* 书源管理界面书源菜单添加单书源搜索书籍功能
* 复web阅读时app未退出阅读界面导致的进度bug by Xwite
**2022/10/09**

@ -43,6 +43,7 @@ object AppConst {
const val bookGroupAudioId = -3L
const val bookGroupNetNoneId = -4L
const val bookGroupLocalNoneId = -5L
const val bookGroupErrorId = -11L
const val notificationIdRead = -1122391
const val notificationIdAudio = -1122392

@ -12,6 +12,11 @@ object BookType {
*/
const val text = 0b1000
/**
* 16 更新失败
*/
const val updateError = 0b10000
/**
* 32 音频
*/
@ -35,9 +40,10 @@ object BookType {
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@IntDef(text, audio, image, webFile)
@IntDef(text, updateError, audio, image, webFile, local)
annotation class Type
/**
* 本地书籍书源标志
*/

@ -105,9 +105,14 @@ abstract class AppDatabase : RoomDatabase() {
)
db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show)
select ${AppConst.bookGroupLocalNoneId}, '本地未分组', -8, 0
select ${AppConst.bookGroupLocalNoneId}, '本地未分组', -6, 0
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalNoneId})"""
)
db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show)
select ${AppConst.bookGroupErrorId}, '更新失败', -1, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupErrorId})"""
)
db.execSQL("update book_sources set loginUi = null where loginUi = 'null'")
db.execSQL("update rssSources set loginUi = null where loginUi = 'null'")
db.execSQL("update httpTTS set loginUi = null where loginUi = 'null'")

@ -53,6 +53,9 @@ interface BookDao {
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
fun flowSearch(key: String): Flow<List<Book>>
@Query("SELECT * FROM books where type & ${BookType.updateError} > 0 order by durChapterTime desc")
fun flowUpdateError(): Flow<List<Book>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun getBooksByGroup(group: Long): List<Book>

@ -25,6 +25,7 @@ interface BookGroupDao {
or (groupId = -1 and show > 0)
or (groupId = -2 and show > 0 and (select count(*) from books where type & ${BookType.local} > 0) > 0)
or (groupId = -3 and show > 0 and (select count(*) from books where type & ${BookType.audio} > 0) > 0)
or (groupId = -11 and show > 0 and (select count(*) from books where type & ${BookType.updateError} > 0) > 0)
or (groupId = -4 and show > 0
and (
select count(*) from books

@ -134,6 +134,9 @@ interface BookSourceDao {
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
val allGroupsUnProcessed: List<String>
@get:Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''")
val allEnabledGroupsUnProcessed: List<String>
@Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource?
@ -175,6 +178,11 @@ interface BookSourceDao {
return dealGroups(allGroupsUnProcessed)
}
val allEnabledGroups: List<String>
get() {
return dealGroups(allEnabledGroupsUnProcessed)
}
fun flowGroups(): Flow<List<String>> {
return flowGroupsUnProcessed().map { list ->
dealGroups(list)

@ -166,7 +166,7 @@ interface JsExtensions {
fun importScript(path: String): String {
val result = when {
path.startsWith("http") -> cacheFile(path) ?: ""
path.isContentScheme() -> DocumentUtils.readText(appCtx, Uri.parse(path))
path.isUri() -> Uri.parse(path).readText(appCtx)
path.startsWith("/storage") -> FileUtils.readText(path)
else -> readTxtFile(path)
}

@ -1,17 +1,16 @@
@file:Suppress("unused")
package io.legado.app.help.book
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import io.legado.app.constant.BookSourceType
import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.AppConfig.defaultBookTreeUri
import io.legado.app.exception.NoStackTraceException
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.getFile
import io.legado.app.help.config.AppConfig.defaultBookTreeUri
import io.legado.app.utils.*
import java.io.File
import splitties.init.appCtx
val Book.isAudio: Boolean
@ -54,25 +53,16 @@ val Book.isOnLineTxt: Boolean
fun Book.getLocalUri(): Uri {
if (isLocal) {
val originBookUri = if (bookUrl.isContentScheme()) {
Uri.parse(bookUrl)
} else {
Uri.fromFile(File(bookUrl))
}
val originBookUri = if (bookUrl.isContentScheme()) {
Uri.parse(bookUrl)
} else {
Uri.fromFile(File(bookUrl))
}
//不同的设备书籍保存路径可能不一样 优先尝试寻找当前保存路径下的文件
defaultBookTreeUri ?: return originBookUri
val treeUri = Uri.parse(defaultBookTreeUri)
return if (treeUri.isContentScheme()) {
DocumentFile.fromTreeUri(appCtx, treeUri)?.run {
findFile(originName)?.let {
if (it.exists()) it.uri else originBookUri
} ?: originBookUri
} ?: originBookUri
} else {
val treeFile = File(treeUri.path!!)
val file = treeFile.getFile(originName)
if (file.exists()) Uri.fromFile(file) else originBookUri
}
val treeFileDoc = FileDoc.fromUri(treeUri, true)
return treeFileDoc.find(originName, 5)?.uri ?: originBookUri
}
throw NoStackTraceException("不是本地书籍")
}
@ -84,6 +74,27 @@ fun Book.getRemoteUrl(): String? {
return null
}
fun Book.setType(@BookType.Type vararg types: Int) {
type = 0
addType(*types)
}
fun Book.addType(@BookType.Type vararg types: Int) {
types.forEach {
type = type or it
}
}
fun Book.removeType(@BookType.Type vararg types: Int) {
types.forEach {
type = type and it.inv()
}
}
fun Book.clearType() {
type = 0
}
fun Book.upType() {
if (type < 8) {
type = when (type) {

@ -145,7 +145,7 @@ class Coroutine<T>(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): Job {
return (scope + Dispatchers.Main).launch(start = startOption) {
return (scope.plus(Dispatchers.Main)).launch(start = startOption) {
try {
start?.let { dispatchVoidCallback(this, it) }
ensureActive()

@ -75,4 +75,14 @@ suspend fun BookSource.clearExploreKindsCache() {
aCache.remove(exploreKindsKey)
exploreKindsMap.remove(getExploreKindsKey())
}
}
fun BookSource.contains(word: String?): Boolean {
if (word.isNullOrEmpty()) {
return true
}
return bookSourceName.contains(word)
|| bookSourceUrl.contains(word)
|| bookSourceGroup?.contains(word) == true
|| bookSourceComment?.contains(word) == true
}

@ -19,7 +19,7 @@ object ImportOldData {
when (doc.name) {
"myBookShelf.json" ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri).let { json ->
doc.uri.readText(context).let { json ->
val importCount = importOldBookshelf(json)
context.toastOnUi("成功导入书籍${importCount}")
}
@ -28,7 +28,7 @@ object ImportOldData {
}
"myBookSource.json" ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri).let { json ->
doc.uri.readText(context).let { json ->
val importCount = importOldSource(json)
context.toastOnUi("成功导入书源${importCount}")
}
@ -37,7 +37,7 @@ object ImportOldData {
}
"myBookReplaceRule.json" ->
kotlin.runCatching {
DocumentUtils.readText(context, doc.uri).let { json ->
doc.uri.readText(context).let { json ->
val importCount = importOldReplaceRule(json)
context.toastOnUi("成功导入替换规则${importCount}")
}

@ -358,9 +358,9 @@ open class WebDav(val path: String, val authorization: Authorization) {
val exception = document.getElementsByTag("s:exception").firstOrNull()?.text()
val message = document.getElementsByTag("s:message").firstOrNull()?.text()
if (exception == "ObjectNotFound") {
throw ObjectNotFoundException(message ?: "$path doesn't exist")
throw ObjectNotFoundException(message ?: "$path doesn't exist. code:${response.code}")
}
throw WebDavException(message ?: "未知错误")
throw WebDavException(message ?: "未知错误 code:${response.code}")
}
}

@ -9,6 +9,7 @@ import android.graphics.BitmapFactory
import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
@ -36,6 +37,7 @@ import io.legado.app.utils.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.Main
import splitties.systemservices.audioManager
import splitties.systemservices.powerManager
/**
* 音频播放服务
@ -61,6 +63,9 @@ class AudioPlayService : BaseService(),
private set
}
private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:webService")
}
private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this)
}
@ -82,8 +87,10 @@ class AudioPlayService : BaseService(),
private var upPlayProgressJob: Job? = null
private var playSpeed: Float = 1f
@SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
wakeLock.acquire()
isRun = true
upNotification()
exoPlayer.addListener(this)
@ -120,6 +127,7 @@ class AudioPlayService : BaseService(),
override fun onDestroy() {
super.onDestroy()
wakeLock.release()
isRun = false
abandonFocus()
exoPlayer.release()

@ -8,6 +8,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.annotation.CallSuper
@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import splitties.systemservices.audioManager
import splitties.systemservices.powerManager
/**
* 朗读服务
@ -55,6 +57,9 @@ abstract class BaseReadAloudService : BaseService(),
}
}
private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:readAloud")
}
private val mFocusRequest: AudioFocusRequestCompat by lazy {
MediaHelp.buildAudioFocusRequestCompat(this)
}
@ -77,8 +82,10 @@ abstract class BaseReadAloudService : BaseService(),
}
}
@SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
wakeLock.acquire()
isRun = true
pause = false
initMediaSession()
@ -90,6 +97,7 @@ abstract class BaseReadAloudService : BaseService(),
override fun onDestroy() {
super.onDestroy()
wakeLock.release()
isRun = false
pause = true
abandonFocus()

@ -1,8 +1,10 @@
package io.legado.app.service
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import io.legado.app.R
import io.legado.app.base.BaseService
@ -14,6 +16,7 @@ import io.legado.app.receiver.NetworkChangedListener
import io.legado.app.utils.*
import io.legado.app.web.HttpServer
import io.legado.app.web.WebSocketServer
import splitties.systemservices.powerManager
import java.io.IOException
@ -33,6 +36,9 @@ class WebService : BaseService() {
}
private val wakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:webService")
}
private var httpServer: HttpServer? = null
private var webSocketServer: WebSocketServer? = null
private var notificationContent = ""
@ -40,8 +46,10 @@ class WebService : BaseService() {
NetworkChangedListener(this)
}
@SuppressLint("WakelockTimeout")
override fun onCreate() {
super.onCreate()
wakeLock.acquire()
isRun = true
notificationContent = getString(R.string.service_starting)
upNotification()
@ -73,6 +81,7 @@ class WebService : BaseService() {
override fun onDestroy() {
super.onDestroy()
wakeLock.release()
networkChangedListener.unRegister()
isRun = false
if (httpServer?.isAlive == true) {

@ -2,16 +2,11 @@ package io.legado.app.ui.association
import android.app.Application
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern.bookFileRegex
import io.legado.app.exception.NoStackTraceException
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.inputStream
import io.legado.app.utils.isJson
import io.legado.app.utils.printOnDebug
import java.io.File
import io.legado.app.utils.*
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
val importBookLiveData = MutableLiveData<Uri>()
@ -24,15 +19,9 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
execute {
lateinit var fileName: String
//如果是普通的url,需要根据返回的内容判断是什么
if (uri.scheme == "file" || uri.scheme == "content") {
fileName = if (uri.scheme == "file") {
val file = File(uri.path.toString())
file.name
} else {
val file = DocumentFile.fromSingleUri(context, uri)
if (file?.exists() != true) throw NoStackTraceException("文件不存在")
file.name ?: ""
}
if (uri.isContentScheme() || uri.isFileScheme()) {
val fileDoc = FileDoc.fromUri(uri, false)
fileName = fileDoc.name
kotlin.runCatching {
if (uri.inputStream(context).isJson()) {
importJson(uri)

@ -46,22 +46,19 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
private val selectFolder = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
if (uri.isContentScheme()) {
AppConfig.importBookPath = uri.toString()
initRootDoc(true)
} else {
AppConfig.importBookPath = uri.path
initRootDoc(true)
}
AppConfig.importBookPath = uri.toString()
initRootDoc(true)
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.setTitle(R.string.book_local)
launch {
setBookStorage()
initView()
initEvent()
if (setBookStorage() && AppConfig.importBookPath.isNullOrBlank()) {
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
}
initData()
}
}
@ -150,35 +147,43 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
}
private fun initRootDoc(changedFolder: Boolean = false) {
val lastPath = AppConfig.importBookPath
when {
viewModel.rootDoc != null && !changedFolder -> upPath()
lastPath.isNullOrEmpty() -> {
if (viewModel.rootDoc != null && !changedFolder) {
upPath()
} else {
val lastPath = AppConfig.importBookPath
if (lastPath.isNullOrBlank()) {
binding.tvEmptyMsg.visible()
selectFolder.launch()
}
lastPath.isContentScheme() -> {
val rootUri = Uri.parse(lastPath)
kotlin.runCatching {
val doc = DocumentFile.fromTreeUri(this, rootUri)
if (doc == null || doc.name.isNullOrEmpty()) {
} else {
val rootUri = if (lastPath.isUri()) {
Uri.parse(lastPath)
} else {
Uri.fromFile(File(lastPath))
}
when {
rootUri.isContentScheme() -> {
kotlin.runCatching {
val doc = DocumentFile.fromTreeUri(this, rootUri)
if (doc == null || doc.name.isNullOrEmpty()) {
binding.tvEmptyMsg.visible()
selectFolder.launch()
} else {
viewModel.subDocs.clear()
viewModel.rootDoc = FileDoc.fromDocumentFile(doc)
upDocs(viewModel.rootDoc!!)
}
}.onFailure {
binding.tvEmptyMsg.visible()
selectFolder.launch()
}
}
Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
binding.tvEmptyMsg.visible()
selectFolder.launch()
} else {
viewModel.subDocs.clear()
viewModel.rootDoc = FileDoc.fromDocumentFile(doc)
upDocs(viewModel.rootDoc!!)
}
}.onFailure {
binding.tvEmptyMsg.visible()
selectFolder.launch()
else -> initRootPath(rootUri.path!!)
}
}
Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
binding.tvEmptyMsg.visible()
selectFolder.launch()
}
else -> initRootPath(lastPath)
}
}
@ -228,7 +233,7 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
binding.tvPath.text = path
adapter.selectedUris.clear()
adapter.clearItems()
viewModel.loadDoc(lastDoc.uri)
viewModel.loadDoc(lastDoc)
}
/**

@ -102,16 +102,16 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}
}
fun loadDoc(uri: Uri) {
fun loadDoc(fileDoc: FileDoc) {
execute {
val docList = DocumentUtils.listFiles(uri) { item ->
val docList = fileDoc.list { item ->
when {
item.name.startsWith(".") -> false
item.isDir -> true
else -> item.name.matches(bookFileRegex)
}
}
dataCallback?.setItems(docList)
dataCallback?.setItems(docList!!)
}.onError {
context.toastOnUi("获取文件列表出错\n${it.localizedMessage}")
}
@ -132,7 +132,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}
kotlin.runCatching {
val list = ArrayList<FileDoc>()
DocumentUtils.listFiles(fileDoc.uri).forEach { docItem ->
fileDoc.list()!!.forEach { docItem ->
if (!scope.isActive) {
finally?.invoke()
return

@ -18,6 +18,7 @@ import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.getRemoteUrl
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.removeType
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.ObjectNotFoundException
import io.legado.app.model.BookCover
@ -236,6 +237,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
bookSource = source
bookData.value?.changeTo(book, toc)
if (inBookshelf) {
book.removeType(BookType.updateError)
appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray())
}

@ -184,6 +184,7 @@ class BookshelfManageActivity :
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(viewModel.groupId)
}.conflate().map { list ->
val books = if (searchKey.isNullOrBlank()) {

@ -3,10 +3,12 @@ package io.legado.app.ui.book.manage
import android.app.Application
import androidx.compose.runtime.mutableStateOf
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookSource
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.removeType
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook
import io.legado.app.utils.toastOnUi
@ -58,6 +60,7 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
context.toastOnUi("获取目录出错\n${it.localizedMessage}")
}.getOrNull()?.let { toc ->
book.changeTo(newBook, toc)
book.removeType(BookType.updateError)
appDb.bookDao.insert(newBook)
appDb.bookChapterDao.insert(*toc.toTypedArray())
}

@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@ -19,6 +20,7 @@ import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.removeType
import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ReadAloud
@ -215,6 +217,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
changeSourceCoroutine = execute {
ReadBook.upMsg(context.getString(R.string.loading))
ReadBook.book?.changeTo(book, toc)
book.removeType(BookType.updateError)
appDb.bookDao.insert(book)
appDb.bookChapterDao.insert(*toc.toTypedArray())
ReadBook.resetData(book)

@ -221,6 +221,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
private fun initData() {
searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
viewModel.searchScope.stateLiveData.observe(this) {
searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
}
viewModel.isSearchLiveData.observe(this) {
if (it) {
startSearch()
@ -400,6 +403,14 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
}
}
override fun finish() {
if (searchView.hasFocus()) {
searchView.clearFocus()
return
}
super.finish()
}
companion object {
fun start(context: Context, key: String?) {

@ -1,5 +1,6 @@
package io.legado.app.ui.book.search
import androidx.lifecycle.MutableLiveData
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.AppConfig
@ -13,12 +14,18 @@ data class SearchScope(private var scope: String) {
constructor(groups: List<String>) : this(groups.joinToString(","))
constructor(source: BookSource) : this("${source.bookSourceName}::${source.bookSourceUrl}")
constructor(source: BookSource) : this(
"${
source.bookSourceName.replace("::", "")
}::${source.bookSourceUrl}"
)
override fun toString(): String {
return scope
}
val stateLiveData = MutableLiveData("")
fun update(scope: String) {
this.scope = scope
}
@ -54,9 +61,9 @@ data class SearchScope(private var scope: String) {
}
if (list.isEmpty()) {
list.add("全部书源")
}
return list
}
return list
}
/**
* 搜索范围书源
@ -70,12 +77,20 @@ data class SearchScope(private var scope: String) {
}
}
} else {
scope.splitNotBlank(",").forEach {
list.addAll(appDb.bookSourceDao.getByGroup(it))
val oldScope = scope.splitNotBlank(",")
val newScope = oldScope.filter {
val bookSources = appDb.bookSourceDao.getEnabledByGroup(it)
list.addAll(bookSources)
bookSources.isNotEmpty()
}
if (oldScope.size != newScope.size) {
update(newScope)
stateLiveData.postValue("")
}
}
if (list.isEmpty()) {
scope = ""
stateLiveData.postValue("")
return appDb.bookSourceDao.allEnabled
}
return list.sortedBy { it.customOrder }

@ -14,6 +14,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.DialogSearchScopeBinding
import io.legado.app.databinding.ItemCheckBoxBinding
import io.legado.app.databinding.ItemRadioButtonBinding
import io.legado.app.help.source.contains
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.applyTint
import io.legado.app.utils.setLayout
@ -72,7 +73,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
}
private fun initOtherView() {
binding.rgScope.setOnCheckedChangeListener { group, checkedId ->
binding.rgScope.setOnCheckedChangeListener { _, checkedId ->
binding.toolBar.menu.findItem(R.id.menu_screen)?.isVisible = checkedId == R.id.rb_source
upData()
}
@ -101,7 +102,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
private fun initData() {
launch {
groups = withContext(IO) {
appDb.bookSourceDao.allGroups
appDb.bookSourceDao.allEnabledGroups
}
sources = withContext(IO) {
appDb.bookSourceDao.allEnabled
@ -116,9 +117,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
withContext(IO) {
if (binding.rbSource.isChecked) {
sources.filter { source ->
screenText?.let { screenText ->
source.bookSourceName.contains(screenText)
} ?: true
source.contains(screenText)
}.let {
screenSources.clear()
screenSources.addAll(it)

@ -193,7 +193,7 @@ class SearchContentActivity :
}
}
val isLocalBook: Boolean
private val isLocalBook: Boolean
get() = viewModel.book?.isLocal == true
override fun openSearchResult(searchResult: SearchResult, index: Int) {

@ -24,6 +24,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import java.io.File
/**
* 字体选择对话框
*/
class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
Toolbar.OnMenuItemClickListener,
FontAdapter.CallBack {
@ -35,11 +38,11 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
}
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
if (uri.toString().isContentScheme()) {
if (uri.isContentScheme()) {
putPrefString(PreferKey.fontFolder, uri.toString())
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc != null) {
loadFontFiles(doc)
loadFontFiles(FileDoc.fromDocumentFile(doc))
} else {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
loadFontFilesByPermission(path)
@ -75,7 +78,7 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
if (fontPath.isContentScheme()) {
val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath))
if (doc?.canRead() == true) {
loadFontFiles(doc)
loadFontFiles(FileDoc.fromDocumentFile(doc))
} else {
openFolder()
}
@ -117,40 +120,29 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
private fun getLocalFonts(): ArrayList<FileDoc> {
val path = FileUtils.getPath(requireContext().externalFiles, "font")
return DocumentUtils.listFiles(path) {
return File(path).listFileDocs {
it.name.matches(fontRegex)
}
}
private fun loadFontFiles(doc: DocumentFile) {
execute {
val fontItems = DocumentUtils.listFiles(doc.uri) {
it.name.matches(fontRegex)
}
mergeFontItems(fontItems, getLocalFonts())
}.onSuccess {
adapter.setItems(it)
}.onError {
toastOnUi("getFontFiles:${it.localizedMessage}")
}
}
private fun loadFontFilesByPermission(path: String) {
PermissionsCompat.Builder(this@FontSelectDialog)
.addPermissions(*Permissions.Group.STORAGE)
.rationale(R.string.tip_perm_request_storage)
.onGranted {
loadFontFiles(path)
loadFontFiles(
FileDoc.fromFile(File(path))
)
}
.request()
}
private fun loadFontFiles(path: String) {
private fun loadFontFiles(fileDoc: FileDoc) {
execute {
val fontItems = DocumentUtils.listFiles(path) {
val fontItems = fileDoc.list {
it.name.matches(fontRegex)
}
mergeFontItems(fontItems, getLocalFonts())
mergeFontItems(fontItems!!, getLocalFonts())
}.onSuccess {
adapter.setItems(it)
}.onError {

@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
@ -12,7 +13,9 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppWebDav
import io.legado.app.help.DefaultData
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.addType
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.removeType
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.model.CacheBook
@ -126,6 +129,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
WebBook.getBookInfoAwait(source, book)
}
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
book.removeType(BookType.updateError)
if (book.bookUrl == bookUrl) {
appDb.bookDao.update(book)
} else {
@ -137,6 +141,8 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.insert(*toc.toTypedArray())
addDownload(source, book)
}.onError(upTocPool) {
book.addType(BookType.updateError)
appDb.bookDao.update(book)
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
}.onCancel(upTocPool) {
upTocCancel(bookUrl)

@ -118,6 +118,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(groupId)
}.conflate().map { list ->
when (getPrefInt(PreferKey.bookshelfSort)) {

@ -130,6 +130,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
else -> appDb.bookDao.flowByGroup(groupId)
}.conflate().map { list ->
when (getPrefInt(PreferKey.bookshelfSort)) {

@ -15,6 +15,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource
import io.legado.app.databinding.FragmentRssBinding
import io.legado.app.databinding.ItemRssBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
import io.legado.app.lib.theme.primaryTextColor
import io.legado.app.ui.rss.article.RssSortActivity
@ -179,7 +180,13 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
}
override fun del(rssSource: RssSource) {
viewModel.del(rssSource)
alert(R.string.draw) {
setMessage(getString(R.string.sure_del) + "\n" + rssSource.sourceName)
noButton()
yesButton {
viewModel.del(rssSource)
}
}
}
override fun disable(rssSource: RssSource) {

@ -1,225 +0,0 @@
package io.legado.app.utils
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import io.legado.app.exception.NoStackTraceException
import splitties.init.appCtx
import java.io.File
import java.nio.charset.Charset
@Suppress("MemberVisibilityCanBePrivate")
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 delete(root: DocumentFile, fileName: String, vararg subDirs: String) {
val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
parent?.findFile(fileName)?.delete()
}
fun createFileIfNotExist(
root: DocumentFile,
fileName: String,
mimeType: String = "",
vararg subDirs: String
): DocumentFile? {
val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
return parent?.findFile(fileName) ?: parent?.createFile(mimeType, fileName)
}
fun createFolderIfNotExist(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
@Throws(Exception::class)
fun writeText(
context: Context,
data: String,
fileUri: Uri,
charset: Charset = Charsets.UTF_8
): Boolean {
return writeBytes(context, data.toByteArray(charset), fileUri)
}
@JvmStatic
@Throws(Exception::class)
fun writeBytes(context: Context, data: ByteArray, fileUri: Uri): Boolean {
context.contentResolver.openOutputStream(fileUri)?.let {
it.write(data)
it.close()
return true
}
return false
}
@JvmStatic
@Throws(Exception::class)
fun readText(context: Context, uri: Uri): String {
return String(readBytes(context, uri))
}
@JvmStatic
@Throws(Exception::class)
fun readBytes(context: Context, uri: Uri): ByteArray {
context.contentResolver.openInputStream(uri)?.let {
val len: Int = it.available()
val buffer = ByteArray(len)
it.read(buffer)
it.close()
return buffer
} ?: throw NoStackTraceException("打开文件失败\n${uri}")
}
@Throws(Exception::class)
fun listFiles(uri: Uri, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList<FileDoc> {
if (!uri.isContentScheme()) {
return listFiles(uri.path!!, filter)
}
val childrenUri = DocumentsContract
.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
val docList = arrayListOf<FileDoc>()
var cursor: Cursor? = null
try {
cursor = appCtx.contentResolver.query(
childrenUri, arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_MIME_TYPE
), null, null, DocumentsContract.Document.COLUMN_DISPLAY_NAME
)
cursor?.let {
val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
if (cursor.moveToFirst()) {
do {
val item = FileDoc(
name = cursor.getString(nci),
isDir = cursor.getString(mci) == DocumentsContract.Document.MIME_TYPE_DIR,
size = cursor.getLong(sci),
lastModified = cursor.getLong(dci),
uri = DocumentsContract
.buildDocumentUriUsingTree(uri, cursor.getString(ici))
)
if (filter == null || filter.invoke(item)) {
docList.add(item)
}
} while (cursor.moveToNext())
}
}
} finally {
cursor?.close()
}
return docList
}
@Throws(Exception::class)
fun listFiles(path: String, filter: ((file: FileDoc) -> Boolean)? = null): ArrayList<FileDoc> {
val docList = arrayListOf<FileDoc>()
val file = File(path)
file.listFiles()?.forEach {
val item = FileDoc(
it.name,
it.isDirectory,
it.length(),
it.lastModified(),
Uri.fromFile(it)
)
if (filter == null || filter.invoke(item)) {
docList.add(item)
}
}
return docList
}
}
data class FileDoc(
val name: String,
val isDir: Boolean,
val size: Long,
val lastModified: Long,
val uri: Uri
) {
override fun toString(): String {
return if (uri.isContentScheme()) uri.toString() else uri.path!!
}
val isContentScheme get() = uri.isContentScheme()
fun readBytes(): ByteArray {
return uri.readBytes(appCtx)
}
companion object {
fun fromDocumentFile(doc: DocumentFile): FileDoc {
return FileDoc(
name = doc.name ?: "",
isDir = doc.isDirectory,
size = doc.length(),
lastModified = doc.lastModified(),
uri = doc.uri
)
}
fun fromFile(file: File): FileDoc {
return FileDoc(
name = file.name,
isDir = file.isDirectory,
size = file.length(),
lastModified = file.lastModified(),
uri = Uri.fromFile(file)
)
}
}
}
@Throws(Exception::class)
fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
DocumentUtils.writeText(context, data, this.uri, charset)
}
@Throws(Exception::class)
fun DocumentFile.writeBytes(context: Context, data: ByteArray) {
DocumentUtils.writeBytes(context, data, this.uri)
}
@Throws(Exception::class)
fun DocumentFile.readText(context: Context): String {
return DocumentUtils.readText(context, this.uri)
}
@Throws(Exception::class)
fun DocumentFile.readBytes(context: Context): ByteArray {
return DocumentUtils.readBytes(context, this.uri)
}

@ -0,0 +1,47 @@
package io.legado.app.utils
import androidx.documentfile.provider.DocumentFile
@Suppress("MemberVisibilityCanBePrivate")
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 delete(root: DocumentFile, fileName: String, vararg subDirs: String) {
val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
parent?.findFile(fileName)?.delete()
}
fun createFileIfNotExist(
root: DocumentFile,
fileName: String,
mimeType: String = "",
vararg subDirs: String
): DocumentFile? {
val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs)
return parent?.findFile(fileName) ?: parent?.createFile(mimeType, fileName)
}
fun createFolderIfNotExist(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
}
}

@ -0,0 +1,255 @@
@file:Suppress("unused")
package io.legado.app.utils
import android.app.DownloadManager
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
import io.legado.app.exception.NoStackTraceException
import splitties.init.appCtx
import splitties.systemservices.downloadManager
import java.io.File
import java.nio.charset.Charset
data class FileDoc(
val name: String,
val isDir: Boolean,
val size: Long,
val lastModified: Long,
val uri: Uri
) {
override fun toString(): String {
return if (uri.isContentScheme()) uri.toString() else uri.path!!
}
val isContentScheme get() = uri.isContentScheme()
fun readBytes(): ByteArray {
return uri.readBytes(appCtx)
}
companion object {
fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
if (uri.isContentScheme()) {
val doc = if (isDir) {
DocumentFile.fromTreeUri(appCtx, uri)!!
} else if (uri.host == "downloads") {
val query = DownloadManager.Query()
query.setFilterById(uri.lastPathSegment!!.toLong())
downloadManager.query(query).use {
if (it.moveToFirst()) {
val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
val lUri = it.getString(lUriColum)
DocumentFile.fromSingleUri(appCtx, Uri.parse(lUri))!!
} else {
DocumentFile.fromSingleUri(appCtx, uri)!!
}
}
} else {
DocumentFile.fromSingleUri(appCtx, uri)!!
}
return FileDoc(doc.name ?: "", isDir, doc.length(), doc.lastModified(), doc.uri)
}
val file = File(uri.path!!)
return FileDoc(file.name, isDir, file.length(), file.lastModified(), uri)
}
fun fromDocumentFile(doc: DocumentFile): FileDoc {
return FileDoc(
name = doc.name ?: "",
isDir = doc.isDirectory,
size = doc.length(),
lastModified = doc.lastModified(),
uri = doc.uri
)
}
fun fromFile(file: File): FileDoc {
return FileDoc(
name = file.name,
isDir = file.isDirectory,
size = file.length(),
lastModified = file.lastModified(),
uri = Uri.fromFile(file)
)
}
}
}
/**
* 过滤器
*/
typealias FileDocFilter = (file: FileDoc) -> Boolean
private val projection by lazy {
arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_SIZE,
DocumentsContract.Document.COLUMN_MIME_TYPE
)
}
/**
* 返回子文件列表,如果不是文件夹则返回null
*/
fun FileDoc.list(filter: FileDocFilter? = null): ArrayList<FileDoc>? {
if (isDir) {
if (uri.isContentScheme()) {
/**
* DocumentFile listFiles() 非常的慢,所以这里直接从数据库查询
*/
val childrenUri = DocumentsContract
.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
val docList = arrayListOf<FileDoc>()
var cursor: Cursor? = null
try {
cursor = appCtx.contentResolver.query(
childrenUri,
projection,
null,
null,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
)
cursor?.let {
val ici = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
val nci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
val sci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE)
val mci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE)
val dci = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
if (cursor.moveToFirst()) {
do {
val item = FileDoc(
name = cursor.getString(nci),
isDir = cursor.getString(mci) == DocumentsContract.Document.MIME_TYPE_DIR,
size = cursor.getLong(sci),
lastModified = cursor.getLong(dci),
uri = DocumentsContract.buildDocumentUriUsingTree(
uri,
cursor.getString(ici)
)
)
if (filter == null || filter.invoke(item)) {
docList.add(item)
}
} while (cursor.moveToNext())
}
}
} finally {
cursor?.close()
}
return docList
} else {
return File(uri.path!!).listFileDocs(filter)
}
}
return null
}
/**
* 查找文档, 如果存在则返回文档,如果不存在返回空
* @param name 文件名
* @param depth 查找文件夹深度
*/
fun FileDoc.find(name: String, depth: Int = 0): FileDoc? {
val list = list()
list?.forEach {
if (it.name == name) {
return it
}
}
if (depth > 0) {
list?.forEach {
if (it.isDir) {
val fileDoc = it.find(name, depth - 1)
if (fileDoc != null) {
return fileDoc
}
}
}
}
return null
}
fun FileDoc.createFileIfNotExist(
fileName: String,
mimeType: String = "",
vararg subDirs: String
): FileDoc {
return if (uri.isContentScheme()) {
val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!!
val tmp = DocumentUtils.createFileIfNotExist(documentFile, fileName, mimeType, *subDirs)!!
FileDoc.fromDocumentFile(tmp)
} else {
val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
val tmp = FileUtils.createFileIfNotExist(path)
FileDoc.fromFile(tmp)
}
}
fun FileDoc.createFolderIfNotExist(
vararg subDirs: String
): FileDoc {
return if (uri.isContentScheme()) {
val documentFile = DocumentFile.fromTreeUri(appCtx, uri)!!
val tmp = DocumentUtils.createFolderIfNotExist(documentFile, *subDirs)!!
FileDoc.fromDocumentFile(tmp)
} else {
val path = FileUtils.getPath(uri.path!!, *subDirs)
val tmp = FileUtils.createFolderIfNotExist(path)
FileDoc.fromFile(tmp)
}
}
fun FileDoc.exists(
fileName: String,
vararg subDirs: String
): Boolean {
return if (uri.isContentScheme()) {
DocumentUtils.exists(DocumentFile.fromTreeUri(appCtx, uri)!!, fileName, *subDirs)
} else {
val path = FileUtils.getPath(uri.path!!, *subDirs) + File.separator + fileName
FileUtils.exist(path)
}
}
/**
* DocumentFile listFiles() 非常的慢,尽量不要使用
*/
fun DocumentFile.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc>? {
return FileDoc.fromDocumentFile(this).list(filter)
}
@Throws(Exception::class)
fun DocumentFile.writeText(context: Context, data: String, charset: Charset = Charsets.UTF_8) {
uri.writeText(context, data, charset)
}
@Throws(Exception::class)
fun DocumentFile.writeBytes(context: Context, data: ByteArray) {
uri.writeBytes(context, data)
}
@Throws(Exception::class)
fun DocumentFile.readText(context: Context): String {
return String(readBytes(context))
}
@Throws(Exception::class)
fun DocumentFile.readBytes(context: Context): ByteArray {
return context.contentResolver.openInputStream(uri)?.let {
val len: Int = it.available()
val buffer = ByteArray(len)
it.read(buffer)
it.close()
return buffer
} ?: throw NoStackTraceException("打开文件失败\n${uri}")
}

@ -1,5 +1,6 @@
package io.legado.app.utils
import android.net.Uri
import java.io.File
fun File.getFile(vararg subDirFiles: String): File {
@ -11,3 +12,23 @@ fun File.exists(vararg subDirFiles: String): Boolean {
return getFile(*subDirFiles).exists()
}
@Throws(Exception::class)
fun File.listFileDocs(filter: FileDocFilter? = null): ArrayList<FileDoc> {
val docList = arrayListOf<FileDoc>()
listFiles()
listFiles()?.forEach {
val item = FileDoc(
it.name,
it.isDirectory,
it.length(),
it.lastModified(),
Uri.fromFile(it)
)
if (filter == null || filter.invoke(item)) {
docList.add(item)
}
}
return docList
}

@ -4,7 +4,6 @@ import android.os.Environment
import android.webkit.MimeTypeMap
import androidx.annotation.IntDef
import splitties.init.appCtx
import java.io.*
import java.nio.charset.Charset
import java.text.SimpleDateFormat
@ -115,7 +114,7 @@ object FileUtils {
const val BY_EXTENSION_DESC = 7
@IntDef(value = [BY_NAME_ASC, BY_NAME_DESC, BY_TIME_ASC, BY_TIME_DESC, BY_SIZE_ASC, BY_SIZE_DESC, BY_EXTENSION_ASC, BY_EXTENSION_DESC])
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@Retention(AnnotationRetention.SOURCE)
annotation class SortType
/**

@ -1,35 +1,64 @@
package io.legado.app.utils
import android.util.Log
import androidx.core.os.postDelayed
import com.script.SimpleBindings
import io.legado.app.constant.AppConst
import io.legado.app.exception.RegexTimeoutException
import io.legado.app.help.CrashHandler
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.suspendCancellableCoroutine
import splitties.init.appCtx
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
private val handler by lazy { buildMainHandler() }
/**
* 带有超时检测的正则替换
*/
fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
val startTime = System.currentTimeMillis()
val charSequence = this
suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
val charSequence = this@replace
val isJs = replacement.startsWith("@js:")
val replacement1 = if (isJs) replacement.substring(4) else replacement
val pattern = regex.toPattern()
val matcher = pattern.matcher(charSequence)
val stringBuffer = StringBuffer()
while (matcher.find()) {
if (System.currentTimeMillis() - startTime > timeout) {
val timeoutMsg = "替换超时,将禁用替换规则"
throw RegexTimeoutException(timeoutMsg)
return suspendCancellableCoroutine { block ->
val coroutine = Coroutine.async {
try {
val pattern = regex.toPattern()
val matcher = pattern.matcher(charSequence)
val stringBuffer = StringBuffer()
while (matcher.find()) {
if (isJs) {
val bindings = SimpleBindings()
bindings["result"] = matcher.group()
val jsResult =
AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
matcher.appendReplacement(stringBuffer, jsResult)
} else {
matcher.appendReplacement(stringBuffer, replacement1)
}
}
matcher.appendTail(stringBuffer)
Log.e("regex", "end")
block.resume(stringBuffer.toString())
} catch (e: Exception) {
block.resumeWithException(e)
}
}
if (isJs) {
val bindings = SimpleBindings()
bindings["result"] = matcher.group()
val jsResult = AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
matcher.appendReplacement(stringBuffer, jsResult)
} else {
matcher.appendReplacement(stringBuffer, replacement1)
handler.postDelayed(timeout) {
if (coroutine.isActive) {
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
val exception = RegexTimeoutException(timeoutMsg)
block.cancel(exception)
appCtx.longToastOnUi(timeoutMsg)
CrashHandler.saveCrashInfo2File(exception)
handler.postDelayed(3000) {
if (coroutine.isActive) {
appCtx.restart()
}
}
}
}
}
matcher.appendTail(stringBuffer)
return stringBuffer.toString()
}

@ -19,9 +19,7 @@ fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
fun String.parseToUri(): Uri {
return if (isContentScheme()) {
Uri.parse(this)
} else {
return if (isUri()) Uri.parse(this) else {
Uri.fromFile(File(this))
}
}

@ -13,9 +13,12 @@ import io.legado.app.lib.permission.PermissionsCompat
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.charset.Charset
fun Uri.isContentScheme() = this.scheme == "content"
fun Uri.isFileScheme() = this.scheme == "file"
/**
* 读取URI
*/
@ -100,7 +103,13 @@ fun Fragment.readUri(uri: Uri?, success: (fileDoc: FileDoc, inputStream: InputSt
@Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray {
return if (this.isContentScheme()) {
DocumentUtils.readBytes(context, this)
context.contentResolver.openInputStream(this)?.let {
val len: Int = it.available()
val buffer = ByteArray(len)
it.read(buffer)
it.close()
return buffer
} ?: throw NoStackTraceException("打开文件失败\n${this}")
} else {
val path = RealPathUtil.getPath(context, this)
if (path?.isNotEmpty() == true) {
@ -124,7 +133,12 @@ fun Uri.writeBytes(
byteArray: ByteArray
): Boolean {
if (this.isContentScheme()) {
return DocumentUtils.writeBytes(context, byteArray, this)
context.contentResolver.openOutputStream(this)?.let {
it.write(byteArray)
it.close()
return true
}
return false
} else {
val path = RealPathUtil.getPath(context, this)
if (path?.isNotEmpty() == true) {
@ -136,8 +150,8 @@ fun Uri.writeBytes(
}
@Throws(Exception::class)
fun Uri.writeText(context: Context, text: String): Boolean {
return writeBytes(context, text.toByteArray())
fun Uri.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8): Boolean {
return writeBytes(context, text.toByteArray(charset))
}
fun Uri.writeBytes(

@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
org.gradle.jvmargs=-Xmx3000m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects

Loading…
Cancel
Save