diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c40894..c2d9234 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,10 +10,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 # 获取打包秘钥 - name: Checkout Android Keystore - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: fengyuecanzhu/Key token: ${{ secrets.KEY_TOKEN }} # 连接仓库的token,需要单独配置 @@ -27,18 +27,18 @@ jobs: version=$VERSION_CODE versionN=v${version:0:1}.${version:1:1}.${version:2:1} - echo ::set-output name=need_create_release::"$CREATE_RELEASE" - echo ::set-output name=version_name::"$versionN" + echo need_create_release="$CREATE_RELEASE" >> $GITHUB_OUTPUT + echo version_name="$versionN" >> $GITHUB_OUTPUT echo need_create_release=$CREATE_RELEASE echo version_name=$versionN if [ $CREATE_RELEASE == 'true' -a ${{ github.ref }} == 'refs/heads/master' ];then - echo ::set-output name=lanzou_folder_id::"1608604" - echo ::set-output name=lanzou_share_url::"https://fycz.lanzoui.com/b00ngso7e" + echo lanzou_folder_id="1608604" >> $GITHUB_OUTPUT + echo lanzou_share_url="https://fycz.lanzoui.com/b00ngso7e" >> $GITHUB_OUTPUT else - echo ::set-output name=lanzou_folder_id::"2226473" - echo ::set-output name=lanzou_share_url::"https://fycz.lanzoui.com/b00nu1f8d" + echo lanzou_folder_id="2226473" >> $GITHUB_OUTPUT + echo lanzou_share_url="https://fycz.lanzoui.com/b00nu1f8d" >> $GITHUB_OUTPUT fi # 编译打包 - name: Build With Gradle @@ -59,7 +59,7 @@ jobs: if [[ ${{ steps.config.outputs.need_create_release }} != 'true' ]];then path="$GITHUB_WORKSPACE/app/build/outputs/apk/debug" fi - echo ::set-output name=file_path::"$path" + echo file_path="$path" >> $GITHUB_OUTPUT - name: Upload Lanzou run: | echo "上传APP至蓝奏云" diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml new file mode 100644 index 0000000..5367e8e --- /dev/null +++ b/.idea/assetWizardSettings.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index ded8613..443bdfd 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,21 +3,35 @@ diff --git a/app/build.gradle b/app/build.gradle index 4adf40e..f7282fc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -293,7 +293,11 @@ dependencies { } //https://github.com/fengyuecanzhu/Maple - implementation("me.fycz.maple:maple:1.9") + implementation("me.fycz.maple:maple:2.0") + + //加解密类库,有些书源使用 + //noinspection GradleDependency,GradlePackageUpdate + implementation('cn.hutool:hutool-crypto:5.8.10') } greendao { diff --git a/app/release.md b/app/release.md index 87aa655..eee13ce 100644 --- a/app/release.md +++ b/app/release.md @@ -1,9 +1,5 @@ -* 1、修复阅读界面概率性闪退的问题 -* 2、关于界面新增插件加载结果 -* 3、修复书源订阅失败的问题 -* 4、修复字体下载失败的问题 -* 5、\[设置-缓存设置\]新增清除广告文件 -* 6、修复检查更新失败的问题 -* 7、修复获取更新链接失败的问题 -* 8、更新订阅书源链接 -* 9、修复其他已知bug \ No newline at end of file +* 1、[书籍详情界面]取消书籍简介展开时最大行数限制 +* 2、修复从数据库中读取章节时部分数据项缺失的bug +* 3、目录列表添加更新时间显示 +* 4、更新部分书源接口 +* 5、修复插件加载bug \ No newline at end of file diff --git a/app/src/main/assets/updatelog.fy b/app/src/main/assets/updatelog.fy index da9e060..eb771b1 100644 --- a/app/src/main/assets/updatelog.fy +++ b/app/src/main/assets/updatelog.fy @@ -1,3 +1,11 @@ +2022.12.26 +风月读书v2.5.1 +1、[书籍详情界面]取消书籍简介展开时最大行数限制 +2、修复从数据库中读取章节时部分数据项缺失的bug +3、目录列表添加更新时间显示 +4、更新部分书源接口 +5、修复插件加载bug + 2022.08.02 风月读书v2.5.0 1、修复阅读界面概率性闪退的问题 diff --git a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java index 0250f26..28f3340 100644 --- a/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java +++ b/app/src/main/java/xyz/fycz/myreader/common/APPCONST.java @@ -18,6 +18,7 @@ package xyz.fycz.myreader.common; +import android.annotation.SuppressLint; import android.os.Environment; import android.provider.Settings; @@ -31,13 +32,15 @@ import xyz.fycz.myreader.util.utils.FileUtils; import java.io.File; import java.lang.reflect.Type; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Map; import java.util.regex.Pattern; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; - +@SuppressLint("SimpleDateFormat") public class APPCONST { public static String publicKey = "fyds2.0";//服务端公钥 @@ -163,6 +166,8 @@ public class APPCONST { public static final String androidId = getAndroidId(); + public static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + public static String getAndroidId() { return Settings.System.getString(App.getmContext().getContentResolver(), Settings.Secure.ANDROID_ID); } diff --git a/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java b/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java index bda9661..bc649f3 100644 --- a/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java +++ b/app/src/main/java/xyz/fycz/myreader/common/URLCONST.java @@ -56,7 +56,8 @@ public class URLCONST { public static final String QUOTATION = "https://v1.hitokoto.cn/?encode=json&charset=utf-8"; - public static final String DEFAULT_PLUGIN_CONFIG_URL = "https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/Plugin/release/config_FYReader.json"; + //public static final String DEFAULT_PLUGIN_CONFIG_URL = "https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/Plugin/release/config_FYReader.json"; + public static final String DEFAULT_PLUGIN_CONFIG_URL = "http://101.43.83.105:3000/fengyue/FYReader-Res/raw/branch/main/Plugin/release/config_FYReader.json"; public static String getDefaultDomain() { return SharedPreUtils.getInstance().getString("domain", "fycz.me"); diff --git a/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt b/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt index f6f18c4..2ed1685 100644 --- a/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt +++ b/app/src/main/java/xyz/fycz/myreader/entity/PluginConfig.kt @@ -31,5 +31,5 @@ data class PluginConfig( val changelog: String ) { constructor(name: String, versionCode: Int) : - this(name, versionCode, "", "", "", "") + this(name, versionCode, "", "", "", "插件加载失败,当前为默认插件") } diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt b/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt index 5b0fa0d..0219964 100644 --- a/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt +++ b/app/src/main/java/xyz/fycz/myreader/greendao/service/CacheManager.kt @@ -19,6 +19,7 @@ package xyz.fycz.myreader.greendao.service import android.database.Cursor +import androidx.collection.LruCache import xyz.fycz.myreader.application.App import xyz.fycz.myreader.greendao.DbManager import xyz.fycz.myreader.greendao.entity.Cache @@ -31,6 +32,7 @@ import java.lang.Exception object CacheManager { private val queryTTFMap = hashMapOf>() + private val memoryLruCache = object : LruCache(100) {} /** * saveTime 单位为秒 @@ -49,6 +51,19 @@ object CacheManager { } } + fun putMemory(key: String, value: String) { + memoryLruCache.put(key, value) + } + + //从内存中获取数据 使用lruCache + fun getFromMemory(key: String): String? { + return memoryLruCache.get(key) + } + + fun deleteMemory(key: String) { + memoryLruCache.remove(key) + } + fun get(key: String): String? { var str: String? = null try { @@ -94,6 +109,14 @@ object CacheManager { return null } + fun putFile(key: String, value: String, saveTime: Int = 0) { + ACache.get().put(key, value, saveTime) + } + + fun getFile(key: String): String? { + return ACache.get().getAsString(key) + } + fun delete(key: String) { DbManager.getDaoSession().cacheDao.deleteByKey(key) ACache.get(App.getmContext()).remove(key) diff --git a/app/src/main/java/xyz/fycz/myreader/greendao/service/ChapterService.java b/app/src/main/java/xyz/fycz/myreader/greendao/service/ChapterService.java index 20e1bbd..2385020 100644 --- a/app/src/main/java/xyz/fycz/myreader/greendao/service/ChapterService.java +++ b/app/src/main/java/xyz/fycz/myreader/greendao/service/ChapterService.java @@ -66,6 +66,7 @@ public class ChapterService extends BaseService { chapter.setContent(cursor.getString(8)); chapter.setStart(cursor.getInt(9)); chapter.setEnd(cursor.getInt(10)); + chapter.setVariable(cursor.getString(11)); chapters.add(chapter); } } catch (Exception e) { @@ -95,10 +96,14 @@ public class ChapterService extends BaseService { if (StringHelper.isEmpty(bookId)) return new ArrayList<>(); + return DbManager.getDaoSession().getChapterDao() + .queryBuilder() + .where(ChapterDao.Properties.BookId.eq(bookId)) + .orderAsc(ChapterDao.Properties.Number) + .list(); + /*String sql = "select * from chapter where book_id = ? order by number"; - String sql = "select * from chapter where book_id = ? order by number"; - - return findChapters(sql, new String[]{bookId}); + return findChapters(sql, new String[]{bookId});*/ } /** diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsEncodeUtils.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsEncodeUtils.kt new file mode 100644 index 0000000..8d804aa --- /dev/null +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsEncodeUtils.kt @@ -0,0 +1,496 @@ +/* + * This file is part of FYReader. + * FYReader is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FYReader is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with FYReader. If not, see . + * + * Copyright (C) 2020 - 2022 fengyuecanzhu + */ + +package xyz.fycz.myreader.model.third3.analyzeRule + +import android.util.Base64 +import cn.hutool.crypto.digest.DigestUtil +import cn.hutool.crypto.digest.HMac +import cn.hutool.crypto.symmetric.SymmetricCrypto +import xyz.fycz.myreader.util.utils.MD5Utils + +/** + * js加解密扩展类, 在js中通过java变量调用 + * 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md + */ +interface JsEncodeUtils { + + fun md5Encode(str: String): String { + return MD5Utils.md5Encode(str) + } + + fun md5Encode16(str: String): String { + return MD5Utils.md5Encode16(str) + } + + + //******************对称加密解密************************// + + /** + * 在js中这样使用 + * java.createSymmetricCrypto(transformation, key, iv).decrypt(data) + * java.createSymmetricCrypto(transformation, key, iv).decryptStr(data) + + * java.createSymmetricCrypto(transformation, key, iv).encrypt(data) + * java.createSymmetricCrypto(transformation, key, iv).encryptBase64(data) + * java.createSymmetricCrypto(transformation, key, iv).encryptHex(data) + */ + + /* 调用SymmetricCrypto key为null时使用随机密钥*/ + fun createSymmetricCrypto( + transformation: String, + key: ByteArray?, + iv: ByteArray? + ): SymmetricCrypto { + val symmetricCrypto = SymmetricCrypto(transformation, key) + return if (iv != null && iv.isNotEmpty()) symmetricCrypto.setIv(iv) else symmetricCrypto + } + + fun createSymmetricCrypto( + transformation: String, + key: ByteArray + ): SymmetricCrypto { + return createSymmetricCrypto(transformation, key, null) + } + + fun createSymmetricCrypto( + transformation: String, + key: String + ): SymmetricCrypto { + return createSymmetricCrypto(transformation, key, null) + } + + fun createSymmetricCrypto( + transformation: String, + key: String, + iv: String? + ): SymmetricCrypto { + return createSymmetricCrypto( + transformation, key.encodeToByteArray(), iv?.encodeToByteArray() + ) + } + + //******************对称加密解密old************************// + + /////AES + /** + * AES 解码为 ByteArray + * @param str 传入的AES加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decrypt(str)") + ) + fun aesDecodeToByteArray( + str: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).decrypt(str) + } + + /** + * AES 解码为 String + * @param str 传入的AES加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(str)") + ) + fun aesDecodeToString( + str: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(str) + } + + /** + * AES解码为String,算法参数经过Base64加密 + * + * @param data 加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 解密后的字符串 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun aesDecodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto( + "AES/${mode}/${padding}", + Base64.decode(key, Base64.NO_WRAP), + Base64.decode(iv, Base64.NO_WRAP) + ).decryptStr(data) + } + + /** + * 已经base64的AES 解码为 ByteArray + * @param str 传入的AES Base64加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decrypt(str)") + ) + fun aesBase64DecodeToByteArray( + str: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).decrypt(str) + } + + /** + * 已经base64的AES 解码为 String + * @param str 传入的AES Base64加密的数据 + * @param key AES 解密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(str)") + ) + fun aesBase64DecodeToString( + str: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(str) + } + + /** + * 加密aes为ByteArray + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decrypt(data)") + ) + fun aesEncodeToByteArray( + data: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).decrypt(data) + } + + /** + * 加密aes为String + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun aesEncodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(data) + } + + /** + * 加密aes后Base64化的ByteArray + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data).toByteArray()") + ) + fun aesEncodeToBase64ByteArray( + data: String, key: String, transformation: String, iv: String + ): ByteArray? { + return createSymmetricCrypto(transformation, key, iv).encryptBase64(data).toByteArray() + } + + /** + * 加密aes后Base64化的String + * @param data 传入的原始数据 + * @param key AES加密的key + * @param transformation AES加密的方式 + * @param iv ECB模式的偏移向量 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun aesEncodeToBase64String( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).encryptBase64(data) + } + + + /** + * AES加密并转为Base64,算法参数经过Base64加密 + * + * @param data 被加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 加密后的Base64 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun aesEncodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto("AES/${mode}/${padding}", key, iv).encryptBase64(data) + } + + /////DES + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun desDecodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(data) + } + + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun desBase64DecodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).decryptStr(data) + } + + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encrypt(data)") + ) + fun desEncodeToString( + data: String, key: String, transformation: String, iv: String + ): String? { + return String(createSymmetricCrypto(transformation, key, iv).encrypt(data)) + } + + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun desEncodeToBase64String( + data: String, key: String, transformation: String, iv: String + ): String? { + return createSymmetricCrypto(transformation, key, iv).encryptBase64(data) + } + + //////3DES + /** + * 3DES解密 + * + * @param data 加密的字符串 + * @param key 密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv 加盐 + * @return 解密后的字符串 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun tripleDESDecodeStr( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto("DESede/${mode}/${padding}", key, iv).decryptStr(data) + } + + /** + * 3DES解密,算法参数经过Base64加密 + * + * @param data 加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 解密后的字符串 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).decryptStr(data)") + ) + fun tripleDESDecodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto( + "DESede/${mode}/${padding}", + Base64.decode(key, Base64.NO_WRAP), + iv.encodeToByteArray() + ).decryptStr(data) + } + + + /** + * 3DES加密并转为Base64 + * + * @param data 被加密的字符串 + * @param key 密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv 加盐 + * @return 加密后的Base64 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun tripleDESEncodeBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto("DESede/${mode}/${padding}", key, iv) + .encryptBase64(data) + } + + /** + * 3DES加密并转为Base64,算法参数经过Base64加密 + * + * @param data 被加密的字符串 + * @param key Base64后的密钥 + * @param mode 模式 + * @param padding 补码方式 + * @param iv Base64后的加盐 + * @return 加密后的Base64 + */ + @Deprecated( + "过于繁琐弃用", + ReplaceWith("createSymmetricCrypto(transformation, key, iv).encryptBase64(data)") + ) + fun tripleDESEncodeArgsBase64Str( + data: String, + key: String, + mode: String, + padding: String, + iv: String + ): String? { + return createSymmetricCrypto( + "DESede/${mode}/${padding}", + Base64.decode(key, Base64.NO_WRAP), + iv.encodeToByteArray() + ).encryptBase64(data) + } + +//******************消息摘要/散列消息鉴别码************************// + + /** + * 生成摘要,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @return 16进制字符串 + */ + fun digestHex( + data: String, + algorithm: String, + ): String { + return DigestUtil.digester(algorithm).digestHex(data) + } + + /** + * 生成摘要,并转为Base64字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @return Base64字符串 + */ + fun digestBase64Str( + data: String, + algorithm: String, + ): String { + return Base64.encodeToString(DigestUtil.digester(algorithm).digest(data), Base64.NO_WRAP) + } + + /** + * 生成散列消息鉴别码,并转为16进制字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @param key 密钥 + * @return 16进制字符串 + */ + @Suppress("FunctionName") + fun HMacHex( + data: String, + algorithm: String, + key: String + ): String { + return HMac(algorithm, key.toByteArray()).digestHex(data) + } + + /** + * 生成散列消息鉴别码,并转为Base64字符串 + * + * @param data 被摘要数据 + * @param algorithm 签名算法 + * @param key 密钥 + * @return Base64字符串 + */ + @Suppress("FunctionName") + fun HMacBase64( + data: String, + algorithm: String, + key: String + ): String { + return Base64.encodeToString( + HMac(algorithm, key.toByteArray()).digest(data), + Base64.NO_WRAP + ) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt index 82cda80..98bd8b9 100644 --- a/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt +++ b/app/src/main/java/xyz/fycz/myreader/model/third3/analyzeRule/JsExtensions.kt @@ -22,19 +22,22 @@ import android.net.Uri import android.util.Base64 import android.util.Log import androidx.annotation.Keep +import cn.hutool.core.util.HexUtil import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking -import nl.siegmann.epublib.epub.PackageDocumentBase.dateFormat import org.jsoup.Connection import org.jsoup.Jsoup import xyz.fycz.myreader.application.App import xyz.fycz.myreader.common.APPCONST +import xyz.fycz.myreader.common.APPCONST.dateFormat import xyz.fycz.myreader.greendao.service.CacheManager import xyz.fycz.myreader.greendao.service.CookieStore import xyz.fycz.myreader.model.third3.BaseSource import xyz.fycz.myreader.model.third3.Debug +import xyz.fycz.myreader.model.third3.NoStackTraceException import xyz.fycz.myreader.model.third3.http.* +import xyz.fycz.myreader.util.ToastUtils import xyz.fycz.myreader.util.ZipUtils import xyz.fycz.myreader.util.utils.* import java.io.ByteArrayInputStream @@ -54,7 +57,7 @@ import java.util.zip.ZipInputStream */ @Keep @Suppress("unused") -interface JsExtensions { +interface JsExtensions : JsEncodeUtils { val TAG: String? get() = JsExtensions::class.simpleName @@ -141,11 +144,52 @@ interface JsExtensions { BackstageWebView( url = url, html = html, - javaScript = js + javaScript = js, + headerMap = getSource()?.getHeaderMap(true), + tag = getSource()?.getKey() ).getStrResponse().body } } + /** + * 可从网络,本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本 + */ + fun importScript(path: String): String { + val result = when { + path.startsWith("http") -> cacheFile(path) ?: "" + path.isUri() -> String(DocumentUtil.readBytes(App.getmContext(), Uri.parse(path))) + path.startsWith("/storage") -> FileUtils.readText(path) + else -> readTxtFile(path) + } + if (result.isBlank()) throw NoStackTraceException("$path 内容获取失败或者为空") + return result + } + + /** + * 缓存以文本方式保存的文件 如.js .txt等 + * @param urlStr 网络文件的链接 + * @return 返回缓存后的文件内容 + */ + fun cacheFile(urlStr: String): String? { + return cacheFile(urlStr, 0) + } + + /** + * 缓存以文本方式保存的文件 如.js .txt等 + * @param saveTime 缓存时间,单位:秒 + */ + fun cacheFile(urlStr: String, saveTime: Int): String? { + val key = md5Encode16(urlStr) + val cache = CacheManager.getFile(key) + if (cache.isNullOrBlank()) { + log("首次下载 $urlStr") + val value = ajax(urlStr) ?: return null + CacheManager.putFile(key, value, saveTime) + return value + } + return cache + } + /** * 实现16进制字符串转文件 * @param content 需要转成文件的16进制字符串 @@ -172,20 +216,45 @@ interface JsExtensions { * js实现重定向拦截,网络访问get */ fun get(urlStr: String, headers: Map): Connection.Response { - return Jsoup.connect(urlStr) + val response = Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) .headers(headers) .method(Connection.Method.GET) .execute() + val cookies = response.cookies() + CookieStore.mapToCookie(cookies)?.let { + val domain = NetworkUtils.getSubDomain(urlStr) + CacheManager.putMemory("${domain}_cookieJar", it) + } + return response + } + + /** + * js实现重定向拦截,网络访问head,不返回Response Body更省流量 + */ + fun head(urlStr: String, headers: Map): Connection.Response { + val response = Jsoup.connect(urlStr) + .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) + .ignoreContentType(true) + .followRedirects(false) + .headers(headers) + .method(Connection.Method.HEAD) + .execute() + val cookies = response.cookies() + CookieStore.mapToCookie(cookies)?.let { + val domain = NetworkUtils.getSubDomain(urlStr) + CacheManager.putMemory("${domain}_cookieJar", it) + } + return response } /** * 网络访问post */ fun post(urlStr: String, body: String, headers: Map): Connection.Response { - return Jsoup.connect(urlStr) + val response = Jsoup.connect(urlStr) .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory) .ignoreContentType(true) .followRedirects(false) @@ -193,6 +262,12 @@ interface JsExtensions { .headers(headers) .method(Connection.Method.POST) .execute() + val cookies = response.cookies() + CookieStore.mapToCookie(cookies)?.let { + val domain = NetworkUtils.getSubDomain(urlStr) + CacheManager.putMemory("${domain}_cookieJar", it) + } + return response } /** @@ -241,12 +316,19 @@ interface JsExtensions { return EncoderUtils.base64Encode(str, flags) } - fun md5Encode(str: String): String { - return MD5Utils.md5Encode(str) + /* HexString 解码为字节数组 */ + fun hexDecodeToByteArray(hex: String): ByteArray? { + return HexUtil.decodeHex(hex) + } + + /* hexString 解码为utf8String*/ + fun hexDecodeToString(hex: String): String? { + return HexUtil.decodeHexStr(hex) } - fun md5Encode16(str: String): String { - return MD5Utils.md5Encode16(str) + /* utf8 编码为hexString */ + fun hexEncodeToString(utf8: String): String? { + return HexUtil.encodeHexStr(utf8) } /** @@ -499,174 +581,48 @@ interface JsExtensions { } /** - * 输出调试日志 + * 弹窗提示 */ - fun log(msg: String): String { - getSource()?.let { - Debug.log(it.getKey(), msg) - } ?: Debug.log(msg) - if (App.isDebug()) { - Log.d(TAG + "-" + getSource()?.getKey(), msg) - } - return msg + fun toast(msg: Any?) { + ToastUtils.showInfo("${getSource()?.getTag()}: ${msg.toString()}") } /** - * 生成UUID + * 弹窗提示 停留时间较长 */ - fun randomUUID(): String { - return UUID.randomUUID().toString() + fun longToast(msg: Any?) { + toast(msg) } /** - * AES 解码为 ByteArray - * @param str 传入的AES加密的数据 - * @param key AES 解密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 - */ - fun aesDecodeToByteArray( - str: String, key: String, transformation: String, iv: String - ): ByteArray? { - return try { - EncoderUtils.decryptAES( - data = str.encodeToByteArray(), - key = key.encodeToByteArray(), - transformation, - iv.encodeToByteArray() - ) - } catch (e: Exception) { - Log.e(TAG, e.toString()) - log(e.localizedMessage ?: "aesDecodeToByteArrayERROR") - null - } - } - - /** - * AES 解码为 String - * @param str 传入的AES加密的数据 - * @param key AES 解密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 - */ - - fun aesDecodeToString( - str: String, key: String, transformation: String, iv: String - ): String? { - return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) } - } - - /** - * 已经base64的AES 解码为 ByteArray - * @param str 传入的AES Base64加密的数据 - * @param key AES 解密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 - */ - - fun aesBase64DecodeToByteArray( - str: String, key: String, transformation: String, iv: String - ): ByteArray? { - return try { - EncoderUtils.decryptBase64AES( - str.encodeToByteArray(), - key.encodeToByteArray(), - transformation, - iv.encodeToByteArray() - ) - } catch (e: Exception) { - Log.e(TAG, e.toString()) - log(e.localizedMessage ?: "aesDecodeToByteArrayERROR") - null - } - } - - /** - * 已经base64的AES 解码为 String - * @param str 传入的AES Base64加密的数据 - * @param key AES 解密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 - */ - - fun aesBase64DecodeToString( - str: String, key: String, transformation: String, iv: String - ): String? { - return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) } - } - - /** - * 加密aes为ByteArray - * @param data 传入的原始数据 - * @param key AES加密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 + * 输出调试日志 */ - fun aesEncodeToByteArray( - data: String, key: String, transformation: String, iv: String - ): ByteArray? { - return try { - EncoderUtils.encryptAES( - data.encodeToByteArray(), - key = key.encodeToByteArray(), - transformation, - iv.encodeToByteArray() - ) - } catch (e: Exception) { - Log.e(TAG, e.toString()) - log(e.localizedMessage ?: "aesEncodeToByteArrayERROR") - null + fun log(msg: String): String { + getSource()?.let { + Debug.log(it.getKey(), msg) + } ?: Debug.log(msg) + if (App.isDebug()) { + Log.d(TAG + "-" + getSource()?.getKey(), msg) } + return msg } /** - * 加密aes为String - * @param data 传入的原始数据 - * @param key AES加密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 - */ - fun aesEncodeToString( - data: String, key: String, transformation: String, iv: String - ): String? { - return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) } - } - - /** - * 加密aes后Base64化的ByteArray - * @param data 传入的原始数据 - * @param key AES加密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 + * 输出对象类型 */ - fun aesEncodeToBase64ByteArray( - data: String, key: String, transformation: String, iv: String - ): ByteArray? { - return try { - EncoderUtils.encryptAES2Base64( - data.encodeToByteArray(), - key.encodeToByteArray(), - transformation, - iv.encodeToByteArray() - ) - } catch (e: Exception) { - Log.e(TAG, e.toString()) - log(e.localizedMessage ?: "aesEncodeToBase64ByteArrayERROR") - null + fun logType(any: Any?) { + if (any == null) { + log("null") + } else { + log(any.javaClass.name) } } /** - * 加密aes后Base64化的String - * @param data 传入的原始数据 - * @param key AES加密的key - * @param transformation AES加密的方式 - * @param iv ECB模式的偏移向量 + * 生成UUID */ - fun aesEncodeToBase64String( - data: String, key: String, transformation: String, iv: String - ): String? { - return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) } + fun randomUUID(): String { + return UUID.randomUUID().toString() } fun android(): String { diff --git a/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java b/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java index 9572509..851d8d1 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/activity/BookDetailedActivity.java @@ -361,7 +361,7 @@ public class BookDetailedActivity extends BaseActivity() { diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookMarkAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookMarkAdapter.java index dad8fa8..5d567ca 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookMarkAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/BookMarkAdapter.java @@ -67,7 +67,6 @@ public class BookMarkAdapter extends ArrayAdapter { viewHolder = new ViewHolder(); convertView = LayoutInflater.from(getContext()).inflate(mResourceId,null); viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_chapter_title); - viewHolder.vLine = (View) convertView.findViewById(R.id.v_line); convertView.setTag(viewHolder); }else { viewHolder = (ViewHolder) convertView.getTag(); @@ -78,14 +77,7 @@ public class BookMarkAdapter extends ArrayAdapter { private void initView(int postion,final ViewHolder viewHolder){ final BookMark bookMark = getItem(postion); - assert bookMark != null; viewHolder.tvTitle.setText(String.format("%s[%s]", bookMark.getTitle(), bookMark.getBookMarkReadPosition() + 1)); - if (ChapterService.isChapterCached(bookMark)){ - viewHolder.tvTitle.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(getContext(), R.drawable.selector_category_load),null,null,null); - } else { - viewHolder.tvTitle.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(getContext(),R.drawable.selector_category_unload),null,null,null); - } - viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.textSecondary)); /*if (!setting.isDayStyle()) { viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.sys_night_word)); viewHolder.vLine.setBackground(getContext().getDrawable(R.color.sys_dialog_setting_line)); @@ -134,9 +126,7 @@ public class BookMarkAdapter extends ArrayAdapter { } class ViewHolder{ - TextView tvTitle; - View vLine; } } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/ChapterTitleAdapter.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/ChapterTitleAdapter.java index 8e115b3..99f7b49 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/ChapterTitleAdapter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/ChapterTitleAdapter.java @@ -19,12 +19,15 @@ package xyz.fycz.myreader.ui.adapter; import android.content.Context; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Filter; +import android.widget.ImageView; import android.widget.TextView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -37,7 +40,6 @@ import xyz.fycz.myreader.greendao.entity.Chapter; import xyz.fycz.myreader.greendao.service.ChapterService; import java.util.ArrayList; -import java.util.Collections; import java.util.List; @@ -72,8 +74,9 @@ public class ChapterTitleAdapter extends ArrayAdapter { if (convertView == null) { viewHolder = new ViewHolder(); convertView = LayoutInflater.from(getContext()).inflate(mResourceId, null); - viewHolder.tvTitle = (TextView) convertView.findViewById(R.id.tv_chapter_title); - viewHolder.vLine = (View) convertView.findViewById(R.id.v_line); + viewHolder.tvTitle = convertView.findViewById(R.id.tv_chapter_title); + viewHolder.tvTag = convertView.findViewById(R.id.tv_tag); + viewHolder.ivIcon = convertView.findViewById(R.id.iv_icon); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); @@ -86,21 +89,30 @@ public class ChapterTitleAdapter extends ArrayAdapter { final Chapter chapter = getItem(postion); // viewHolder.tvTitle.setText("【" + chapter.getTitle() + "】"); viewHolder.tvTitle.setText(chapter.getTitle()); + //viewHolder.ivIcon.setImageResource(R.drawable.ic_cloud_download); if (ChapterService.isChapterCached(chapter) || chapter.getEnd() > 0) { - viewHolder.tvTitle.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(getContext(),R.drawable.selector_category_load), null, null, null); + viewHolder.ivIcon.setVisibility(View.INVISIBLE); + viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.textPrimary)); } else { - viewHolder.tvTitle.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(getContext(),R.drawable.selector_category_unload), null, null, null); + viewHolder.ivIcon.setVisibility(View.VISIBLE); + viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.textSecondary)); } - viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.textSecondary)); /*if (!setting.isDayStyle()) { viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.sys_night_word)); viewHolder.vLine.setBackground(getContext().getDrawable(R.color.sys_dialog_setting_line)); }else { viewHolder.tvTitle.setTextColor(getContext().getColor(R.color.title_black)); }*/ - + if (TextUtils.isEmpty(chapter.getUpdateTime())) { + viewHolder.tvTag.setVisibility(View.GONE); + } else { + viewHolder.tvTag.setText(chapter.getUpdateTime()); + viewHolder.tvTag.setVisibility(View.VISIBLE); + } if (chapter.getNumber() == mBook.getHisttoryChapterNum()) { viewHolder.tvTitle.setTextColor(getContext().getResources().getColor(R.color.colorAccent)); + /*viewHolder.ivIcon.setImageResource(R.drawable.ic_check); + viewHolder.ivIcon.setVisibility(View.VISIBLE);*/ } } @@ -152,7 +164,8 @@ public class ChapterTitleAdapter extends ArrayAdapter { class ViewHolder { TextView tvTitle; - View vLine; + TextView tvTag; + ImageView ivIcon; } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/CatalogHolder.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/CatalogHolder.java index d73b67d..6b8f5cf 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/CatalogHolder.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/CatalogHolder.java @@ -18,6 +18,8 @@ package xyz.fycz.myreader.ui.adapter.holder; +import android.view.View; +import android.widget.ImageView; import android.widget.TextView; import androidx.core.content.ContextCompat; @@ -34,23 +36,21 @@ import xyz.fycz.myreader.greendao.service.ChapterService; */ public class CatalogHolder extends ViewHolderImpl { private TextView tvTitle; + private ImageView ivIcon; @Override protected int getItemLayoutId() { - return R.layout.listview_chapter_title_item; + return R.layout.item_chapter; } @Override public void initView() { tvTitle = findById(R.id.tv_chapter_title); + ivIcon = findById(R.id.iv_icon); } @Override public void onBind(RecyclerView.ViewHolder holder, Chapter data, int pos) { - if (ChapterService.isChapterCached(data) || data.getEnd() > 0) { - tvTitle.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(getContext(),R.drawable.selector_category_load), null, null, null); - } else { - tvTitle.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(getContext(),R.drawable.selector_category_unload), null, null, null); - } tvTitle.setText(data.getTitle()); + ivIcon.setVisibility(View.GONE); } } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/FindBookHolder.java b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/FindBookHolder.java index 56c3783..673d3e5 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/FindBookHolder.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/adapter/holder/FindBookHolder.java @@ -49,6 +49,7 @@ import xyz.fycz.myreader.webapi.BookApi; import xyz.fycz.myreader.webapi.crawler.ReadCrawlerUtil; import xyz.fycz.myreader.webapi.crawler.base.BookInfoCrawler; import xyz.fycz.myreader.webapi.crawler.base.ReadCrawler; +import xyz.fycz.myreader.webapi.crawler.source.ThirdCrawler; import xyz.fycz.myreader.widget.CoverImageView; /** @@ -96,25 +97,29 @@ public class FindBookHolder extends ViewHolderImpl { tvBookName.setText(data.getName()); if (!StringHelper.isEmpty(data.getAuthor())) { tvAuthor.setText(data.getAuthor()); - }else { + } else { tvAuthor.setText(""); } initTagList(data); if (!StringHelper.isEmpty(data.getNewestChapterTitle())) { tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, data.getNewestChapterTitle())); + tvNewestChapter.setVisibility(View.VISIBLE); } else { data.setNewestChapterTitle(""); tvNewestChapter.setText(""); + tvNewestChapter.setVisibility(View.GONE); } if (!StringHelper.isEmpty(data.getDesc())) { tvDesc.setText(String.format("简介:%s", data.getDesc())); + tvDesc.setVisibility(View.VISIBLE); } else { data.setDesc(""); tvDesc.setText(""); + tvDesc.setVisibility(View.GONE); } if (!StringHelper.isEmpty(source.getSourceName()) && !"未知书源".equals(source.getSourceName())) tvSource.setText(String.format("书源:%s", source.getSourceName())); - if (needGetInfo(data) && rc instanceof BookInfoCrawler) { + if (rc instanceof BookInfoCrawler && needGetInfo(data)) { Log.i(data.getName(), "initOtherInfo"); BookInfoCrawler bic = (BookInfoCrawler) rc; BookApi.getBookInfo(data, bic).compose(RxUtils::toSimpleSingle) @@ -132,9 +137,15 @@ public class FindBookHolder extends ViewHolderImpl { //简介 if (StringHelper.isEmpty(tvDesc.getText().toString())) { tvDesc.setText(String.format("简介:%s", book.getDesc())); + tvNewestChapter.setVisibility(View.VISIBLE); + } else { + tvNewestChapter.setVisibility(View.GONE); } if (StringHelper.isEmpty(tvNewestChapter.getText().toString())) { tvNewestChapter.setText(getContext().getString(R.string.newest_chapter, book.getNewestChapterTitle())); + tvNewestChapter.setVisibility(View.VISIBLE); + } else { + tvNewestChapter.setVisibility(View.GONE); } if (!StringHelper.isEmpty(book.getAuthor())) { tvAuthor.setText(book.getAuthor()); @@ -163,6 +174,7 @@ public class FindBookHolder extends ViewHolderImpl { tflBookTag.setAdapter(new BookTagAdapter(getContext(), tagList, 11)); } } + private boolean needGetInfo(Book bookBean) { if (StringHelper.isEmpty(bookBean.getAuthor())) return true; if (StringHelper.isEmpty(bookBean.getType())) return true; @@ -170,10 +182,11 @@ public class FindBookHolder extends ViewHolderImpl { if (StringHelper.isEmpty(bookBean.getNewestChapterTitle())) return true; return StringHelper.isEmpty(bookBean.getImgUrl()); } + /** * Here is the key method to apply the animation */ - protected void setAnimation(View viewToAnimate, int position){ + protected void setAnimation(View viewToAnimate, int position) { // If the bound view wasn't previously displayed on screen, it's animated Animation animation = AnimationUtils.loadAnimation(viewToAnimate.getContext(), R.anim.anim_recycle_item); diff --git a/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java b/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java index 88a4f1e..b0cc145 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/fragment/MineFragment.java @@ -37,6 +37,7 @@ import androidx.activity.result.contract.ActivityResultContract; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.hjq.permissions.OnPermissionCallback; import com.kongzue.dialogx.dialogs.BottomMenu; import org.jetbrains.annotations.NotNull; @@ -45,6 +46,7 @@ import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.List; import io.reactivex.Single; import io.reactivex.SingleOnSubscribe; @@ -84,6 +86,7 @@ import xyz.fycz.myreader.util.ToastUtils; import xyz.fycz.myreader.util.utils.AdUtils; import xyz.fycz.myreader.util.utils.NetworkUtils; import xyz.fycz.myreader.util.utils.RxUtils; +import xyz.fycz.myreader.util.utils.StoragePermissionUtils; /** * @author fengyue @@ -205,7 +208,7 @@ public class MineFragment extends BaseFragment { @Override protected void initClick() { super.initClick(); - binding.mineRlUser.setOnClickListener(v -> { + binding.mineRlUser.setOnClickListener(v -> StoragePermissionUtils.request(this, (permissions, all) -> { if (isLogin) { Intent intent = new Intent(getActivity(), UserInfoActivity.class); startActivityForResult(intent, APPCONST.REQUEST_LOGOUT); @@ -213,7 +216,7 @@ public class MineFragment extends BaseFragment { Intent intent = new Intent(getActivity(), LoginActivity.class); getActivity().startActivityForResult(intent, APPCONST.REQUEST_LOGIN); } - }); + })); binding.mineRlSyn.setOnClickListener(v -> { if (!isLogin) { ToastUtils.showWarring("请先登录!"); diff --git a/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookMarkPresenter.java b/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookMarkPresenter.java index b3ad661..ed396bc 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookMarkPresenter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/presenter/BookMarkPresenter.java @@ -21,7 +21,6 @@ package xyz.fycz.myreader.ui.presenter; import android.app.Activity; import android.content.Intent; import xyz.fycz.myreader.R; -import xyz.fycz.myreader.application.SysManager; import xyz.fycz.myreader.base.BasePresenter; import xyz.fycz.myreader.common.APPCONST; import xyz.fycz.myreader.greendao.entity.Book; @@ -74,7 +73,7 @@ public class BookMarkPresenter implements BasePresenter { private void initBookMarkList() { mBookMarks = (ArrayList) mBookMarkService.findBookAllBookMarkByBookId(mBook.getId()); - mBookMarkAdapter = new BookMarkAdapter(mBookMarkFragment.getActivity(), R.layout.listview_chapter_title_item, mBookMarks); + mBookMarkAdapter = new BookMarkAdapter(mBookMarkFragment.getActivity(), R.layout.item_chapter, mBookMarks); mBookMarkFragment.getLvBookmarkList().setAdapter(mBookMarkAdapter); } diff --git a/app/src/main/java/xyz/fycz/myreader/ui/presenter/CatalogPresenter.java b/app/src/main/java/xyz/fycz/myreader/ui/presenter/CatalogPresenter.java index 07fcba4..ba3123e 100644 --- a/app/src/main/java/xyz/fycz/myreader/ui/presenter/CatalogPresenter.java +++ b/app/src/main/java/xyz/fycz/myreader/ui/presenter/CatalogPresenter.java @@ -24,7 +24,6 @@ import android.view.View; import org.jetbrains.annotations.NotNull; -import io.reactivex.disposables.Disposable; import xyz.fycz.myreader.R; import xyz.fycz.myreader.application.App; import xyz.fycz.myreader.base.BasePresenter; @@ -137,7 +136,7 @@ public class CatalogPresenter implements BasePresenter { //设置布局管理器 int curChapterPosition; curChapterPosition = mBook.getHisttoryChapterNum(); - mChapterTitleAdapter = new ChapterTitleAdapter(mCatalogFragment.getContext(), R.layout.listview_chapter_title_item, mChapters, mBook); + mChapterTitleAdapter = new ChapterTitleAdapter(mCatalogFragment.getContext(), R.layout.item_chapter, mChapters, mBook); mCatalogFragment.getLvChapterList().setAdapter(mChapterTitleAdapter); mCatalogFragment.getLvChapterList().setSelection(curChapterPosition); } diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt index 58d8835..71ed2ad 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/ACache.kt @@ -52,7 +52,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) @JvmOverloads fun get( - ctx: Context, + ctx: Context = App.getmContext(), cacheName: String = "ACache", maxSize: Long = MAX_SIZE.toLong(), maxCount: Int = MAX_COUNT, diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java b/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java index 982c5c6..e484729 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/OkHttpUtils.java @@ -214,7 +214,9 @@ public class OkHttpUtils { } public static String getBakUpdateInfo() throws IOException { - return OkHttpUtils.getHtml("https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/FYReader-Update/" + + return OkHttpUtils.getHtml( + //"https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/FYReader-Update/" + + "http://101.43.83.105:3000/fengyue/FYReader-Res/raw/branch/main/Plugin/" + (App.isDebug() ? "debug" : "release") + "/content.txt"); } diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt index ba9e704..e6252ad 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/PluginUtils.kt @@ -58,24 +58,27 @@ object PluginUtils { val oldConfig = GSON.fromJsonObject( SharedPreUtils.getInstance().getString("pluginConfig") ) ?: PluginConfig("dynamic.dex", 100) - launch { loadAppLoader(App.getmContext(), config) } - val configJson = getProxyClient().newCallResponseBody { - url(pluginConfigUrl) - }.text() - config = GSON.fromJsonObject(configJson) - if (config != null) { + try { + val configJson = getProxyClient().newCallResponseBody(5) { + url(pluginConfigUrl) + }.text() + config = GSON.fromJsonObject(configJson) if (config!!.versionCode > oldConfig.versionCode) { downloadPlugin(config!!) - SharedPreUtils.getInstance().putString("pluginConfig", configJson) } - } else { + } catch (e: Exception) { config = oldConfig + e.printStackTrace() + errorMsg = e.stackTraceToString() } - if (config!!.md5.lowercase(Locale.getDefault()) - != getPluginMD5(config!!)?.lowercase(Locale.getDefault()) - ) { - downloadPlugin(config!!) + kotlin.runCatching { + if (config!!.md5.lowercase(Locale.getDefault()) + != getPluginMD5(config!!)?.lowercase(Locale.getDefault()) + ) { + downloadPlugin(config!!) + } } + Log.d(TAG, config!!.toString()) }.onSuccess { loadAppLoader(App.getmContext(), config) } @@ -117,6 +120,7 @@ object PluginUtils { hasLoad = true loadSuccess = true } + SharedPreUtils.getInstance().putString("pluginConfig", GSON.toJson(config)) } catch (e: Exception) { e.printStackTrace() errorMsg = e.stackTraceToString() diff --git a/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt b/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt index 0182804..87d191a 100644 --- a/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt +++ b/app/src/main/java/xyz/fycz/myreader/util/utils/StringExtensions.kt @@ -23,6 +23,7 @@ package xyz.fycz.myreader.util.utils import android.icu.text.Collator import android.icu.util.ULocale import android.net.Uri +import android.text.Editable import java.io.File import java.util.* @@ -30,14 +31,19 @@ fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true +fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) + fun String.parseToUri(): Uri { - return if (isContentScheme()) { - Uri.parse(this) - } else { + return if (isUri()) Uri.parse(this) else { Uri.fromFile(File(this)) } } +fun String?.isUri(): Boolean { + this ?: return false + return this.startsWith("file://", true) || isContentScheme() +} + fun String?.isAbsUrl() = this?.let { it.startsWith("http://", true) || it.startsWith("https://", true) @@ -65,8 +71,22 @@ fun String?.isJsonArray(): Boolean = str.startsWith("[") && str.endsWith("]") } ?: false -fun String.splitNotBlank(vararg delimiter: String): Array = run { - this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() +fun String?.isXml(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("<") && str.endsWith(">") + } ?: false + +fun String?.isTrue(nullIsTrue: Boolean = false): Boolean { + if (this.isNullOrBlank() || this == "null") { + return nullIsTrue + } + return !this.trim().matches("(?i)^(false|no|not|0)$".toRegex()) +} + +fun String.splitNotBlank(vararg delimiter: String, limit: Int = 0): Array = run { + this.split(*delimiter, limit = limit).map { it.trim() }.filterNot { it.isBlank() } + .toTypedArray() } fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { @@ -97,3 +117,4 @@ fun String.toStringArray(): Array { } } + diff --git a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/source/find/Third3FindCrawler.kt b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/source/find/Third3FindCrawler.kt index 3875cc8..83d74de 100644 --- a/app/src/main/java/xyz/fycz/myreader/webapi/crawler/source/find/Third3FindCrawler.kt +++ b/app/src/main/java/xyz/fycz/myreader/webapi/crawler/source/find/Third3FindCrawler.kt @@ -18,11 +18,95 @@ package xyz.fycz.myreader.webapi.crawler.source.find +import io.reactivex.Observable +import xyz.fycz.myreader.application.App +import xyz.fycz.myreader.entity.FindKind +import xyz.fycz.myreader.entity.thirdsource.source3.ExploreKind3 import xyz.fycz.myreader.greendao.entity.rule.BookSource +import xyz.fycz.myreader.util.utils.ACache +import xyz.fycz.myreader.util.utils.GSON +import xyz.fycz.myreader.util.utils.fromJsonArray +import xyz.fycz.myreader.util.utils.isJsonArray /** * @author fengyue * @date 2022/1/20 15:33 */ class Third3FindCrawler(source: BookSource) : ThirdFindCrawler(source) { + override fun initData(): Observable { + return Observable.create { emitter -> + val exploreUrl = source.findRule.url + if (exploreUrl.isNullOrBlank()) { + emitter.onNext(false) + emitter.onComplete() + return@create + } + val kinds = arrayListOf() + var ruleStr = exploreUrl + kotlin.runCatching { + if (exploreUrl.startsWith("", false) + || exploreUrl.startsWith("@js:", false) + ) { + val aCache = ACache.get(cacheName = "explore") + ruleStr = aCache.getAsString(source.sourceUrl) ?: "" + if (ruleStr.isBlank()) { + val jsStr = if (exploreUrl.startsWith("@")) { + exploreUrl.substring(4) + } else { + exploreUrl.substring(4, exploreUrl.lastIndexOf("<")) + } + ruleStr = source.evalJS(jsStr).toString().trim() + aCache.put(source.sourceUrl, ruleStr) + } + } + if (ruleStr.isJsonArray()) { + GSON.fromJsonArray(ruleStr)?.let { + kinds.addAll(it) + } + } else { + ruleStr.split("(&&|\n)+".toRegex()).forEach { kindStr -> + val kindCfg = kindStr.split("::") + kinds.add(ExploreKind3(kindCfg.getOrNull(0) ?: "", kindCfg.getOrNull(1))) + } + } + var children = arrayListOf() + var groupName = name + var nameCount = 0 + kinds.forEach { + if (it.title.isBlank()) { + if (children.size > 0) { + nameCount++ + kindsMap[groupName] = children + children = arrayListOf() + } + groupName = "$name[$nameCount]" + } else if (it.url.isNullOrBlank()) { + if (children.size > 0) { + kindsMap[groupName] = children + children = arrayListOf() + } + groupName = it.title.replace("\\s".toRegex(), "") + } else { + val findKindBean = FindKind().apply { + tag = source.sourceUrl + name = it.title.replace("\\s".toRegex(), "") + url = it.url + } + children.add(findKindBean) + } + } + if (children.size > 0) { + kindsMap[groupName] = children + } + emitter.onNext(true) + }.onFailure { + emitter.onNext(false) + kinds.add(ExploreKind3("ERROR:${it.localizedMessage}", it.stackTraceToString())) + if (App.isDebug()) { + it.printStackTrace() + } + } + emitter.onComplete() + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/ic_item_category_download.png b/app/src/main/res/drawable-xhdpi/ic_item_category_download.png deleted file mode 100644 index bb61a3c..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_item_category_download.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_item_category_normal.png b/app/src/main/res/drawable-xhdpi/ic_item_category_normal.png deleted file mode 100644 index b4e9b49..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_item_category_normal.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_cloud_download.xml b/app/src/main/res/drawable/ic_cloud_download.xml new file mode 100644 index 0000000..ffd5f56 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud_download.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/app/src/main/res/drawable/selector_category_load.xml b/app/src/main/res/drawable/selector_category_load.xml deleted file mode 100644 index ff1473f..0000000 --- a/app/src/main/res/drawable/selector_category_load.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_category_unload.xml b/app/src/main/res/drawable/selector_category_unload.xml deleted file mode 100644 index 15267c1..0000000 --- a/app/src/main/res/drawable/selector_category_unload.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_chapter.xml b/app/src/main/res/layout/item_chapter.xml new file mode 100644 index 0000000..3f97495 --- /dev/null +++ b/app/src/main/res/layout/item_chapter.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_book_detail_content.xml b/app/src/main/res/layout/layout_book_detail_content.xml index a1d9970..364323c 100644 --- a/app/src/main/res/layout/layout_book_detail_content.xml +++ b/app/src/main/res/layout/layout_book_detail_content.xml @@ -64,7 +64,7 @@ android:id="@+id/book_detail_tv_catalog" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:padding="15dp" + android:padding="12dp" android:text="最新章节" android:textColor="@color/textPrimary" android:textSize="15sp" /> @@ -73,8 +73,8 @@ android:id="@+id/book_detail_tv_catalog_more" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="right" - android:padding="15dp" + android:gravity="end" + android:padding="12dp" android:text="更多" android:textColor="@color/textSecondary" android:textSize="15sp" /> @@ -84,7 +84,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/book_detail_tv_catalog" - android:foregroundGravity="center" /> + android:foregroundGravity="center" + android:paddingHorizontal="4dp" /> - - - - - - - - \ No newline at end of file diff --git a/app/version_code.properties b/app/version_code.properties index 8a7c6ac..7ec427e 100644 --- a/app/version_code.properties +++ b/app/version_code.properties @@ -17,5 +17,5 @@ # #Fri Jun 18 21:45:31 CST 2021 -VERSION_CODE=250 +VERSION_CODE=251 CREATE_RELEASE=true diff --git a/dynamic/build.gradle b/dynamic/build.gradle index 0e68753..587053c 100644 --- a/dynamic/build.gradle +++ b/dynamic/build.gradle @@ -152,7 +152,7 @@ dependencies { compileOnly('org.apache.commons:commons-text:1.9') //https://github.com/fengyuecanzhu/Maple - compileOnly("me.fycz.maple:maple:1.9") + compileOnly("me.fycz.maple:maple:2.0") compileOnly project(":app") compileOnly project(":DialogX") } \ No newline at end of file diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt b/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt index 0fed8d4..33cdecd 100644 --- a/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt +++ b/dynamic/src/main/java/xyz/fycz/dynamic/AppLoadImpl.kt @@ -57,6 +57,7 @@ class AppLoadImpl : IAppLoader { App246Fix3::class.java, App246Fix4::class.java, App246Fix5::class.java, + App250Fix::class.java, ) override fun onLoad(appParam: AppParam) { diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix.kt b/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix.kt index 67ae22e..77d11f1 100644 --- a/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix.kt +++ b/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix.kt @@ -39,7 +39,7 @@ import xyz.fycz.myreader.util.utils.ScreenUtils * @author fengyue * @date 2022/6/3 15:34 */ -@AppFix([243, 244, 245, 246], ["[设置-缓存设置]新增清除广告文件"], "2022-06-03") +@AppFix([243, 244, 245, 246, 250], ["[设置-缓存设置]新增清除广告文件"], "2022-06-03") class App246Fix : AppFixHandle { override fun onFix(key: String): BooleanArray { diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix5.kt b/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix5.kt index 9d626b5..8c85034 100644 --- a/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix5.kt +++ b/dynamic/src/main/java/xyz/fycz/dynamic/fix/App246Fix5.kt @@ -200,7 +200,8 @@ class App246Fix5 : AppFixHandle { @Throws(IOException::class) fun getBakUpdateInfo(): String { return OkHttpUtils.getHtml( - "https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/FYReader-Update/" + + //"https://gitlab.com/fengyuecanzhu/fyreader-resource/-/raw/main/FYReader-Update/" + + "http://101.43.83.105:3000/fengyue/FYReader-Res/raw/branch/main/Plugin/" + (if (App.isDebug()) "debug" else "release") + "/content.txt" ) diff --git a/dynamic/src/main/java/xyz/fycz/dynamic/fix/App250Fix.kt b/dynamic/src/main/java/xyz/fycz/dynamic/fix/App250Fix.kt new file mode 100644 index 0000000..924e95c --- /dev/null +++ b/dynamic/src/main/java/xyz/fycz/dynamic/fix/App250Fix.kt @@ -0,0 +1,92 @@ +/* + * This file is part of FYReader. + * FYReader is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * FYReader is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with FYReader. If not, see . + * + * Copyright (C) 2020 - 2022 fengyuecanzhu + */ + +package xyz.fycz.dynamic.fix + +import android.widget.TextView +import androidx.viewbinding.ViewBinding +import me.fycz.maple.MapleBridge +import me.fycz.maple.MapleUtils +import me.fycz.maple.MethodReplacement +import xyz.fycz.myreader.greendao.DbManager +import xyz.fycz.myreader.greendao.entity.Chapter +import xyz.fycz.myreader.greendao.gen.ChapterDao +import xyz.fycz.myreader.greendao.service.ChapterService +import xyz.fycz.myreader.ui.activity.BookDetailedActivity + +/** + * @author fengyue + * @date 2022/8/11 16:44 + */ +@AppFix( + [243, 244, 245, 246, 250], + ["[书籍详情界面]取消书籍简介展开时最大行数限制(无法显示完全)", + "修复从数据库中读取章节时部分数据项缺失的bug"], + "2022-08-11" +) +class App250Fix : AppFixHandle { + override fun onFix(key: String): BooleanArray { + return handleFix( + key, + "showMoreDesc" to { fxShowMoreDesc() }, + "findBookAllChapterByBookId" to { fxFindBookAllChapterByBookId() }, + ) + } + + private fun fxFindBookAllChapterByBookId() { + MapleUtils.findAndHookMethod( + ChapterService::class.java, + "findBookAllChapterByBookId", + String::class.java, + object : MethodReplacement() { + override fun replaceHookedMethod(param: MapleBridge.MethodHookParam): Any { + val bookId = param.args[0] as String? + if (bookId.isNullOrBlank()) { + return emptyList() + } + return DbManager.getDaoSession().chapterDao + .queryBuilder() + .where(ChapterDao.Properties.BookId.eq(bookId)) + .orderAsc(ChapterDao.Properties.Number) + .list() + } + } + ) + } + + fun fxShowMoreDesc() { + MapleUtils.findAndHookMethod( + BookDetailedActivity::class.java, + "showMoreDesc", + object : MethodReplacement() { + override fun replaceHookedMethod(param: MapleBridge.MethodHookParam) { + val binding = + MapleUtils.getObjectField(param.thisObject, "binding") as ViewBinding + val icBinding = MapleUtils.getObjectField(binding, "ic") as ViewBinding + val bookDetailTvDesc = + MapleUtils.getObjectField(icBinding, "bookDetailTvDesc") as TextView + if (bookDetailTvDesc.maxLines == 5) { + bookDetailTvDesc.maxLines = Int.MAX_VALUE + } else { + bookDetailTvDesc.maxLines = 5 + } + } + } + ) + } +} \ No newline at end of file