Merge remote-tracking branch 'origin/master'

pull/1905/head
kunfei 3 years ago
commit 53f0340098
  1. 24
      app/build.gradle
  2. 3
      app/src/main/AndroidManifest.xml
  3. 43
      app/src/main/assets/help/jsHelp.md
  4. 2
      app/src/main/assets/updateLog.md
  5. 2
      app/src/main/assets/web/bookshelf/css/detail.ac2331a7.css
  6. 4
      app/src/main/assets/web/bookshelf/index.html
  7. 0
      app/src/main/assets/web/bookshelf/js/about.74ccd735.js
  8. 2
      app/src/main/assets/web/bookshelf/js/app.7cd09a57.js
  9. 8
      app/src/main/assets/web/bookshelf/js/chunk-vendors.d7fcc132.js
  10. 1
      app/src/main/assets/web/bookshelf/js/detail.bb04bd6b.js
  11. 1
      app/src/main/assets/web/bookshelf/js/detail.d5b3eb30.js
  12. 5
      app/src/main/java/io/legado/app/constant/AppConst.kt
  13. 212
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  14. 2
      app/src/main/java/io/legado/app/lib/prefs/ColorPreference.kt
  15. 2
      app/src/main/java/io/legado/app/lib/prefs/SwitchPreference.kt
  16. 36
      app/src/main/java/io/legado/app/lib/theme/MaterialValueHelper.kt
  17. 15
      app/src/main/java/io/legado/app/lib/theme/ThemeStore.kt
  18. 6
      app/src/main/java/io/legado/app/lib/theme/TintHelper.kt
  19. 24
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  20. 3
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBook.kt
  21. 5
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBookAdapter.kt
  22. 5
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBookManager.kt
  23. 2
      app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt
  24. 27
      app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt
  25. 3
      app/src/main/java/io/legado/app/ui/widget/image/PhotoView.kt
  26. 62
      app/src/main/java/io/legado/app/utils/DigestUtils.kt
  27. 192
      app/src/main/java/io/legado/app/utils/EncoderUtils.kt
  28. 24
      app/src/main/java/io/legado/app/utils/MD5Utils.kt
  29. 40
      build.gradle
  30. 10
      epublib/build.gradle
  31. 7
      gradle.properties
  32. 4
      gradle/wrapper/gradle-wrapper.properties
  33. 30
      settings.gradle

@ -1,9 +1,11 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'de.timfreiheit.resourceplaceholders'
apply plugin: 'com.google.gms.google-services'
plugins {
id "com.android.application"
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize'
id 'kotlin-kapt'
id "de.timfreiheit.resourceplaceholders"
id "com.google.gms.google-services"
}
apply from: 'download.gradle'
static def releaseTime() {
@ -16,11 +18,12 @@ def gitCommits = Integer.parseInt('git rev-list HEAD --count'.execute().text.tri
android {
compileSdkVersion 32
buildToolsVersion '32.0.0'
buildToolsVersion "32.0.0"
namespace 'io.legado.app'
kotlinOptions {
jvmTarget = "11"
}
namespace 'io.legado.app'
signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig {
@ -114,6 +117,9 @@ android {
// Adds exported schema location as test app assets.
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
lint {
checkDependencies true
}
tasks.withType(JavaCompile) {
//options.compilerArgs << "-Xlint:unchecked"
}
@ -249,7 +255,7 @@ dependencies {
implementation('com.github.liuyueyi.quick-chinese-transfer:quick-transfer-core:0.2.3')
//
implementation('cn.hutool:hutool-crypto:5.8.1')
//implementation('cn.hutool:hutool-crypto:5.8.1')
//renderscriptToolkit
implementation('com.github.android:renderscript-intrinsics-replacement-toolkit:b6363490c3')

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.legado.app">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

@ -54,7 +54,11 @@ java.put(key, value)
java.ajax(urlStr): String
java.ajaxAll(urlList: Array<String>): Array<StrResponse?>
//返回Response 方法body() code() message() header() raw() toString()
java.connect(urlStr): Response<String>
java.connect(urlStr): StrResponse
java.post(url: String, body: String, headerMap: Map<String, String>): Connection.Response
java.get(url: String, headerMap: Map<String, String>): Connection.Response
java.head(url: String, headerMap: Map<String, String>): Connection.Response
* 使用webView访问网络
* @param html 直接用webView载入的html, 如果html为空直接访问url
@ -129,12 +133,10 @@ deleteFile(path: String)
****
> [常见加密解密算法介绍](https://www.yijiyong.com/algorithm/encryption/01-intro.html)
> [相关概念](https://blog.csdn.net/OrangeJack/article/details/82913804)
> [Android支持的transformation](https://developer.android.google.cn/reference/kotlin/javax/crypto/Cipher?hl=en)
* AES
> transformation默认实现AES/ECB/PKCS5Padding
```
* @param data 传入的原始数据
* @param key AES加密的key
* @param transformation AES加密的方式 例如AES/ECB/PKCS5Padding
* @param iv ECB模式的偏移向量
java.aesDecodeToString(str: String, key: String, transformation: String, iv: String)
java.aesBase64DecodeToString(str: String, key: String, transformation: String, iv: String)
@ -143,24 +145,37 @@ java.aesEncodeToString(str: String, key: String, transformation: String, iv: Str
java.aesEncodeToBase64String(str: String, key: String, transformation: String, iv: String)
```
* DES
> transformation默认实现DES/ECB/PKCS5Padding
```
java.desDecodeToString(str: String, key: String, transformation: String, iv: String)
java.desBase64DecodeToString(str: String, key: String, transformation: String, iv: String)
java.desEncodeToString(str: String, key: String, transformation: String, iv: String)
java.desEncodeToBase64String(str: String, key: String, transformation: String, iv: String)
```
* 3DES
> tansformation默认实现DESede/ECB/PKCS5Padding
```
* @param data 被加密的字符串
* @param key 密钥
* @param mode 模式 ECB/CBC/CFB/OFB/CTR
* @param padding 补码方式 NoPadding/PKCS5Padding/
* @param iv 加盐
java.tripleDESEncodeBase64Str(data: String,key: String,mode: String,padding: String,iv: String): String?
java.tripleDESDecodeStr(data: String,key: String,mode: String,padding: String,iv: String): String?
```
* 摘要
> algorithm支持MD5 SHA-1 SHA-224 SHA-256 SHA-384 SHA-512
```
java.digestHex(data: String, algorithm: String,): String?
java.digestBase64Str(data: String, algorithm: String,): String?
```
* HMac(散列消息鉴别码)
> algorithm支持DESMAC DESMAC/CFB8 DESedeMAC DESedeMAC/CFB8 DESedeMAC64 DESwithISO9797 HmacMD5 HmacSHA* ISO9797ALG3MAC PBEwithSHA*
```
* @param data 被摘要数据
* @param algorithm 签名算法 MD5/SHA1/SHA256/SHA512
java.digestHex(data: String,algorithm: String,): String?
java.HMacHex(data: String, algorithm: String, key: String): String
java.digestBase64Str(data: String,algorithm: String,): String?
java.HMacBase64(data: String, algorithm: String, key: String): String
```
* md5
```

@ -15,6 +15,8 @@
* 修复部分txt章节结尾乱码bug
* 优化翻页流畅度
* js添加des HMac
* 登录ui用户信息默认加密方式改变,需要重新登录
**2022/05/21**

@ -1,3 +1,3 @@
<!DOCTYPE html><html lang="zh-CN" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.65a00131.css" rel="prefetch"><link href="css/detail.12a39aa7.css" rel="prefetch"><link href="js/about.cee6f6d7.js" rel="prefetch"><link href="js/detail.bb04bd6b.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.aa604b87.js" rel="preload" as="script"><link href="js/chunk-vendors.ca94fdd0.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
<!DOCTYPE html><html lang="zh-CN" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.65a00131.css" rel="prefetch"><link href="css/detail.ac2331a7.css" rel="prefetch"><link href="js/about.74ccd735.js" rel="prefetch"><link href="js/detail.d5b3eb30.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.7cd09a57.js" rel="preload" as="script"><link href="js/chunk-vendors.d7fcc132.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
display: none;
}</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.ca94fdd0.js"></script><script src="js/app.aa604b87.js"></script></body></html>
}</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.d7fcc132.js"></script><script src="js/app.7cd09a57.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -5,7 +5,6 @@ import android.content.pm.PackageManager
import android.provider.Settings
import com.script.javascript.RhinoScriptEngine
import io.legado.app.BuildConfig
import io.legado.app.R
import splitties.init.appCtx
import java.text.SimpleDateFormat
@ -57,7 +56,9 @@ object AppConst {
)
@SuppressLint("PrivateResource")
val sysElevation = appCtx.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
val sysElevation =
appCtx.resources.getDimension(com.google.android.material.R.dimen.design_appbar_elevation)
.toInt()
val androidId: String by lazy {
Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID) ?: "null"

@ -3,9 +3,6 @@ package io.legado.app.help
import android.net.Uri
import android.util.Base64
import androidx.annotation.Keep
import cn.hutool.crypto.digest.DigestUtil
import cn.hutool.crypto.symmetric.AES
import cn.hutool.crypto.symmetric.DESede
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.dateFormat
import io.legado.app.constant.AppLog
@ -299,14 +296,6 @@ interface JsExtensions {
return EncoderUtils.base64Encode(str, flags)
}
fun md5Encode(str: String): String {
return MD5Utils.md5Encode(str)
}
fun md5Encode16(str: String): String {
return MD5Utils.md5Encode16(str)
}
/**
* 格式化时间
*/
@ -602,6 +591,13 @@ interface JsExtensions {
return UUID.randomUUID().toString()
}
fun androidId(): String {
return AppConst.androidId
}
//******************对称加密解密************************//
/////AES
/**
* AES 解码为 ByteArray
* @param str 传入的AES加密的数据
@ -640,6 +636,31 @@ interface JsExtensions {
return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
}
/**
* AES解码为String算法参数经过Base64加密
*
* @param data 加密的字符串
* @param key Base64后的密钥
* @param mode 模式
* @param padding 补码方式
* @param iv Base64后的加盐
* @return 解密后的字符串
*/
fun aesDecodeArgsBase64Str(
data: String,
key: String,
mode: String,
padding: String,
iv: String
): String? {
return EncoderUtils.decryptAES(
data.encodeToByteArray(),
Base64.decode(key, Base64.NO_WRAP),
"AES/${mode}/${padding}",
Base64.decode(iv, Base64.NO_WRAP)
)?.let { String(it) }
}
/**
* 已经base64的AES 解码为 ByteArray
* @param str 传入的AES Base64加密的数据
@ -753,35 +774,78 @@ interface JsExtensions {
return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }
}
fun androidId(): String {
return AppConst.androidId
}
/**
* AES解密算法参数经过Base64加密
* AES加密并转为Base64算法参数经过Base64加密
*
* @param data 加密的字符串
* @param data 加密的字符串
* @param key Base64后的密钥
* @param mode 模式
* @param padding 补码方式
* @param iv Base64后的加盐
* @return 解密后的字符串
* @return 加密后的Base64
*/
fun aesDecodeArgsBase64Str(
fun aesEncodeArgsBase64Str(
data: String,
key: String,
mode: String,
padding: String,
iv: String
): String? {
return AES(
mode,
padding,
return EncoderUtils.encryptAES2Base64(
data.encodeToByteArray(),
Base64.decode(key, Base64.NO_WRAP),
"AES/${mode}/${padding}",
Base64.decode(iv, Base64.NO_WRAP)
).decryptStr(data)
)?.let { String(it) }
}
/////DES
fun desDecodeToString(
data: String, key: String, transformation: String, iv: String
): String? {
return EncoderUtils.decryptDES(
data.encodeToByteArray(),
key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)?.let { String(it) }
}
fun desBase64DecodeToString(
data: String, key: String, transformation: String, iv: String
): String? {
return EncoderUtils.decryptBase64DES(
data.encodeToByteArray(),
key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)?.let { String(it) }
}
fun desEncodeToString(
data: String, key: String, transformation: String, iv: String
): String? {
return EncoderUtils.encryptDES(
data.encodeToByteArray(),
key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)?.let { String(it) }
}
fun desEncodeToBase64String(
data: String, key: String, transformation: String, iv: String
): String? {
return EncoderUtils.encryptDES2Base64(
data.encodeToByteArray(),
key.encodeToByteArray(),
transformation,
iv.encodeToByteArray()
)?.let { String(it) }
}
//////3DES
/**
* 3DES解密
*
@ -799,7 +863,12 @@ interface JsExtensions {
padding: String,
iv: String
): String? {
return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).decryptStr(data)
return EncoderUtils.decryptDESede(
data.encodeToByteArray(),
key.encodeToByteArray(),
"DESede/${mode}/${padding}",
iv.encodeToByteArray()
)?.let { String(it) }
}
/**
@ -819,38 +888,14 @@ interface JsExtensions {
padding: String,
iv: String
): String? {
return DESede(
mode,
padding,
return EncoderUtils.decryptDESede(
data.encodeToByteArray(),
Base64.decode(key, Base64.NO_WRAP),
"DESede/${mode}/${padding}",
Base64.decode(iv, Base64.NO_WRAP)
).decryptStr(data)
)?.let { String(it) }
}
/**
* AES加密并转为Base64算法参数经过Base64加密
*
* @param data 被加密的字符串
* @param key Base64后的密钥
* @param mode 模式
* @param padding 补码方式
* @param iv Base64后的加盐
* @return 加密后的Base64
*/
fun aesEncodeArgsBase64Str(
data: String,
key: String,
mode: String,
padding: String,
iv: String
): String? {
return AES(
mode,
padding,
Base64.decode(key, Base64.NO_WRAP),
Base64.decode(iv, Base64.NO_WRAP)
).encryptBase64(data)
}
/**
* 3DES加密并转为Base64
@ -869,7 +914,12 @@ interface JsExtensions {
padding: String,
iv: String
): String? {
return DESede(mode, padding, key.toByteArray(), iv.toByteArray()).encryptBase64(data)
return EncoderUtils.encryptDESede2Base64(
data.encodeToByteArray(),
key.encodeToByteArray(),
"DESede/${mode}/${padding}",
iv.encodeToByteArray()
)?.let { String(it) }
}
/**
@ -889,14 +939,16 @@ interface JsExtensions {
padding: String,
iv: String
): String? {
return DESede(
mode,
padding,
return EncoderUtils.encryptDESede2Base64(
data.encodeToByteArray(),
Base64.decode(key, Base64.NO_WRAP),
"DESede/${mode}/${padding}",
Base64.decode(iv, Base64.NO_WRAP)
).encryptBase64(data)
)?.let { String(it) }
}
//******************消息摘要/散列消息鉴别码************************//
/**
* 生成摘要并转为16进制字符串
*
@ -907,8 +959,8 @@ interface JsExtensions {
fun digestHex(
data: String,
algorithm: String,
): String? {
return DigestUtil.digester(algorithm).digestHex(data)
): String {
return DigestUtils.getDigest(algorithm, data)
}
/**
@ -921,8 +973,48 @@ interface JsExtensions {
fun digestBase64Str(
data: String,
algorithm: String,
): String? {
return Base64.encodeToString(DigestUtil.digester(algorithm).digest(data), Base64.NO_WRAP)
): String {
return Base64.encodeToString(DigestUtils.getDigest(algorithm, data.toByteArray()), Base64.NO_WRAP)
}
/**
* 生成散列消息鉴别码并转为16进制字符串
*
* @param data 被摘要数据
* @param algorithm 签名算法
* @param key 密钥
* @return 16进制字符串
*/
fun HMacHex(
data: String,
algorithm: String,
key: String
): String {
return DigestUtils.getHMac(algorithm, key, data)
}
/**
* 生成散列消息鉴别码并转为Base64字符串
*
* @param data 被摘要数据
* @param algorithm 签名算法
* @param key 密钥
* @return Base64字符串
*/
fun HMacBase64(
data: String,
algorithm: String,
key: String
): String {
return Base64.encodeToString(DigestUtils.getHMac(algorithm, key.toByteArray(), data.toByteArray()), Base64.NO_WRAP)
}
fun md5Encode(str: String): String {
return MD5Utils.md5Encode(str)
}
fun md5Encode16(str: String): String {
return MD5Utils.md5Encode16(str)
}
}

@ -120,7 +120,7 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
override fun onBindViewHolder(holder: PreferenceViewHolder) {
val v = io.legado.app.lib.prefs.Preference.bindView<ColorPanelView>(
context, holder, icon, title, summary, widgetLayoutResource,
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30
com.jaredrummler.android.colorpicker.R.id.cpv_preference_preview_color_panel, 30, 30
)
if (v is ColorPanelView) {
v.color = mColor

@ -30,7 +30,7 @@ class SwitchPreference(context: Context, attrs: AttributeSet) :
title,
summary,
widgetLayoutResource,
R.id.switchWidget,
androidx.preference.R.id.switchWidget,
isBottomBackground = isBottomBackground
)
if (v is SwitchCompat && !v.isInEditMode) {

@ -19,34 +19,52 @@ import io.legado.app.utils.dpToPx
@ColorInt
fun Context.getPrimaryTextColor(dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(this, R.color.primary_text_default_material_light)
} else ContextCompat.getColor(this, R.color.primary_text_default_material_dark)
ContextCompat.getColor(this, androidx.appcompat.R.color.primary_text_default_material_light)
} else ContextCompat.getColor(
this,
androidx.appcompat.R.color.primary_text_default_material_dark
)
}
@ColorInt
fun Context.getSecondaryTextColor(dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(this, R.color.secondary_text_default_material_light)
ContextCompat.getColor(
this,
androidx.appcompat.R.color.secondary_text_default_material_light
)
} else {
ContextCompat.getColor(this, R.color.secondary_text_default_material_dark)
ContextCompat.getColor(
this,
androidx.appcompat.R.color.secondary_text_default_material_dark
)
}
}
@ColorInt
fun Context.getPrimaryDisabledTextColor(dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(this, R.color.primary_text_disabled_material_light)
ContextCompat.getColor(
this,
androidx.appcompat.R.color.primary_text_disabled_material_light
)
} else {
ContextCompat.getColor(this, R.color.primary_text_disabled_material_dark)
ContextCompat.getColor(this, androidx.appcompat.R.color.primary_text_disabled_material_dark)
}
}
@ColorInt
fun Context.getSecondaryDisabledTextColor(dark: Boolean): Int {
return if (dark) {
ContextCompat.getColor(this, R.color.secondary_text_disabled_material_light)
ContextCompat.getColor(
this,
androidx.appcompat.R.color.secondary_text_disabled_material_light
)
} else {
ContextCompat.getColor(this, R.color.secondary_text_disabled_material_dark)
ContextCompat.getColor(
this,
androidx.appcompat.R.color.secondary_text_disabled_material_dark
)
}
}
@ -124,7 +142,7 @@ val Context.elevation: Float
ThemeUtils.resolveFloat(
this,
android.R.attr.elevation,
resources.getDimension(R.dimen.design_appbar_elevation)
resources.getDimension(com.google.android.material.R.dimen.design_appbar_elevation)
)
} else {
AppConfig.elevation.toFloat().dpToPx()

@ -9,7 +9,6 @@ import androidx.annotation.CheckResult
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import io.legado.app.R
import io.legado.app.utils.ColorUtils
import splitties.init.appCtx
@ -189,7 +188,11 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
fun primaryColor(context: Context = appCtx): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_PRIMARY_COLOR,
ThemeUtils.resolveColor(context, R.attr.colorPrimary, Color.parseColor("#455A64"))
ThemeUtils.resolveColor(
context,
androidx.appcompat.R.attr.colorPrimary,
Color.parseColor("#455A64")
)
)
}
@ -200,7 +203,7 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
ThemeStorePrefKeys.KEY_PRIMARY_COLOR_DARK,
ThemeUtils.resolveColor(
context,
R.attr.colorPrimaryDark,
androidx.appcompat.R.attr.colorPrimaryDark,
Color.parseColor("#37474F")
)
)
@ -211,7 +214,11 @@ private constructor(private val mContext: Context) : ThemeStoreInterface {
fun accentColor(context: Context = appCtx): Int {
return prefs(context).getInt(
ThemeStorePrefKeys.KEY_ACCENT_COLOR,
ThemeUtils.resolveColor(context, R.attr.colorAccent, Color.parseColor("#263238"))
ThemeUtils.resolveColor(
context,
androidx.appcompat.R.attr.colorAccent,
Color.parseColor("#263238")
)
)
}

@ -31,9 +31,9 @@ object TintHelper {
// Light ripple is actually translucent black, and vice versa
return ContextCompat.getColor(
context, if (useDarkRipple)
R.color.ripple_material_light
androidx.appcompat.R.color.ripple_material_light
else
R.color.ripple_material_dark
androidx.appcompat.R.color.ripple_material_dark
)
}
@ -170,7 +170,7 @@ object TintHelper {
val rd = view.background as RippleDrawable
@SuppressLint("PrivateResource") val unchecked = ContextCompat.getColor(
view.context,
if (isDark) R.color.ripple_material_dark else R.color.ripple_material_light
if (isDark) androidx.appcompat.R.color.ripple_material_dark else androidx.appcompat.R.color.ripple_material_light
)
val checked = ColorUtils.adjustAlpha(color, 0.4f)
val sl = ColorStateList(

@ -219,6 +219,8 @@ object LocalBook {
fileName: String,
source: BaseSource? = null,
): Uri {
AppConfig.defaultBookTreeUri
?: throw NoStackTraceException("没有设置书籍保存位置!")
val bytes = when {
str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
@ -241,7 +243,7 @@ object LocalBook {
return type ?: fileType
}
private fun saveBookFile(
fun saveBookFile(
bytes: ByteArray,
fileName: String
): Uri {
@ -269,6 +271,26 @@ object LocalBook {
}
}
fun isOnBookShelf(
fileName: String
): Boolean {
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
if (defaultBookTreeUri.isNullOrBlank()) throw NoStackTraceException("没有设置书籍保存位置!")
val treeUri = Uri.parse(defaultBookTreeUri)
var bookUrl: String = ""
if (treeUri.isContentScheme()) {
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
var doc = treeDoc!!.findFile(fileName) ?: return false
bookUrl = doc.uri.toString()
} else {
val treeFile = File(treeUri.path!!)
val file = treeFile.getFile(fileName)
if (!file.exists()) return false
bookUrl = file.absolutePath
}
return appDb.bookDao.getBook(bookUrl) != null
}
//文件类书源 合并在线书籍信息 在线 > 本地
fun mergeBook(localBook: Book, onLineBook: Book?): Book {
onLineBook ?: return localBook

@ -5,5 +5,6 @@ data class RemoteBook(
val urlName: String,
val size: Long,
val contentType: String,
val lastModify: Long
val lastModify: Long,
val isOnBookShelf: Boolean
)

@ -2,11 +2,12 @@ package io.legado.app.ui.book.remote
import android.content.Context
import android.view.ViewGroup
import cn.hutool.core.date.LocalDateTimeUtil
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.databinding.ItemRemoteBookBinding
import io.legado.app.utils.ConvertUtils
import java.text.SimpleDateFormat
import java.util.Date
/**
@ -38,7 +39,7 @@ class RemoteBookAdapter (context: Context, val callBack: CallBack) :
tvName.text = item.filename.substringBeforeLast(".")
tvContentType.text = item.contentType
tvSize.text = ConvertUtils.formatFileSize(item.size)
tvDate.text = LocalDateTimeUtil.format(LocalDateTimeUtil.of(item.lastModify), "yyyy-MM-dd")
tvDate.text = SimpleDateFormat("yyyy-MM-dd").format(Date(item.lastModify))
}
}

@ -2,11 +2,8 @@ package io.legado.app.ui.book.remote
import android.net.Uri
abstract class RemoteBookManager {
protected val remoteBookFolder : String = "books"
protected val contentTypeList: ArrayList<String> = arrayListOf("epub","txt")
abstract suspend fun initRemoteContext()
abstract suspend fun getRemoteBookList(): MutableList<RemoteBook>
abstract suspend fun upload(localBookUri: Uri): Boolean
@ -15,5 +12,5 @@ abstract class RemoteBookManager {
/**
* @return String下载到本地的路径
*/
abstract suspend fun getRemoteBook(remoteBook: RemoteBook): String?
abstract suspend fun getRemoteBook(remoteBook: RemoteBook): Uri?
}

@ -73,7 +73,7 @@ class RemoteBookViewModel(application: Application): BaseViewModel(application){
execute {
val downloadBookPath = RemoteBookWebDav.getRemoteBook(remoteBook)
downloadBookPath?.let {
LocalBook.importFile(Uri.parse(it))
LocalBook.importFile(it)
}
}.onFinally {
finally.invoke()

@ -3,11 +3,13 @@ package io.legado.app.ui.book.remote.manager
import android.net.Uri
import io.legado.app.constant.PreferKey
import io.legado.app.constant.AppPattern.bookFileRegex
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.WebDavFile
import io.legado.app.model.localBook.LocalBook
import io.legado.app.ui.book.remote.RemoteBook
import io.legado.app.ui.book.remote.RemoteBookManager
import io.legado.app.utils.*
@ -17,7 +19,6 @@ import java.io.File
object RemoteBookWebDav : RemoteBookManager() {
private val remoteBookUrl get() = "${rootWebDavUrl}${remoteBookFolder}"
private val localSaveFolder get() = "${appCtx.externalFiles.absolutePath}${File.separator}${remoteBookFolder}"
init {
runBlocking {
@ -68,31 +69,35 @@ object RemoteBookWebDav : RemoteBookManager() {
val fileExtension = webDavFileName.substringAfterLast(".")
//扩展名符合阅读的格式则认为是书籍
if (contentTypeList.contains(fileExtension)) {
remoteBooks.add(RemoteBook(webDavFileName,webDavUrlName,webDavFile.size,fileExtension,webDavFile.lastModify))
if (bookFileRegex.matches(webDavFileName)) {
val isOnBookShelf = LocalBook.isOnBookShelf(webDavFileName)
remoteBooks.add(
RemoteBook(
webDavFileName, webDavUrlName, webDavFile.size,
fileExtension, webDavFile.lastModify, isOnBookShelf
)
)
}
}
} ?: throw NoStackTraceException("webDav没有配置")
return remoteBooks
}
override suspend fun getRemoteBook(remoteBook: RemoteBook): String? {
val saveFilePath= "${localSaveFolder}${File.separator}${remoteBook.filename}"
kotlin.runCatching {
override suspend fun getRemoteBook(remoteBook: RemoteBook): Uri? {
return kotlin.runCatching {
AppWebDav.authorization?.let {
FileUtils.createFolderIfNotExist(localSaveFolder).run {
val webdav = WebDav(
remoteBook.urlName,
it
)
webdav.downloadTo(saveFilePath, true)
webdav.download().let { bytes ->
LocalBook.saveBookFile(bytes, remoteBook.filename)
}
}
}.onFailure {
it.printStackTrace()
return null
}
return saveFilePath
null
}.getOrNull()
}
/**

@ -17,7 +17,6 @@ import android.widget.ImageView
import android.widget.OverScroller
import android.widget.Scroller
import androidx.appcompat.widget.AppCompatImageView
import io.legado.app.R
import io.legado.app.ui.widget.image.photo.Info
import io.legado.app.ui.widget.image.photo.OnRotateListener
import io.legado.app.ui.widget.image.photo.RotateGestureDetector
@ -917,7 +916,7 @@ class PhotoView @JvmOverloads constructor(
var viewParent: ViewParent = target.parent
while (viewParent is View) {
val view: View = viewParent
if (view.id == R.id.content) return
if (view.id == androidx.constraintlayout.widget.R.id.content) return
position[0] -= view.scrollX
position[1] -= view.scrollY
position[0] += view.left

@ -0,0 +1,62 @@
package io.legado.app.utils
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
object DigestUtils {
/**
* 消息摘要
* 支持MD5 SHA-1 SHA-224 SHA-256 SHA-384 SHA-512
* https://developer.android.google.cn/reference/java/security/MessageDigest?hl=en
*/
fun getDigest(
algorithm: String,
data: String?
): String {
data ?: return ""
val bytes = getDigest(algorithm, data.toByteArray())
return StringUtils.byteToHexString(bytes)
}
fun getDigest(
algorithm: String,
data: ByteArray
): ByteArray {
return kotlin.runCatching {
val messageDigest = MessageDigest.getInstance(algorithm)
messageDigest.digest(data)
}.getOrThrow()
}
/**
* 散列消息鉴别码
* 支持DESMAC DESMAC/CFB8 DESedeMAC DESedeMAC/CFB8 DESedeMAC64 DESwithISO9797 HmacMD5 HmacSHA* ISO9797ALG3MAC PBEwithSHA*
* https://developer.android.google.cn/reference/kotlin/javax/crypto/Mac?hl=en
*/
fun getHMac(
algorithm: String,
key: String,
data: String?
): String {
data ?: return ""
val bytes = getHMac(algorithm, key.toByteArray(), data.toByteArray())
return StringUtils.byteToHexString(bytes)
}
fun getHMac(
algorithm: String,
key: ByteArray,
data: ByteArray
): ByteArray {
return kotlin.runCatching {
val mac= Mac.getInstance(algorithm)
val keySpec = SecretKeySpec(key, algorithm)
mac.init(keySpec)
mac.update(data)
mac.doFinal()
}.getOrThrow()
}
}

@ -6,6 +6,9 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* transformations https://developer.android.google.cn/reference/kotlin/javax/crypto/Cipher?hl=en
*/
@Suppress("unused")
object EncoderUtils {
@ -47,7 +50,7 @@ object EncoderUtils {
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the Base64-encode bytes of AES encryption
@ -56,7 +59,7 @@ object EncoderUtils {
fun encryptAES2Base64(
data: ByteArray?,
key: ByteArray?,
transformation: String? = "DES/ECB/PKCS5Padding",
transformation: String? = "AES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return Base64.encode(encryptAES(data, key, transformation, iv), Base64.NO_WRAP)
@ -68,7 +71,7 @@ object EncoderUtils {
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES encryption
@ -77,7 +80,7 @@ object EncoderUtils {
fun encryptAES(
data: ByteArray?,
key: ByteArray?,
transformation: String? = "DES/ECB/PKCS5Padding",
transformation: String? = "AES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return symmetricTemplate(data, key, "AES", transformation!!, iv, true)
@ -90,7 +93,7 @@ object EncoderUtils {
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption for Base64-encode bytes
@ -99,7 +102,7 @@ object EncoderUtils {
fun decryptBase64AES(
data: ByteArray?,
key: ByteArray?,
transformation: String = "DES/ECB/PKCS5Padding",
transformation: String = "AES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
@ -111,7 +114,7 @@ object EncoderUtils {
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* 加密算法/加密模式/填充类型, *AES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption
@ -120,12 +123,185 @@ object EncoderUtils {
fun decryptAES(
data: ByteArray?,
key: ByteArray?,
transformation: String = "DES/ECB/PKCS5Padding",
transformation: String = "AES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return symmetricTemplate(data, key, "AES", transformation, iv, false)
}
//////////DES Start
/**
* Return the Base64-encode bytes of DES encryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the Base64-encode bytes of AES encryption
*/
@Throws(Exception::class)
fun encryptDES2Base64(
data: ByteArray?,
key: ByteArray?,
transformation: String? = "DES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return Base64.encode(encryptDES(data, key, transformation, iv), Base64.NO_WRAP)
}
/**
* Return the bytes of DES encryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES encryption
*/
@Throws(Exception::class)
fun encryptDES(
data: ByteArray?,
key: ByteArray?,
transformation: String? = "DES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return symmetricTemplate(data, key, "DES", transformation!!, iv, true)
}
/**
* Return the bytes of DES decryption for Base64-encode bytes.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption for Base64-encode bytes
*/
@Throws(Exception::class)
fun decryptBase64DES(
data: ByteArray?,
key: ByteArray?,
transformation: String = "DES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return decryptDES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
}
/**
* Return the bytes of DES decryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DES/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption
*/
@Throws(Exception::class)
fun decryptDES(
data: ByteArray?,
key: ByteArray?,
transformation: String = "DES/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return symmetricTemplate(data, key, "DES", transformation, iv, false)
}
//////////DESede Start
/**
* Return the Base64-encode bytes of DESede encryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the Base64-encode bytes of AES encryption
*/
@Throws(Exception::class)
fun encryptDESede2Base64(
data: ByteArray?,
key: ByteArray?,
transformation: String? = "DESede/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return Base64.encode(encryptDESede(data, key, transformation, iv), Base64.NO_WRAP)
}
/**
* Return the bytes of DESede encryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES encryption
*/
@Throws(Exception::class)
fun encryptDESede(
data: ByteArray?,
key: ByteArray?,
transformation: String? = "DESede/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return symmetricTemplate(data, key, "DESede", transformation!!, iv, true)
}
/**
* Return the bytes of DESede decryption for Base64-encode bytes.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption for Base64-encode bytes
*/
@Throws(Exception::class)
fun decryptBase64DESede(
data: ByteArray?,
key: ByteArray?,
transformation: String = "DESede/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return decryptDESede(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
}
/**
* Return the bytes of DESede decryption.
*
* @param data The data.
* @param key The key.
* @param transformation The name of the transformation,
* 加密算法/加密模式/填充类型, *DESede/CBC/PKCS5Padding*.
* @param iv The buffer with the IV. The contents of the
* buffer are copied to protect against subsequent modification.
* @return the bytes of AES decryption
*/
@Throws(Exception::class)
fun decryptDESede(
data: ByteArray?,
key: ByteArray?,
transformation: String = "DESede/ECB/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return symmetricTemplate(data, key, "DESede", transformation, iv, false)
}
/**
* Return the bytes of symmetric encryption or decryption.

@ -1,9 +1,5 @@
package io.legado.app.utils
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
/**
* 将字符串转化为MD5
*/
@ -11,25 +7,7 @@ import java.security.NoSuchAlgorithmException
object MD5Utils {
fun md5Encode(str: String?): String {
if (str == null) return ""
var reStr = ""
try {
val md5: MessageDigest = MessageDigest.getInstance("MD5")
val bytes: ByteArray = md5.digest(str.toByteArray())
val stringBuffer: StringBuilder = StringBuilder()
for (b in bytes) {
val bt: Int = b.toInt() and 0xff
if (bt < 16) {
stringBuffer.append(0)
}
stringBuffer.append(Integer.toHexString(bt))
}
reStr = stringBuffer.toString()
} catch (e: NoSuchAlgorithmException) {
e.printOnDebug()
}
return reStr
return DigestUtils.getDigest("MD5", str)
}
fun md5Encode16(str: String): String {

@ -1,36 +1,22 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.6.10'
repositories {
//
google()
mavenCentral()
maven { url 'https://plugins.gradle.org/m2/' }
//,,
//maven { url 'https://maven.aliyun.com/repository/google' }
//maven { url 'https://maven.aliyun.com/repository/public' }
//maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.4'
classpath 'de.undercouch:gradle-download-task:5.0.1'
classpath 'com.google.gms:google-services:4.3.10'
ext{
kotlin_version = '1.6.10'
agp_version='7.2.0'
}
}
allprojects {
repositories {
//
google()
mavenCentral()
maven { url 'https://jitpack.io' }
//,,
//maven { url 'https://maven.aliyun.com/repository/google' }
//maven { url 'https://maven.aliyun.com/repository/public' }
}
plugins {
id 'com.android.application' version "$agp_version" apply false
id 'com.android.library' version "$agp_version" apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
id "de.timfreiheit.resourceplaceholders" version "0.4" apply false
id "de.undercouch.download" version "5.1.0" apply false
id "com.google.gms.google-services" version "4.3.10" apply false
}
task clean(type: Delete) {

@ -3,12 +3,12 @@ plugins {
}
android {
compileSdkVersion 31
buildToolsVersion '31.0.0'
compileSdkVersion 32
buildToolsVersion '32.0.0'
namespace 'me.ag2s.epublib'
defaultConfig {
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@ -21,11 +21,15 @@ android {
}
android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lint {
checkDependencies true
}
}
dependencies {

@ -19,8 +19,11 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.enableResourceOptimizations=true
android.enableNewResourceShrinker=true
android.experimental.enableNewResourceShrinker.preciseShrinking=true
# enables namespacing of each library's R class so that its R class includes only the resources declared in the library itself
# and none from the library's dependencies, thereby reducing the size of the R class for that library.
android.nonTransitiveRClass=true
#https://chromiumdash.appspot.com/releases?platform=Android
CronetVersion=101.0.4951.61

@ -1,6 +1,6 @@
#Fri May 07 15:24:46 CST 2021
#Mon May 23 22:07:39 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

@ -1 +1,31 @@
pluginManagement {
repositories {
//
gradlePluginPortal()
google()
mavenCentral()
//,,
//maven {url"https://maven-central-asia.storage-download.googleapis.com/maven2/"}
//maven { url 'https://maven.aliyun.com/repository/google' }
//maven { url 'https://maven.aliyun.com/repository/public' }
//maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
//
google()
mavenCentral()
maven { url 'https://jitpack.io' }
//,,
//maven {url"https://maven-central-asia.storage-download.googleapis.com/maven2/"}
//maven { url 'https://maven.aliyun.com/repository/google' }
//maven { url 'https://maven.aliyun.com/repository/public' }
}
}
rootProject.name = 'legado'
include ':app',':epublib'

Loading…
Cancel
Save