Horis 1 год назад
Родитель
Сommit
da54129a37

+ 41 - 2
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -19,6 +19,7 @@ import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
 import com.google.android.material.snackbar.Snackbar
 import com.google.android.material.snackbar.Snackbar
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
 import io.legado.app.base.VMBaseActivity
@@ -49,6 +50,7 @@ import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
 import io.legado.app.ui.widget.recycler.ItemTouchCallback
 import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.ui.widget.recycler.VerticalDivider
 import io.legado.app.utils.ACache
 import io.legado.app.utils.ACache
+import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.applyTint
 import io.legado.app.utils.applyTint
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.dpToPx
 import io.legado.app.utils.dpToPx
@@ -103,6 +105,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
     override var sortAscending = true
     override var sortAscending = true
         private set
         private set
     private var snackBar: Snackbar? = null
     private var snackBar: Snackbar? = null
+    private var showDuplicationSource = false
+    private val bookSourceDecoration by lazy {
+        BookSourceDecoration(adapter)
+    }
+    private val hostMap = hashMapOf<String, String>()
     private val qrResult = registerForActivityResult(QrCodeResult()) {
     private val qrResult = registerForActivityResult(QrCodeResult()) {
         it ?: return@registerForActivityResult
         it ?: return@registerForActivityResult
         showDialogFragment(ImportBookSourceDialog(it))
         showDialogFragment(ImportBookSourceDialog(it))
@@ -265,6 +272,11 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 searchView.setQuery(getString(R.string.disabled_explore), true)
                 searchView.setQuery(getString(R.string.disabled_explore), true)
             }
             }
 
 
+            R.id.menu_show_same_source -> {
+                item.isChecked = !item.isChecked
+                toggleDuplicationSourceView(item.isChecked)
+            }
+
             R.id.menu_help -> showHelp("SourceMBookHelp")
             R.id.menu_help -> showHelp("SourceMBookHelp")
         }
         }
         if (item.groupId == R.id.source_group) {
         if (item.groupId == R.id.source_group) {
@@ -335,7 +347,12 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                     appDb.bookSourceDao.flowSearch(searchKey)
                     appDb.bookSourceDao.flowSearch(searchKey)
                 }
                 }
             }.map { data ->
             }.map { data ->
-                if (sortAscending) {
+                hostMap.clear()
+                if (showDuplicationSource) {
+                    data.sortedWith(
+                        compareBy<BookSourcePart> { getSourceHost(it.bookSourceUrl) == "#" }
+                            .thenBy { getSourceHost(it.bookSourceUrl) })
+                } else if (sortAscending) {
                     when (sort) {
                     when (sort) {
                         BookSourceSort.Weight -> data.sortedBy { it.weight }
                         BookSourceSort.Weight -> data.sortedBy { it.weight }
                         BookSourceSort.Name -> data.sortedWith { o1, o2 ->
                         BookSourceSort.Name -> data.sortedWith { o1, o2 ->
@@ -383,7 +400,8 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 AppLog.put("书源界面更新书源出错", it)
                 AppLog.put("书源界面更新书源出错", it)
             }.flowOn(IO).conflate().collect { data ->
             }.flowOn(IO).conflate().collect { data ->
                 adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking)
                 adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking)
-                itemTouchCallback.isCanDrag = sort == BookSourceSort.Default
+                itemTouchCallback.isCanDrag =
+                    sort == BookSourceSort.Default && !showDuplicationSource
                 delay(500)
                 delay(500)
             }
             }
         }
         }
@@ -667,6 +685,21 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
         }
         }
     }
     }
 
 
+    private fun toggleDuplicationSourceView(enable: Boolean) {
+        showDuplicationSource = enable
+        adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+            override fun onChanged() {
+                if (enable) {
+                    binding.recyclerView.addItemDecoration(bookSourceDecoration)
+                } else {
+                    binding.recyclerView.removeItemDecoration(bookSourceDecoration)
+                }
+                adapter.unregisterAdapterDataObserver(this)
+            }
+        })
+        upBookSource(searchView.query?.toString())
+    }
+
     /**
     /**
      * 保持亮屏
      * 保持亮屏
      */
      */
@@ -686,6 +719,12 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
             .upCountView(adapter.selection.size, adapter.itemCount)
             .upCountView(adapter.selection.size, adapter.itemCount)
     }
     }
 
 
+    override fun getSourceHost(origin: String): String {
+        return hostMap.getOrPut(origin) {
+            NetworkUtils.getSubDomainOrNull(origin) ?: "#"
+        }
+    }
+
     override fun onQueryTextChange(newText: String?): Boolean {
     override fun onQueryTextChange(newText: String?): Boolean {
         newText?.let {
         newText?.let {
             upBookSource(it)
             upBookSource(it)

+ 13 - 0
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt

@@ -272,6 +272,18 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         callBack.upCountView()
         callBack.upCountView()
     }
     }
 
 
+    fun getHeaderText(position: Int): String {
+        val source = getItem(position)!!
+        return callBack.getSourceHost(source.bookSourceUrl)
+    }
+
+    fun isItemHeader(position: Int): Boolean {
+        if (position == 0) return true
+        val lastHost = getHeaderText(position - 1)
+        val curHost = getHeaderText(position)
+        return lastHost != curHost
+    }
+
     override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
     override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
         val srcItem = getItem(srcPosition)
         val srcItem = getItem(srcPosition)
         val targetItem = getItem(targetPosition)
         val targetItem = getItem(targetPosition)
@@ -344,5 +356,6 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         fun enable(enable: Boolean, bookSource: BookSourcePart)
         fun enable(enable: Boolean, bookSource: BookSourcePart)
         fun enableExplore(enable: Boolean, bookSource: BookSourcePart)
         fun enableExplore(enable: Boolean, bookSource: BookSourcePart)
         fun upCountView()
         fun upCountView()
+        fun getSourceHost(origin: String): String
     }
     }
 }
 }

+ 69 - 0
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceDecoration.kt

@@ -0,0 +1,69 @@
+package io.legado.app.ui.book.source.manage
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.text.TextPaint
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import io.legado.app.lib.theme.accentColor
+import io.legado.app.lib.theme.backgroundColor
+import io.legado.app.utils.dpToPx
+import io.legado.app.utils.spToPx
+import splitties.init.appCtx
+
+class BookSourceDecoration(val adapter: BookSourceAdapter): RecyclerView.ItemDecoration() {
+
+    private val headerLeft = 16f.dpToPx()
+    private val headerHeight = 32f.dpToPx()
+
+    private val headerPaint = Paint().apply {
+        color = appCtx.backgroundColor
+    }
+    private val textPaint = TextPaint().apply {
+        textSize = 16f.spToPx()
+        color = appCtx.accentColor
+        isAntiAlias = true
+    }
+    private val textRect = Rect()
+
+    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+        val count = parent.childCount
+        for (i in 0 until count) {
+            val view = parent.getChildAt(i)
+            val position = parent.getChildLayoutPosition(view)
+            val isHeader = adapter.isItemHeader(position)
+            if (isHeader) {
+                c.drawRect(
+                    0f,
+                    view.top - headerHeight,
+                    parent.width.toFloat(),
+                    view.top.toFloat(),
+                    headerPaint
+                )
+                val headerText = adapter.getHeaderText(position)
+                textPaint.getTextBounds(headerText, 0, headerText.length, textRect)
+                c.drawText(
+                    headerText,
+                    headerLeft,
+                    (view.top - headerHeight) + headerHeight / 2 + textRect.height() / 2,
+                    textPaint
+                )
+            }
+        }
+    }
+
+    override fun getItemOffsets(
+        outRect: Rect,
+        view: View,
+        parent: RecyclerView,
+        state: RecyclerView.State
+    ) {
+        val position = parent.getChildLayoutPosition(view)
+        val isHeader = adapter.isItemHeader(position)
+        if (isHeader) {
+            outRect.top = headerHeight.toInt()
+        }
+    }
+
+}

+ 18 - 2
app/src/main/java/io/legado/app/utils/NetworkUtils.kt

@@ -178,6 +178,19 @@ object NetworkUtils {
         }.getOrDefault(baseUrl)
         }.getOrDefault(baseUrl)
     }
     }
 
 
+    fun getSubDomainOrNull(url: String): String? {
+        val baseUrl = getBaseUrl(url) ?: return null
+        return kotlin.runCatching {
+            val mURL = URL(baseUrl)
+            val host: String = mURL.host
+            //mURL.scheme https/http
+            //判断是否为ip
+            if (isIPAddress(host)) return host
+            //PublicSuffixDatabase处理域名
+            PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) ?: host
+        }.getOrDefault(null)
+    }
+
     fun getDomain(url: String): String {
     fun getDomain(url: String): String {
         val baseUrl = getBaseUrl(url) ?: return url
         val baseUrl = getBaseUrl(url) ?: return url
         return kotlin.runCatching {
         return kotlin.runCatching {
@@ -219,14 +232,17 @@ object NetworkUtils {
      * @return True if the input parameter is a valid IPv4 address.
      * @return True if the input parameter is a valid IPv4 address.
      */
      */
     fun isIPv4Address(input: String?): Boolean {
     fun isIPv4Address(input: String?): Boolean {
-        return input != null && Validator.isIpv4(input)
+        return input != null && input.isNotEmpty()
+                && input[0] in '1'..'9'
+                && input.count { it == '.' } == 3
+                && Validator.isIpv4(input)
     }
     }
 
 
     /**
     /**
      * Check if valid IPV6 address.
      * Check if valid IPV6 address.
      */
      */
     fun isIPv6Address(input: String?): Boolean {
     fun isIPv6Address(input: String?): Boolean {
-        return input != null && Validator.isIpv6(input)
+        return input != null && input.contains(":") && Validator.isIpv6(input)
     }
     }
 
 
     /**
     /**

+ 6 - 0
app/src/main/res/menu/book_source.xml

@@ -132,6 +132,12 @@
         android:title="@string/import_by_qr_code"
         android:title="@string/import_by_qr_code"
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
+    <item
+        android:id="@+id/menu_show_same_source"
+        android:checkable="true"
+        android:title="@string/show_same_source"
+        app:showAsAction="never" />
+
     <item
     <item
         android:id="@+id/menu_help"
         android:id="@+id/menu_help"
         android:icon="@drawable/ic_help"
         android:icon="@drawable/ic_help"

+ 1 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -1174,4 +1174,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 1 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -1177,4 +1177,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 1 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -1177,4 +1177,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 4 - 0
app/src/main/res/values-v24/strings.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="show_same_source">显示重复书源</string>
+</resources>

+ 1 - 0
app/src/main/res/values-vi/strings.xml

@@ -1173,4 +1173,5 @@ Còn </string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 1 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1174,4 +1174,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 1 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1176,4 +1176,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -1176,4 +1176,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1177,4 +1177,5 @@
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
     <string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
+    <string name="show_same_source">显示重复书源</string>
 </resources>
 </resources>