Эх сурвалжийг харах

Merge remote-tracking branch 'origin/master'

kunfei 3 жил өмнө
parent
commit
3e2db0d8d1

+ 45 - 8
app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt

@@ -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>
 

+ 1 - 1
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,

+ 5 - 4
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,

+ 5 - 5
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")
+    }
 
-}
+}

+ 1 - 1
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()) {

+ 1 - 1
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -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
     }

+ 6 - 4
app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt

@@ -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()
     }
 

+ 2 - 0
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

+ 14 - 3
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<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)
             }
 

+ 114 - 6
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<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

+ 0 - 3
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 {

+ 2 - 6
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 {

+ 8 - 2
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)
                 }
             }
         }