diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt index e20d3ceed..1e4188492 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt @@ -31,6 +31,16 @@ interface BookSourceDao { ) fun flowSearch(searchKey: String): Flow> + @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 + @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> - @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 + + @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> - @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> @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc") fun flowExplore(): Flow> - @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> - @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> @Query( @@ -147,6 +175,15 @@ interface BookSourceDao { @get:Query("select * from book_sources where enabled = 1 order by customOrder") val allEnabled: List + @get:Query("select * from book_sources where enabled = 0 order by customOrder") + val allDisabled: List + + @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'") + val allNoGroup: List + + @get:Query("select * from book_sources where loginUrl is not null and loginUrl != ''") + val allLogin: List + @get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder") val allTextEnabled: List diff --git a/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt b/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt index c00b4d52b..8126db352 100644 --- a/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt +++ b/app/src/main/java/io/legado/app/help/book/ContentProcessor.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt index 917c0b1b4..306ab4fb0 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt index c1f1476c1..8ff3776aa 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDavFile.kt @@ -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") + } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index b915899ea..e0ef8e36d 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -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()) { diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 8270557fd..90368f44e 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -134,7 +134,7 @@ object LocalBook { } if (book.isEpub) { content = content?.replace("<img", "< img", true) ?: return null - return StringEscapeUtils.unescapeHtml3(content) + return StringEscapeUtils.unescapeHtml4(content) } return content } diff --git a/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt b/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt index d087687db..8fa7d0223 100644 --- a/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt +++ b/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt @@ -40,7 +40,9 @@ class RemoteBookWebDav( val remoteWebDavFileList: List = 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() } diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt index 0106e5de3..808821ab0 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index e45a69937..7dd8414e4 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -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 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 viewModel.saveToFile(adapter.selection.toBookSource()) { + R.id.menu_share_source -> viewModel.saveToFile( + adapter, + searchKey, + sortAscending, + sort + ) { share(it) } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index 73f2f22aa..2934e5ef6 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -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, success: (file: File) -> Unit) { + private fun saveToFile(sources: List, 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 { + 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 diff --git a/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt index 83441d970..668a79d1f 100644 --- a/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt +++ b/app/src/main/java/io/legado/app/utils/HtmlFormatter.kt @@ -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 { diff --git a/app/src/main/java/io/legado/app/utils/MD5Utils.kt b/app/src/main/java/io/legado/app/utils/MD5Utils.kt index beaac7530..3cf170cf8 100644 --- a/app/src/main/java/io/legado/app/utils/MD5Utils.kt +++ b/app/src/main/java/io/legado/app/utils/MD5Utils.kt @@ -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 { diff --git a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt index 0dfcc3c2e..eff8cb173 100644 --- a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -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) } } }