Merge branch 'gedoor:master' into master
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
package="io.legado.app">
|
package="io.legado.app">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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_MEDIA_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_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",
|
"downloadUrlRule": "$.data@js:if (result == '') \n '' \n else \n 'https://shuyuan.mgz6.cc/shuyuan/' + result",
|
||||||
"summary": "有效期2天"
|
"summary": "有效期2天"
|
||||||
}
|
}
|
||||||
@@ -11,13 +11,21 @@
|
|||||||
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
|
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
|
||||||
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
|
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
|
||||||
|
|
||||||
**2022/10/13**
|
**2022/10/16**
|
||||||
|
|
||||||
|
* 添加更新失败分组
|
||||||
|
* web服务和朗读服务添加唤醒锁
|
||||||
|
* 修复搜索禁用书源的bug
|
||||||
|
* 修复本地书籍很慢的问题
|
||||||
|
|
||||||
|
**2022/10/14**
|
||||||
|
|
||||||
* 更新cronet: 106.0.5249.126
|
* 更新cronet: 106.0.5249.126
|
||||||
* 替换规则支持js,可以用js判断匹配到的内容决定替换为什么,匹配到的内容变量为result,替换为@js:开头则自动采用js判断替换内容
|
* 替换规则支持js,可以用js判断匹配到的内容决定替换为什么,匹配到的内容变量为result,替换为@js:开头则自动采用js判断替换内容
|
||||||
* 书架管理添加筛选功能
|
* 书架管理添加筛选功能
|
||||||
* 搜索支持搜索范围多分组和单书源设置
|
* 搜索支持搜索范围多分组和单书源设置
|
||||||
* 书源管理界面书源菜单添加单书源搜索数据功能
|
* 书源管理界面书源菜单添加单书源搜索书籍功能
|
||||||
|
* 复web阅读时app未退出阅读界面导致的进度bug by Xwite
|
||||||
|
|
||||||
**2022/10/09**
|
**2022/10/09**
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ object AppConst {
|
|||||||
const val bookGroupAudioId = -3L
|
const val bookGroupAudioId = -3L
|
||||||
const val bookGroupNetNoneId = -4L
|
const val bookGroupNetNoneId = -4L
|
||||||
const val bookGroupLocalNoneId = -5L
|
const val bookGroupLocalNoneId = -5L
|
||||||
|
const val bookGroupErrorId = -11L
|
||||||
|
|
||||||
const val notificationIdRead = -1122391
|
const val notificationIdRead = -1122391
|
||||||
const val notificationIdAudio = -1122392
|
const val notificationIdAudio = -1122392
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ object BookType {
|
|||||||
*/
|
*/
|
||||||
const val text = 0b1000
|
const val text = 0b1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 16 更新失败
|
||||||
|
*/
|
||||||
|
const val updateError = 0b10000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 32 音频
|
* 32 音频
|
||||||
*/
|
*/
|
||||||
@@ -35,9 +40,10 @@ object BookType {
|
|||||||
|
|
||||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
@IntDef(text, audio, image, webFile)
|
@IntDef(text, updateError, audio, image, webFile, local)
|
||||||
annotation class Type
|
annotation class Type
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 本地书籍书源标志
|
* 本地书籍书源标志
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -105,9 +105,14 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
)
|
)
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"""insert into book_groups(groupId, groupName, 'order', show)
|
"""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})"""
|
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 book_sources set loginUi = null where loginUi = 'null'")
|
||||||
db.execSQL("update rssSources 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'")
|
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||'%'")
|
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
|
||||||
fun flowSearch(key: String): Flow<List<Book>>
|
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")
|
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
|
||||||
fun getBooksByGroup(group: Long): List<Book>
|
fun getBooksByGroup(group: Long): List<Book>
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ interface BookGroupDao {
|
|||||||
or (groupId = -1 and show > 0)
|
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 = -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 = -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
|
or (groupId = -4 and show > 0
|
||||||
and (
|
and (
|
||||||
select count(*) from books
|
select count(*) from books
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ interface BookSourceDao {
|
|||||||
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
|
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
|
||||||
val allGroupsUnProcessed: List<String>
|
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")
|
@Query("select * from book_sources where bookSourceUrl = :key")
|
||||||
fun getBookSource(key: String): BookSource?
|
fun getBookSource(key: String): BookSource?
|
||||||
|
|
||||||
@@ -175,6 +178,11 @@ interface BookSourceDao {
|
|||||||
return dealGroups(allGroupsUnProcessed)
|
return dealGroups(allGroupsUnProcessed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val allEnabledGroups: List<String>
|
||||||
|
get() {
|
||||||
|
return dealGroups(allEnabledGroupsUnProcessed)
|
||||||
|
}
|
||||||
|
|
||||||
fun flowGroups(): Flow<List<String>> {
|
fun flowGroups(): Flow<List<String>> {
|
||||||
return flowGroupsUnProcessed().map { list ->
|
return flowGroupsUnProcessed().map { list ->
|
||||||
dealGroups(list)
|
dealGroups(list)
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ interface JsExtensions {
|
|||||||
fun importScript(path: String): String {
|
fun importScript(path: String): String {
|
||||||
val result = when {
|
val result = when {
|
||||||
path.startsWith("http") -> cacheFile(path) ?: ""
|
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)
|
path.startsWith("/storage") -> FileUtils.readText(path)
|
||||||
else -> readTxtFile(path)
|
else -> readTxtFile(path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
package io.legado.app.help.book
|
package io.legado.app.help.book
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import io.legado.app.constant.BookSourceType
|
import io.legado.app.constant.BookSourceType
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookSource
|
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.exception.NoStackTraceException
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.help.config.AppConfig.defaultBookTreeUri
|
||||||
import io.legado.app.utils.getFile
|
import io.legado.app.utils.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
|
|
||||||
val Book.isAudio: Boolean
|
val Book.isAudio: Boolean
|
||||||
@@ -62,17 +61,8 @@ fun Book.getLocalUri(): Uri {
|
|||||||
//不同的设备书籍保存路径可能不一样 优先尝试寻找当前保存路径下的文件
|
//不同的设备书籍保存路径可能不一样 优先尝试寻找当前保存路径下的文件
|
||||||
defaultBookTreeUri ?: return originBookUri
|
defaultBookTreeUri ?: return originBookUri
|
||||||
val treeUri = Uri.parse(defaultBookTreeUri)
|
val treeUri = Uri.parse(defaultBookTreeUri)
|
||||||
return if (treeUri.isContentScheme()) {
|
val treeFileDoc = FileDoc.fromUri(treeUri, true)
|
||||||
DocumentFile.fromTreeUri(appCtx, treeUri)?.run {
|
return treeFileDoc.find(originName, 5)?.uri ?: originBookUri
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw NoStackTraceException("不是本地书籍")
|
throw NoStackTraceException("不是本地书籍")
|
||||||
}
|
}
|
||||||
@@ -84,6 +74,27 @@ fun Book.getRemoteUrl(): String? {
|
|||||||
return null
|
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() {
|
fun Book.upType() {
|
||||||
if (type < 8) {
|
if (type < 8) {
|
||||||
type = when (type) {
|
type = when (type) {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class Coroutine<T>(
|
|||||||
context: CoroutineContext,
|
context: CoroutineContext,
|
||||||
block: suspend CoroutineScope.() -> T
|
block: suspend CoroutineScope.() -> T
|
||||||
): Job {
|
): Job {
|
||||||
return (scope + Dispatchers.Main).launch(start = startOption) {
|
return (scope.plus(Dispatchers.Main)).launch(start = startOption) {
|
||||||
try {
|
try {
|
||||||
start?.let { dispatchVoidCallback(this, it) }
|
start?.let { dispatchVoidCallback(this, it) }
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|||||||
@@ -76,3 +76,13 @@ suspend fun BookSource.clearExploreKindsCache() {
|
|||||||
exploreKindsMap.remove(getExploreKindsKey())
|
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) {
|
when (doc.name) {
|
||||||
"myBookShelf.json" ->
|
"myBookShelf.json" ->
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
DocumentUtils.readText(context, doc.uri).let { json ->
|
doc.uri.readText(context).let { json ->
|
||||||
val importCount = importOldBookshelf(json)
|
val importCount = importOldBookshelf(json)
|
||||||
context.toastOnUi("成功导入书籍${importCount}")
|
context.toastOnUi("成功导入书籍${importCount}")
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ object ImportOldData {
|
|||||||
}
|
}
|
||||||
"myBookSource.json" ->
|
"myBookSource.json" ->
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
DocumentUtils.readText(context, doc.uri).let { json ->
|
doc.uri.readText(context).let { json ->
|
||||||
val importCount = importOldSource(json)
|
val importCount = importOldSource(json)
|
||||||
context.toastOnUi("成功导入书源${importCount}")
|
context.toastOnUi("成功导入书源${importCount}")
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ object ImportOldData {
|
|||||||
}
|
}
|
||||||
"myBookReplaceRule.json" ->
|
"myBookReplaceRule.json" ->
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
DocumentUtils.readText(context, doc.uri).let { json ->
|
doc.uri.readText(context).let { json ->
|
||||||
val importCount = importOldReplaceRule(json)
|
val importCount = importOldReplaceRule(json)
|
||||||
context.toastOnUi("成功导入替换规则${importCount}")
|
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 exception = document.getElementsByTag("s:exception").firstOrNull()?.text()
|
||||||
val message = document.getElementsByTag("s:message").firstOrNull()?.text()
|
val message = document.getElementsByTag("s:message").firstOrNull()?.text()
|
||||||
if (exception == "ObjectNotFound") {
|
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.media.AudioManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@@ -36,6 +37,7 @@ import io.legado.app.utils.*
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import splitties.systemservices.audioManager
|
import splitties.systemservices.audioManager
|
||||||
|
import splitties.systemservices.powerManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 音频播放服务
|
* 音频播放服务
|
||||||
@@ -61,6 +63,9 @@ class AudioPlayService : BaseService(),
|
|||||||
private set
|
private set
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val wakeLock by lazy {
|
||||||
|
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "legado:webService")
|
||||||
|
}
|
||||||
private val mFocusRequest: AudioFocusRequestCompat by lazy {
|
private val mFocusRequest: AudioFocusRequestCompat by lazy {
|
||||||
MediaHelp.buildAudioFocusRequestCompat(this)
|
MediaHelp.buildAudioFocusRequestCompat(this)
|
||||||
}
|
}
|
||||||
@@ -82,8 +87,10 @@ class AudioPlayService : BaseService(),
|
|||||||
private var upPlayProgressJob: Job? = null
|
private var upPlayProgressJob: Job? = null
|
||||||
private var playSpeed: Float = 1f
|
private var playSpeed: Float = 1f
|
||||||
|
|
||||||
|
@SuppressLint("WakelockTimeout")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
wakeLock.acquire()
|
||||||
isRun = true
|
isRun = true
|
||||||
upNotification()
|
upNotification()
|
||||||
exoPlayer.addListener(this)
|
exoPlayer.addListener(this)
|
||||||
@@ -120,6 +127,7 @@ class AudioPlayService : BaseService(),
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
wakeLock.release()
|
||||||
isRun = false
|
isRun = false
|
||||||
abandonFocus()
|
abandonFocus()
|
||||||
exoPlayer.release()
|
exoPlayer.release()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.os.PowerManager
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
@@ -30,6 +31,7 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.systemservices.audioManager
|
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 {
|
private val mFocusRequest: AudioFocusRequestCompat by lazy {
|
||||||
MediaHelp.buildAudioFocusRequestCompat(this)
|
MediaHelp.buildAudioFocusRequestCompat(this)
|
||||||
}
|
}
|
||||||
@@ -77,8 +82,10 @@ abstract class BaseReadAloudService : BaseService(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WakelockTimeout")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
wakeLock.acquire()
|
||||||
isRun = true
|
isRun = true
|
||||||
pause = false
|
pause = false
|
||||||
initMediaSession()
|
initMediaSession()
|
||||||
@@ -90,6 +97,7 @@ abstract class BaseReadAloudService : BaseService(),
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
wakeLock.release()
|
||||||
isRun = false
|
isRun = false
|
||||||
pause = true
|
pause = true
|
||||||
abandonFocus()
|
abandonFocus()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package io.legado.app.service
|
package io.legado.app.service
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.PowerManager
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseService
|
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.utils.*
|
||||||
import io.legado.app.web.HttpServer
|
import io.legado.app.web.HttpServer
|
||||||
import io.legado.app.web.WebSocketServer
|
import io.legado.app.web.WebSocketServer
|
||||||
|
import splitties.systemservices.powerManager
|
||||||
|
|
||||||
import java.io.IOException
|
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 httpServer: HttpServer? = null
|
||||||
private var webSocketServer: WebSocketServer? = null
|
private var webSocketServer: WebSocketServer? = null
|
||||||
private var notificationContent = ""
|
private var notificationContent = ""
|
||||||
@@ -40,8 +46,10 @@ class WebService : BaseService() {
|
|||||||
NetworkChangedListener(this)
|
NetworkChangedListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WakelockTimeout")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
wakeLock.acquire()
|
||||||
isRun = true
|
isRun = true
|
||||||
notificationContent = getString(R.string.service_starting)
|
notificationContent = getString(R.string.service_starting)
|
||||||
upNotification()
|
upNotification()
|
||||||
@@ -73,6 +81,7 @@ class WebService : BaseService() {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
wakeLock.release()
|
||||||
networkChangedListener.unRegister()
|
networkChangedListener.unRegister()
|
||||||
isRun = false
|
isRun = false
|
||||||
if (httpServer?.isAlive == true) {
|
if (httpServer?.isAlive == true) {
|
||||||
|
|||||||
@@ -2,16 +2,11 @@ package io.legado.app.ui.association
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.AppPattern.bookFileRegex
|
import io.legado.app.constant.AppPattern.bookFileRegex
|
||||||
import io.legado.app.exception.NoStackTraceException
|
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.utils.inputStream
|
import io.legado.app.utils.*
|
||||||
import io.legado.app.utils.isJson
|
|
||||||
import io.legado.app.utils.printOnDebug
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
|
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
|
||||||
val importBookLiveData = MutableLiveData<Uri>()
|
val importBookLiveData = MutableLiveData<Uri>()
|
||||||
@@ -24,15 +19,9 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
|||||||
execute {
|
execute {
|
||||||
lateinit var fileName: String
|
lateinit var fileName: String
|
||||||
//如果是普通的url,需要根据返回的内容判断是什么
|
//如果是普通的url,需要根据返回的内容判断是什么
|
||||||
if (uri.scheme == "file" || uri.scheme == "content") {
|
if (uri.isContentScheme() || uri.isFileScheme()) {
|
||||||
fileName = if (uri.scheme == "file") {
|
val fileDoc = FileDoc.fromUri(uri, false)
|
||||||
val file = File(uri.path.toString())
|
fileName = fileDoc.name
|
||||||
file.name
|
|
||||||
} else {
|
|
||||||
val file = DocumentFile.fromSingleUri(context, uri)
|
|
||||||
if (file?.exists() != true) throw NoStackTraceException("文件不存在")
|
|
||||||
file.name ?: ""
|
|
||||||
}
|
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
if (uri.inputStream(context).isJson()) {
|
if (uri.inputStream(context).isJson()) {
|
||||||
importJson(uri)
|
importJson(uri)
|
||||||
|
|||||||
@@ -46,22 +46,19 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
|
|||||||
|
|
||||||
private val selectFolder = registerForActivityResult(HandleFileContract()) {
|
private val selectFolder = registerForActivityResult(HandleFileContract()) {
|
||||||
it.uri?.let { uri ->
|
it.uri?.let { uri ->
|
||||||
if (uri.isContentScheme()) {
|
|
||||||
AppConfig.importBookPath = uri.toString()
|
AppConfig.importBookPath = uri.toString()
|
||||||
initRootDoc(true)
|
initRootDoc(true)
|
||||||
} else {
|
|
||||||
AppConfig.importBookPath = uri.path
|
|
||||||
initRootDoc(true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
binding.titleBar.setTitle(R.string.book_local)
|
binding.titleBar.setTitle(R.string.book_local)
|
||||||
launch {
|
launch {
|
||||||
setBookStorage()
|
|
||||||
initView()
|
initView()
|
||||||
initEvent()
|
initEvent()
|
||||||
|
if (setBookStorage() && AppConfig.importBookPath.isNullOrBlank()) {
|
||||||
|
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
|
||||||
|
}
|
||||||
initData()
|
initData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,15 +147,21 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initRootDoc(changedFolder: Boolean = false) {
|
private fun initRootDoc(changedFolder: Boolean = false) {
|
||||||
|
if (viewModel.rootDoc != null && !changedFolder) {
|
||||||
|
upPath()
|
||||||
|
} else {
|
||||||
val lastPath = AppConfig.importBookPath
|
val lastPath = AppConfig.importBookPath
|
||||||
when {
|
if (lastPath.isNullOrBlank()) {
|
||||||
viewModel.rootDoc != null && !changedFolder -> upPath()
|
|
||||||
lastPath.isNullOrEmpty() -> {
|
|
||||||
binding.tvEmptyMsg.visible()
|
binding.tvEmptyMsg.visible()
|
||||||
selectFolder.launch()
|
selectFolder.launch()
|
||||||
|
} else {
|
||||||
|
val rootUri = if (lastPath.isUri()) {
|
||||||
|
Uri.parse(lastPath)
|
||||||
|
} else {
|
||||||
|
Uri.fromFile(File(lastPath))
|
||||||
}
|
}
|
||||||
lastPath.isContentScheme() -> {
|
when {
|
||||||
val rootUri = Uri.parse(lastPath)
|
rootUri.isContentScheme() -> {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val doc = DocumentFile.fromTreeUri(this, rootUri)
|
val doc = DocumentFile.fromTreeUri(this, rootUri)
|
||||||
if (doc == null || doc.name.isNullOrEmpty()) {
|
if (doc == null || doc.name.isNullOrEmpty()) {
|
||||||
@@ -178,7 +181,9 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
|
|||||||
binding.tvEmptyMsg.visible()
|
binding.tvEmptyMsg.visible()
|
||||||
selectFolder.launch()
|
selectFolder.launch()
|
||||||
}
|
}
|
||||||
else -> initRootPath(lastPath)
|
else -> initRootPath(rootUri.path!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +233,7 @@ class ImportBookActivity : BaseImportBookActivity<ActivityImportBookBinding, Imp
|
|||||||
binding.tvPath.text = path
|
binding.tvPath.text = path
|
||||||
adapter.selectedUris.clear()
|
adapter.selectedUris.clear()
|
||||||
adapter.clearItems()
|
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 {
|
execute {
|
||||||
val docList = DocumentUtils.listFiles(uri) { item ->
|
val docList = fileDoc.list { item ->
|
||||||
when {
|
when {
|
||||||
item.name.startsWith(".") -> false
|
item.name.startsWith(".") -> false
|
||||||
item.isDir -> true
|
item.isDir -> true
|
||||||
else -> item.name.matches(bookFileRegex)
|
else -> item.name.matches(bookFileRegex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataCallback?.setItems(docList)
|
dataCallback?.setItems(docList!!)
|
||||||
}.onError {
|
}.onError {
|
||||||
context.toastOnUi("获取文件列表出错\n${it.localizedMessage}")
|
context.toastOnUi("获取文件列表出错\n${it.localizedMessage}")
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
}
|
}
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val list = ArrayList<FileDoc>()
|
val list = ArrayList<FileDoc>()
|
||||||
DocumentUtils.listFiles(fileDoc.uri).forEach { docItem ->
|
fileDoc.list()!!.forEach { docItem ->
|
||||||
if (!scope.isActive) {
|
if (!scope.isActive) {
|
||||||
finally?.invoke()
|
finally?.invoke()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import io.legado.app.exception.NoStackTraceException
|
|||||||
import io.legado.app.help.book.BookHelp
|
import io.legado.app.help.book.BookHelp
|
||||||
import io.legado.app.help.book.getRemoteUrl
|
import io.legado.app.help.book.getRemoteUrl
|
||||||
import io.legado.app.help.book.isLocal
|
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.help.coroutine.Coroutine
|
||||||
import io.legado.app.lib.webdav.ObjectNotFoundException
|
import io.legado.app.lib.webdav.ObjectNotFoundException
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
@@ -236,6 +237,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
bookSource = source
|
bookSource = source
|
||||||
bookData.value?.changeTo(book, toc)
|
bookData.value?.changeTo(book, toc)
|
||||||
if (inBookshelf) {
|
if (inBookshelf) {
|
||||||
|
book.removeType(BookType.updateError)
|
||||||
appDb.bookDao.insert(book)
|
appDb.bookDao.insert(book)
|
||||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ class BookshelfManageActivity :
|
|||||||
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
|
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
|
||||||
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
|
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
|
||||||
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
|
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
|
||||||
|
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
|
||||||
else -> appDb.bookDao.flowByGroup(viewModel.groupId)
|
else -> appDb.bookDao.flowByGroup(viewModel.groupId)
|
||||||
}.conflate().map { list ->
|
}.conflate().map { list ->
|
||||||
val books = if (searchKey.isNullOrBlank()) {
|
val books = if (searchKey.isNullOrBlank()) {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package io.legado.app.ui.book.manage
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.help.book.isLocal
|
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.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
@@ -58,6 +60,7 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
|
|||||||
context.toastOnUi("获取目录出错\n${it.localizedMessage}")
|
context.toastOnUi("获取目录出错\n${it.localizedMessage}")
|
||||||
}.getOrNull()?.let { toc ->
|
}.getOrNull()?.let { toc ->
|
||||||
book.changeTo(newBook, toc)
|
book.changeTo(newBook, toc)
|
||||||
|
book.removeType(BookType.updateError)
|
||||||
appDb.bookDao.insert(newBook)
|
appDb.bookDao.insert(newBook)
|
||||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
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.BookHelp
|
||||||
import io.legado.app.help.book.ContentProcessor
|
import io.legado.app.help.book.ContentProcessor
|
||||||
import io.legado.app.help.book.isLocal
|
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.AppConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.ReadAloud
|
import io.legado.app.model.ReadAloud
|
||||||
@@ -215,6 +217,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
changeSourceCoroutine = execute {
|
changeSourceCoroutine = execute {
|
||||||
ReadBook.upMsg(context.getString(R.string.loading))
|
ReadBook.upMsg(context.getString(R.string.loading))
|
||||||
ReadBook.book?.changeTo(book, toc)
|
ReadBook.book?.changeTo(book, toc)
|
||||||
|
book.removeType(BookType.updateError)
|
||||||
appDb.bookDao.insert(book)
|
appDb.bookDao.insert(book)
|
||||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||||
ReadBook.resetData(book)
|
ReadBook.resetData(book)
|
||||||
|
|||||||
@@ -221,6 +221,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
|||||||
|
|
||||||
private fun initData() {
|
private fun initData() {
|
||||||
searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
|
searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
|
||||||
|
viewModel.searchScope.stateLiveData.observe(this) {
|
||||||
|
searchScopeAdapter.setItems(viewModel.searchScope.displayNames)
|
||||||
|
}
|
||||||
viewModel.isSearchLiveData.observe(this) {
|
viewModel.isSearchLiveData.observe(this) {
|
||||||
if (it) {
|
if (it) {
|
||||||
startSearch()
|
startSearch()
|
||||||
@@ -400,6 +403,14 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
if (searchView.hasFocus()) {
|
||||||
|
searchView.clearFocus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.finish()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun start(context: Context, key: String?) {
|
fun start(context: Context, key: String?) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.book.search
|
package io.legado.app.ui.book.search
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.help.config.AppConfig
|
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(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 {
|
override fun toString(): String {
|
||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val stateLiveData = MutableLiveData("")
|
||||||
|
|
||||||
fun update(scope: String) {
|
fun update(scope: String) {
|
||||||
this.scope = scope
|
this.scope = scope
|
||||||
}
|
}
|
||||||
@@ -70,12 +77,20 @@ data class SearchScope(private var scope: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scope.splitNotBlank(",").forEach {
|
val oldScope = scope.splitNotBlank(",")
|
||||||
list.addAll(appDb.bookSourceDao.getByGroup(it))
|
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()) {
|
if (list.isEmpty()) {
|
||||||
scope = ""
|
scope = ""
|
||||||
|
stateLiveData.postValue("")
|
||||||
return appDb.bookSourceDao.allEnabled
|
return appDb.bookSourceDao.allEnabled
|
||||||
}
|
}
|
||||||
return list.sortedBy { it.customOrder }
|
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.DialogSearchScopeBinding
|
||||||
import io.legado.app.databinding.ItemCheckBoxBinding
|
import io.legado.app.databinding.ItemCheckBoxBinding
|
||||||
import io.legado.app.databinding.ItemRadioButtonBinding
|
import io.legado.app.databinding.ItemRadioButtonBinding
|
||||||
|
import io.legado.app.help.source.contains
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.utils.applyTint
|
import io.legado.app.utils.applyTint
|
||||||
import io.legado.app.utils.setLayout
|
import io.legado.app.utils.setLayout
|
||||||
@@ -72,7 +73,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initOtherView() {
|
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
|
binding.toolBar.menu.findItem(R.id.menu_screen)?.isVisible = checkedId == R.id.rb_source
|
||||||
upData()
|
upData()
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
private fun initData() {
|
private fun initData() {
|
||||||
launch {
|
launch {
|
||||||
groups = withContext(IO) {
|
groups = withContext(IO) {
|
||||||
appDb.bookSourceDao.allGroups
|
appDb.bookSourceDao.allEnabledGroups
|
||||||
}
|
}
|
||||||
sources = withContext(IO) {
|
sources = withContext(IO) {
|
||||||
appDb.bookSourceDao.allEnabled
|
appDb.bookSourceDao.allEnabled
|
||||||
@@ -116,9 +117,7 @@ class SearchScopeDialog : BaseDialogFragment(R.layout.dialog_search_scope) {
|
|||||||
withContext(IO) {
|
withContext(IO) {
|
||||||
if (binding.rbSource.isChecked) {
|
if (binding.rbSource.isChecked) {
|
||||||
sources.filter { source ->
|
sources.filter { source ->
|
||||||
screenText?.let { screenText ->
|
source.contains(screenText)
|
||||||
source.bookSourceName.contains(screenText)
|
|
||||||
} ?: true
|
|
||||||
}.let {
|
}.let {
|
||||||
screenSources.clear()
|
screenSources.clear()
|
||||||
screenSources.addAll(it)
|
screenSources.addAll(it)
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class SearchContentActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLocalBook: Boolean
|
private val isLocalBook: Boolean
|
||||||
get() = viewModel.book?.isLocal == true
|
get() = viewModel.book?.isLocal == true
|
||||||
|
|
||||||
override fun openSearchResult(searchResult: SearchResult, index: Int) {
|
override fun openSearchResult(searchResult: SearchResult, index: Int) {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import kotlinx.coroutines.Dispatchers.Main
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字体选择对话框
|
||||||
|
*/
|
||||||
class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
|
class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
|
||||||
Toolbar.OnMenuItemClickListener,
|
Toolbar.OnMenuItemClickListener,
|
||||||
FontAdapter.CallBack {
|
FontAdapter.CallBack {
|
||||||
@@ -35,11 +38,11 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
|
|||||||
}
|
}
|
||||||
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
|
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
|
||||||
it.uri?.let { uri ->
|
it.uri?.let { uri ->
|
||||||
if (uri.toString().isContentScheme()) {
|
if (uri.isContentScheme()) {
|
||||||
putPrefString(PreferKey.fontFolder, uri.toString())
|
putPrefString(PreferKey.fontFolder, uri.toString())
|
||||||
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
|
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||||
if (doc != null) {
|
if (doc != null) {
|
||||||
loadFontFiles(doc)
|
loadFontFiles(FileDoc.fromDocumentFile(doc))
|
||||||
} else {
|
} else {
|
||||||
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
|
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
|
||||||
loadFontFilesByPermission(path)
|
loadFontFilesByPermission(path)
|
||||||
@@ -75,7 +78,7 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
|
|||||||
if (fontPath.isContentScheme()) {
|
if (fontPath.isContentScheme()) {
|
||||||
val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath))
|
val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath))
|
||||||
if (doc?.canRead() == true) {
|
if (doc?.canRead() == true) {
|
||||||
loadFontFiles(doc)
|
loadFontFiles(FileDoc.fromDocumentFile(doc))
|
||||||
} else {
|
} else {
|
||||||
openFolder()
|
openFolder()
|
||||||
}
|
}
|
||||||
@@ -117,40 +120,29 @@ class FontSelectDialog : BaseDialogFragment(R.layout.dialog_font_select),
|
|||||||
|
|
||||||
private fun getLocalFonts(): ArrayList<FileDoc> {
|
private fun getLocalFonts(): ArrayList<FileDoc> {
|
||||||
val path = FileUtils.getPath(requireContext().externalFiles, "font")
|
val path = FileUtils.getPath(requireContext().externalFiles, "font")
|
||||||
return DocumentUtils.listFiles(path) {
|
return File(path).listFileDocs {
|
||||||
it.name.matches(fontRegex)
|
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) {
|
private fun loadFontFilesByPermission(path: String) {
|
||||||
PermissionsCompat.Builder(this@FontSelectDialog)
|
PermissionsCompat.Builder(this@FontSelectDialog)
|
||||||
.addPermissions(*Permissions.Group.STORAGE)
|
.addPermissions(*Permissions.Group.STORAGE)
|
||||||
.rationale(R.string.tip_perm_request_storage)
|
.rationale(R.string.tip_perm_request_storage)
|
||||||
.onGranted {
|
.onGranted {
|
||||||
loadFontFiles(path)
|
loadFontFiles(
|
||||||
|
FileDoc.fromFile(File(path))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.request()
|
.request()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFontFiles(path: String) {
|
private fun loadFontFiles(fileDoc: FileDoc) {
|
||||||
execute {
|
execute {
|
||||||
val fontItems = DocumentUtils.listFiles(path) {
|
val fontItems = fileDoc.list {
|
||||||
it.name.matches(fontRegex)
|
it.name.matches(fontRegex)
|
||||||
}
|
}
|
||||||
mergeFontItems(fontItems, getLocalFonts())
|
mergeFontItems(fontItems!!, getLocalFonts())
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
adapter.setItems(it)
|
adapter.setItems(it)
|
||||||
}.onError {
|
}.onError {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
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.AppWebDav
|
||||||
import io.legado.app.help.DefaultData
|
import io.legado.app.help.DefaultData
|
||||||
import io.legado.app.help.book.BookHelp
|
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.isLocal
|
||||||
|
import io.legado.app.help.book.removeType
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.model.CacheBook
|
import io.legado.app.model.CacheBook
|
||||||
@@ -126,6 +129,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
WebBook.getBookInfoAwait(source, book)
|
WebBook.getBookInfoAwait(source, book)
|
||||||
}
|
}
|
||||||
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
||||||
|
book.removeType(BookType.updateError)
|
||||||
if (book.bookUrl == bookUrl) {
|
if (book.bookUrl == bookUrl) {
|
||||||
appDb.bookDao.update(book)
|
appDb.bookDao.update(book)
|
||||||
} else {
|
} else {
|
||||||
@@ -137,6 +141,8 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||||
addDownload(source, book)
|
addDownload(source, book)
|
||||||
}.onError(upTocPool) {
|
}.onError(upTocPool) {
|
||||||
|
book.addType(BookType.updateError)
|
||||||
|
appDb.bookDao.update(book)
|
||||||
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
|
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
|
||||||
}.onCancel(upTocPool) {
|
}.onCancel(upTocPool) {
|
||||||
upTocCancel(bookUrl)
|
upTocCancel(bookUrl)
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
|
|||||||
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
|
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
|
||||||
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
|
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
|
||||||
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
|
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
|
||||||
|
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
|
||||||
else -> appDb.bookDao.flowByGroup(groupId)
|
else -> appDb.bookDao.flowByGroup(groupId)
|
||||||
}.conflate().map { list ->
|
}.conflate().map { list ->
|
||||||
when (getPrefInt(PreferKey.bookshelfSort)) {
|
when (getPrefInt(PreferKey.bookshelfSort)) {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
|
|||||||
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
|
AppConst.bookGroupAudioId -> appDb.bookDao.flowAudio()
|
||||||
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
|
AppConst.bookGroupNetNoneId -> appDb.bookDao.flowNetNoGroup()
|
||||||
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
|
AppConst.bookGroupLocalNoneId -> appDb.bookDao.flowLocalNoGroup()
|
||||||
|
AppConst.bookGroupErrorId -> appDb.bookDao.flowUpdateError()
|
||||||
else -> appDb.bookDao.flowByGroup(groupId)
|
else -> appDb.bookDao.flowByGroup(groupId)
|
||||||
}.conflate().map { list ->
|
}.conflate().map { list ->
|
||||||
when (getPrefInt(PreferKey.bookshelfSort)) {
|
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.data.entities.RssSource
|
||||||
import io.legado.app.databinding.FragmentRssBinding
|
import io.legado.app.databinding.FragmentRssBinding
|
||||||
import io.legado.app.databinding.ItemRssBinding
|
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.primaryColor
|
||||||
import io.legado.app.lib.theme.primaryTextColor
|
import io.legado.app.lib.theme.primaryTextColor
|
||||||
import io.legado.app.ui.rss.article.RssSortActivity
|
import io.legado.app.ui.rss.article.RssSortActivity
|
||||||
@@ -179,8 +180,14 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun del(rssSource: RssSource) {
|
override fun del(rssSource: RssSource) {
|
||||||
|
alert(R.string.draw) {
|
||||||
|
setMessage(getString(R.string.sure_del) + "\n" + rssSource.sourceName)
|
||||||
|
noButton()
|
||||||
|
yesButton {
|
||||||
viewModel.del(rssSource)
|
viewModel.del(rssSource)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun disable(rssSource: RssSource) {
|
override fun disable(rssSource: RssSource) {
|
||||||
viewModel.disable(rssSource)
|
viewModel.disable(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
|
package io.legado.app.utils
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
fun File.getFile(vararg subDirFiles: String): File {
|
fun File.getFile(vararg subDirFiles: String): File {
|
||||||
@@ -11,3 +12,23 @@ fun File.exists(vararg subDirFiles: String): Boolean {
|
|||||||
return getFile(*subDirFiles).exists()
|
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 android.webkit.MimeTypeMap
|
||||||
import androidx.annotation.IntDef
|
import androidx.annotation.IntDef
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -115,7 +114,7 @@ object FileUtils {
|
|||||||
const val BY_EXTENSION_DESC = 7
|
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])
|
@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
|
annotation class SortType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,35 +1,64 @@
|
|||||||
package io.legado.app.utils
|
package io.legado.app.utils
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.os.postDelayed
|
||||||
import com.script.SimpleBindings
|
import com.script.SimpleBindings
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.exception.RegexTimeoutException
|
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 {
|
suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
|
||||||
val startTime = System.currentTimeMillis()
|
val charSequence = this@replace
|
||||||
val charSequence = this
|
|
||||||
val isJs = replacement.startsWith("@js:")
|
val isJs = replacement.startsWith("@js:")
|
||||||
val replacement1 = if (isJs) replacement.substring(4) else replacement
|
val replacement1 = if (isJs) replacement.substring(4) else replacement
|
||||||
|
return suspendCancellableCoroutine { block ->
|
||||||
|
val coroutine = Coroutine.async {
|
||||||
|
try {
|
||||||
val pattern = regex.toPattern()
|
val pattern = regex.toPattern()
|
||||||
val matcher = pattern.matcher(charSequence)
|
val matcher = pattern.matcher(charSequence)
|
||||||
val stringBuffer = StringBuffer()
|
val stringBuffer = StringBuffer()
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
if (System.currentTimeMillis() - startTime > timeout) {
|
|
||||||
val timeoutMsg = "替换超时,将禁用替换规则"
|
|
||||||
throw RegexTimeoutException(timeoutMsg)
|
|
||||||
}
|
|
||||||
if (isJs) {
|
if (isJs) {
|
||||||
val bindings = SimpleBindings()
|
val bindings = SimpleBindings()
|
||||||
bindings["result"] = matcher.group()
|
bindings["result"] = matcher.group()
|
||||||
val jsResult = AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
|
val jsResult =
|
||||||
|
AppConst.SCRIPT_ENGINE.eval(replacement1, bindings).toString()
|
||||||
matcher.appendReplacement(stringBuffer, jsResult)
|
matcher.appendReplacement(stringBuffer, jsResult)
|
||||||
} else {
|
} else {
|
||||||
matcher.appendReplacement(stringBuffer, replacement1)
|
matcher.appendReplacement(stringBuffer, replacement1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
matcher.appendTail(stringBuffer)
|
matcher.appendTail(stringBuffer)
|
||||||
return stringBuffer.toString()
|
Log.e("regex", "end")
|
||||||
|
block.resume(stringBuffer.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
block.resumeWithException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
|
|||||||
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||||||
|
|
||||||
fun String.parseToUri(): Uri {
|
fun String.parseToUri(): Uri {
|
||||||
return if (isContentScheme()) {
|
return if (isUri()) Uri.parse(this) else {
|
||||||
Uri.parse(this)
|
|
||||||
} else {
|
|
||||||
Uri.fromFile(File(this))
|
Uri.fromFile(File(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ import io.legado.app.lib.permission.PermissionsCompat
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
|
||||||
fun Uri.isContentScheme() = this.scheme == "content"
|
fun Uri.isContentScheme() = this.scheme == "content"
|
||||||
|
|
||||||
|
fun Uri.isFileScheme() = this.scheme == "file"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取URI
|
* 读取URI
|
||||||
*/
|
*/
|
||||||
@@ -100,7 +103,13 @@ fun Fragment.readUri(uri: Uri?, success: (fileDoc: FileDoc, inputStream: InputSt
|
|||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun Uri.readBytes(context: Context): ByteArray {
|
fun Uri.readBytes(context: Context): ByteArray {
|
||||||
return if (this.isContentScheme()) {
|
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 {
|
} else {
|
||||||
val path = RealPathUtil.getPath(context, this)
|
val path = RealPathUtil.getPath(context, this)
|
||||||
if (path?.isNotEmpty() == true) {
|
if (path?.isNotEmpty() == true) {
|
||||||
@@ -124,7 +133,12 @@ fun Uri.writeBytes(
|
|||||||
byteArray: ByteArray
|
byteArray: ByteArray
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (this.isContentScheme()) {
|
if (this.isContentScheme()) {
|
||||||
return DocumentUtils.writeBytes(context, byteArray, this)
|
context.contentResolver.openOutputStream(this)?.let {
|
||||||
|
it.write(byteArray)
|
||||||
|
it.close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
} else {
|
} else {
|
||||||
val path = RealPathUtil.getPath(context, this)
|
val path = RealPathUtil.getPath(context, this)
|
||||||
if (path?.isNotEmpty() == true) {
|
if (path?.isNotEmpty() == true) {
|
||||||
@@ -136,8 +150,8 @@ fun Uri.writeBytes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun Uri.writeText(context: Context, text: String): Boolean {
|
fun Uri.writeText(context: Context, text: String, charset: Charset = Charsets.UTF_8): Boolean {
|
||||||
return writeBytes(context, text.toByteArray())
|
return writeBytes(context, text.toByteArray(charset))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Uri.writeBytes(
|
fun Uri.writeBytes(
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# 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.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# 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
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
|||||||
Reference in New Issue
Block a user