Merge pull request #1842 from Xwite/master

书源支持输入验证码 手动验证网站防爬措施
pull/1846/head^2
kunfei 3 years ago committed by GitHub
commit 1cc6999e5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      app/src/main/AndroidManifest.xml
  2. 7
      app/src/main/assets/help/jsHelp.md
  3. 4
      app/src/main/assets/updateLog.md
  4. 22
      app/src/main/java/io/legado/app/help/CacheManager.kt
  5. 21
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  6. 69
      app/src/main/java/io/legado/app/help/SourceVerificationHelp.kt
  7. 26
      app/src/main/java/io/legado/app/ui/association/VerificationCodeActivity.kt
  8. 84
      app/src/main/java/io/legado/app/ui/association/VerificationCodeDialog.kt
  9. 11
      app/src/main/java/io/legado/app/ui/browser/WebViewActivity.kt
  10. 12
      app/src/main/java/io/legado/app/ui/browser/WebViewModel.kt
  11. 2
      app/src/main/java/io/legado/app/ui/login/WebViewLoginFragment.kt
  12. 70
      app/src/main/res/layout/dialog_verification_code_view.xml
  13. 13
      app/src/main/res/menu/source_webview_login.xml
  14. 6
      app/src/main/res/menu/web_view.xml
  15. 2
      app/src/main/res/values-es-rES/strings.xml
  16. 2
      app/src/main/res/values-ja-rJP/strings.xml
  17. 2
      app/src/main/res/values-pt-rBR/strings.xml
  18. 2
      app/src/main/res/values-zh-rHK/strings.xml
  19. 2
      app/src/main/res/values-zh-rTW/strings.xml
  20. 2
      app/src/main/res/values-zh/strings.xml
  21. 2
      app/src/main/res/values/strings.xml

@ -359,6 +359,13 @@
<data android:scheme="yuedu" /> <data android:scheme="yuedu" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- 验证码输入 -->
<activity
android:name=".ui.association.VerificationCodeActivity"
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:exported="true"
android:theme="@style/AppTheme.Transparent">
</activity>
<!-- 打开文件 --> <!-- 打开文件 -->
<activity <activity
android:name=".ui.association.FileAssociationActivity" android:name=".ui.association.FileAssociationActivity"

@ -67,12 +67,19 @@ java.webView(html: String?, url: String?, js: String?): String
* @param url 要打开的链接 * @param url 要打开的链接
* @param title 浏览器的标题 * @param title 浏览器的标题
java.startBrowser(url: String, title: String) java.startBrowser(url: String, title: String)
* 使用内置浏览器打开链接,并等待网页结果 .body()获取网页内容
java.startBrowserAwait(url: String, title: String): StrResponse
``` ```
* 调试 * 调试
``` ```
java.log(msg) java.log(msg)
java.logType(var) java.logType(var)
``` ```
* 获取用户输入的验证码
```
java.getVerificationCode(imageUrl)
```
* 弹窗提示 * 弹窗提示
``` ```
java.longToast(msg: Any?) java.longToast(msg: Any?)

@ -11,6 +11,10 @@
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
**2022/05/04**
* js添加 getVerificationCode startBrowserAwait
**2022/05/02** **2022/05/02**
* 优化强调色和文字颜色一样的情况下一些文字的显示 * 优化强调色和文字颜色一样的情况下一些文字的显示

@ -1,5 +1,6 @@
package io.legado.app.help package io.legado.app.help
import androidx.collection.LruCache
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Cache import io.legado.app.data.entities.Cache
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
@ -10,6 +11,11 @@ import splitties.init.appCtx
object CacheManager { object CacheManager {
private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>() private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>()
private val memoryLruCache = object : LruCache<String, Cache>(100) {
override fun sizeOf(key: String, value: Cache): Int {
return 1
}
}
/** /**
* saveTime 单位为秒 * saveTime 单位为秒
@ -23,13 +29,26 @@ object CacheManager {
is ByteArray -> ACache.get(appCtx).put(key, value, saveTime) is ByteArray -> ACache.get(appCtx).put(key, value, saveTime)
else -> { else -> {
val cache = Cache(key, value.toString(), deadline) val cache = Cache(key, value.toString(), deadline)
memoryLruCache.put(key, cache)
appDb.cacheDao.insert(cache) appDb.cacheDao.insert(cache)
} }
} }
} }
fun get(key: String): String? { fun get(key: String): String? {
return appDb.cacheDao.get(key, System.currentTimeMillis()) return getFromMemory(key) ?: appDb.cacheDao.get(key, System.currentTimeMillis())
}
//从内存中获取数据 使用lrucache 支持过期功能
private fun getFromMemory(key: String): String? {
val cache = memoryLruCache.get(key) ?: return null
val deadline = cache!!.deadline
return if (deadline == 0L || deadline > System.currentTimeMillis()) {
cache!!.value
} else {
memoryLruCache.remove(key)
null
}
} }
fun getInt(key: String): Int? { fun getInt(key: String): Int? {
@ -70,6 +89,7 @@ object CacheManager {
fun delete(key: String) { fun delete(key: String) {
appDb.cacheDao.delete(key) appDb.cacheDao.delete(key)
memoryLruCache.remove(key)
ACache.get(appCtx).remove(key) ACache.get(appCtx).remove(key)
} }
} }

@ -15,7 +15,6 @@ import io.legado.app.help.http.*
import io.legado.app.model.Debug import io.legado.app.model.Debug
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.analyzeRule.QueryTTF import io.legado.app.model.analyzeRule.QueryTTF
import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -133,16 +132,26 @@ interface JsExtensions {
} }
/** /**
* 使用内置浏览器打开链接可用于获取验证码 手动验证网站防爬 * 使用内置浏览器打开链接手动验证网站防爬
* @param url 要打开的链接 * @param url 要打开的链接
* @param title 浏览器页面的标题 * @param title 浏览器页面的标题
*/ */
fun startBrowser(url: String, title: String) { fun startBrowser(url: String, title: String) {
appCtx.startActivity<WebViewActivity> { SourceVerificationHelp.startBrowser(getSource(), url, title)
putExtra("title", title)
putExtra("url", url)
IntentData.put(url, getSource()?.getHeaderMap(true))
} }
/**
* 使用内置浏览器打开链接并等待网页结果
*/
fun startBrowserAwait(url: String, title: String): StrResponse {
return StrResponse(url, SourceVerificationHelp.getVerificationResult(getSource(), url, title, true))
}
/**
* 打开图片验证码对话框等待返回验证结果
*/
fun getVerificationCode(imageUrl: String): String {
return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
} }
/** /**

@ -0,0 +1,69 @@
package io.legado.app.help
import io.legado.app.utils.startActivity
import io.legado.app.constant.AppLog
import io.legado.app.exception.NoStackTraceException
import io.legado.app.data.entities.BaseSource
import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.ui.association.VerificationCodeActivity
import splitties.init.appCtx
import kotlinx.coroutines.runBlocking
object SourceVerificationHelp {
/**
* 获取书源验证结果
* 图片验证码 防爬 滑动验证码 点击字符 等等
*/
fun getVerificationResult(source: BaseSource?, url: String, title: String, useBrowser: Boolean): String {
source ?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
return runBlocking {
val key = "${source?.getKey() ?: ""}_verificationResult"
CacheManager.delete(key)
if (!useBrowser) {
appCtx.startActivity<VerificationCodeActivity> {
putExtra("imageUrl", url)
putExtra("sourceOrigin", source?.getKey())
}
} else {
startBrowser(source, url, title, true)
}
var waitUserInput: Boolean = false
while(CacheManager.get(key) == null) {
if (!waitUserInput) {
AppLog.putDebug("等待返回验证结果...")
waitUserInput = true
}
}
CacheManager.get(key)!!.let {
if (it.isBlank()) {
throw NoStackTraceException("验证结果为空")
} else {
it
}
}
}
}
/**
* 启动内置浏览器
@param saveResult 保存网页源代码到数据库
*/
fun startBrowser(source: BaseSource?, url: String, title: String, saveResult: Boolean? = false) {
source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
appCtx.startActivity<WebViewActivity> {
putExtra("title", title)
putExtra("url", url)
putExtra("sourceOrigin", source?.getKey())
putExtra("sourceVerificationEnable", saveResult)
IntentData.put(url, source?.getHeaderMap(true))
}
}
private fun checkActivityStatus(): Boolean {
return true
}
}

@ -0,0 +1,26 @@
package io.legado.app.ui.association
import android.os.Bundle
import io.legado.app.base.BaseActivity
import io.legado.app.databinding.ActivityTranslucenceBinding
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.viewbindingdelegate.viewBinding
/**
* 验证码
*/
class VerificationCodeActivity :
BaseActivity<ActivityTranslucenceBinding>() {
override val binding by viewBinding(ActivityTranslucenceBinding::inflate)
override fun onActivityCreated(savedInstanceState: Bundle?) {
intent.getStringExtra("imageUrl")?.let {
val sourceOrigin = intent.getStringExtra("sourceOrigin")
showDialogFragment(
VerificationCodeDialog(it, sourceOrigin)
)
} ?: finish()
}
}

@ -0,0 +1,84 @@
package io.legado.app.ui.association
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.net.Uri
import com.bumptech.glide.request.RequestOptions
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.databinding.DialogVerificationCodeViewBinding
import io.legado.app.help.CacheManager
import io.legado.app.help.glide.ImageLoader
import io.legado.app.help.glide.OkHttpModelLoader
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.setLayout
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.viewbindingdelegate.viewBinding
/**
* 图片验证码对话框
* 结果保存在数据库中
* val key = "${sourceOrigin ?: ""}_verificationResult"
* CacheManager.get(key)
*/
class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view) {
constructor(imageUrl: String, sourceOrigin: String? = null) : this() {
arguments = Bundle().apply {
putString("sourceOrigin", sourceOrigin)
putString("imageUrl", imageUrl)
}
}
val binding by viewBinding(DialogVerificationCodeViewBinding::bind)
override fun onStart() {
super.onStart()
setLayout(1f, ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.run {
toolBar.setBackgroundColor(primaryColor)
val sourceOrigin = arguments?.getString("sourceOrigin")
val key = "${sourceOrigin ?: ""}_verificationResult"
arguments?.getString("imageUrl")?.let { imageUrl ->
ImageLoader.load(requireContext(), imageUrl).apply {
sourceOrigin?.let {
apply(
RequestOptions().set(
OkHttpModelLoader.sourceOriginOption,
it
)
)
}
}.error(R.drawable.image_loading_error)
.into(ivImage)
ivImage.setOnClickListener {
showDialogFragment(PhotoDialog(imageUrl, sourceOrigin))
}
}
tvOk.setOnClickListener {
val verificationCode = binding.verificationCode.text.toString()
verificationCode?.let {
CacheManager.put(key, it)
dismiss()
}
}
tvCancel.setOnClickListener {
dismiss()
}
}
}
override fun onDestroy() {
val sourceOrigin = arguments?.getString("sourceOrigin")
val key = "${sourceOrigin ?: ""}_verificationResult"
CacheManager.get(key) ?: CacheManager.put(key, "")
super.onDestroy()
activity?.finish()
}
}

@ -26,6 +26,7 @@ import io.legado.app.ui.document.HandleFileContract
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import java.net.URLDecoder import java.net.URLDecoder
import kotlinx.coroutines.runBlocking
class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() { class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
@ -65,6 +66,12 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
when (item.itemId) { when (item.itemId) {
R.id.menu_open_in_browser -> openUrl(viewModel.baseUrl) R.id.menu_open_in_browser -> openUrl(viewModel.baseUrl)
R.id.menu_copy_url -> sendToClip(viewModel.baseUrl) R.id.menu_copy_url -> sendToClip(viewModel.baseUrl)
R.id.menu_ok -> {
if (viewModel.sourceVerificationEnable) {
binding.titleBar.snackbar(R.string.ok)
finish()
}
}
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }
@ -135,7 +142,6 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
viewModel.saveImage(webPic, path) viewModel.saveImage(webPic, path)
} }
} }
private fun selectSaveFolder() { private fun selectSaveFolder() {
val default = arrayListOf<SelectItem<Int>>() val default = arrayListOf<SelectItem<Int>>()
val path = ACache.get(this).getAsString(imagePathKey) val path = ACache.get(this).getAsString(imagePathKey)
@ -176,6 +182,9 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
runBlocking {
viewModel.saveVerificationResult()
}
binding.webView.destroy() binding.webView.destroy()
} }

@ -9,6 +9,7 @@ import androidx.documentfile.provider.DocumentFile
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.CacheManager
import io.legado.app.help.IntentData import io.legado.app.help.IntentData
import io.legado.app.help.http.newCallResponseBody import io.legado.app.help.http.newCallResponseBody
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
@ -21,6 +22,8 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
var baseUrl: String = "" var baseUrl: String = ""
var html: String? = null var html: String? = null
val headerMap: HashMap<String, String> = hashMapOf() val headerMap: HashMap<String, String> = hashMapOf()
var sourceVerificationEnable: Boolean = false
var sourceOrigin: String = ""
fun initData( fun initData(
intent: Intent, intent: Intent,
@ -29,6 +32,8 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
execute { execute {
val url = intent.getStringExtra("url") val url = intent.getStringExtra("url")
?: throw NoStackTraceException("url不能为空") ?: throw NoStackTraceException("url不能为空")
sourceOrigin = intent.getStringExtra("sourceOrigin") ?: ""
sourceVerificationEnable = intent.getBooleanExtra("sourceVerificationEnable", false)
val headerMapF = IntentData.get<Map<String, String>>(url) val headerMapF = IntentData.get<Map<String, String>>(url)
val analyzeUrl = AnalyzeUrl(url, headerMapF = headerMapF) val analyzeUrl = AnalyzeUrl(url, headerMapF = headerMapF)
baseUrl = analyzeUrl.url baseUrl = analyzeUrl.url
@ -78,5 +83,12 @@ class WebViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun saveVerificationResult() {
if (sourceVerificationEnable) {
val key = "${sourceOrigin}_verificationResult"
html = AnalyzeUrl(baseUrl, headerMapF = headerMap).getStrResponse(useWebView = false).body
CacheManager.put(key, html ?: "")
}
}
} }

@ -35,7 +35,7 @@ class WebViewLoginFragment : BaseFragment(R.layout.fragment_web_view_login) {
} }
override fun onCompatCreateOptionsMenu(menu: Menu) { override fun onCompatCreateOptionsMenu(menu: Menu) {
menuInflater.inflate(R.menu.source_login, menu) menuInflater.inflate(R.menu.source_webview_login, menu)
} }
override fun onCompatOptionsItemSelected(item: MenuItem) { override fun onCompatOptionsItemSelected(item: MenuItem) {

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarStyle"
app:title="@string/input_verification_code"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextAppearance="@style/ToolbarTitle" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_image"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="50dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="3dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
tools:ignore="UnusedAttribute" />
<io.legado.app.ui.widget.text.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingTop="3dp">
<io.legado.app.lib.theme.view.ThemeEditText
android:id="@+id/verification_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/verification_code"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="12dp"
android:paddingRight="12dp"
app:justifyContent="flex_end">
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/cancel"
tools:ignore="RtlHardcoded" />
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/tv_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/ok"
tools:ignore="RtlHardcoded" />
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/menu_ok"
android:icon="@drawable/ic_check"
android:title="@string/ok"
app:showAsAction="always" />
</menu>

@ -2,6 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_ok"
android:icon="@drawable/ic_check"
android:title="@string/ok"
app:showAsAction="always" />
<item <item
android:id="@+id/menu_open_in_browser" android:id="@+id/menu_open_in_browser"
android:title="@string/open_in_browser" android:title="@string/open_in_browser"

@ -969,5 +969,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

@ -972,5 +972,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

@ -972,5 +972,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

@ -969,5 +969,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

@ -971,5 +971,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

@ -971,5 +971,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

@ -972,5 +972,7 @@
<string name="change_source_batch">批量换源</string> <string name="change_source_batch">批量换源</string>
<string name="book_type_different">书籍类型不一样</string> <string name="book_type_different">书籍类型不一样</string>
<string name="soure_change_source">是否确认换源</string> <string name="soure_change_source">是否确认换源</string>
<string name="input_verification_code">输入验证码</string>
<string name="verification_code">验证码</string>
<!-- string end --> <!-- string end -->
</resources> </resources>

Loading…
Cancel
Save