Merge remote-tracking branch 'upstream/master'

pull/1251/head
Jason Yao 4 years ago
commit b0f367f8aa
  1. 99
      .github/workflows/release.yml
  2. 49
      .github/workflows/test.yml
  3. 6
      CHANGELOG.md
  4. 40
      README.md
  5. BIN
      app/cronetlib/cronet_api.jar
  6. BIN
      app/cronetlib/cronet_impl_common_java.jar
  7. BIN
      app/cronetlib/src/cronet_api-src.jar
  8. BIN
      app/cronetlib/src/cronet_impl_common_java-src.jar
  9. 2
      app/src/main/assets/epub/chapter.html
  10. 16
      app/src/main/assets/updateLog.md
  11. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  12. 10
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  13. 3
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  14. 2
      app/src/main/java/io/legado/app/data/entities/BookGroup.kt
  15. 7
      app/src/main/java/io/legado/app/data/entities/rule/LogInRule.kt
  16. 1
      app/src/main/java/io/legado/app/help/ImageLoader.kt
  17. 5
      app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt
  18. 2
      app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt
  19. 5
      app/src/main/java/io/legado/app/service/AudioPlayService.kt
  20. 2
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  21. 44
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt
  22. 23
      app/src/main/java/io/legado/app/ui/association/ImportBookSourceViewModel.kt
  23. 78
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleDialog.kt
  24. 67
      app/src/main/java/io/legado/app/ui/association/ImportReplaceRuleViewModel.kt
  25. 45
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt
  26. 22
      app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt
  27. 4
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayViewModel.kt
  28. 15
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  29. 4
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  30. 5
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt
  31. 14
      app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt
  32. 55
      app/src/main/java/io/legado/app/ui/book/group/GroupEdit.kt
  33. 121
      app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt
  34. 26
      app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt
  35. 45
      app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt
  36. 23
      app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt
  37. 10
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  38. 14
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  39. 8
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  40. 12
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  41. 4
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  42. 10
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  43. 4
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  44. 4
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  45. 6
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  46. 2
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  47. 68
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  48. 1
      app/src/main/java/io/legado/app/ui/dict/DictDialog.kt
  49. 52
      app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt
  50. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterGrid.kt
  51. 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BooksAdapterList.kt
  52. 6
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
  53. 5
      app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt
  54. 30
      app/src/main/java/io/legado/app/utils/ActivityExtensions.kt
  55. 32
      app/src/main/java/io/legado/app/utils/ActivityResultContractUtils.kt
  56. 27
      app/src/main/java/io/legado/app/utils/ActivityResultContracts.kt
  57. 29
      app/src/main/java/io/legado/app/utils/SystemUtils.kt
  58. 90
      app/src/main/res/layout/dialog_book_group_edit.xml
  59. 72
      app/src/main/res/layout/dialog_custom_group.xml
  60. 3
      app/src/main/res/layout/item_bookshelf_grid.xml
  61. 3
      app/src/main/res/layout/item_bookshelf_grid_group.xml
  62. 8
      app/src/main/res/layout/item_bookshelf_list.xml
  63. 8
      app/src/main/res/layout/item_bookshelf_list_group.xml
  64. 8
      app/src/main/res/layout/item_download.xml
  65. 5
      app/src/main/res/menu/book_info.xml
  66. 18
      app/src/main/res/menu/import_replace.xml
  67. 1
      app/src/main/res/values-es-rES/strings.xml
  68. 1
      app/src/main/res/values-ja-rJP/strings.xml
  69. 1
      app/src/main/res/values-pt-rBR/strings.xml
  70. 1
      app/src/main/res/values-zh-rHK/strings.xml
  71. 1
      app/src/main/res/values-zh-rTW/strings.xml
  72. 1
      app/src/main/res/values-zh/strings.xml
  73. 2
      app/src/main/res/values/pref_key_value.xml
  74. 1
      app/src/main/res/values/strings.xml
  75. 36
      app/src/main/res/xml/pref_config_theme.xml
  76. 6
      epublib/src/main/java/me/ag2s/epublib/util/ResourceUtil.java

@ -0,0 +1,99 @@
name: Build and Release
on:
push:
branches:
- master
paths:
- 'CHANGELOG.md'
# schedule:
# - cron: '0 4 * * *'
jobs:
build:
runs-on: ubuntu-latest
env:
# 登录蓝奏云后在控制台运行document.cookie
ylogin: ${{ secrets.LANZOU_ID }}
phpdisk_info: ${{ secrets.LANZOU_PSD }}
# 蓝奏云里的文件夹ID(阅读3测试版:2670621)
LANZOU_FOLDER_ID: 'b0f7pt4ja'
# 是否上传到artifact
UPLOAD_ARTIFACT: 'true'
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Release Apk Sign
run: |
echo "给apk增加签名"
cp $GITHUB_WORKSPACE/.github/workflows/legado.jks $GITHUB_WORKSPACE/app/legado.jks
sed '$a\RELEASE_STORE_FILE=./legado.jks' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: Unify Version Name
run: |
echo "统一版本号"
VERSION=$(date -d "8 hour" -u +3.%y.%m%d%H)
echo "RELEASE_VERSION=$VERSION" >> $GITHUB_ENV
sed "/def version/c def version = \"$VERSION\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease --build-cache --parallel
echo "开始进行cronet构建"
./gradlew assembleCronetRelease --build-cache --parallel
- name: Organize the Files
run: |
mkdir -p ${{ github.workspace }}/apk/
cp -rf ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk ${{ github.workspace }}/apk/
- name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2
with:
name: legado apk
path: ${{ github.workspace }}/apk/*.apk
- name: Upload App To Lanzou
if: ${{ env.ylogin }}
run: |
path="$GITHUB_WORKSPACE/apk/"
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f7pt4ja"
- name: Release
uses: softprops/action-gh-release@59c3b4891632ff9a897f99a91d7bc557467a3a22
with:
name: legado_app_${{ env.RELEASE_VERSION }}
tag_name: ${{ env.RELEASE_VERSION }}
body_path: ${{ github.workspace }}/CHANGELOG.md
draft: false
prerelease: false
files: ${{ github.workspace }}/apk/*.apk
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Push Assets To "release" Branch
run: |
cd $GITHUB_WORKSPACE/apk || exit 1
git init
git config --local user.name "github-actions[bot]"
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -b release
git add *.apk
git commit -m "${{ env.RELEASE_VERSION }}"
git remote add origin "https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}"
git push -f -u origin release
- name: Purge Jsdelivr Cache
run: |
result=$(curl -s https://purge.jsdelivr.net/gh/${{ github.repository }}@release/)
if echo $result |grep -q 'success.*true'; then
echo "jsdelivr缓存更新成功"
else
echo $result
fi

@ -1,8 +1,6 @@
name: Android CI name: Test Build
on: on:
release:
types: [published]
push: push:
branches: branches:
- master - master
@ -32,7 +30,6 @@ jobs:
key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} key: ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: | restore-keys: |
${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}- ${{ runner.os }}-legado-${{ hashFiles('**/updateLog.md') }}-
- name: Clear 18PlusList.txt - name: Clear 18PlusList.txt
run: | run: |
echo "清空18PlusList.txt" echo "清空18PlusList.txt"
@ -45,36 +42,52 @@ jobs:
sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i sed '$a\RELEASE_KEY_ALIAS=legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i sed '$a\RELEASE_STORE_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i sed '$a\RELEASE_KEY_PASSWORD=gedoor_legado' $GITHUB_WORKSPACE/gradle.properties -i
- name: Unify Version Name
run: |
echo "统一版本号"
VERSION=$(date -d "8 hour" -u +3.%y.%m%d%H)
VERSIONL=$(date -d "8 hour" -u +3.%y.%m%d%H%M)
echo "VERSION=$VERSIONL" >> $GITHUB_ENV
sed "/def version/c def version = \"$VERSION\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle
run: |
echo "开始进行release构建"
chmod +x gradlew
./gradlew assembleAppRelease --build-cache --parallel
echo "开始进行cronet构建"
./gradlew assembleCronetRelease --build-cache --parallel
echo "修改文件名"
mkdir -p ${{ github.workspace }}/apk/
for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do
mv "$file" ${{ github.workspace }}/apk/`echo "$file"|sed 's/.*\/\(.*\)\(\.apk\)$/\1_原包名\2/'`
done
- name: Apk Live Together - name: Apk Live Together
run: | run: |
echo "设置apk共存" echo "设置apk共存"
sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i sed "s/'.release'/'.releaseA'/" $GITHUB_WORKSPACE/app/build.gradle -i
sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i sed 's/.release/.releaseA/' $GITHUB_WORKSPACE/app/google-services.json -i
echo "统一版本号"
sed "/def version/c def version = \"${{ env.VERSION }}\"" $GITHUB_WORKSPACE/app/build.gradle -i
- name: Build With Gradle - name: Build With Gradle
run: | run: |
echo "开始进行release构建" echo "开始进行release构建"
chmod +x gradlew chmod +x gradlew
./gradlew assembleAppRelease --build-cache --parallel ./gradlew assembleAppRelease --build-cache --parallel
echo "开始进行cronet构建"
./gradlew assembleCronetRelease --build-cache --parallel
echo "修改文件名"
for file in `ls ${{ github.workspace }}/app/build/outputs/apk/*/*/*.apk`; do
mv "$file" ${{ github.workspace }}/apk/`echo "$file"|sed 's/.*\/\(.*\)\(\.apk\)$/\1_共存\2/'`
done
- name: Upload App To Artifact - name: Upload App To Artifact
if: ${{ env.UPLOAD_ARTIFACT != 'false' }} if: ${{ env.UPLOAD_ARTIFACT != 'false' }}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: legado apk name: legado apk
path: ${{ github.workspace }}/app/build/outputs/apk/app/release/*.apk path: ${{ github.workspace }}/apk/*.apk
- name: Upload App To Lanzou - name: Upload App To Lanzou
if: ${{ env.ylogin }} if: ${{ env.ylogin }}
run: | run: |
path="$GITHUB_WORKSPACE/app/build/outputs/apk/app/release" path="$GITHUB_WORKSPACE/apk/"
files=$(ls $path) python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path" "$LANZOU_FOLDER_ID"
for f in $files
do
if [[ $f == *"apk" ]]; then
file=$f
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 文件:$file"
break
fi
done
python3 $GITHUB_WORKSPACE/.github/scripts/lzy_web.py "$path/$file" "$LANZOU_FOLDER_ID"
echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b" echo "[$(date -u -d '+8 hour' '+%Y.%m.%d %H:%M:%S')] 分享链接: https://kunfei.lanzoux.com/b0f810h4b"

@ -1,4 +1,4 @@
# 1.0.0 (2020-02-09) **2021/08/09**
1. 修复选择文字不能选择单个文字的bug
2.

@ -18,7 +18,12 @@
* [web端](https://github.com/celetor/web-yuedu3) * [web端](https://github.com/celetor/web-yuedu3)
## 下载 ## 下载
<a href="https://play.google.com/store/apps/details?id=io.legado.play.release"><img width="200px" alt="Google Play" src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"/></a> or <a href="https://www.coolapk.com/apk/io.legado.app.release"><img width="100px" height="100px" alt="CoolApk" src="https://github.com/Celeter/build/raw/master/.github/scripts/coolapk.png"/></a> or [Releases](https://github.com/gedoor/legado/releases/latest) * [google play](https://play.google.com/store/apps/details?id=io.legado.play.release)
* [酷安](https://www.coolapk.com/apk/io.legado.app.release)
* [release](https://github.com/gedoor/legado/releases/latest)
* [jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/legado@release/)
* [网盘](https://kunfei.lanzoui.com/b0f7pt4ja)
* [测试版](https://kunfei.lanzoui.com/b0f810h4b)
## 阅读API ## 阅读API
* 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。 * 阅读3.0 提供了2种方式的API:`Web方式`和`Content Provider方式`。您可以在[这里](api.md)根据需要自行调用。
@ -27,24 +32,21 @@
* path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版 * path类型解释: 书源,订阅源,替换规则,本地txt小说目录规则,在线朗读引擎,主题,阅读排版
## 感谢 ## 感谢
``` > * org.jsoup:jsoup
org.jsoup:jsoup > * cn.wanghaomiao:JsoupXpath
cn.wanghaomiao:JsoupXpath > * com.jayway.jsonpath:json-path
com.jayway.jsonpath:json-path > * com.github.gedoor:rhino-android
com.github.gedoor:rhino-android > * com.squareup.okhttp3:okhttp
com.squareup.okhttp3:okhttp > * com.github.bumptech.glide:glide
com.ljx.rxhttp:rxhttp > * org.nanohttpd:nanohttpd
com.github.bumptech.glide:glide > * org.nanohttpd:nanohttpd-websocket
org.nanohttpd:nanohttpd > * cn.bingoogolapple:bga-qrcode-zxing
org.nanohttpd:nanohttpd-websocket > * com.jaredrummler:colorpicker
cn.bingoogolapple:bga-qrcode-zxing > * org.apache.commons:commons-text
com.jaredrummler:colorpicker > * io.noties.markwon:core
org.apache.commons:commons-text > * io.noties.markwon:image-glide
io.noties.markwon:core > * com.hankcs:hanlp
io.noties.markwon:image-glide > * com.positiondev.epublib:epublib-core
com.hankcs:hanlp
com.positiondev.epublib:epublib-core
```
## 免责声明 ## 免责声明
https://gedoor.github.io/MyBookshelf/disclaimer.html https://gedoor.github.io/MyBookshelf/disclaimer.html

Binary file not shown.

@ -10,6 +10,8 @@
<!--<div class="logo"> <!--<div class="logo">
<img alt="" class="logo" src="../Images/logo.png"/> <img alt="" class="logo" src="../Images/logo.png"/>
</div>--> </div>-->
<br/>
<br/>
<h2 class="head">{title}</h2> <h2 class="head">{title}</h2>
{content} {content}
</body> </body>

@ -10,10 +10,22 @@
* 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。 * 阅读只是一个转码工具,不提供内容,第一次安装app,需要自己手动导入书源,可以从公众号 **[开源阅读]**、QQ群、酷安评论里获取由书友制作分享的书源。
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则出现问题, 出现简体变化问题检查一下简繁转换是否关闭。 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则出现问题, 出现简体变化问题检查一下简繁转换是否关闭。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
* 关于最近版本有时候界面没有数据的问题是因为把LiveData组件换成了谷歌推荐的Flow组件导致的问题,正在查找解决办法
**2021/08/02** **2021/08/09**
* 关于最近版本有时候界面没有数据的问题是因为把LiveData组件换成了谷歌推荐的Flow组件导致的问题,正在查找解决办法 1. 修复选择文字不能选择单个文字的bug
2. 分组可选择封面
**2021/08/08**
1. 背景图片添加模糊设置
2. 书籍信息界面添加置顶操作
3. 自动翻页时屏幕常亮
4. 字典:中文使用百度汉语字典,英文使用海词字典。 by ag2s20150909
5. 导入规则时可以选择添加分组还是替换分组
**2021/08/02**
1. 换源界面功能添加:置顶,置底,删除 by h11128 1. 换源界面功能添加:置顶,置底,删除 by h11128
2. Cronet:优化 by ag2s20150909 2. Cronet:优化 by ag2s20150909

@ -24,4 +24,5 @@ object EventBus {
const val CHECK_SOURCE = "checkSource" const val CHECK_SOURCE = "checkSource"
const val CHECK_SOURCE_DONE = "checkSourceDone" const val CHECK_SOURCE_DONE = "checkSourceDone"
const val TIP_COLOR = "tipColor" const val TIP_COLOR = "tipColor"
const val SOURCE_CHANGED = "sourceChanged"
} }

@ -24,7 +24,7 @@ val appDb by lazy {
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class, RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class], RuleSub::class],
version = 33, version = 34,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -60,7 +60,7 @@ abstract class AppDatabase : RoomDatabase() {
migration_19_20, migration_20_21, migration_21_22, migration_22_23, migration_19_20, migration_20_21, migration_21_22, migration_22_23,
migration_23_24, migration_24_25, migration_25_26, migration_26_27, migration_23_24, migration_24_25, migration_25_26, migration_26_27,
migration_27_28, migration_28_29, migration_29_30, migration_30_31, migration_27_28, migration_28_29, migration_29_30, migration_30_31,
migration_31_32, migration_32_33 migration_31_32, migration_32_33, migration_33_34
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
@ -331,6 +331,12 @@ abstract class AppDatabase : RoomDatabase() {
) )
} }
} }
private val migration_33_34 = object : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
}
}
} }
} }

@ -62,6 +62,9 @@ interface BookDao {
@get:Query("SELECT COUNT(*) FROM books") @get:Query("SELECT COUNT(*) FROM books")
val allBookCount: Int val allBookCount: Int
@get:Query("select min(`order`) from books")
val minOrder: Int
@get:Query("select max(`order`) from books") @get:Query("select max(`order`) from books")
val maxOrder: Int val maxOrder: Int

@ -14,6 +14,7 @@ data class BookGroup(
@PrimaryKey @PrimaryKey
val groupId: Long = 0b1, val groupId: Long = 0b1,
var groupName: String, var groupName: String,
var cover: String? = null,
var order: Int = 0, var order: Int = 0,
var show: Boolean = true var show: Boolean = true
) : Parcelable { ) : Parcelable {
@ -36,6 +37,7 @@ data class BookGroup(
if (other is BookGroup) { if (other is BookGroup) {
return other.groupId == groupId return other.groupId == groupId
&& other.groupName == groupName && other.groupName == groupName
&& other.cover == cover
&& other.order == order && other.order == order
&& other.show == show && other.show == show
} }

@ -0,0 +1,7 @@
package io.legado.app.data.entities.rule
data class LogInRule(
val ui: HashMap<String, String>,
val logInUrl: String,
val checkJs: String
)

@ -12,6 +12,7 @@ import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import java.io.File import java.io.File
@Suppress("unused")
object ImageLoader { object ImageLoader {
/** /**

@ -27,7 +27,12 @@ class CronetInterceptor(private val cookieJar: CookieJar?) : Interceptor {
response response
} catch (e: Exception) { } catch (e: Exception) {
//遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理 //遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理
if (e.message.toString().contains("ERR_CERT_DATE_INVALID", true)) {
chain.proceed(original) chain.proceed(original)
} else {
throw e
}
} }

@ -147,7 +147,6 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
private const val TAG = "Callback" private const val TAG = "Callback"
private const val MAX_FOLLOW_COUNT = 20 private const val MAX_FOLLOW_COUNT = 20
@Suppress("DEPRECATION")
private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol { private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol {
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault()) val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault())
// Log.e("Cronet", responseInfo.url) // Log.e("Cronet", responseInfo.url)
@ -160,6 +159,7 @@ class CronetUrlRequestCallback @JvmOverloads internal constructor(
Protocol.QUIC Protocol.QUIC
} }
negotiatedProtocol.contains("spdy") -> { negotiatedProtocol.contains("spdy") -> {
@Suppress("DEPRECATION")
Protocol.SPDY_3 Protocol.SPDY_3
} }
negotiatedProtocol.contains("h2") -> { negotiatedProtocol.contains("h2") -> {

@ -161,6 +161,7 @@ class AudioPlayService : BaseService(),
} }
private fun resume() { private fun resume() {
try {
pause = false pause = false
if (!mediaPlayer.isPlaying) { if (!mediaPlayer.isPlaying) {
mediaPlayer.start() mediaPlayer.start()
@ -172,6 +173,10 @@ class AudioPlayService : BaseService(),
AudioPlay.status = Status.PLAY AudioPlay.status = Status.PLAY
postEvent(EventBus.AUDIO_STATE, Status.PLAY) postEvent(EventBus.AUDIO_STATE, Status.PLAY)
upNotification() upNotification()
} catch (e: Exception) {
e.printStackTrace()
stopSelf()
}
} }
private fun adjustProgress(position: Int) { private fun adjustProgress(position: Int) {

@ -1,5 +1,6 @@
package io.legado.app.service package io.legado.app.service
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -225,6 +226,7 @@ abstract class BaseReadAloudService : BaseService(),
/** /**
* 初始化MediaSession, 注册多媒体按钮 * 初始化MediaSession, 注册多媒体按钮
*/ */
@SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() { private fun initMediaSession() {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {

@ -20,7 +20,7 @@ import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -107,7 +107,7 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
} }
binding.tvFooterLeft.visible() binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener { binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll() val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b -> viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) { if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll viewModel.selectStatus[index] = !selectAll
@ -144,16 +144,16 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
} }
private fun upSelectText() { private fun upSelectText() {
if (viewModel.isSelectAll()) { if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_cancel_count, R.string.select_cancel_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} else { } else {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_all_count, R.string.select_all_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} }
@ -169,9 +169,18 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> { R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
private fun alertCustomGroup(item: MenuItem) {
alert(R.string.diy_edit_source_group) { alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>() val groups = linkedSetOf<String>()
appDb.bookSourceDao.allGroup.forEach { group -> appDb.bookSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
@ -184,21 +193,22 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList
alertBinding.root alertBinding.root
} }
okButton { okButton {
alertBinding.editView.text?.toString()?.let { group -> viewModel.isAddGroup = alertBinding.swAddGroup.isChecked
viewModel.groupName = group viewModel.groupName = alertBinding.editView.text?.toString()
item.title = getString(R.string.diy_edit_source_group_title, group) if (viewModel.groupName.isNullOrBlank()) {
item.title = getString(R.string.diy_source_group)
} else {
val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName)
if (viewModel.isAddGroup) {
item.title = "+$group"
} else {
item.title = group
}
} }
} }
noButton() noButton()
}.show() }.show()
} }
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
inner class SourcesAdapter(context: Context) : inner class SourcesAdapter(context: Context) :
RecyclerAdapter<BookSource, ItemSourceImportBinding>(context) { RecyclerAdapter<BookSource, ItemSourceImportBinding>(context) {

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -18,8 +19,10 @@ import io.legado.app.help.storage.Restore
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.splitNotBlank
class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) { class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null var groupName: String? = null
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>() val successLiveData = MutableLiveData<Int>()
@ -28,7 +31,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
val checkSources = arrayListOf<BookSource?>() val checkSources = arrayListOf<BookSource?>()
val selectStatus = arrayListOf<Boolean>() val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean { val isSelectAll: Boolean
get() {
selectStatus.forEach { selectStatus.forEach {
if (!it) { if (!it) {
return false return false
@ -37,7 +41,8 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
return true return true
} }
fun selectCount(): Int { val selectCount: Int
get() {
var count = 0 var count = 0
selectStatus.forEach { selectStatus.forEach {
if (it) { if (it) {
@ -49,6 +54,7 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
fun importSelect(finally: () -> Unit) { fun importSelect(finally: () -> Unit) {
execute { execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName val keepName = AppConfig.importKeepName
val selectSource = arrayListOf<BookSource>() val selectSource = arrayListOf<BookSource>()
selectStatus.forEachIndexed { index, b -> selectStatus.forEachIndexed { index, b ->
@ -61,8 +67,17 @@ class ImportBookSourceViewModel(app: Application) : BaseViewModel(app) {
source.customOrder = it.customOrder source.customOrder = it.customOrder
} }
} }
if (groupName != null) { if (!group.isNullOrEmpty()) {
source.bookSourceGroup = groupName if (isAddGroup) {
val groups = linkedSetOf<String>()
source.bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.let {
groups.addAll(it)
}
groups.add(group)
source.bookSourceGroup = groups.joinToString(",")
} else {
source.bookSourceGroup = group
}
} }
selectSource.add(source) selectSource.add(source)
} }

@ -5,8 +5,10 @@ import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -14,15 +16,23 @@ import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.dialog.WaitDialog import io.legado.app.ui.widget.dialog.WaitDialog
import io.legado.app.utils.dp
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible import io.legado.app.utils.visible
class ImportReplaceRuleDialog : BaseDialogFragment() { class ImportReplaceRuleDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener {
companion object { companion object {
fun start( fun start(
@ -71,6 +81,7 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
binding.toolBar.setBackgroundColor(primaryColor) binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_replace_rule) binding.toolBar.setTitle(R.string.import_replace_rule)
binding.rotateLoading.show() binding.rotateLoading.show()
initMenu()
adapter = SourcesAdapter(requireContext()) adapter = SourcesAdapter(requireContext())
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
@ -89,7 +100,7 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
} }
binding.tvFooterLeft.visible() binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener { binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll() val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b -> viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) { if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll viewModel.selectStatus[index] = !selectAll
@ -125,17 +136,65 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
viewModel.import(source) viewModel.import(source)
} }
private fun initMenu() {
binding.toolBar.setOnMenuItemClickListener(this)
binding.toolBar.inflateMenu(R.menu.import_replace)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return true
}
private fun alertCustomGroup(item: MenuItem) {
alert(R.string.diy_edit_source_group) {
val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>()
appDb.bookSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
textInputLayout.setHint(R.string.group_name)
editView.setFilterValues(groups.toList())
editView.dropDownHeight = 180.dp
}
customView {
alertBinding.root
}
okButton {
viewModel.isAddGroup = alertBinding.swAddGroup.isChecked
viewModel.groupName = alertBinding.editView.text?.toString()
if (viewModel.groupName.isNullOrBlank()) {
item.title = getString(R.string.diy_source_group)
} else {
val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName)
if (viewModel.isAddGroup) {
item.title = "+$group"
} else {
item.title = group
}
}
}
noButton()
}.show()
}
private fun upSelectText() { private fun upSelectText() {
if (viewModel.isSelectAll()) { if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_cancel_count, R.string.select_cancel_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allRules.size viewModel.allRules.size
) )
} else { } else {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_all_count, R.string.select_all_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allRules.size viewModel.allRules.size
) )
} }
@ -157,6 +216,15 @@ class ImportReplaceRuleDialog : BaseDialogFragment() {
binding.run { binding.run {
cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition] cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
cbSourceName.text = item.name cbSourceName.text = item.name
val localRule = viewModel.checkRules[holder.layoutPosition]
tvSourceState.text = when {
localRule == null -> "新规则"
item.pattern != localRule.pattern
|| item.replacement != localRule.replacement
|| item.isRegex != localRule.isRegex
|| item.scope != localRule.scope -> "更新"
else -> "已存在"
}
} }
} }

@ -3,6 +3,7 @@ package io.legado.app.ui.association
import android.app.Application import android.app.Application
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -11,8 +12,11 @@ import io.legado.app.help.http.okHttpClient
import io.legado.app.help.http.text import io.legado.app.help.http.text
import io.legado.app.help.storage.OldReplace import io.legado.app.help.storage.OldReplace
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.splitNotBlank
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) { class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>() val successLiveData = MutableLiveData<Int>()
@ -20,7 +24,8 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
val checkRules = arrayListOf<ReplaceRule?>() val checkRules = arrayListOf<ReplaceRule?>()
val selectStatus = arrayListOf<Boolean>() val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean { val isSelectAll: Boolean
get() {
selectStatus.forEach { selectStatus.forEach {
if (!it) { if (!it) {
return false return false
@ -29,7 +34,8 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
return true return true
} }
fun selectCount(): Int { val selectCount: Int
get() {
var count = 0 var count = 0
selectStatus.forEach { selectStatus.forEach {
if (it) { if (it) {
@ -39,6 +45,42 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
return count return count
} }
fun importSelect(finally: () -> Unit) {
execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName
val selectRules = arrayListOf<ReplaceRule>()
selectStatus.forEachIndexed { index, b ->
if (b) {
val rule = allRules[index]
if (keepName) {
checkRules[index]?.let {
rule.name = it.name
rule.group = it.group
rule.order = it.order
}
}
if (!group.isNullOrEmpty()) {
if (isAddGroup) {
val groups = linkedSetOf<String>()
rule.group?.splitNotBlank(AppPattern.splitGroupRegex)?.let {
groups.addAll(it)
}
groups.add(group)
rule.group = groups.joinToString(",")
} else {
rule.group = group
}
}
selectRules.add(rule)
}
}
appDb.replaceRuleDao.insert(*selectRules.toTypedArray())
}.onFinally {
finally.invoke()
}
}
fun import(text: String) { fun import(text: String) {
execute { execute {
if (text.isAbsUrl()) { if (text.isAbsUrl()) {
@ -59,27 +101,12 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
} }
} }
fun importSelect(finally: () -> Unit) {
execute {
val keepName = AppConfig.importKeepName
val selectRules = arrayListOf<ReplaceRule>()
selectStatus.forEachIndexed { index, b ->
if (b) {
val rule = allRules[index]
selectRules.add(rule)
}
}
appDb.replaceRuleDao.insert(*selectRules.toTypedArray())
}.onFinally {
finally.invoke()
}
}
private fun comparisonSource() { private fun comparisonSource() {
execute { execute {
allRules.forEach { allRules.forEach {
checkRules.add(null) val rule = appDb.replaceRuleDao.findById(it.id)
selectStatus.add(false) checkRules.add(rule)
selectStatus.add(rule == null)
} }
}.onSuccess { }.onSuccess {
successLiveData.postValue(allRules.size) successLiveData.postValue(allRules.size)

@ -20,7 +20,7 @@ import io.legado.app.constant.AppPattern
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogCustomGroupBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.databinding.ItemSourceImportBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -80,6 +80,7 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
return inflater.inflate(R.layout.dialog_recycler_view, container) return inflater.inflate(R.layout.dialog_recycler_view, container)
} }
@SuppressLint("NotifyDataSetChanged")
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor) binding.toolBar.setBackgroundColor(primaryColor)
binding.toolBar.setTitle(R.string.import_rss_source) binding.toolBar.setTitle(R.string.import_rss_source)
@ -103,7 +104,7 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
} }
binding.tvFooterLeft.visible() binding.tvFooterLeft.visible()
binding.tvFooterLeft.setOnClickListener { binding.tvFooterLeft.setOnClickListener {
val selectAll = viewModel.isSelectAll() val selectAll = viewModel.isSelectAll
viewModel.selectStatus.forEachIndexed { index, b -> viewModel.selectStatus.forEachIndexed { index, b ->
if (b != !selectAll) { if (b != !selectAll) {
viewModel.selectStatus[index] = !selectAll viewModel.selectStatus[index] = !selectAll
@ -140,16 +141,16 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
} }
private fun upSelectText() { private fun upSelectText() {
if (viewModel.isSelectAll()) { if (viewModel.isSelectAll) {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_cancel_count, R.string.select_cancel_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} else { } else {
binding.tvFooterLeft.text = getString( binding.tvFooterLeft.text = getString(
R.string.select_all_count, R.string.select_all_count,
viewModel.selectCount(), viewModel.selectCount,
viewModel.allSources.size viewModel.allSources.size
) )
} }
@ -165,9 +166,18 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_new_group -> { R.id.menu_new_group -> alertCustomGroup(item)
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
private fun alertCustomGroup(item: MenuItem) {
alert(R.string.diy_edit_source_group) { alert(R.string.diy_edit_source_group) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogCustomGroupBinding.inflate(layoutInflater).apply {
val groups = linkedSetOf<String>() val groups = linkedSetOf<String>()
appDb.rssSourceDao.allGroup.forEach { group -> appDb.rssSourceDao.allGroup.forEach { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
@ -180,21 +190,22 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe
alertBinding.root alertBinding.root
} }
okButton { okButton {
alertBinding.editView.text?.toString()?.let { group -> viewModel.isAddGroup = alertBinding.swAddGroup.isChecked
viewModel.groupName = group viewModel.groupName = alertBinding.editView.text?.toString()
item.title = getString(R.string.diy_edit_source_group_title, group) if (viewModel.groupName.isNullOrBlank()) {
item.title = getString(R.string.diy_source_group)
} else {
val group = getString(R.string.diy_edit_source_group_title, viewModel.groupName)
if (viewModel.isAddGroup) {
item.title = "+$group"
} else {
item.title = group
}
} }
} }
noButton() noButton()
}.show() }.show()
} }
R.id.menu_Keep_original_name -> {
item.isChecked = !item.isChecked
putPrefBoolean(PreferKey.importKeepName, item.isChecked)
}
}
return false
}
inner class SourcesAdapter(context: Context) : inner class SourcesAdapter(context: Context) :
RecyclerAdapter<RssSource, ItemSourceImportBinding>(context) { RecyclerAdapter<RssSource, ItemSourceImportBinding>(context) {

@ -5,6 +5,7 @@ import androidx.lifecycle.MutableLiveData
import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPath
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.RssSource
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
@ -16,6 +17,7 @@ import io.legado.app.help.storage.Restore
import io.legado.app.utils.* import io.legado.app.utils.*
class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) { class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
var isAddGroup = false
var groupName: String? = null var groupName: String? = null
val errorLiveData = MutableLiveData<String>() val errorLiveData = MutableLiveData<String>()
val successLiveData = MutableLiveData<Int>() val successLiveData = MutableLiveData<Int>()
@ -24,7 +26,8 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
val checkSources = arrayListOf<RssSource?>() val checkSources = arrayListOf<RssSource?>()
val selectStatus = arrayListOf<Boolean>() val selectStatus = arrayListOf<Boolean>()
fun isSelectAll(): Boolean { val isSelectAll: Boolean
get() {
selectStatus.forEach { selectStatus.forEach {
if (!it) { if (!it) {
return false return false
@ -33,7 +36,8 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
return true return true
} }
fun selectCount(): Int { val selectCount: Int
get() {
var count = 0 var count = 0
selectStatus.forEach { selectStatus.forEach {
if (it) { if (it) {
@ -45,6 +49,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
fun importSelect(finally: () -> Unit) { fun importSelect(finally: () -> Unit) {
execute { execute {
val group = groupName?.trim()
val keepName = AppConfig.importKeepName val keepName = AppConfig.importKeepName
val selectSource = arrayListOf<RssSource>() val selectSource = arrayListOf<RssSource>()
selectStatus.forEachIndexed { index, b -> selectStatus.forEachIndexed { index, b ->
@ -57,8 +62,17 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
source.customOrder = it.customOrder source.customOrder = it.customOrder
} }
} }
if (groupName != null) { if (!group.isNullOrEmpty()) {
source.sourceGroup = groupName if (isAddGroup) {
val groups = linkedSetOf<String>()
source.sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.let {
groups.addAll(it)
}
groups.add(group)
source.sourceGroup = groups.joinToString(",")
} else {
source.sourceGroup = group
}
} }
selectSource.add(source) selectSource.add(source)
} }

@ -4,12 +4,14 @@ import android.app.Application
import android.content.Intent import android.content.Intent
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -98,6 +100,8 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
} else { } else {
loadChapterList(book1) { upChangeDurChapterIndex(book1, oldTocSize, it) } loadChapterList(book1) { upChangeDurChapterIndex(book1, oldTocSize, it) }
} }
}.onFinally {
postEvent(EventBus.SOURCE_CHANGED, book1.bookUrl)
} }
} }

@ -1,5 +1,6 @@
package io.legado.app.ui.book.cache package io.legado.app.ui.book.cache
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@ -44,6 +45,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
override val viewModel by viewModels<CacheViewModel>() override val viewModel by viewModels<CacheViewModel>()
private val exportBookPathKey = "exportBookPath" private val exportBookPathKey = "exportBookPath"
private val exportTypes = arrayListOf("txt", "epub")
lateinit var adapter: CacheAdapter lateinit var adapter: CacheAdapter
private var booksFlowJob: Job? = null private var booksFlowJob: Job? = null
private var menu: Menu? = null private var menu: Menu? = null
@ -91,6 +93,10 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace menu.findItem(R.id.menu_enable_replace)?.isChecked = AppConfig.exportUseReplace
menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav
menu.findItem(R.id.menu_export_type)?.title =
"${getString(R.string.export_type)}(${getTypeName()})"
menu.findItem(R.id.menu_export_charset)?.title =
"${getString(R.string.export_charset)}(${AppConfig.exportCharset})"
return super.onMenuOpened(featureId, menu) return super.onMenuOpened(featureId, menu)
} }
@ -173,6 +179,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
} }
} }
@SuppressLint("NotifyDataSetChanged")
private fun initGroupData() { private fun initGroupData() {
launch { launch {
appDb.bookGroupDao.flowAll().collect { appDb.bookGroupDao.flowAll().collect {
@ -309,8 +316,14 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}.show() }.show()
} }
private fun getTypeName(): String {
return exportTypes.getOrElse(AppConfig.exportType) {
exportTypes[0]
}
}
private fun showExportTypeConfig() { private fun showExportTypeConfig() {
selector(R.string.export_type, arrayListOf("txt", "epub")) { _, i -> selector(R.string.export_type, exportTypes) { _, i ->
AppConfig.exportType = i AppConfig.exportType = i
} }
} }

@ -33,7 +33,7 @@ import javax.script.SimpleBindings
class CacheViewModel(application: Application) : BaseViewModel(application) { class CacheViewModel(application: Application) : BaseViewModel(application) {
fun getExportFileName(book: Book): String { private fun getExportFileName(book: Book): String {
val jsStr = AppConfig.bookExportFileName val jsStr = AppConfig.bookExportFileName
if (jsStr.isNullOrBlank()) { if (jsStr.isNullOrBlank()) {
return "${book.name} 作者:${book.getRealAuthor()}" return "${book.name} 作者:${book.getRealAuthor()}"
@ -56,6 +56,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
}.onError { }.onError {
finally(it.localizedMessage ?: "ERROR") finally(it.localizedMessage ?: "ERROR")
it.printStackTrace()
}.onSuccess { }.onSuccess {
finally(context.getString(R.string.success)) finally(context.getString(R.string.success))
} }
@ -185,6 +186,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
}.onError { }.onError {
finally(it.localizedMessage ?: "ERROR") finally(it.localizedMessage ?: "ERROR")
it.printStackTrace()
}.onSuccess { }.onSuccess {
finally(context.getString(R.string.success)) finally(context.getString(R.string.success))
} }

@ -62,6 +62,11 @@ class ChangeSourceAdapter(
when (it) { when (it) {
"name" -> tvOrigin.text = item.originName "name" -> tvOrigin.text = item.originName
"latest" -> tvLast.text = item.getDisplayLastChapterTitle() "latest" -> tvLast.text = item.getDisplayLastChapterTitle()
"upCurSource" -> if (callBack.bookUrl == item.bookUrl) {
ivChecked.visible()
} else {
ivChecked.invisible()
}
} }
} }
} }

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.* import android.view.*
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -11,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
@ -218,12 +220,12 @@ class ChangeSourceDialog : BaseDialogFragment(),
} }
override fun deleteSource(searchBook: SearchBook) { override fun deleteSource(searchBook: SearchBook) {
viewModel.del(searchBook)
if (bookUrl == searchBook.bookUrl) { if (bookUrl == searchBook.bookUrl) {
viewModel.firstSourceOrNull(searchBook)?.let { viewModel.firstSourceOrNull(searchBook)?.let {
changeSource(it) changeSource(it)
} }
} }
viewModel.del(searchBook)
} }
private fun changeSource(searchBook: SearchBook) { private fun changeSource(searchBook: SearchBook) {
@ -259,6 +261,16 @@ class ChangeSourceDialog : BaseDialogFragment(),
} }
} }
override fun observeLiveBus() {
observeEvent<String>(EventBus.SOURCE_CHANGED) {
adapter.notifyItemRangeChanged(
0,
adapter.itemCount,
bundleOf(Pair("upCurSource", bookUrl))
)
}
}
interface CallBack { interface CallBack {
val oldBook: Book? val oldBook: Book?
fun changeTo(book: Book) fun changeTo(book: Book)

@ -1,55 +0,0 @@
package io.legado.app.ui.book.group
import android.content.Context
import android.view.LayoutInflater
import io.legado.app.R
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.requestInputMethod
object GroupEdit {
fun show(context: Context, layoutInflater: LayoutInflater, bookGroup: BookGroup) = context.run {
alert(title = getString(R.string.group_edit)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.setHint(R.string.group_name)
editView.setText(bookGroup.groupName)
}
if (bookGroup.groupId >= 0) {
neutralButton(R.string.delete) {
deleteGroup(context, bookGroup)
}
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
bookGroup.groupName = it
Coroutine.async {
appDb.bookGroupDao.update(bookGroup)
}
}
}
noButton()
}.show().requestInputMethod()
}
private fun deleteGroup(context: Context, bookGroup: BookGroup) = context.run {
alert(R.string.delete, R.string.sure_del) {
okButton {
Coroutine.async {
appDb.bookGroupDao.delete(bookGroup)
val books = appDb.bookDao.getBooksByGroup(bookGroup.groupId)
books.forEach {
it.group = it.group - bookGroup.groupId
}
appDb.bookDao.update(*books.toTypedArray())
}
}
noButton()
}.show()
}
}

@ -0,0 +1,121 @@
package io.legado.app.ui.book.group
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogBookGroupEditBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.primaryColor
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import splitties.views.onClick
class GroupEditDialog : BaseDialogFragment() {
companion object {
fun start(fragmentManager: FragmentManager, bookGroup: BookGroup? = null) {
GroupEditDialog().apply {
arguments = Bundle().apply {
putParcelable("group", bookGroup)
}
}.show(fragmentManager, "bookGroupEdit")
}
}
private val binding by viewBinding(DialogBookGroupEditBinding::bind)
private val viewModel by viewModels<GroupViewModel>()
private var bookGroup: BookGroup? = null
val selectImage = registerForActivityResult(SelectImageContract()) {
it?.second?.let { uri ->
if (uri.isContentScheme()) {
binding.ivCover.load(uri.toString())
} else {
binding.ivCover.load(uri.path)
}
}
}
override fun onStart() {
super.onStart()
val dm = requireActivity().windowSize
dialog?.window?.setLayout(
(dm.widthPixels * 0.9).toInt(),
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_book_group_edit, container)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
bookGroup = arguments?.getParcelable("group")
bookGroup?.let {
binding.tieGroupName.setText(it.groupName)
binding.ivCover.load(it.cover)
} ?: let {
binding.toolBar.title = getString(R.string.add_group)
binding.btnDelete.gone()
binding.ivCover.load()
}
binding.run {
ivCover.onClick {
selectImage.launch(null)
}
btnCancel.onClick {
dismiss()
}
btnOk.onClick {
val groupName = tieGroupName.text?.toString()
if (groupName.isNullOrEmpty()) {
toastOnUi("分组名称不能为空")
} else {
bookGroup?.let {
it.groupName = groupName
it.cover = binding.ivCover.path
viewModel.upGroup(it) {
dismiss()
}
} ?: let {
viewModel.addGroup(groupName, binding.ivCover.path) {
dismiss()
}
}
}
}
btnDelete.onClick {
deleteGroup {
bookGroup?.let {
viewModel.delGroup(it) {
dismiss()
}
}
}
}
}
}
private fun deleteGroup(ok: () -> Unit) {
alert(R.string.delete, R.string.sure_del) {
okButton {
ok.invoke()
}
noButton()
}.show()
}
}

@ -1,6 +1,5 @@
package io.legado.app.ui.book.group package io.legado.app.ui.book.group
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -18,17 +17,14 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemBookGroupManageBinding import io.legado.app.databinding.ItemBookGroupManageBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible import io.legado.app.utils.visible
import io.legado.app.utils.windowSize import io.legado.app.utils.windowSize
@ -94,29 +90,11 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> addGroup() R.id.menu_add -> GroupEditDialog.start(childFragmentManager)
} }
return true return true
} }
@SuppressLint("InflateParams")
private fun addGroup() {
alert(title = getString(R.string.add_group)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.setHint(R.string.group_name)
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
if (it.isNotBlank()) {
viewModel.addGroup(it)
}
}
}
noButton()
}.show().requestInputMethod()
}
private inner class GroupAdapter(context: Context) : private inner class GroupAdapter(context: Context) :
RecyclerAdapter<BookGroup, ItemBookGroupManageBinding>(context), RecyclerAdapter<BookGroup, ItemBookGroupManageBinding>(context),
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
@ -144,7 +122,7 @@ class GroupManageDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
binding.run { binding.run {
tvEdit.setOnClickListener { tvEdit.setOnClickListener {
getItem(holder.layoutPosition)?.let { bookGroup -> getItem(holder.layoutPosition)?.let { bookGroup ->
GroupEdit.show(context, layoutInflater, bookGroup) GroupEditDialog.start(childFragmentManager, bookGroup)
} }
} }
swShow.setOnCheckedChangeListener { buttonView, isChecked -> swShow.setOnCheckedChangeListener { buttonView, isChecked ->

@ -1,6 +1,5 @@
package io.legado.app.ui.book.group package io.legado.app.ui.book.group
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -20,16 +19,13 @@ import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.databinding.DialogBookGroupPickerBinding import io.legado.app.databinding.DialogBookGroupPickerBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.ItemGroupSelectBinding import io.legado.app.databinding.ItemGroupSelectBinding
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.ItemTouchCallback import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint import io.legado.app.utils.applyTint
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.windowSize import io.legado.app.utils.windowSize
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -116,46 +112,11 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) { when (item?.itemId) {
R.id.menu_add -> addGroup() R.id.menu_add -> GroupEditDialog.start(childFragmentManager)
} }
return true return true
} }
@SuppressLint("InflateParams")
private fun addGroup() {
alert(title = getString(R.string.add_group)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.setHint(R.string.group_name)
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
if (it.isNotBlank()) {
viewModel.addGroup(it)
}
}
}
noButton()
}.show().requestInputMethod()
}
@SuppressLint("InflateParams")
private fun editGroup(bookGroup: BookGroup) {
alert(title = getString(R.string.group_edit)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.setHint(R.string.group_name)
editView.setText(bookGroup.groupName)
}
customView { alertBinding.root }
yesButton {
alertBinding.editView.text?.toString()?.let {
viewModel.upGroup(bookGroup.copy(groupName = it))
}
}
noButton()
}.show().requestInputMethod()
}
private inner class GroupAdapter(context: Context) : private inner class GroupAdapter(context: Context) :
RecyclerAdapter<BookGroup, ItemGroupSelectBinding>(context), RecyclerAdapter<BookGroup, ItemGroupSelectBinding>(context),
ItemTouchCallback.Callback { ItemTouchCallback.Callback {
@ -192,7 +153,9 @@ class GroupSelectDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener
} }
} }
} }
tvEdit.setOnClickListener { getItem(holder.layoutPosition)?.let { editGroup(it) } } tvEdit.setOnClickListener {
GroupEditDialog.start(childFragmentManager, getItem(holder.layoutPosition))
}
} }
} }

@ -7,7 +7,15 @@ import io.legado.app.data.entities.BookGroup
class GroupViewModel(application: Application) : BaseViewModel(application) { class GroupViewModel(application: Application) : BaseViewModel(application) {
fun addGroup(groupName: String) { fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
execute {
appDb.bookGroupDao.update(*bookGroup)
}.onFinally {
finally?.invoke()
}
}
fun addGroup(groupName: String, cover: String?, finally: () -> Unit) {
execute { execute {
var id = 1L var id = 1L
val idsSum = appDb.bookGroupDao.idsSum val idsSum = appDb.bookGroupDao.idsSum
@ -17,19 +25,16 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
val bookGroup = BookGroup( val bookGroup = BookGroup(
groupId = id, groupId = id,
groupName = groupName, groupName = groupName,
cover = cover,
order = appDb.bookGroupDao.maxOrder.plus(1) order = appDb.bookGroupDao.maxOrder.plus(1)
) )
appDb.bookGroupDao.insert(bookGroup) appDb.bookGroupDao.insert(bookGroup)
}.onFinally {
finally()
} }
} }
fun upGroup(vararg bookGroup: BookGroup) { fun delGroup(vararg bookGroup: BookGroup, finally: () -> Unit) {
execute {
appDb.bookGroupDao.update(*bookGroup)
}
}
fun delGroup(vararg bookGroup: BookGroup) {
execute { execute {
appDb.bookGroupDao.delete(*bookGroup) appDb.bookGroupDao.delete(*bookGroup)
bookGroup.forEach { group -> bookGroup.forEach { group ->
@ -39,6 +44,8 @@ class GroupViewModel(application: Application) : BaseViewModel(application) {
} }
appDb.bookDao.update(*books.toTypedArray()) appDb.bookDao.update(*books.toTypedArray())
} }
}.onFinally {
finally()
} }
} }

@ -15,6 +15,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions.bitmapTransform import com.bumptech.glide.request.RequestOptions.bitmapTransform
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.constant.Theme import io.legado.app.constant.Theme
import io.legado.app.data.appDb import io.legado.app.data.appDb
@ -36,6 +37,7 @@ import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.* import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -151,6 +153,14 @@ class BookInfoActivity :
viewModel.loadBookInfo(it, false) viewModel.loadBookInfo(it, false)
} }
} }
R.id.menu_login -> viewModel.bookSource?.let {
startActivity<SourceLoginActivity> {
putExtra("sourceUrl", it.bookSourceUrl)
putExtra("loginUrl", it.loginUrl)
putExtra("userAgent", it.getHeaderMap()[AppConst.UA_NAME])
}
}
R.id.menu_top -> viewModel.topBook()
R.id.menu_copy_book_url -> viewModel.bookData.value?.bookUrl?.let { R.id.menu_copy_book_url -> viewModel.bookData.value?.bookUrl?.let {
sendToClip(it) sendToClip(it)
} ?: toastOnUi(R.string.no_book) } ?: toastOnUi(R.string.no_book)

@ -5,6 +5,7 @@ import android.content.Intent
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@ -13,6 +14,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
@ -167,6 +169,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
upChangeDurChapterIndex(newBook, oldTocSize, it) upChangeDurChapterIndex(newBook, oldTocSize, it)
} }
} }
}.onFinally {
postEvent(EventBus.SOURCE_CHANGED, newBook.bookUrl)
} }
} }
@ -192,6 +196,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun topBook() {
execute {
bookData.value?.let { book ->
val minOrder = appDb.bookDao.minOrder
book.order = minOrder - 1
appDb.bookDao.update(book)
}
}
}
fun saveBook(success: (() -> Unit)? = null) { fun saveBook(success: (() -> Unit)? = null) {
execute { execute {
bookData.value?.let { book -> bookData.value?.let { book ->

@ -5,7 +5,6 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import io.legado.app.R import io.legado.app.R
@ -23,9 +22,8 @@ class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(), VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
ChangeCoverDialog.CallBack { ChangeCoverDialog.CallBack {
private val selectCoverResult = private val selectCover = registerForActivityResult(SelectImageContract()) {
registerForActivityResult(ActivityResultContracts.GetContent()) { it?.second?.let { uri ->
it?.let { uri ->
coverChangeTo(uri) coverChangeTo(uri)
} }
} }
@ -62,7 +60,7 @@ class BookInfoEditActivity :
} }
} }
tvSelectCover.setOnClickListener { tvSelectCover.setOnClickListener {
selectCoverResult.launch("image/*") selectCover.launch(null)
} }
tvRefreshCover.setOnClickListener { tvRefreshCover.setOnClickListener {
viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString() viewModel.book?.customCoverUrl = tieCoverUrl.text?.toString()

@ -686,6 +686,8 @@ class ReadBookActivity : ReadBookBaseActivity(),
binding.readView.upContent(1) binding.readView.upContent(1)
autoPagePlus() autoPagePlus()
binding.readMenu.setAutoPage(true) binding.readMenu.setAutoPage(true)
screenTimeOut = -1L
screenOffTimerStart()
} }
} }
@ -694,14 +696,15 @@ class ReadBookActivity : ReadBookBaseActivity(),
mHandler.removeCallbacks(autoPageRunnable) mHandler.removeCallbacks(autoPageRunnable)
binding.readView.upContent() binding.readView.upContent()
binding.readMenu.setAutoPage(false) binding.readMenu.setAutoPage(false)
upScreenTimeOut()
} }
private fun autoPagePlus() { private fun autoPagePlus() {
var delayMillis = ReadBookConfig.autoReadSpeed * 1000L / binding.readView.height var delayMillis = ReadBookConfig.autoReadSpeed * 1000L / binding.readView.height
var scrollOffset = 1 var scrollOffset = 1
if (delayMillis < 20) { if (delayMillis < 20) {
var delayInt=delayMillis.toInt() var delayInt = delayMillis.toInt()
if(delayInt==0)delayInt =1 if (delayInt == 0) delayInt = 1
scrollOffset = 20 / delayInt scrollOffset = 20 / delayInt
delayMillis = 20 delayMillis = 20
} }
@ -970,9 +973,8 @@ class ReadBookActivity : ReadBookBaseActivity(),
} }
private fun upScreenTimeOut() { private fun upScreenTimeOut() {
getPrefString(PreferKey.keepLight)?.let { val keepLightPrefer = getPrefString(PreferKey.keepLight)?.toInt() ?: 0
screenTimeOut = it.toLong() * 1000 screenTimeOut = keepLightPrefer * 1000L
}
screenOffTimerStart() screenOffTimerStart()
} }

@ -5,6 +5,7 @@ import android.content.Intent
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
@ -20,6 +21,7 @@ import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook import io.legado.app.service.help.ReadBook
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
@ -207,6 +209,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
upChangeDurChapterIndex(newBook, oldTocSize, it) upChangeDurChapterIndex(newBook, oldTocSize, it)
} }
} }
}.onFinally {
postEvent(EventBus.SOURCE_CHANGED, newBook.bookUrl)
} }
} }

@ -6,7 +6,6 @@ import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
import io.legado.app.R import io.legado.app.R
@ -46,9 +45,10 @@ class BgTextConfigDialog : BaseDialogFragment() {
private var primaryTextColor = 0 private var primaryTextColor = 0
private var secondaryTextColor = 0 private var secondaryTextColor = 0
private val importFormNet = "网络导入" private val importFormNet = "网络导入"
private val selectBgImage = registerForActivityResult(ActivityResultContracts.GetContent()) { private val selectBgImage = registerForActivityResult(SelectImageContract()) {
it ?: return@registerForActivityResult it?.second?.let { uri ->
setBgFromUri(it) setBgFromUri(uri)
}
} }
private val selectExportDir = registerForActivityResult(FilePicker()) { private val selectExportDir = registerForActivityResult(FilePicker()) {
it ?: return@registerForActivityResult it ?: return@registerForActivityResult
@ -122,7 +122,7 @@ class BgTextConfigDialog : BaseDialogFragment() {
ivBg.setImageResource(R.drawable.ic_image) ivBg.setImageResource(R.drawable.ic_image)
ivBg.setColorFilter(primaryTextColor) ivBg.setColorFilter(primaryTextColor)
root.setOnClickListener { root.setOnClickListener {
selectBgImage.launch("image/*") selectBgImage.launch(null)
} }
} }
} }

@ -250,7 +250,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
selectStart[2] != charIndex selectStart[2] != charIndex
) { ) {
if (selectToInt(relativePos, lineIndex, charIndex) if (selectToInt(relativePos, lineIndex, charIndex)
< selectToInt(selectEnd) <= selectToInt(selectEnd)
) { ) {
selectStart[0] = relativePos selectStart[0] = relativePos
selectStart[1] = lineIndex selectStart[1] = lineIndex
@ -276,7 +276,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|| selectEnd[2] != charIndex || selectEnd[2] != charIndex
) { ) {
if (selectToInt(relativePos, lineIndex, charIndex) if (selectToInt(relativePos, lineIndex, charIndex)
> selectToInt(selectStart) >= selectToInt(selectStart)
) { ) {
selectEnd[0] = relativePos selectEnd[0] = relativePos
selectEnd[1] = lineIndex selectEnd[1] = lineIndex

@ -85,7 +85,9 @@ class PageView(context: Context) : FrameLayout(context) {
vwTopDivider.visible(it.showHeaderLine) vwTopDivider.visible(it.showHeaderLine)
vwBottomDivider.visible(it.showFooterLine) vwBottomDivider.visible(it.showFooterLine)
pageNvBar.layoutParams = pageNvBar.layoutParams.apply { pageNvBar.layoutParams = pageNvBar.layoutParams.apply {
height = if (it.hideNavigationBar) 0 else App.navigationBarHeight height =
if (it.hideNavigationBar) 0
else App.navigationBarHeight
} }
} }
contentTextView.upVisibleRect() contentTextView.upVisibleRect()

@ -43,9 +43,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
upContent() upContent()
} }
var isScroll = false var isScroll = false
var prevPage: PageView = PageView(context) val prevPage by lazy { PageView(context) }
var curPage: PageView = PageView(context) val curPage by lazy { PageView(context) }
var nextPage: PageView = PageView(context) val nextPage by lazy { PageView(context) }
val defaultAnimationSpeed = 300 val defaultAnimationSpeed = 300
private var pressDown = false private var pressDown = false
private var isMove = false private var isMove = false

@ -343,9 +343,11 @@ object ChapterProvider {
} }
x = x1 x = x1
} }
if (words.size > bodyIndent.length) {
val words1 = words.copyOfRange(bodyIndent.length, words.size) val words1 = words.copyOfRange(bodyIndent.length, words.size)
addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList) addCharsToLineMiddle(textLine, words1, textPaint, desiredWidth, x, srcList)
} }
}
/** /**
* 无缩进,两端对齐 * 无缩进,两端对齐

@ -42,7 +42,7 @@ class ThemeConfigFragment : BasePreferenceFragment(),
private val requestCodeCoverDark = 112 private val requestCodeCoverDark = 112
private val requestCodeBgLight = 121 private val requestCodeBgLight = 121
private val requestCodeBgDark = 122 private val requestCodeBgDark = 122
private val selectImage = registerForActivityResult(ActivityResultContractUtils.SelectImage()) { private val selectImage = registerForActivityResult(SelectImageContract()) {
val uri = it?.second ?: return@registerForActivityResult val uri = it?.second ?: return@registerForActivityResult
when (it.first) { when (it.first) {
requestCodeCover -> setCoverFromUri(PreferKey.defaultCover, uri) requestCodeCover -> setCoverFromUri(PreferKey.defaultCover, uri)
@ -164,37 +164,9 @@ class ThemeConfigFragment : BasePreferenceFragment(),
recreateActivities() recreateActivities()
} }
"themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList")
"saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) "saveDayTheme", "saveNightTheme" -> alertSaveTheme(key)
PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) { PreferKey.bgImage -> selectBgAction(false)
selectImage.launch(requestCodeBgLight) PreferKey.bgImageN -> selectBgAction(true)
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.bgImage)
upTheme(false)
} else {
selectImage.launch(requestCodeBgLight)
}
}
}
PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) {
selectImage.launch(requestCodeBgDark)
} else {
selector(items = arrayListOf("删除图片", "选择图片")) { _, i ->
if (i == 0) {
removePref(PreferKey.bgImageN)
upTheme(true)
} else {
selectImage.launch(requestCodeBgDark)
}
}
}
PreferKey.bgImageBlurring -> alertImageBlurring(PreferKey.bgImageBlurring) {
upTheme(false)
}
PreferKey.bgImageNBlurring -> alertImageBlurring(PreferKey.bgImageNBlurring) {
upTheme(true)
}
PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) {
selectImage.launch(requestCodeCover) selectImage.launch(requestCodeCover)
} else { } else {
@ -223,7 +195,7 @@ class ThemeConfigFragment : BasePreferenceFragment(),
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private fun saveThemeAlert(key: String) { private fun alertSaveTheme(key: String) {
alert(R.string.theme_name) { alert(R.string.theme_name) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
textInputLayout.hint = "name" textInputLayout.hint = "name"
@ -245,6 +217,36 @@ class ThemeConfigFragment : BasePreferenceFragment(),
}.show() }.show()
} }
private fun selectBgAction(isNight: Boolean) {
val bgKey = if (isNight) PreferKey.bgImageN else PreferKey.bgImage
val blurringKey = if (isNight) PreferKey.bgImageNBlurring else PreferKey.bgImageBlurring
val actions = arrayListOf(
getString(R.string.background_image_blurring),
getString(R.string.select_image)
)
if (!getPrefString(bgKey).isNullOrEmpty()) {
actions.add(getString(R.string.delete))
}
selector(items = actions) { _, i ->
when (i) {
0 -> alertImageBlurring(blurringKey) {
upTheme(isNight)
}
1 -> {
if (isNight) {
selectImage.launch(requestCodeBgDark)
} else {
selectImage.launch(requestCodeBgLight)
}
}
2 -> {
removePref(bgKey)
upTheme(isNight)
}
}
}
}
private fun alertImageBlurring(preferKey: String, success: () -> Unit) { private fun alertImageBlurring(preferKey: String, success: () -> Unit) {
alert(R.string.background_image_blurring) { alert(R.string.background_image_blurring) {
val alertBinding = DialogImageBlurringBinding.inflate(layoutInflater).apply { val alertBinding = DialogImageBlurringBinding.inflate(layoutInflater).apply {

@ -63,6 +63,7 @@ class DictDialog : BaseDialogFragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.tvDict.text = Html.fromHtml(it, Html.FROM_HTML_MODE_LEGACY) binding.tvDict.text = Html.fromHtml(it, Html.FROM_HTML_MODE_LEGACY)
} else { } else {
@Suppress("DEPRECATION")
binding.tvDict.text = Html.fromHtml(it) binding.tvDict.text = Html.fromHtml(it)
} }
} }

@ -8,12 +8,27 @@ import io.legado.app.help.http.newCallStrResponse
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.util.regex.Pattern
class DictViewModel(application: Application) : BaseViewModel(application) { class DictViewModel(application: Application) : BaseViewModel(application) {
var dictHtmlData: MutableLiveData<String> = MutableLiveData() var dictHtmlData: MutableLiveData<String> = MutableLiveData()
fun dict(word: String) { fun dict(word: String) {
if(isChinese(word)){
baiduDict(word)
}else{
haiciDict(word)
}
}
/**
* 海词英文词典
*
* @param word
*/
private fun haiciDict(word: String) {
execute { execute {
val body = okHttpClient.newCallStrResponse { val body = okHttpClient.newCallStrResponse {
get("https://apii.dict.cn/mini.php", mapOf(Pair("q", word))) get("https://apii.dict.cn/mini.php", mapOf(Pair("q", word)))
@ -25,7 +40,44 @@ class DictViewModel(application: Application) : BaseViewModel(application) {
}.onError { }.onError {
context.toastOnUi(it.localizedMessage) context.toastOnUi(it.localizedMessage)
} }
}
/**
* 百度汉语词典
*
* @param word
*/
private fun baiduDict(word: String) {
execute {
val body = okHttpClient.newCallStrResponse {
get("https://dict.baidu.com/s", mapOf(Pair("wd", word)))
}.body
val jsoup = Jsoup.parse(body!!)
jsoup.select("script").remove()//移除script
jsoup.select("#word-header").remove()//移除单字的header
jsoup.select("#term-header").remove()//移除词语的header
jsoup.select(".more-button").remove()//移除展示更多
jsoup.select(".disactive").remove()
jsoup.select("#download-wrapper").remove()//移除下载广告
jsoup.select("#right-panel").remove()//移除右侧广告
jsoup.select("#content-panel")
}.onSuccess {
dictHtmlData.postValue(it.html())
}.onError {
context.toastOnUi(it.localizedMessage)
}
}
/**
* 判断是否包含汉字
* @param str
* @return
*/
fun isChinese(str: String): Boolean {
val p = Pattern.compile("[\u4e00-\u9fa5]")
val m = p.matcher(str)
return m.find()
} }
} }

@ -56,6 +56,7 @@ class BooksAdapterGrid(context: Context, callBack: CallBack) :
binding.run { binding.run {
val item = callBack.getItem(position) as BookGroup val item = callBack.getItem(position) as BookGroup
tvName.text = item.groupName tvName.text = item.groupName
ivCover.load(item.cover)
} }
} }
@ -88,6 +89,7 @@ class BooksAdapterGrid(context: Context, callBack: CallBack) :
val item = callBack.getItem(position) val item = callBack.getItem(position)
if (item is BookGroup) { if (item is BookGroup) {
tvName.text = item.groupName tvName.text = item.groupName
ivCover.load(item.cover)
} }
root.setOnClickListener { root.setOnClickListener {
callBack.onItemClick(position) callBack.onItemClick(position)

@ -55,6 +55,7 @@ class BooksAdapterList(context: Context, callBack: CallBack) :
binding.run { binding.run {
val item = callBack.getItem(position) as BookGroup val item = callBack.getItem(position) as BookGroup
tvName.text = item.groupName tvName.text = item.groupName
ivCover.load(item.cover)
} }
} }
@ -90,6 +91,7 @@ class BooksAdapterList(context: Context, callBack: CallBack) :
val item = callBack.getItem(position) val item = callBack.getItem(position)
if (item is BookGroup) { if (item is BookGroup) {
tvName.text = item.groupName tvName.text = item.groupName
ivCover.load(item.cover)
flHasNew.gone() flHasNew.gone()
ivAuthor.gone() ivAuthor.gone()
ivLast.gone() ivLast.gone()

@ -22,7 +22,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.audio.AudioPlayActivity
import io.legado.app.ui.book.group.GroupEdit import io.legado.app.ui.book.group.GroupEditDialog
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
@ -120,7 +120,6 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
AppConst.bookGroupNoneId -> appDb.bookDao.flowNoGroup() AppConst.bookGroupNoneId -> appDb.bookDao.flowNoGroup()
else -> appDb.bookDao.flowByGroup(groupId) else -> appDb.bookDao.flowByGroup(groupId)
}.collect { list -> }.collect { list ->
binding.tvEmptyMsg.isGone = list.isNotEmpty()
books = when (getPrefInt(PreferKey.bookshelfSort)) { books = when (getPrefInt(PreferKey.bookshelfSort)) {
1 -> list.sortedByDescending { 1 -> list.sortedByDescending {
it.latestChapterTime it.latestChapterTime
@ -136,6 +135,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
} }
} }
booksAdapter.notifyDataSetChanged() booksAdapter.notifyDataSetChanged()
binding.tvEmptyMsg.isGone = getItemCount() > 0
} }
} }
} }
@ -190,7 +190,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
putExtra("name", item.name) putExtra("name", item.name)
putExtra("author", item.author) putExtra("author", item.author)
} }
is BookGroup -> GroupEdit.show(requireContext(), layoutInflater, item) is BookGroup -> GroupEditDialog.start(childFragmentManager, item)
} }
} }

@ -30,6 +30,8 @@ class CoverImageView @JvmOverloads constructor(
) { ) {
internal var width: Float = 0.toFloat() internal var width: Float = 0.toFloat()
internal var height: Float = 0.toFloat() internal var height: Float = 0.toFloat()
var path: String? = null
private set
private var nameHeight = 0f private var nameHeight = 0f
private var authorHeight = 0f private var authorHeight = 0f
private val namePaint by lazy { private val namePaint by lazy {
@ -131,7 +133,8 @@ class CoverImageView @JvmOverloads constructor(
} }
} }
fun load(path: String?, name: String?, author: String?) { fun load(path: String? = null, name: String? = null, author: String? = null) {
this.path = path
setText(name, author) setText(name, author)
if (AppConfig.useDefaultCover) { if (AppConfig.useDefaultCover) {
ImageLoader.load(context, defaultDrawable) ImageLoader.load(context, defaultDrawable)

@ -3,6 +3,8 @@ package io.legado.app.utils
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowMetrics import android.view.WindowMetrics
@ -22,13 +24,39 @@ val Activity.windowSize: DisplayMetrics
return displayMetrics return displayMetrics
} }
/**
* 返回NavigationBar是否存在
* 该方法需要在View完全被绘制出来之后调用否则判断不了
* 在比如 onWindowFocusChanged方法中可以得到正确的结果
*/
val Activity.isNavigationBarExist: Boolean
get() {
val viewGroup = window.decorView as? ViewGroup
if (viewGroup != null) {
for (i in 0 until viewGroup.childCount) {
viewGroup.getChildAt(i).context.packageName
if (viewGroup.getChildAt(i).id != View.NO_ID
&& "navigationBarBackground" == resources.getResourceEntryName(
viewGroup.getChildAt(
i
).id
)
) {
return true
}
}
}
return false
}
/** /**
* 该方法需要在View完全被绘制出来之后调用否则判断不了 * 该方法需要在View完全被绘制出来之后调用否则判断不了
* 在比如 onWindowFocusChanged方法中可以得到正确的结果 * 在比如 onWindowFocusChanged方法中可以得到正确的结果
*/ */
val Activity.navigationBarHeight: Int val Activity.navigationBarHeight: Int
get() { get() {
if (SystemUtils.isNavigationBarExist(this)) { if (isNavigationBarExist) {
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
return resources.getDimensionPixelSize(resourceId) return resources.getDimensionPixelSize(resourceId)
} }

@ -1,32 +0,0 @@
package io.legado.app.utils
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
object ActivityResultContractUtils {
class SelectImage : ActivityResultContract<Int, Pair<Int?, Uri?>?>() {
var requestCode: Int? = null
override fun createIntent(context: Context, input: Int?): Intent {
requestCode = input
return Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*")
}
override fun parseResult(resultCode: Int, intent: Intent?): Pair<Int?, Uri?>? {
if (resultCode == RESULT_OK) {
return Pair(requestCode, intent?.data)
}
return null
}
}
}

@ -0,0 +1,27 @@
package io.legado.app.utils
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
class SelectImageContract : ActivityResultContract<Int, Pair<Int?, Uri?>?>() {
var requestCode: Int? = null
override fun createIntent(context: Context, input: Int?): Intent {
requestCode = input
return Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*")
}
override fun parseResult(resultCode: Int, intent: Intent?): Pair<Int?, Uri?>? {
if (resultCode == RESULT_OK) {
return Pair(requestCode, intent?.data)
}
return null
}
}

@ -8,15 +8,11 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.view.View
import android.view.ViewGroup
@Suppress("unused") @Suppress("unused")
object SystemUtils { object SystemUtils {
private const val NAVIGATION = "navigationBarBackground"
fun getScreenOffTime(context: Context): Int { fun getScreenOffTime(context: Context): Int {
var screenOffTime = 0 var screenOffTime = 0
kotlin.runCatching { kotlin.runCatching {
@ -49,29 +45,4 @@ object SystemUtils {
} }
} }
/**
* 返回NavigationBar是否存在
* 该方法需要在View完全被绘制出来之后调用否则判断不了
* 在比如 onWindowFocusChanged方法中可以得到正确的结果
*/
fun isNavigationBarExist(activity: Activity?): Boolean {
activity?.let {
val viewGroup = it.window.decorView as? ViewGroup
if (viewGroup != null) {
for (i in 0 until viewGroup.childCount) {
viewGroup.getChildAt(i).context.packageName
if (viewGroup.getChildAt(i).id != View.NO_ID
&& NAVIGATION == activity.resources.getResourceEntryName(
viewGroup.getChildAt(
i
).id
)
) {
return true
}
}
}
}
return false
}
} }

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_menu">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_menu"
android:elevation="5dp"
android:theme="?attr/actionBarStyle"
app:displayHomeAsUp="false"
app:fitStatusBar="false"
app:layout_constraintTop_toTopOf="parent"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:title="@string/group_edit"
app:titleTextAppearance="@style/ToolbarTitle" />
<io.legado.app.ui.widget.image.CoverImageView
android:id="@+id/iv_cover"
android:layout_width="90dp"
android:layout_height="126dp"
android:layout_margin="6dp"
android:contentDescription="@string/img_cover"
android:scaleType="centerCrop"
android:src="@drawable/image_cover_default"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tool_bar"
tools:ignore="ImageContrastCheck" />
<io.legado.app.ui.widget.text.TextInputLayout
android:id="@+id/til_group_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:hint="@string/group_name"
app:layout_constraintLeft_toRightOf="@+id/iv_cover"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tool_bar"
app:layout_constraintBottom_toTopOf="@+id/btn_ok">
<io.legado.app.ui.widget.text.EditText
android:id="@+id/tie_group_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/btn_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:layout_marginLeft="12dp"
android:text="@string/delete"
app:layout_constraintBottom_toBottomOf="@id/btn_ok"
app:layout_constraintLeft_toRightOf="@+id/iv_cover"
app:layout_constraintTop_toTopOf="@+id/btn_ok"
tools:ignore="RtlHardcoded" />
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/btn_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:layout_marginRight="12dp"
android:text="@string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/til_group_name"
tools:ignore="RtlHardcoded" />
<io.legado.app.ui.widget.text.AccentTextView
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="@id/btn_ok"
app:layout_constraintRight_toLeftOf="@+id/btn_ok"
app:layout_constraintTop_toTopOf="@+id/btn_ok" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="12dp"
android:overScrollMode="ifContentScrolls">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp"
android:paddingBottom="6dp">
<TextView
android:id="@+id/tv_add_group_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/add_group"
android:textColor="@color/primaryText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/sw_add_group" />
<TextView
android:id="@+id/tv_add_group_s"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/custom_group_summary"
android:textColor="@color/secondaryText"
app:layout_constraintTop_toBottomOf="@+id/tv_add_group_title"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/sw_add_group" />
<io.legado.app.lib.theme.view.ATESwitch
android:id="@+id/sw_add_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOff="@string/replace"
android:textOn="@string/add"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="SpeakableTextPresentCheck" />
</androidx.constraintlayout.widget.ConstraintLayout>
<io.legado.app.ui.widget.text.TextInputLayout
android:id="@+id/text_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="6dp">
<io.legado.app.ui.widget.text.AutoCompleteTextView
android:id="@+id/edit_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</io.legado.app.ui.widget.text.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

@ -32,7 +32,8 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
android:transitionName="img_cover" android:transitionName="img_cover"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute,ImageContrastCheck"
tools:layout_editor_absoluteX="8dp" />
<io.legado.app.ui.widget.text.BadgeView <io.legado.app.ui.widget.text.BadgeView
android:id="@+id/bv_unread" android:id="@+id/bv_unread"

@ -32,7 +32,8 @@
android:src="@drawable/image_cover_default" android:src="@drawable/image_cover_default"
android:transitionName="img_cover" android:transitionName="img_cover"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute,ImageContrastCheck"
tools:layout_editor_absoluteX="8dp" />
<io.legado.app.ui.widget.text.BadgeView <io.legado.app.ui.widget.text.BadgeView
android:id="@+id/bv_unread" android:id="@+id/bv_unread"

@ -99,7 +99,7 @@
app:layout_constraintLeft_toRightOf="@+id/iv_author" app:layout_constraintLeft_toRightOf="@+id/iv_author"
app:layout_constraintRight_toRightOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_name" app:layout_constraintTop_toBottomOf="@+id/tv_name"
tools:ignore="RtlSymmetry" /> tools:ignore="RtlSymmetry,TextContrastCheck" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_read" android:id="@+id/iv_read"
@ -127,7 +127,8 @@
app:layout_constraintBottom_toTopOf="@id/tv_last" app:layout_constraintBottom_toTopOf="@id/tv_last"
app:layout_constraintLeft_toRightOf="@+id/iv_read" app:layout_constraintLeft_toRightOf="@+id/iv_read"
app:layout_constraintRight_toRightOf="@+id/fl_has_new" app:layout_constraintRight_toRightOf="@+id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_author" /> app:layout_constraintTop_toBottomOf="@+id/tv_author"
tools:ignore="TextContrastCheck" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_last" android:id="@+id/iv_last"
@ -155,7 +156,8 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/iv_last" app:layout_constraintLeft_toRightOf="@+id/iv_last"
app:layout_constraintRight_toRightOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_read" /> app:layout_constraintTop_toBottomOf="@+id/tv_read"
tools:ignore="TextContrastCheck" />
<View <View
android:id="@+id/vw_foreground" android:id="@+id/vw_foreground"

@ -99,7 +99,7 @@
app:layout_constraintLeft_toRightOf="@+id/iv_author" app:layout_constraintLeft_toRightOf="@+id/iv_author"
app:layout_constraintRight_toRightOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_name" app:layout_constraintTop_toBottomOf="@+id/tv_name"
tools:ignore="RtlSymmetry" /> tools:ignore="RtlSymmetry,TextContrastCheck" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_read" android:id="@+id/iv_read"
@ -127,7 +127,8 @@
app:layout_constraintBottom_toTopOf="@id/tv_last" app:layout_constraintBottom_toTopOf="@id/tv_last"
app:layout_constraintLeft_toRightOf="@+id/iv_read" app:layout_constraintLeft_toRightOf="@+id/iv_read"
app:layout_constraintRight_toRightOf="@+id/fl_has_new" app:layout_constraintRight_toRightOf="@+id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_author" /> app:layout_constraintTop_toBottomOf="@+id/tv_author"
tools:ignore="TextContrastCheck" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_last" android:id="@+id/iv_last"
@ -155,7 +156,8 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/iv_last" app:layout_constraintLeft_toRightOf="@+id/iv_last"
app:layout_constraintRight_toRightOf="@id/fl_has_new" app:layout_constraintRight_toRightOf="@id/fl_has_new"
app:layout_constraintTop_toBottomOf="@+id/tv_read" /> app:layout_constraintTop_toBottomOf="@+id/tv_read"
tools:ignore="TextContrastCheck" />
<View <View
android:id="@+id/vw_foreground" android:id="@+id/vw_foreground"

@ -3,6 +3,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"> android:padding="16dp">
<TextView <TextView
@ -10,6 +11,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/primaryText"
tools:text="@string/name"
app:layout_constraintRight_toLeftOf="@+id/iv_download" app:layout_constraintRight_toLeftOf="@+id/iv_download"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" /> app:layout_constraintLeft_toLeftOf="parent" />
@ -19,6 +22,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/primaryText"
tools:text="@string/author"
app:layout_constraintRight_toLeftOf="@+id/iv_download" app:layout_constraintRight_toLeftOf="@+id/iv_download"
app:layout_constraintTop_toBottomOf="@+id/tv_name" app:layout_constraintTop_toBottomOf="@+id/tv_name"
app:layout_constraintLeft_toLeftOf="parent" /> app:layout_constraintLeft_toLeftOf="parent" />
@ -28,6 +33,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:singleLine="true"
android:textColor="@color/secondaryText"
tools:text="@string/action_download"
app:layout_constraintRight_toLeftOf="@+id/iv_download" app:layout_constraintRight_toLeftOf="@+id/iv_download"
app:layout_constraintTop_toBottomOf="@id/tv_author" app:layout_constraintTop_toBottomOf="@id/tv_author"
app:layout_constraintLeft_toLeftOf="parent" /> app:layout_constraintLeft_toLeftOf="parent" />
@ -52,6 +59,7 @@
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:padding="10dp" android:padding="10dp"
android:text="@string/export" android:text="@string/export"
android:textColor="@color/primaryText"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" /> app:layout_constraintRight_toRightOf="parent" />

@ -24,6 +24,11 @@
android:title="@string/login" android:title="@string/login"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_top"
android:title="@string/to_top"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_copy_book_url" android:id="@+id/menu_copy_book_url"
android:title="@string/copy_book_url" android:title="@string/copy_book_url"

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_new_group"
android:title="@string/diy_source_group"
app:showAsAction="always"
tools:ignore="AlwaysShowAction" />
<item
android:id="@+id/menu_Keep_original_name"
android:title="@string/keep_original_name"
android:checkable="true"
app:showAsAction="never" />
</menu>

@ -848,4 +848,5 @@
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知错误</string> <string name="unknown_error">未知错误</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -848,4 +848,5 @@
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知错误</string> <string name="unknown_error">未知错误</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -848,4 +848,5 @@
<string name="dict">Dicionários</string> <string name="dict">Dicionários</string>
<string name="unknown_error">未知错误</string> <string name="unknown_error">未知错误</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -849,5 +849,6 @@
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知錯誤</string> <string name="unknown_error">未知錯誤</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -850,5 +850,6 @@
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知錯誤</string> <string name="unknown_error">未知錯誤</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -851,5 +851,6 @@
<string name="unknown_error">未知错误</string> <string name="unknown_error">未知错误</string>
<string name="autobackup_fail">自动备份失败</string> <string name="autobackup_fail">自动备份失败</string>
<string name="end">结束</string> <string name="end">结束</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -10,6 +10,6 @@
<string name="home_page_url" translatable="false">https://gedoor.github.io/MyBookshelf/</string> <string name="home_page_url" translatable="false">https://gedoor.github.io/MyBookshelf/</string>
<string name="latest_release_url" translatable="false">https://github.com/gedoor/legado/releases/latest</string> <string name="latest_release_url" translatable="false">https://github.com/gedoor/legado/releases/latest</string>
<string name="latest_release_api" translatable="false">https://api.github.com/repos/gedoor/legado/releases/latest</string> <string name="latest_release_api" translatable="false">https://api.github.com/repos/gedoor/legado/releases/latest</string>
<string name="tg_url" translatable="false">https://t.me/yueduguanfang</string> <string name="tg_url" translatable="false">https://t.me/legado_channels</string>
<string name="discord_url" translatable="false">https://discord.gg/qDE52P5xGW</string> <string name="discord_url" translatable="false">https://discord.gg/qDE52P5xGW</string>
</resources> </resources>

@ -853,5 +853,6 @@
<string name="unknown_error">未知错误</string> <string name="unknown_error">未知错误</string>
<string name="autobackup_fail">Autobackup failed</string> <string name="autobackup_fail">Autobackup failed</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string>
</resources> </resources>

@ -82,14 +82,6 @@
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImage"
android:title="@string/background_image" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImageBlurring"
android:title="@string/background_image_blurring" />
<io.legado.app.ui.widget.prefs.ColorPreference <io.legado.app.ui.widget.prefs.ColorPreference
android:defaultValue="@color/md_grey_200" android:defaultValue="@color/md_grey_200"
android:key="colorBottomBackground" android:key="colorBottomBackground"
@ -100,12 +92,13 @@
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImage"
android:title="@string/background_image" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="defaultCover" android:key="defaultCover"
android:title="@string/default_cover" android:title="@string/default_cover" />
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="saveDayTheme" android:key="saveDayTheme"
@ -149,14 +142,6 @@
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImageNight"
android:title="@string/background_image" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImageNightBlurring"
android:title="@string/background_image_blurring" />
<io.legado.app.ui.widget.prefs.ColorPreference <io.legado.app.ui.widget.prefs.ColorPreference
android:defaultValue="@color/md_grey_800" android:defaultValue="@color/md_grey_800"
android:key="colorBottomBackgroundNight" android:key="colorBottomBackgroundNight"
@ -165,17 +150,20 @@
app:cpv_dialogType="preset" app:cpv_dialogType="preset"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference
android:key="backgroundImageNight"
android:title="@string/background_image" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="defaultCoverDark" android:key="defaultCoverDark"
android:title="@string/default_cover" android:title="@string/default_cover" />
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
<io.legado.app.ui.widget.prefs.Preference <io.legado.app.ui.widget.prefs.Preference
android:key="saveNightTheme" android:key="saveNightTheme"
android:summary="@string/save_night_theme_summary" android:summary="@string/save_night_theme_summary"
android:title="@string/save_theme_config" android:title="@string/save_theme_config"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</io.legado.app.ui.widget.prefs.PreferenceCategory> </io.legado.app.ui.widget.prefs.PreferenceCategory>

@ -51,9 +51,9 @@ public class ResourceUtil {
public static Resource createPublicResource(String name, String author, String intro, String kind, String wordCount, String model, String href) { public static Resource createPublicResource(String name, String author, String intro, String kind, String wordCount, String model, String href) {
String html = model.replace("{name}", name) String html = model.replace("{name}", name)
.replace("{author}", author) .replace("{author}", author)
.replace("{kind}", kind) .replace("{kind}", kind == null ? "" : kind)
.replace("{wordCount}", wordCount) .replace("{wordCount}", wordCount == null ? "" : wordCount)
.replace("{intro}", StringUtil.formatHtml(intro)); .replace("{intro}", StringUtil.formatHtml(intro == null ? "" : intro));
return new Resource(html.getBytes(), href); return new Resource(html.getBytes(), href);
} }

Loading…
Cancel
Save