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 函数可触发书籍信息和目录刷新
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -175,6 +175,8 @@ class KeyboardToolPop(
|
||||
|
||||
fun sendText(text: String)
|
||||
|
||||
fun onUndoClicked() {}
|
||||
fun onRedoClicked() {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user