mirror of
https://github.com/gedoor/legado.git
synced 2025-08-10 00:52:30 +00:00
Merge branch 'gedoor:master' into master
This commit is contained in:
@@ -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)
|
||||
}
|
||||
47
app/src/main/java/io/legado/app/utils/DocumentUtils.kt
Normal file
47
app/src/main/java/io/legado/app/utils/DocumentUtils.kt
Normal file
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
255
app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
Normal file
255
app/src/main/java/io/legado/app/utils/FileDocExtensions.kt
Normal file
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user