Merge remote-tracking branch 'origin/master'

This commit is contained in:
kunfei
2023-06-15 12:34:07 +08:00
13 changed files with 204 additions and 44 deletions
@@ -31,6 +31,16 @@ interface BookSourceDao {
)
fun flowSearch(searchKey: String): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources
where bookSourceName like '%' || :searchKey || '%'
or bookSourceGroup like '%' || :searchKey || '%'
or bookSourceUrl like '%' || :searchKey || '%'
or bookSourceComment like '%' || :searchKey || '%'
order by customOrder asc"""
)
fun search(searchKey: String): List<BookSource>
@Query(
"""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
@@ -56,27 +66,45 @@ interface BookSourceDao {
)
fun flowGroupSearch(searchKey: String): Flow<List<BookSourcePart>>
@Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
@Query(
"""select * from book_sources
where bookSourceGroup = :searchKey
or bookSourceGroup like :searchKey || ',%'
or bookSourceGroup like '%,' || :searchKey
or bookSourceGroup like '%,' || :searchKey || ',%'
order by customOrder asc"""
)
fun groupSearch(searchKey: String): List<BookSource>
@Query(
"""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
from book_sources where enabled = 1 order by customOrder asc""")
from book_sources where enabled = 1 order by customOrder asc"""
)
fun flowEnabled(): Flow<List<BookSourcePart>>
@Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
@Query(
"""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
from book_sources where enabled = 0 order by customOrder asc""")
from book_sources where enabled = 0 order by customOrder asc"""
)
fun flowDisabled(): Flow<List<BookSourcePart>>
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
fun flowExplore(): Flow<List<BookSource>>
@Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
@Query(
"""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
from book_sources where loginUrl is not null and loginUrl != ''""")
from book_sources where loginUrl is not null and loginUrl != ''"""
)
fun flowLogin(): Flow<List<BookSourcePart>>
@Query("""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
@Query(
"""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
trim(loginUrl) <> '' hasLoginUrl, lastUpdateTime, respondTime, weight, trim(exploreUrl) <> '' hasExploreUrl
from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'""")
from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'"""
)
fun flowNoGroup(): Flow<List<BookSourcePart>>
@Query(
@@ -147,6 +175,15 @@ interface BookSourceDao {
@get:Query("select * from book_sources where enabled = 1 order by customOrder")
val allEnabled: List<BookSource>
@get:Query("select * from book_sources where enabled = 0 order by customOrder")
val allDisabled: List<BookSource>
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'")
val allNoGroup: List<BookSource>
@get:Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
val allLogin: List<BookSource>
@get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder")
val allTextEnabled: List<BookSource>
@@ -108,7 +108,7 @@ class ContentProcessor private constructor(
if (matcher.find()) {
mContent = mContent.substring(matcher.end())
sameTitleRemoved = true
} else if (useReplace) {
} else if (useReplace && book.getUseReplaceRule()) {
title = Pattern.quote(
chapter.getDisplayTitle(
contentReplaceRules,
@@ -174,12 +174,13 @@ open class WebDav(
val baseUrl = NetworkUtils.getBaseUrl(urlStr)
for (element in elements) {
//依然是优化支持 caddy 自建的 WebDav ,其目录后缀都为“/”, 所以删除“/”的判定,不然无法获取该目录项
val href = URLDecoder.decode(element.findNS("href", ns)[0].text(), "UTF-8")
val href = element.findNS("href", ns)[0].text().replace("+", "%20")
val hrefDecode = URLDecoder.decode(href, "UTF-8")
.removeSuffix("/")
val fileName = href.substringAfterLast("/")
val fileName = hrefDecode.substringAfterLast("/")
val webDavFile: WebDav
try {
val urlName = href.ifEmpty {
val urlName = hrefDecode.ifEmpty {
url.file.replace("/", "")
}
val contentType = element
@@ -199,7 +200,7 @@ open class WebDav(
.toInstant(ZoneOffset.of("+8")).toEpochMilli()
}
}.getOrNull() ?: 0
val fullURL = NetworkUtils.getAbsoluteURL(baseUrl, href)
val fullURL = NetworkUtils.getAbsoluteURL(baseUrl, hrefDecode)
webDavFile = WebDavFile(
fullURL,
authorization,
@@ -13,10 +13,10 @@ class WebDavFile(
val contentType: String,
val resourceType: String,
val lastModify: Long
) : WebDav(urlStr, authorization) {
) : WebDav(urlStr, authorization) {
val isDir by lazy {
contentType == "httpd/unix-directory" || resourceType.lowercase().contains("collection")
}
val isDir by lazy {
contentType == "httpd/unix-directory" || resourceType.lowercase().contains("collection")
}
}
}
@@ -272,7 +272,7 @@ class AnalyzeRule(
}
if (result == null) result = ""
val str = if (unescape) {
StringEscapeUtils.unescapeHtml3(result.toString())
StringEscapeUtils.unescapeHtml4(result.toString())
} else result.toString()
if (isUrl) {
return if (str.isBlank()) {
@@ -134,7 +134,7 @@ object LocalBook {
}
if (book.isEpub) {
content = content?.replace("&lt;img", "&lt; img", true) ?: return null
return StringEscapeUtils.unescapeHtml3(content)
return StringEscapeUtils.unescapeHtml4(content)
}
return content
}
@@ -40,7 +40,9 @@ class RemoteBookWebDav(
val remoteWebDavFileList: List<WebDavFile> = WebDav(path, authorization).listFiles()
//转化远程文件信息到本地对象
remoteWebDavFileList.forEach { webDavFile ->
if (webDavFile.isDir || bookFileRegex.matches(webDavFile.displayName) || archiveFileRegex.matches(webDavFile.displayName)
if (webDavFile.isDir
|| bookFileRegex.matches(webDavFile.displayName)
|| archiveFileRegex.matches(webDavFile.displayName)
) {
//扩展名符合阅读的格式则认为是书籍
remoteBooks.add(RemoteBook(webDavFile))
@@ -70,7 +72,7 @@ class RemoteBookWebDav(
if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用")
val localBookUri = Uri.parse(book.bookUrl)
val putUrl = "$rootBookUrl${File.separator}${book.originName}"
val webDav = WebDav(putUrl, authorization)
val webDav = WebDav(putUrl, authorization)
if (localBookUri.isContentScheme()) {
webDav.upload(
byteArray = localBookUri.readBytes(appCtx),
@@ -80,8 +82,8 @@ class RemoteBookWebDav(
webDav.upload(localBookUri.path!!)
}
book.origin = BookType.webDavTag + CustomUrl(putUrl)
.putAttribute("serverID", serverID)
.toString()
.putAttribute("serverID", serverID)
.toString()
book.save()
}
@@ -18,6 +18,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import org.apache.commons.text.StringEscapeUtils
import splitties.init.appCtx
import kotlin.coroutines.coroutineContext
@@ -168,6 +169,7 @@ object BookContent {
//获取正文
var content = analyzeRule.getString(contentRule.content, unescape = false)
content = HtmlFormatter.formatKeepImg(content, rUrl)
content = StringEscapeUtils.unescapeHtml4(content)
//获取下一页链接
if (getNextPageUrl) {
val nextUrlRule = contentRule.nextContentUrl
@@ -17,7 +17,6 @@ import io.legado.app.constant.AppLog
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.toBookSource
import io.legado.app.databinding.ActivityBookSourceBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.DirectLinkUpload
@@ -71,6 +70,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private var sortAscending = true
private var snackBar: Snackbar? = null
private var isPaused = false
private var searchKey: String? = null
private val qrResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
showDialogFragment(ImportBookSourceDialog(it))
@@ -235,6 +235,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
private fun upBookSource(searchKey: String? = null) {
this.searchKey = searchKey
sourceFlowJob?.cancel()
sourceFlowJob = launch {
when {
@@ -376,7 +377,12 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
R.id.menu_bottom_sel -> viewModel.bottomSource(*adapter.selection.toTypedArray())
R.id.menu_add_group -> selectionAddToGroups()
R.id.menu_remove_group -> selectionRemoveFromGroups()
R.id.menu_export_selection -> viewModel.saveToFile(adapter.selection.toBookSource()) { file ->
R.id.menu_export_selection -> viewModel.saveToFile(
adapter,
searchKey,
sortAscending,
sort
) { file ->
exportDir.launch {
mode = HandleFileContract.EXPORT
fileData = HandleFileContract.FileData(
@@ -387,7 +393,12 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
}
}
R.id.menu_share_source -> viewModel.saveToFile(adapter.selection.toBookSource()) {
R.id.menu_share_source -> viewModel.saveToFile(
adapter,
searchKey,
sortAscending,
sort
) {
share(it)
}
@@ -2,14 +2,19 @@ package io.legado.app.ui.book.source.manage
import android.app.Application
import android.text.TextUtils
import io.legado.app.R
import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.toBookSource
import io.legado.app.help.config.SourceConfig
import io.legado.app.utils.*
import splitties.init.appCtx
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import io.legado.app.ui.book.source.manage.BookSourceActivity.Sort
/**
* 书源管理数据修改
@@ -56,10 +61,10 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
if (items.isEmpty()) return
execute {
val firstSortNumber = items[0].customOrder
items.forEachIndexed { index, bookSource ->
bookSource.customOrder = firstSortNumber + index
val array = items.mapIndexed { index, bookSource ->
bookSource.copy(customOrder = firstSortNumber + index)
}
appDb.bookSourceDao.upOrder(items)
appDb.bookSourceDao.upOrder(array)
}
}
@@ -122,13 +127,15 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}
@Suppress("BlockingMethodInNonBlockingContext")
fun saveToFile(sources: List<BookSource>, success: (file: File) -> Unit) {
private fun saveToFile(sources: List<BookSource>, success: (file: File) -> Unit) {
execute {
val path = "${context.filesDir}/shareBookSource.json"
FileUtils.delete(path)
val file = FileUtils.createFileWithReplace(path)
FileOutputStream(file).use {
GSON.writeToOutputStream(it, sources)
FileOutputStream(file).use { out ->
BufferedOutputStream(out, 64 * 1024).use {
GSON.writeToOutputStream(it, sources)
}
}
file
}.onSuccess {
@@ -138,6 +145,107 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}
}
fun saveToFile(
adapter: BookSourceAdapter,
searchKey: String?,
sortAscending: Boolean,
sort: Sort,
success: (file: File) -> Unit
) {
execute {
val selection = adapter.selection
val selectedRate = selection.size.toFloat() / adapter.itemCount.toFloat()
val sources = if (selectedRate == 1f) {
getBookSources(searchKey, sortAscending, sort)
} else if (selectedRate < 0.3) {
selection.toBookSource()
} else {
val keys = selection.map { it.bookSourceUrl }.toHashSet()
val bookSources = getBookSources(searchKey, sortAscending, sort)
bookSources.filter {
keys.contains(it.bookSourceUrl)
}
}
saveToFile(sources, success)
}
}
private fun getBookSources(
searchKey: String?,
sortAscending: Boolean,
sort: Sort
): List<BookSource> {
return when {
searchKey.isNullOrEmpty() -> {
appDb.bookSourceDao.all
}
searchKey == appCtx.getString(R.string.enabled) -> {
appDb.bookSourceDao.allEnabled
}
searchKey == appCtx.getString(R.string.disabled) -> {
appDb.bookSourceDao.allDisabled
}
searchKey == appCtx.getString(R.string.need_login) -> {
appDb.bookSourceDao.allLogin
}
searchKey == appCtx.getString(R.string.no_group) -> {
appDb.bookSourceDao.allNoGroup
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
appDb.bookSourceDao.groupSearch(key)
}
else -> {
appDb.bookSourceDao.search(searchKey)
}
}.let { data ->
if (sortAscending) when (sort) {
Sort.Weight -> data.sortedBy { it.weight }
Sort.Name -> data.sortedWith { o1, o2 ->
o1.bookSourceName.cnCompare(o2.bookSourceName)
}
Sort.Url -> data.sortedBy { it.bookSourceUrl }
Sort.Update -> data.sortedByDescending { it.lastUpdateTime }
Sort.Respond -> data.sortedBy { it.respondTime }
Sort.Enable -> data.sortedWith { o1, o2 ->
var sortNum = -o1.enabled.compareTo(o2.enabled)
if (sortNum == 0) {
sortNum = o1.bookSourceName.cnCompare(o2.bookSourceName)
}
sortNum
}
else -> data
}
else when (sort) {
Sort.Weight -> data.sortedByDescending { it.weight }
Sort.Name -> data.sortedWith { o1, o2 ->
o2.bookSourceName.cnCompare(o1.bookSourceName)
}
Sort.Url -> data.sortedByDescending { it.bookSourceUrl }
Sort.Update -> data.sortedBy { it.lastUpdateTime }
Sort.Respond -> data.sortedByDescending { it.respondTime }
Sort.Enable -> data.sortedWith { o1, o2 ->
var sortNum = o1.enabled.compareTo(o2.enabled)
if (sortNum == 0) {
sortNum = o1.bookSourceName.cnCompare(o2.bookSourceName)
}
sortNum
}
else -> data.reversed()
}
}
}
fun addGroup(group: String) {
execute {
val sources = appDb.bookSourceDao.noGroup
@@ -32,9 +32,6 @@ object HtmlFormatter {
.replace("\\s*\\n+\\s*".toRegex(), "\n  ")
.replace("^[\\n\\s]+".toRegex(), "  ")
.replace("[\\n\\s]+$".toRegex(), "")
.let {
StringEscapeUtils.unescapeHtml3(it)
}
}
fun formatKeepImg(html: String?, redirectUrl: URL? = null): String {
@@ -9,16 +9,12 @@ import java.io.InputStream
@Suppress("unused")
object MD5Utils {
private val MD5Digester by lazy {
DigestUtil.digester("MD5")
}
fun md5Encode(str: String?): String {
return MD5Digester.digestHex(str)
return DigestUtil.digester("MD5").digestHex(str)
}
fun md5Encode(inputStream: InputStream): String {
return MD5Digester.digestHex(inputStream)
return DigestUtil.digester("MD5").digestHex(inputStream)
}
fun md5Encode16(str: String): String {
@@ -1,5 +1,6 @@
package io.legado.app.utils
import android.annotation.SuppressLint
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
@@ -21,6 +22,7 @@ object NetworkUtils {
/**
* 判断是否联网
*/
@SuppressLint("ObsoleteSdkInt")
@Suppress("DEPRECATION")
fun isAvailable(): Boolean {
if (Build.VERSION.SDK_INT < 23) {
@@ -31,7 +33,9 @@ object NetworkUtils {
// 移动数据
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_MOBILE ||
// 以太网
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_ETHERNET
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_ETHERNET ||
// VPN
mWiFiNetworkInfo.type == ConnectivityManager.TYPE_VPN
}
} else {
val network = connectivityManager.activeNetwork
@@ -43,7 +47,9 @@ object NetworkUtils {
// 移动数据
nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
// 以太网
nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
nc.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
// VPN
nc.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}
}
}