Просмотр исходного кода

feat: 合并 sigma 书源编辑与 JS 扩展功能

书源编辑界面:
- 新增事件监听、定制按钮 CheckBox 绑定
- 新增 callBackJs 字段接入正文规则
- 新增 menu_log 菜单项 (AppLogDialog)
- 新增撤销/重做回调 (KeyboardToolPop.CallBack)
- sendText() 智能滚动逻辑
- 保存时清除并发限制缓存

JS 扩展函数 (#1368):
- 新增 refreshBookToc() 方法
- reLoginView() 增加 deltaUp 参数
- upLoginData() 参数类型改为 Map<String, Any?>?
- SourceLoginDialog 适配新回调接口
- ExploreKindUiUseCase 适配新回调接口

BookInfo 事件监听:
- BookInfoViewModel 监听 REFRESH_BOOK_INFO/REFRESH_BOOK_TOC 事件
- 书源 JS 函数可触发书籍信息和目录刷新
HapeLee 4 месяцев назад
Родитель
Сommit
adf3cef981

+ 1 - 0
app/src/main/java/io/legado/app/constant/EventBus.kt

@@ -35,6 +35,7 @@ object EventBus {
     const val UP_MANGA_CONFIG = "upMangaConfig"
     const val PLAY_MODE_CHANGED = "playModeChanged"
     const val REFRESH_BOOK_INFO = "refreshBookInfo"
+    const val REFRESH_BOOK_TOC = "refreshBookToc"
     const val REFRESH_BOOK_CONTENT = "refreshBookContent"
     const val UP_TOC = "upToc"
 

+ 2 - 2
app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt

@@ -72,8 +72,8 @@ class ExploreKindUiUseCase(
             activity = activity,
             source = source,
             callback = object : SourceLoginJsExtensions.Callback {
-                override fun upUiData(data: Map<String, String?>?) = Unit
-                override fun reUiView() = onRefreshKinds()
+                override fun upUiData(data: Map<String, Any?>?) = Unit
+                override fun reUiView(deltaUp: Boolean) = onRefreshKinds()
             }
         )
         withContext(Dispatchers.IO) {

+ 32 - 0
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -74,6 +74,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.update
@@ -99,6 +102,35 @@ class BookInfoViewModel(
     private val _effects = MutableSharedFlow<BookInfoEffect>(extraBufferCapacity = 8)
     val effects = _effects.asSharedFlow()
 
+    init {
+        collectEventBus()
+    }
+
+    private fun collectEventBus() {
+        viewModelScope.launch {
+            eventFlow<Boolean>(EventBus.REFRESH_BOOK_INFO).collect {
+                currentBook?.let { book ->
+                    refreshBook(book)
+                }
+            }
+        }
+        viewModelScope.launch {
+            eventFlow<Boolean>(EventBus.REFRESH_BOOK_TOC).collect {
+                currentBook?.let { book ->
+                    loadChapter(book)
+                }
+            }
+        }
+    }
+
+    private inline fun <reified T> eventFlow(tag: String): Flow<T> = callbackFlow {
+        val obs = androidx.lifecycle.Observer<T> { trySend(it) }
+        com.jeremyliao.liveeventbus.LiveEventBus.get<T>(tag).observeForever(obs)
+        awaitClose {
+            com.jeremyliao.liveeventbus.LiveEventBus.get<T>(tag).removeObserver(obs)
+        }
+    }
+
     private var currentBook: Book? = null
         set(value) {
             field = value

+ 62 - 13
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt

@@ -1,11 +1,13 @@
 package io.legado.app.ui.book.source.edit
 
 import android.content.Intent
+import android.os.Build
 import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
 import android.widget.EditText
 import androidx.activity.viewModels
+import androidx.annotation.RequiresApi
 import androidx.lifecycle.lifecycleScope
 import com.google.android.material.tabs.TabLayout
 import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
@@ -24,9 +26,7 @@ import io.legado.app.help.config.LocalConfig
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.selector
-//import io.legado.app.lib.theme.accentColor
-//import io.legado.app.lib.theme.backgroundColor
-//import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.about.AppLogDialog
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
@@ -146,6 +146,7 @@ class BookSourceEditActivity :
                 ErrorCorrectionLevel.L
             )
 
+            R.id.menu_log -> showDialogFragment<AppLogDialog>()
             R.id.menu_help -> showHelp("ruleHelp")
             R.id.menu_login -> viewModel.save(getSource()) { source ->
                 startActivity<SourceLoginActivity> {
@@ -261,6 +262,8 @@ class BookSourceEditActivity :
                     else -> 0
                 }
             )
+            binding.cbIsEventListener.isChecked = it.eventListener
+            binding.cbIsCustomButton.isChecked = it.customButton
         }
         // 基本信息
         sourceEntities.clear()
@@ -361,6 +364,7 @@ class BookSourceEditActivity :
             add(EditEntity("imageStyle", cr.imageStyle, R.string.rule_image_style))
             add(EditEntity("imageDecode", cr.imageDecode, R.string.rule_image_decode))
             add(EditEntity("payAction", cr.payAction, R.string.rule_pay_action))
+            add(EditEntity("callBackJs", cr.callBackJs, R.string.rule_call_back))
         }
 
 //        val rr = bs.getReviewRule()
@@ -392,6 +396,8 @@ class BookSourceEditActivity :
             1 -> BookSourceType.audio
             else -> BookSourceType.default
         }
+        source.eventListener = binding.cbIsEventListener.isChecked
+        source.customButton = binding.cbIsCustomButton.isChecked
         val searchRule = SearchRule()
         val exploreRule = ExploreRule()
         val bookInfoRule = BookInfoRule()
@@ -559,6 +565,7 @@ class BookSourceEditActivity :
                 "imageStyle" -> contentRule.imageStyle = it.value
                 "imageDecode" -> contentRule.imageDecode = it.value
                 "payAction" -> contentRule.payAction = it.value
+                "callBackJs" -> contentRule.callBackJs = it.value
             }
         }
 //        reviewEntities.forEach {
@@ -592,6 +599,22 @@ class BookSourceEditActivity :
         return source
     }
 
+    @RequiresApi(Build.VERSION_CODES.M)
+    override fun onUndoClicked() {
+        val editText = window.decorView.findFocus()
+        if (editText is EditText) {
+            editText.onTextContextMenuItem(android.R.id.undo)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.M)
+    override fun onRedoClicked() {
+        val editText = window.decorView.findFocus()
+        if (editText is EditText) {
+            editText.onTextContextMenuItem(android.R.id.redo)
+        }
+    }
+
     private fun alertGroups() {
         lifecycleScope.launch {
             val groups = withContext(IO) {
@@ -643,18 +666,44 @@ class BookSourceEditActivity :
     }
 
     override fun sendText(text: String) {
-        if (text.isBlank()) return
         val view = window.decorView.findFocus()
         if (view is EditText) {
-            val start = view.selectionStart
-            val end = view.selectionEnd
-            val edit = view.editableText//获取EditText的文字
-            if (start < 0 || start >= edit.length) {
-                edit.append(text)
-            } else if (start > end) {
-                edit.replace(end, start, text)
-            } else {
-                edit.replace(start, end, text)//光标所在位置插入文字
+            var start = view.selectionStart
+            var end = view.selectionEnd
+            if (start > end) {
+                val temp = start
+                start = end
+                end = temp
+            }
+            if (text.isNotEmpty()) {
+                val edit = view.editableText//获取EditText的文字
+                if (start < 0 || start >= edit.length) {
+                    edit.append(text)
+                } else {
+                    edit.replace(start, end, text)//光标所在位置插入文字
+                }
+            }
+            if (adapter.editEntityMaxLine >= 999) {
+                view.post {
+                    val editTextLocation = IntArray(2)
+                    view.getLocationOnScreen(editTextLocation)
+                    val recyclerViewLocation = IntArray(2)
+                    binding.recyclerView.getLocationOnScreen(recyclerViewLocation)
+                    val layout = view.layout
+                    if (layout != null) {
+                        val line = layout.getLineForOffset(end)
+                        val cursorYInEditText = layout.getLineTop(line)
+                        val cursorYOnScreen = editTextLocation[1] + cursorYInEditText
+                        val cursorYInRecyclerView = cursorYOnScreen - recyclerViewLocation[1]
+                        val recyclerViewBottom = binding.recyclerView.height - 120
+                        if (cursorYInRecyclerView !in 0..recyclerViewBottom) {
+                            val scrollDistance = cursorYInRecyclerView - recyclerViewBottom / 3
+                            if (scrollDistance > 0 && binding.recyclerView.canScrollVertically(1) || scrollDistance < 0 && binding.recyclerView.canScrollVertically(-1)) {
+                                binding.recyclerView.smoothScrollBy(0, scrollDistance)
+                            }
+                        }
+                    }
+                }
             }
         }
     }

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditViewModel.kt

@@ -7,6 +7,7 @@ import io.legado.app.base.BaseViewModel
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
+import io.legado.app.help.ConcurrentRateLimiter.Companion.concurrentRecordMap
 import io.legado.app.help.RuleComplete
 import io.legado.app.help.config.SourceConfig
 import io.legado.app.help.http.CookieStore
@@ -73,6 +74,7 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
             }
             appDb.bookSourceDao.insert(source)
             bookSource = source
+            concurrentRecordMap.remove(source.bookSourceUrl)
             source
         }.onSuccess {
             success?.invoke(it)

+ 20 - 18
app/src/main/java/io/legado/app/ui/login/SourceLoginDialog.kt

@@ -70,22 +70,22 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
             viewModel.source,
             viewModel.bookType,
             callback = object : SourceLoginJsExtensions.Callback {
-                override fun upUiData(data: Map<String, String?>?) {
+                override fun upUiData(data: Map<String, Any?>?) {
                     activity?.runOnUiThread { // 在主线程中更新 UI
                         handleUpUiData(data)
                     }
                 }
 
-                override fun reUiView() {
+                override fun reUiView(deltaUp: Boolean) {
                     activity?.runOnUiThread {
-                        handleReUiView()
+                        handleReUiView(deltaUp)
                     }
                 }
             }
         )
     }
 
-    private fun handleReUiView() {
+    private fun handleReUiView(deltaUp: Boolean = false) {
         val source = viewModel.source ?: return
         val loginUiStr = source.loginUi ?: return
         val codeStr = loginUiStr.let {
@@ -100,18 +100,16 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
             lifecycleScope.launch(Main) {
                 val loginUiJson = evalUiJs(codeStr)
                 rowUis = loginUi(loginUiJson)
-                binding.flexbox.removeAllViews()
-                rowUiBuilder(source, rowUis)
+                rowUiBuilder(source, rowUis, deltaUp)
             }
         } else {
             rowUis = loginUi(loginUiStr)
-            binding.flexbox.removeAllViews()
-            rowUiBuilder(source, rowUis)
+            rowUiBuilder(source, rowUis, deltaUp)
         }
     }
 
     @SuppressLint("SetTextI18n")
-    private fun handleUpUiData(data: Map<String, String?>?) {
+    private fun handleUpUiData(data: Map<String, Any?>?) {
         hasChange = true
         if (data == null) {
             val newLoginInfo: MutableMap<String, String> = mutableMapOf()
@@ -163,22 +161,22 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
             if (index != -1) {
                 when (val rowView = binding.root.findViewById<View>(index + 1000)) {
                     is TextInputLayout -> {
-                        val value = value ?: run {
+                        val textValue = value?.toString() ?: run {
                             val rowUi = rowUis?.getOrNull(index) ?: return@forEach
                             rowUi.default ?: ""
                         }
-                        rowView.editText?.setText(value)
+                        rowView.editText?.setText(textValue)
                     }
 
                     is TextView -> {
                         val rowUi = rowUis?.getOrNull(index) ?: return@forEach
                         when (rowUi.type) {
                             Type.button -> {
-                                rowView.text = value ?: key
+                                rowView.text = value?.toString() ?: key
                             }
 
                             Type.toggle -> {
-                                val char = value ?: run {
+                                val char = value?.toString() ?: run {
                                     val chars =
                                         rowUi.chars?.filterNotNull() ?: listOf("chars is null")
                                     chars.getOrNull(0) ?: ""
@@ -194,8 +192,9 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
                     is LinearLayout -> {
                         val rowUi = rowUis?.getOrNull(index) ?: return@forEach
                         val items = rowUi.chars?.filterNotNull() ?: listOf("chars", "is null")
-                        val index = items.indexOf(value)
-                        loginInfo[rowUi.name] = value ?: run {
+                        val strValue = value?.toString()
+                        val index = items.indexOf(strValue)
+                        loginInfo[rowUi.name] = strValue ?: run {
                             items.getOrNull(0) ?: ""
                         }
                         rowView.findViewById<AppCompatSpinner>(R.id.sp_type)
@@ -203,7 +202,7 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
                     }
                 }
             } else {
-                loginInfo[key] = value ?: ""
+                loginInfo[key] = value?.toString() ?: ""
             }
         }
     }
@@ -240,9 +239,12 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
     }
 
     @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
-    private fun rowUiBuilder(source: BaseSource, rowUis: List<RowUi>?) {
+    private fun rowUiBuilder(source: BaseSource, rowUis: List<RowUi>?, deltaUp: Boolean = false) {
         val loginInfo = viewModel.loginInfo
-        rowUiName.clear()
+        if (!deltaUp) {
+            binding.flexbox.removeAllViews()
+            rowUiName.clear()
+        }
         rowUis?.forEachIndexed { index, rowUi ->
             val type = rowUi.type
             val name = rowUi.name

+ 10 - 5
app/src/main/java/io/legado/app/ui/login/SourceLoginJsExtensions.kt

@@ -27,16 +27,17 @@ class SourceLoginJsExtensions(
 ) : RssJsExtensions(activity, source) {
     private val callbackRef: WeakReference<Callback> = WeakReference(callback)
     interface Callback {
-        fun upUiData(data: Map<String, String?>?)
-        fun reUiView()
+        fun upUiData(data: Map<String, Any?>?)
+        fun reUiView(deltaUp: Boolean = false)
     }
 
-    fun upLoginData(data: Map<String, String?>?) {
+    fun upLoginData(data: Map<String, Any?>?) {
         callbackRef.get()?.upUiData(data)
     }
 
-    fun reLoginView() {
-        callbackRef.get()?.reUiView()
+    @JvmOverloads
+    fun reLoginView(deltaUp: Boolean = false) {
+        callbackRef.get()?.reUiView(deltaUp)
     }
 
     fun refreshExplore() {
@@ -47,6 +48,10 @@ class SourceLoginJsExtensions(
         postEvent(EventBus.REFRESH_BOOK_INFO, true)
     }
 
+    fun refreshBookToc() {
+        postEvent(EventBus.REFRESH_BOOK_TOC, true)
+    }
+
     fun refreshContent() {
         postEvent(EventBus.REFRESH_BOOK_CONTENT, true)
     }

+ 2 - 0
app/src/main/java/io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt

@@ -175,6 +175,8 @@ class KeyboardToolPop(
 
         fun sendText(text: String)
 
+        fun onUndoClicked() {}
+        fun onRedoClicked() {}
     }
 
 }

+ 32 - 0
app/src/main/res/layout/activity_book_source_edit.xml

@@ -73,6 +73,38 @@
 
     </HorizontalScrollView>
 
+    <HorizontalScrollView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:scrollbars="none"
+        android:paddingHorizontal="12dp">
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            tools:ignore="VisualLintBounds">
+
+            <io.legado.app.lib.theme.view.ThemeCheckBox
+                android:id="@+id/cb_is_event_listener"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:checked="false"
+                android:text="@string/is_event_listener" />
+
+            <io.legado.app.lib.theme.view.ThemeCheckBox
+                android:id="@+id/cb_is_custom_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:checked="false"
+                android:text="@string/custom_button" />
+
+        </LinearLayout>
+
+    </HorizontalScrollView>
+
     <com.google.android.material.tabs.TabLayout
         android:id="@+id/tab_layout"
         android:layout_width="match_parent"

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

@@ -1168,6 +1168,9 @@
     <string name="enable_record">Enable record</string>
     <string name="copy_all">Copy all</string>
     <string name="auto_complete">Automatic completion</string>
+    <string name="is_event_listener">EventListener</string>
+    <string name="custom_button">Custom Button</string>
+    <string name="rule_call_back">callBackJs</string>
     <string name="sort_by_size">Sort by size</string>
     <string name="welcome_style">Launch interface style</string>
     <string name="welcome_style_summary">Startup screen image and whether to display text, etc.</string>