Merge remote-tracking branch 'upstream/master'

pull/1251/head
Jason Yao 4 years ago
commit bffc6d6e4a
  1. 1
      app/.gitignore
  2. 18
      app/build.gradle
  3. 1022
      app/schemas/io.legado.app.data.AppDatabase/1.json
  4. 1176
      app/schemas/io.legado.app.data.AppDatabase/10.json
  5. 1182
      app/schemas/io.legado.app.data.AppDatabase/11.json
  6. 1188
      app/schemas/io.legado.app.data.AppDatabase/12.json
  7. 1194
      app/schemas/io.legado.app.data.AppDatabase/13.json
  8. 1194
      app/schemas/io.legado.app.data.AppDatabase/14.json
  9. 1200
      app/schemas/io.legado.app.data.AppDatabase/15.json
  10. 1226
      app/schemas/io.legado.app.data.AppDatabase/16.json
  11. 1226
      app/schemas/io.legado.app.data.AppDatabase/17.json
  12. 1258
      app/schemas/io.legado.app.data.AppDatabase/18.json
  13. 1265
      app/schemas/io.legado.app.data.AppDatabase/19.json
  14. 1028
      app/schemas/io.legado.app.data.AppDatabase/2.json
  15. 1271
      app/schemas/io.legado.app.data.AppDatabase/20.json
  16. 1283
      app/schemas/io.legado.app.data.AppDatabase/21.json
  17. 1277
      app/schemas/io.legado.app.data.AppDatabase/22.json
  18. 1283
      app/schemas/io.legado.app.data.AppDatabase/23.json
  19. 1324
      app/schemas/io.legado.app.data.AppDatabase/24.json
  20. 1368
      app/schemas/io.legado.app.data.AppDatabase/25.json
  21. 1392
      app/schemas/io.legado.app.data.AppDatabase/26.json
  22. 1392
      app/schemas/io.legado.app.data.AppDatabase/27.json
  23. 1404
      app/schemas/io.legado.app.data.AppDatabase/28.json
  24. 1410
      app/schemas/io.legado.app.data.AppDatabase/29.json
  25. 1036
      app/schemas/io.legado.app.data.AppDatabase/3.json
  26. 1485
      app/schemas/io.legado.app.data.AppDatabase/30.json
  27. 1422
      app/schemas/io.legado.app.data.AppDatabase/31.json
  28. 1422
      app/schemas/io.legado.app.data.AppDatabase/32.json
  29. 1417
      app/schemas/io.legado.app.data.AppDatabase/33.json
  30. 1423
      app/schemas/io.legado.app.data.AppDatabase/34.json
  31. 1429
      app/schemas/io.legado.app.data.AppDatabase/35.json
  32. 1093
      app/schemas/io.legado.app.data.AppDatabase/4.json
  33. 1093
      app/schemas/io.legado.app.data.AppDatabase/5.json
  34. 1131
      app/schemas/io.legado.app.data.AppDatabase/6.json
  35. 1131
      app/schemas/io.legado.app.data.AppDatabase/7.json
  36. 1157
      app/schemas/io.legado.app.data.AppDatabase/8.json
  37. 1164
      app/schemas/io.legado.app.data.AppDatabase/9.json
  38. 50
      app/src/androidTest/java/io/legado/app/MigrationTest.kt
  39. 2
      app/src/main/AndroidManifest.xml
  40. 11
      app/src/main/assets/updateLog.md
  41. 72
      app/src/main/assets/web/uploadBook/js/html5_fun.js
  42. 51
      app/src/main/java/io/legado/app/api/controller/BookController.kt
  43. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  44. 273
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  45. 283
      app/src/main/java/io/legado/app/data/DatabaseMigrations.kt
  46. 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  47. 25
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  48. 7
      app/src/main/java/io/legado/app/data/entities/rule/LogInRule.kt
  49. 20
      app/src/main/java/io/legado/app/data/entities/rule/LoginRule.kt
  50. 8
      app/src/main/java/io/legado/app/help/AppConfig.kt
  51. 160
      app/src/main/java/io/legado/app/model/ReadBook.kt
  52. 36
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt
  53. 14
      app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt
  54. 27
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  55. 30
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  56. 30
      app/src/main/java/io/legado/app/service/TTSReadAloudService.kt
  57. 1
      app/src/main/java/io/legado/app/service/help/CacheBook.kt
  58. 2
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  59. 5
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  60. 11
      app/src/main/java/io/legado/app/ui/book/group/GroupEditDialog.kt
  61. 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  62. 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  63. 45
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt
  64. 2
      app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt
  65. 4
      app/src/main/java/io/legado/app/ui/book/login/SourceLoginActivity.kt
  66. 83
      app/src/main/java/io/legado/app/ui/book/login/SourceLoginDialog.kt
  67. 23
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  68. 11
      app/src/main/java/io/legado/app/ui/book/read/ReadBookBaseActivity.kt
  69. 65
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  70. 2
      app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt
  71. 38
      app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt
  72. 2
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  73. 2
      app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt
  74. 2
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  75. 2
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  76. 2
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  77. 2
      app/src/main/java/io/legado/app/ui/book/read/page/api/DataSource.kt
  78. 2
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  79. 2
      app/src/main/java/io/legado/app/ui/book/read/page/provider/TextPageFactory.kt
  80. 11
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  81. 40
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  82. 82
      app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt
  83. 5
      app/src/main/java/io/legado/app/ui/document/FilePickerActivity.kt
  84. 10
      app/src/main/java/io/legado/app/ui/main/bookshelf/style2/BookshelfFragment2.kt
  85. 8
      app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt
  86. 2
      app/src/main/java/io/legado/app/ui/widget/dialog/PhotoDialog.kt
  87. 15
      app/src/main/java/io/legado/app/utils/EncoderUtils.kt
  88. 71
      app/src/main/java/io/legado/app/utils/UriExtensions.kt
  89. 5
      app/src/main/java/io/legado/app/web/HttpServer.kt
  90. 2
      app/src/main/res/layout/dialog_book_group_edit.xml
  91. 70
      app/src/main/res/layout/dialog_bookshelf_config.xml
  92. 23
      app/src/main/res/layout/dialog_login.xml
  93. 5
      app/src/main/res/menu/book_cache.xml
  94. 4
      app/src/main/res/menu/source_login.xml
  95. 4
      app/src/main/res/values-es-rES/strings.xml
  96. 5
      app/src/main/res/values-ja-rJP/strings.xml
  97. 4
      app/src/main/res/values-pt-rBR/strings.xml
  98. 5
      app/src/main/res/values-zh-rHK/strings.xml
  99. 5
      app/src/main/res/values-zh-rTW/strings.xml
  100. 5
      app/src/main/res/values-zh/strings.xml
  101. Some files were not shown because too many files have changed in this diff Show More

1
app/.gitignore vendored

@ -1,2 +1 @@
/build /build
/schemas

@ -14,6 +14,10 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.3'
kotlinOptions {
jvmTarget = "11"
}
signingConfigs { signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) { if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig { myConfig {
@ -102,10 +106,10 @@ android {
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11
} }
kotlinOptions { sourceSets {
jvmTarget = "11" // Adds exported schema location as test app assets.
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
buildToolsVersion '30.0.3'
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
//options.compilerArgs << "-Xlint:unchecked" //options.compilerArgs << "-Xlint:unchecked"
} }
@ -166,7 +170,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
//room //room
def room_version = '2.3.0' def room_version = '2.4.0-alpha04'
implementation("androidx.room:room-runtime:$room_version") implementation("androidx.room:room-runtime:$room_version")
implementation("androidx.room:room-ktx:$room_version") implementation("androidx.room:room-ktx:$room_version")
kapt("androidx.room:room-compiler:$room_version") kapt("androidx.room:room-compiler:$room_version")
@ -193,9 +197,9 @@ dependencies {
implementation('com.github.bumptech.glide:glide:4.12.0') implementation('com.github.bumptech.glide:glide:4.12.0')
//webServer //webServer
implementation('org.nanohttpd:nanohttpd:2.3.1') def nanoHttpdVersion = "2.3.1"
implementation('org.nanohttpd:nanohttpd-websocket:2.3.1') implementation("org.nanohttpd:nanohttpd:$nanoHttpdVersion")
implementation('org.nanohttpd:nanohttpd-apache-fileupload:2.3.1') implementation("org.nanohttpd:nanohttpd-websocket:$nanoHttpdVersion")
// //
implementation('com.github.jenly1314:zxing-lite:2.1.1') implementation('com.github.jenly1314:zxing-lite:2.1.1')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,50 @@
package io.legado.app
import androidx.room.Room
import androidx.room.migration.Migration
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import io.legado.app.data.AppDatabase
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class MigrationTest {
private val TEST_DB = "migration-test"
private val ALL_MIGRATIONS = arrayOf<Migration>(
)
@Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
@Throws(IOException::class)
fun migrateAll() {
// Create earliest version of the database.
helper.createDatabase(TEST_DB, 30).apply {
close()
}
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
Room.databaseBuilder(
InstrumentationRegistry.getInstrumentation().targetContext,
AppDatabase::class.java,
TEST_DB
).addMigrations(*ALL_MIGRATIONS)
.build().apply {
openHelper.writableDatabase
close()
}
}
}

@ -293,7 +293,7 @@
android:launchMode="singleTop" /> android:launchMode="singleTop" />
<!-- 书源登录 --> <!-- 书源登录 -->
<activity <activity
android:name=".ui.login.SourceLoginActivity" android:name="io.legado.app.ui.book.login.SourceLoginActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" /> android:hardwareAccelerated="true" />
<!-- 阅读记录 --> <!-- 阅读记录 -->

@ -12,6 +12,17 @@
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
* 关于最近版本有时候界面没有数据的问题是因为把LiveData组件换成了谷歌推荐的Flow组件导致的问题,正在查找解决办法 * 关于最近版本有时候界面没有数据的问题是因为把LiveData组件换成了谷歌推荐的Flow组件导致的问题,正在查找解决办法
**2021/08/18**
1. 翻到最后一章时自动更新最新章节
2. 朗读添加媒体按键配置
3. 其它一些优化
**2021/08/13**
1. web传书可以使用
2. 修复一些bug
**2021/08/09** **2021/08/09**
1. 修复选择文字不能选择单个文字的bug 1. 修复选择文字不能选择单个文字的bug

@ -123,42 +123,46 @@ try {
//正在上传 //正在上传
isUploading = true; isUploading = true;
//设置上传的数据 //设置上传的数据
var fd = new FormData(); var reader = new FileReader();
fd.append("Filename", file.name); reader.readAsDataURL(file);
fd.append("Filedata", file); reader.onload = function (e) {
fd.append("Upload", "Submit Query"); var data = e.target.result;
//设置当前的上传对象 var fd = new FormData();
currUploadfile = file; fd.append("fileName", file.name);
fd.append("fileData", data);
if (XHR.readyState > 0) { //设置当前的上传对象
XHR = new XMLHttpRequest(); currUploadfile = file;
if (XHR.readyState > 0) {
XHR = new XMLHttpRequest();
}
XHR.upload.addEventListener("progress", progress, false);
XHR.upload.addEventListener("load", requestLoad, false);
XHR.upload.addEventListener("error", error, false);
XHR.upload.addEventListener("abort", abort, false);
XHR.upload.addEventListener("loadend", loadend, false);
XHR.upload.addEventListener("loadstart", loadstart, false);
XHR.open("POST", config.url);
XHR.send(fd);
XHR.onreadystatechange = function () {
//只要上传完成不管成功失败
if (XHR.readyState == 4) {
console.log("onreadystatechange ", XHR.status, +new Date());
if (XHR.status == 200) {
uploadSuccess(currUploadfile, {}, XHR.status)
} else {
uploadError()
}
//进行下一个上传
nextUpload()
}
};
} }
XHR.upload.addEventListener("progress", progress, false);
XHR.upload.addEventListener("load", requestLoad, false);
XHR.upload.addEventListener("error", error, false);
XHR.upload.addEventListener("abort", abort, false);
XHR.upload.addEventListener("loadend", loadend, false);
XHR.upload.addEventListener("loadstart", loadstart, false);
XHR.open("POST", config.url);
XHR.send(fd);
XHR.onreadystatechange = function () {
//只要上传完成不管成功失败
if (XHR.readyState == 4) {
console.log("onreadystatechange ", XHR.status, +new Date());
if (XHR.status == 200) {
uploadSuccess(currUploadfile, {}, XHR.status)
} else {
uploadError()
}
//进行下一个上传
nextUpload()
}
};
} }
//请求完成,无论失败或成功 //请求完成,无论失败或成功

@ -1,8 +1,7 @@
package io.legado.app.api.controller package io.legado.app.api.controller
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import fi.iki.elonen.NanoFileUpload
import fi.iki.elonen.NanoHTTPD
import io.legado.app.R import io.legado.app.R
import io.legado.app.api.ReturnData import io.legado.app.api.ReturnData
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@ -11,15 +10,14 @@ import io.legado.app.data.entities.Book
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ContentProcessor import io.legado.app.help.ContentProcessor
import io.legado.app.help.ImageLoader import io.legado.app.help.ImageLoader
import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.EpubFile import io.legado.app.model.localBook.EpubFile
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.localBook.UmdFile import io.legado.app.model.localBook.UmdFile
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.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.apache.commons.fileupload.disk.DiskFileItemFactory
import splitties.init.appCtx import splitties.init.appCtx
object BookController { object BookController {
@ -193,32 +191,31 @@ object BookController {
} }
} }
private val uploader by lazy { fun addLocalBook(parameters: Map<String, List<String>>): ReturnData {
val dif = DiskFileItemFactory(0, LocalBook.cacheFolder)
NanoFileUpload(dif)
}
fun addLocalBook(session: NanoHTTPD.IHTTPSession, postData: String?): ReturnData {
val returnData = ReturnData() val returnData = ReturnData()
try { try {
uploader.parseRequest(session).forEach { val fileName = parameters["fileName"]?.firstOrNull()
val path = FileUtils.getPath(LocalBook.cacheFolder, it.name) ?: return returnData.setErrorMsg("fileName 不能为空")
val nameAuthor = LocalBook.analyzeNameAuthor(it.name) val fileData = parameters["fileData"]?.firstOrNull()
val book = Book( ?: return returnData.setErrorMsg("fileData 不能为空")
bookUrl = path, val file = FileUtils.createFileIfNotExist(LocalBook.cacheFolder, fileName)
name = nameAuthor.first, val fileBytes = Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
author = nameAuthor.second, file.writeBytes(fileBytes)
originName = it.name, val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
coverUrl = FileUtils.getPath( val book = Book(
appCtx.externalFiles, bookUrl = file.absolutePath,
"covers", name = nameAuthor.first,
"${MD5Utils.md5Encode16(path)}.jpg" author = nameAuthor.second,
) originName = fileName,
coverUrl = FileUtils.getPath(
appCtx.externalFiles,
"covers",
"${MD5Utils.md5Encode16(file.absolutePath)}.jpg"
) )
if (book.isEpub()) EpubFile.upBookInfo(book) )
if (book.isUmd()) UmdFile.upBookInfo(book) if (book.isEpub()) EpubFile.upBookInfo(book)
appDb.bookDao.insert(book) if (book.isUmd()) UmdFile.upBookInfo(book)
} appDb.bookDao.insert(book)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return returnData.setErrorMsg( return returnData.setErrorMsg(

@ -47,6 +47,7 @@ object PreferKey {
const val webDavPassword = "web_dav_password" const val webDavPassword = "web_dav_password"
const val webDavCreateDir = "webDavCreateDir" const val webDavCreateDir = "webDavCreateDir"
const val exportToWebDav = "webDavCacheBackup" const val exportToWebDav = "webDavCacheBackup"
const val exportNoChapterName = "exportNoChapterName"
const val exportType = "exportType" const val exportType = "exportType"
const val changeSourceCheckAuthor = "changeSourceCheckAuthor" const val changeSourceCheckAuthor = "changeSourceCheckAuthor"
const val changeSourceLoadToc = "changeSourceLoadToc" const val changeSourceLoadToc = "changeSourceLoadToc"

@ -4,13 +4,10 @@ import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.androidId
import io.legado.app.data.dao.* import io.legado.app.data.dao.*
import io.legado.app.data.entities.* import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import splitties.init.appCtx import splitties.init.appCtx
import java.util.* import java.util.*
@ -19,13 +16,13 @@ val appDb by lazy {
} }
@Database( @Database(
version = 35,
exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
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 = 34,
exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -54,14 +51,7 @@ abstract class AppDatabase : RoomDatabase() {
fun createDatabase(context: Context) = fun createDatabase(context: Context) =
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.addMigrations( .addMigrations(*DatabaseMigrations.migrations)
migration_10_11, migration_11_12, migration_12_13, migration_13_14,
migration_14_15, migration_15_17, migration_17_18, migration_18_19,
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_27_28, migration_28_29, migration_29_30, migration_30_31,
migration_31_32, migration_32_33, migration_33_34
)
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
.build() .build()
@ -74,269 +64,28 @@ abstract class AppDatabase : RoomDatabase() {
override fun onOpen(db: SupportSQLiteDatabase) { override fun onOpen(db: SupportSQLiteDatabase) {
db.execSQL( db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAllId}, '全部', -10, 1 """insert into book_groups(groupId, groupName, 'order', show)
select ${AppConst.bookGroupAllId}, '全部', -10, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId})""" where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId})"""
) )
db.execSQL( db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupLocalId}, '本地', -9, 1 """insert into book_groups(groupId, groupName, 'order', show)
select ${AppConst.bookGroupLocalId}, '本地', -9, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId})""" where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId})"""
) )
db.execSQL( db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAudioId}, '音频', -8, 1 """insert into book_groups(groupId, groupName, 'order', show)
select ${AppConst.bookGroupAudioId}, '音频', -8, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId})""" where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId})"""
) )
db.execSQL( db.execSQL(
"""insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1 """insert into book_groups(groupId, groupName, 'order', show)
select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})""" where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})"""
) )
if (AppConfig.isGooglePlay) {
db.execSQL(
"""
delete from rssSources where sourceUrl = 'https://github.com/gedoor/legado/releases'
"""
)
}
}
}
private val migration_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE txtTocRules")
database.execSQL(
"""CREATE TABLE txtTocRules(id INTEGER NOT NULL,
name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL,
enable INTEGER NOT NULL, PRIMARY KEY (id))"""
)
}
}
private val migration_11_12 = object : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD style TEXT ")
}
}
private val migration_12_13 = object : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ")
}
}
private val migration_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
`originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT,
`customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL,
`latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL,
`totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL,
`durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL,
`originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))"""
)
database.execSQL("INSERT INTO books_new select * from books ")
database.execSQL("DROP TABLE books")
database.execSQL("ALTER TABLE books_new RENAME TO books")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
}
}
private val migration_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''")
}
}
private val migration_15_17 = object : Migration(15, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
}
}
private val migration_17_18 = object : Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
private val migration_18_19 = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL,
PRIMARY KEY(`androidId`, `bookName`))"""
)
database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${androidId}' as androidId, bookName, readTime from readRecord")
database.execSQL("DROP TABLE readRecord")
database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
}
}
private val migration_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT")
}
}
private val migration_20_21 = object : Migration(20, 21) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1")
}
}
private val migration_21_22 = object : Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
`originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT,
`coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL,
`group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL,
`lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL,
`durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL,
`order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))"""
)
database.execSQL(
"""INSERT INTO books_new select `bookUrl`, `tocUrl`, `origin`, `originName`, `name`, `author`, `kind`, `customTag`, `coverUrl`,
`customCoverUrl`, `intro`, `customIntro`, `charset`, `type`, `group`, `latestChapterTitle`, `latestChapterTime`, `lastCheckTime`,
`lastCheckCount`, `totalChapterNum`, `durChapterTitle`, `durChapterIndex`, `durChapterPos`, `durChapterTime`, `wordCount`, `canUpdate`,
`order`, `originOrder`, `variable`, null
from books"""
)
database.execSQL("DROP TABLE books")
database.execSQL("ALTER TABLE books_new RENAME TO books")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
}
}
private val migration_22_23 = object : Migration(22, 23) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''")
}
}
private val migration_23_24 = object : Migration(23, 24) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)")
}
}
private val migration_24_25 = object : Migration(24, 25) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `sourceSubs`
(`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL,
PRIMARY KEY(`id`))"""
)
}
}
private val migration_25_26 = object : Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL,
`customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))"""
)
database.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ")
database.execSQL("DROP TABLE `sourceSubs`")
} }
} }
private val migration_26_27 = object : Migration(26, 27) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ")
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `bookmarks1` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL,
`bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL,
`bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))"""
)
database.execSQL(
"""insert into `bookmarks1`
select `time`, `bookUrl`, `bookName`, `bookAuthor`, `chapterIndex`, `pageIndex`, `chapterName`, '', `content`
from bookmarks"""
)
database.execSQL(" DROP TABLE `bookmarks` ")
database.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)")
}
}
private val migration_27_28 = object : Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssArticles ADD variable TEXT")
database.execSQL("ALTER TABLE rssStars ADD variable TEXT")
}
}
private val migration_28_29 = object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
}
}
private val migration_29_30 = object : Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `epubChapters`
(`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT,
PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )
"""
)
database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
}
}
private val migration_30_31 = object : Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE readRecord RENAME TO readRecord1")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))
"""
)
database.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1")
}
}
private val migration_31_32 = object : Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `epubChapters`")
}
}
private val migration_32_33 = object : Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL,
`bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL,
`chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL,
`content` TEXT NOT NULL, PRIMARY KEY(`time`))
"""
)
database.execSQL(
"""
CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`)
"""
)
database.execSQL(
"""
insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content)
select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor,
chapterIndex, chapterPos, chapterName, bookText, content from bookmarks_old o
left join books b on o.bookUrl = b.bookUrl
"""
)
}
}
private val migration_33_34 = object : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
}
}
} }
} }

@ -0,0 +1,283 @@
package io.legado.app.data
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.constant.AppConst
object DatabaseMigrations {
val migrations: Array<Migration> by lazy {
arrayOf(
migration_10_11,
migration_11_12,
migration_12_13,
migration_13_14,
migration_14_15,
migration_15_17,
migration_17_18,
migration_18_19,
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_27_28,
migration_28_29,
migration_29_30,
migration_30_31,
migration_31_32,
migration_32_33,
migration_33_34,
migration_34_35
)
}
private val migration_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE txtTocRules")
database.execSQL(
"""CREATE TABLE txtTocRules(id INTEGER NOT NULL,
name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL,
enable INTEGER NOT NULL, PRIMARY KEY (id))"""
)
}
}
private val migration_11_12 = object : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD style TEXT ")
}
}
private val migration_12_13 = object : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ")
}
}
private val migration_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
`originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT,
`customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL,
`latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL,
`totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL,
`durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL,
`originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))"""
)
database.execSQL("INSERT INTO books_new select * from books ")
database.execSQL("DROP TABLE books")
database.execSQL("ALTER TABLE books_new RENAME TO books")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
}
}
private val migration_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''")
}
}
private val migration_15_17 = object : Migration(15, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
}
}
private val migration_17_18 = object : Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
}
}
private val migration_18_19 = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL,
PRIMARY KEY(`androidId`, `bookName`))"""
)
database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${AppConst.androidId}' as androidId, bookName, readTime from readRecord")
database.execSQL("DROP TABLE readRecord")
database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
}
}
private val migration_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT")
}
}
private val migration_20_21 = object : Migration(20, 21) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1")
}
}
private val migration_21_22 = object : Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
`originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT,
`coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL,
`group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL,
`lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL,
`durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL,
`order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))"""
)
database.execSQL(
"""INSERT INTO books_new select `bookUrl`, `tocUrl`, `origin`, `originName`, `name`, `author`, `kind`, `customTag`, `coverUrl`,
`customCoverUrl`, `intro`, `customIntro`, `charset`, `type`, `group`, `latestChapterTitle`, `latestChapterTime`, `lastCheckTime`,
`lastCheckCount`, `totalChapterNum`, `durChapterTitle`, `durChapterIndex`, `durChapterPos`, `durChapterTime`, `wordCount`, `canUpdate`,
`order`, `originOrder`, `variable`, null
from books"""
)
database.execSQL("DROP TABLE books")
database.execSQL("ALTER TABLE books_new RENAME TO books")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
}
}
private val migration_22_23 = object : Migration(22, 23) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''")
}
}
private val migration_23_24 = object : Migration(23, 24) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)")
}
}
private val migration_24_25 = object : Migration(24, 25) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `sourceSubs`
(`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL,
PRIMARY KEY(`id`))"""
)
}
}
private val migration_25_26 = object : Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL,
`customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))"""
)
database.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ")
database.execSQL("DROP TABLE `sourceSubs`")
}
}
private val migration_26_27 = object : Migration(26, 27) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ")
database.execSQL(
"""CREATE TABLE IF NOT EXISTS `bookmarks1` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL,
`bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL,
`bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))"""
)
database.execSQL(
"""insert into `bookmarks1`
select `time`, `bookUrl`, `bookName`, `bookAuthor`, `chapterIndex`, `pageIndex`, `chapterName`, '', `content`
from bookmarks"""
)
database.execSQL(" DROP TABLE `bookmarks` ")
database.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)")
}
}
private val migration_27_28 = object : Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssArticles ADD variable TEXT")
database.execSQL("ALTER TABLE rssStars ADD variable TEXT")
}
}
private val migration_28_29 = object : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
}
}
private val migration_29_30 = object : Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `epubChapters`
(`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT,
PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )
"""
)
database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
}
}
private val migration_30_31 = object : Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE readRecord RENAME TO readRecord1")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))
"""
)
database.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1")
}
}
private val migration_31_32 = object : Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `epubChapters`")
}
}
private val migration_32_33 = object : Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL,
`bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL,
`chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL,
`content` TEXT NOT NULL, PRIMARY KEY(`time`))
"""
)
database.execSQL(
"""
CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`)
"""
)
database.execSQL(
"""
insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content)
select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor,
chapterIndex, chapterPos, chapterName, bookText, content from bookmarks_old o
left join books b on o.bookUrl = b.bookUrl
"""
)
}
}
private val migration_33_34 = object : Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
}
}
private val migration_34_35 = object : Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `book_sources` ADD `concurrentRate` TEXT")
}
}
}

@ -6,7 +6,7 @@ import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils import io.legado.app.utils.MD5Utils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject

@ -29,6 +29,7 @@ data class BookSource(
var bookSourceUrl: String = "", // 地址,包括 http/https var bookSourceUrl: String = "", // 地址,包括 http/https
var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频 var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频
var bookUrlPattern: String? = null, // 详情页url正则 var bookUrlPattern: String? = null, // 详情页url正则
var concurrentRate: String? = null, //并发率
var customOrder: Int = 0, // 手动排序编号 var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用 var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现 var enabledExplore: Boolean = true, // 启用发现
@ -46,6 +47,17 @@ data class BookSource(
var ruleContent: ContentRule? = null // 正文页规则 var ruleContent: ContentRule? = null // 正文页规则
) : Parcelable, JsExtensions { ) : Parcelable, JsExtensions {
@delegate:Transient
@delegate:Ignore
@IgnoredOnParcel
val loginRule by lazy {
if (loginUrl.isJsonObject()) {
return@lazy GSON.fromJsonObject<LoginRule>(loginUrl)
} else {
return@lazy LoginRule(url = loginUrl)
}
}
@delegate:Transient @delegate:Transient
@delegate:Ignore @delegate:Ignore
@IgnoredOnParcel @IgnoredOnParcel
@ -178,6 +190,19 @@ data class BookSource(
private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
class Converters { class Converters {
@TypeConverter
fun loginRuleTString(loginRule: LoginRule?): String = GSON.toJson(loginRule)
@TypeConverter
fun stringToLoginRule(json: String?): LoginRule? {
json ?: return null
return if (json.isJsonObject()) {
GSON.fromJsonObject(json)
} else {
LoginRule(url = json)
}
}
@TypeConverter @TypeConverter
fun exploreRuleToString(exploreRule: ExploreRule?): String = GSON.toJson(exploreRule) fun exploreRuleToString(exploreRule: ExploreRule?): String = GSON.toJson(exploreRule)

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

@ -0,0 +1,20 @@
package io.legado.app.data.entities.rule
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class LoginRule(
var ui: List<RowUi>? = null,
var url: String? = null,
var checkJs: String? = null
) : Parcelable {
@Parcelize
data class RowUi(
var name: String,
var type: String,
) : Parcelable
}

@ -205,7 +205,11 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
set(value) { set(value) {
appCtx.putPrefBoolean(PreferKey.exportToWebDav, value) appCtx.putPrefBoolean(PreferKey.exportToWebDav, value)
} }
var exportNoChapterName: Boolean
get() = appCtx.getPrefBoolean(PreferKey.exportNoChapterName)
set(value) {
appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
}
var exportType: Int var exportType: Int
get() = appCtx.getPrefInt(PreferKey.exportType) get() = appCtx.getPrefInt(PreferKey.exportType)
set(value) { set(value) {
@ -221,7 +225,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val autoChangeSource: Boolean val autoChangeSource: Boolean
get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true) get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
val changeSourceLoadInfo get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc) val changeSourceLoadInfo get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo)
val changeSourceLoadToc get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc) val changeSourceLoadToc get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc)

@ -1,4 +1,4 @@
package io.legado.app.service.help package io.legado.app.model
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.github.liuyueyi.quick.transfer.ChineseUtils import com.github.liuyueyi.quick.transfer.ChineseUtils
@ -10,13 +10,15 @@ import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.storage.BookWebDav import io.legado.app.help.storage.BookWebDav
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.CacheBook
import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ImageProvider import io.legado.app.ui.book.read.page.provider.ImageProvider
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.math.max import kotlin.math.max
@ -44,7 +46,7 @@ object ReadBook {
var readStartTime: Long = System.currentTimeMillis() var readStartTime: Long = System.currentTimeMillis()
fun resetData(book: Book) { fun resetData(book: Book) {
this.book = book ReadBook.book = book
readRecord.bookName = book.name readRecord.bookName = book.name
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0 readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
durChapterIndex = book.durChapterIndex durChapterIndex = book.durChapterIndex
@ -80,10 +82,14 @@ object ReadBook {
} }
fun setProgress(progress: BookProgress) { fun setProgress(progress: BookProgress) {
durChapterIndex = progress.durChapterIndex if (durChapterIndex != progress.durChapterIndex
durChapterPos = progress.durChapterPos || durChapterPos != progress.durChapterPos
clearTextChapter() ) {
loadContent(resetPageOffset = true) durChapterIndex = progress.durChapterIndex
durChapterPos = progress.durChapterPos
clearTextChapter()
loadContent(resetPageOffset = true)
}
} }
fun clearTextChapter() { fun clearTextChapter() {
@ -109,8 +115,8 @@ object ReadBook {
} }
fun upMsg(msg: String?) { fun upMsg(msg: String?) {
if (this.msg != msg) { if (ReadBook.msg != msg) {
this.msg = msg ReadBook.msg = msg
callBack?.upContent() callBack?.upContent()
} }
} }
@ -128,25 +134,39 @@ object ReadBook {
prevTextChapter = curTextChapter prevTextChapter = curTextChapter
curTextChapter = nextTextChapter curTextChapter = nextTextChapter
nextTextChapter = null nextTextChapter = null
book?.let { if (curTextChapter == null) {
if (curTextChapter == null) { loadContent(durChapterIndex, upContent, false)
loadContent(durChapterIndex, upContent, false) } else if (upContent) {
} else if (upContent) { callBack?.upContent()
callBack?.upContent()
}
loadContent(durChapterIndex.plus(1), upContent, false)
Coroutine.async {
val maxChapterIndex =
min(chapterSize - 1, durChapterIndex + AppConfig.preDownloadNum)
for (i in durChapterIndex.plus(2)..maxChapterIndex) {
delay(1000)
download(i)
}
}
} }
loadContent(durChapterIndex.plus(1), upContent, false)
saveRead() saveRead()
callBack?.upView() callBack?.upView()
curPageChanged() curPageChanged()
Coroutine.async {
//预下载
val maxChapterIndex =
min(chapterSize - 1, durChapterIndex + AppConfig.preDownloadNum)
for (i in durChapterIndex.plus(2)..maxChapterIndex) {
delay(1000)
download(i)
}
book?.let { book ->
//最后一章时检查更新
if (durChapterPos == 0 && durChapterIndex == chapterSize - 1) {
webBook?.getChapterList(this, book)
?.onSuccess(IO) { cList ->
if (book.bookUrl == ReadBook.book?.bookUrl
&& cList.size > chapterSize
) {
appDb.bookChapterDao.insert(*cList.toTypedArray())
chapterSize = cList.size
nextTextChapter ?: loadContent(1)
}
}
}
}
}
return true return true
} else { } else {
return false return false
@ -163,24 +183,23 @@ object ReadBook {
nextTextChapter = curTextChapter nextTextChapter = curTextChapter
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
book?.let { if (curTextChapter == null) {
if (curTextChapter == null) { loadContent(durChapterIndex, upContent, false)
loadContent(durChapterIndex, upContent, false) } else if (upContent) {
} else if (upContent) { callBack?.upContent()
callBack?.upContent()
}
loadContent(durChapterIndex.minus(1), upContent, false)
Coroutine.async {
val minChapterIndex = max(0, durChapterIndex - 5)
for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
delay(1000)
download(i)
}
}
} }
loadContent(durChapterIndex.minus(1), upContent, false)
saveRead() saveRead()
callBack?.upView() callBack?.upView()
curPageChanged() curPageChanged()
Coroutine.async {
//预下载
val minChapterIndex = max(0, durChapterIndex - 5)
for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
delay(1000)
download(i)
}
}
return true return true
} else { } else {
return false return false
@ -202,6 +221,9 @@ object ReadBook {
curPageChanged() curPageChanged()
} }
/**
* 当前页面变化
*/
private fun curPageChanged() { private fun curPageChanged() {
callBack?.pageChanged() callBack?.pageChanged()
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
@ -257,7 +279,7 @@ object ReadBook {
fun loadContent( fun loadContent(
index: Int, index: Int,
upContent: Boolean = true, upContent: Boolean = true,
resetPageOffset: Boolean, resetPageOffset: Boolean = false,
success: (() -> Unit)? = null success: (() -> Unit)? = null
) { ) {
book?.let { book -> book?.let { book ->
@ -333,66 +355,6 @@ object ReadBook {
} }
} }
fun searchResultPositions(
pages: List<TextPage>,
indexWithinChapter: Int,
query: String
): Array<Int> {
// calculate search result's pageIndex
var content = ""
pages.map {
content += it.text
}
var count = 1
var index = content.indexOf(query)
while (count != indexWithinChapter) {
index = content.indexOf(query, index + 1)
count += 1
}
val contentPosition = index
var pageIndex = 0
var length = pages[pageIndex].text.length
while (length < contentPosition) {
pageIndex += 1
if (pageIndex > pages.size) {
pageIndex = pages.size
break
}
length += pages[pageIndex].text.length
}
// calculate search result's lineIndex
val currentPage = pages[pageIndex]
var lineIndex = 0
length = length - currentPage.text.length + currentPage.textLines[lineIndex].text.length
while (length < contentPosition) {
lineIndex += 1
if (lineIndex > currentPage.textLines.size) {
lineIndex = currentPage.textLines.size
break
}
length += currentPage.textLines[lineIndex].text.length
}
// charIndex
val currentLine = currentPage.textLines[lineIndex]
length -= currentLine.text.length
val charIndex = contentPosition - length
var addLine = 0
var charIndex2 = 0
// change line
if ((charIndex + query.length) > currentLine.text.length) {
addLine = 1
charIndex2 = charIndex + query.length - currentLine.text.length - 1
}
// changePage
if ((lineIndex + addLine + 1) > currentPage.textLines.size) {
addLine = -1
charIndex2 = charIndex + query.length - currentLine.text.length - 1
}
return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2)
}
/** /**
* 内容加载完成 * 内容加载完成
*/ */

@ -7,7 +7,6 @@ import org.jsoup.select.Collector
import org.jsoup.select.Elements import org.jsoup.select.Elements
import org.jsoup.select.Evaluator import org.jsoup.select.Evaluator
import org.seimicrawler.xpath.JXNode import org.seimicrawler.xpath.JXNode
import java.util.*
/** /**
* Created by GKF on 2018/1/25. * Created by GKF on 2018/1/25.
@ -71,7 +70,7 @@ class AnalyzeByJSoup(doc: Any) {
val results = ArrayList<List<String>>() val results = ArrayList<List<String>>()
for (ruleStrX in ruleStrS) { for (ruleStrX in ruleStrS) {
val temp: List<String>? = val temp: ArrayList<String>? =
if (sourceRule.isCss) { if (sourceRule.isCss) {
val lastIndex = ruleStrX.lastIndexOf('@') val lastIndex = ruleStrX.lastIndexOf('@')
getResultLast( getResultLast(
@ -83,11 +82,8 @@ class AnalyzeByJSoup(doc: Any) {
} }
if (!temp.isNullOrEmpty()) { if (!temp.isNullOrEmpty()) {
results.add(temp)
results.add(temp) //!temp.isNullOrEmpty()时,results.isNotEmpty()为true
if (ruleAnalyzes.elementsType == "||") break if (ruleAnalyzes.elementsType == "||") break
} }
} }
if (results.size > 0) { if (results.size > 0) {
@ -181,7 +177,7 @@ class AnalyzeByJSoup(doc: Any) {
/** /**
* 获取内容列表 * 获取内容列表
*/ */
private fun getResultList(ruleStr: String): List<String>? { private fun getResultList(ruleStr: String): ArrayList<String>? {
if (ruleStr.isEmpty()) return null if (ruleStr.isEmpty()) return null
@ -210,32 +206,42 @@ class AnalyzeByJSoup(doc: Any) {
/** /**
* 根据最后一个规则获取内容 * 根据最后一个规则获取内容
*/ */
private fun getResultLast(elements: Elements, lastRule: String): List<String> { private fun getResultLast(elements: Elements, lastRule: String): ArrayList<String> {
val textS = ArrayList<String>() val textS = ArrayList<String>()
try { try {
when (lastRule) { when (lastRule) {
"text" -> for (element in elements) { "text" -> for (element in elements) {
textS.add(element.text()) val text = element.text()
if (text.isNotEmpty()) {
textS.add(text)
}
} }
"textNodes" -> for (element in elements) { "textNodes" -> for (element in elements) {
val tn = arrayListOf<String>() val tn = arrayListOf<String>()
val contentEs = element.textNodes() val contentEs = element.textNodes()
for (item in contentEs) { for (item in contentEs) {
val temp = item.text().trim { it <= ' ' } val text = item.text().trim { it <= ' ' }
if (temp.isNotEmpty()) { if (text.isNotEmpty()) {
tn.add(temp) tn.add(text)
} }
} }
textS.add(tn.joinToString("\n")) if (tn.isNotEmpty()) {
textS.add(tn.joinToString("\n"))
}
} }
"ownText" -> for (element in elements) { "ownText" -> for (element in elements) {
textS.add(element.ownText()) val text = element.ownText()
if (text.isNotEmpty()) {
textS.add(text)
}
} }
"html" -> { "html" -> {
elements.select("script").remove() elements.select("script").remove()
elements.select("style").remove() elements.select("style").remove()
val html = elements.outerHtml() val html = elements.outerHtml()
textS.add(html) if (html.isNotEmpty()) {
textS.add(html)
}
} }
"all" -> textS.add(elements.outerHtml()) "all" -> textS.add(elements.outerHtml())
else -> for (element in elements) { else -> for (element in elements) {

@ -8,6 +8,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.LifecycleHelp import io.legado.app.help.LifecycleHelp
import io.legado.app.model.ReadBook
import io.legado.app.service.AudioPlayService import io.legado.app.service.AudioPlayService
import io.legado.app.service.BaseReadAloudService import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.help.AudioPlay import io.legado.app.service.help.AudioPlay
@ -15,6 +16,7 @@ import io.legado.app.service.help.ReadAloud
import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.audio.AudioPlayActivity
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.main.MainActivity import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@ -42,10 +44,18 @@ class MediaButtonReceiver : BroadcastReceiver() {
if (action == KeyEvent.ACTION_DOWN) { if (action == KeyEvent.ACTION_DOWN) {
when (keycode) { when (keycode) {
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
ReadAloud.prevParagraph(context) if (context.getPrefBoolean("mediaButtonPerNext", false)) {
ReadBook.moveToPrevChapter(true)
} else {
ReadAloud.prevParagraph(context)
}
} }
KeyEvent.KEYCODE_MEDIA_NEXT -> { KeyEvent.KEYCODE_MEDIA_NEXT -> {
ReadAloud.nextParagraph(context) if (context.getPrefBoolean("mediaButtonPerNext", false)) {
ReadBook.moveToNextChapter(true)
} else {
ReadAloud.nextParagraph(context)
}
} }
else -> readAloud(context) else -> readAloud(context)
} }

@ -21,8 +21,8 @@ import io.legado.app.constant.*
import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentDataHelp
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.model.ReadBook
import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.receiver.MediaButtonReceiver
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
@ -67,6 +67,7 @@ abstract class BaseReadAloudService : BaseService(),
initBroadcastReceiver() initBroadcastReceiver()
upNotification() upNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
doDs()
} }
override fun onDestroy() { override fun onDestroy() {
@ -137,6 +138,8 @@ abstract class BaseReadAloudService : BaseService(),
postEvent(EventBus.ALOUD_STATE, Status.PLAY) postEvent(EventBus.ALOUD_STATE, Status.PLAY)
} }
abstract fun playStop()
@CallSuper @CallSuper
open fun pauseReadAloud(pause: Boolean) { open fun pauseReadAloud(pause: Boolean) {
BaseReadAloudService.pause = pause BaseReadAloudService.pause = pause
@ -157,9 +160,27 @@ abstract class BaseReadAloudService : BaseService(),
abstract fun upSpeechRate(reset: Boolean = false) abstract fun upSpeechRate(reset: Boolean = false)
abstract fun prevP() private fun prevP() {
if (nowSpeak > 0) {
playStop()
nowSpeak--
readAloudNumber -= contentList[nowSpeak].length.minus(1)
play()
} else {
ReadBook.moveToPrevChapter(true)
}
}
abstract fun nextP() private fun nextP() {
if (nowSpeak < contentList.size - 1) {
playStop()
readAloudNumber += contentList[nowSpeak].length.plus(1)
nowSpeak++
play()
} else {
nextChapter()
}
}
private fun setTimer(minute: Int) { private fun setTimer(minute: Int) {
timeMinute = minute timeMinute = minute

@ -6,9 +6,9 @@ import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ReadBook
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
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.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
@ -68,6 +68,10 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
} }
override fun playStop() {
player.stop()
}
private fun downloadAudio() { private fun downloadAudio() {
task?.cancel() task?.cancel()
task = execute { task = execute {
@ -216,30 +220,6 @@ class HttpReadAloudService : BaseReadAloudService(),
downloadAudio() downloadAudio()
} }
/**
* 上一段
*/
override fun prevP() {
if (nowSpeak > 0) {
player.stop()
nowSpeak--
readAloudNumber -= contentList[nowSpeak].length.minus(1)
play()
}
}
/**
* 下一段
*/
override fun nextP() {
if (nowSpeak < contentList.size - 1) {
player.stop()
readAloudNumber += contentList[nowSpeak].length.plus(1)
nowSpeak++
play()
}
}
override fun onPrepared(mp: MediaPlayer?) { override fun onPrepared(mp: MediaPlayer?) {
super.play() super.play()
if (pause) return if (pause) return

@ -9,7 +9,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp import io.legado.app.help.MediaHelp
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@ -76,6 +76,10 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
} }
} }
override fun playStop() {
textToSpeech?.stop()
}
/** /**
* 更新朗读速度 * 更新朗读速度
*/ */
@ -90,30 +94,6 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
} }
} }
/**
* 上一段
*/
override fun prevP() {
if (nowSpeak > 0) {
textToSpeech?.stop()
nowSpeak--
readAloudNumber -= contentList[nowSpeak].length.minus(1)
play()
}
}
/**
* 下一段
*/
override fun nextP() {
if (nowSpeak < contentList.size - 1) {
textToSpeech?.stop()
readAloudNumber += contentList[nowSpeak].length.plus(1)
nowSpeak++
play()
}
}
/** /**
* 暂停朗读 * 暂停朗读
*/ */

@ -5,6 +5,7 @@ import io.legado.app.R
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
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.model.ReadBook
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService import io.legado.app.service.CacheBookService
import io.legado.app.utils.msg import io.legado.app.utils.msg

@ -92,6 +92,7 @@ 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_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName
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 = menu.findItem(R.id.menu_export_type)?.title =
"${getString(R.string.export_type)}(${getTypeName()})" "${getString(R.string.export_type)}(${getTypeName()})"
@ -127,6 +128,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
} }
R.id.menu_export_all -> exportAll() R.id.menu_export_all -> exportAll()
R.id.menu_enable_replace -> AppConfig.exportUseReplace = !item.isChecked R.id.menu_enable_replace -> AppConfig.exportUseReplace = !item.isChecked
R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked
R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked
R.id.menu_export_folder -> { R.id.menu_export_folder -> {
exportPosition = -1 exportPosition = -1

@ -139,7 +139,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
) )
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
BookHelp.getContent(book, chapter).let { content -> BookHelp.getContent(book, chapter).let { content ->
val content1 = contentProcessor var content1 = contentProcessor
.getContent( .getContent(
book, book,
chapter.title.replace("\\r?\\n".toRegex(), " "), chapter.title.replace("\\r?\\n".toRegex(), " "),
@ -148,6 +148,9 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
useReplace useReplace
) )
.joinToString("\n") .joinToString("\n")
if(AppConfig.exportNoChapterName){
content1 = content.toString()
}
append.invoke("\n\n$content1") append.invoke("\n\n$content1")
} }
} }

@ -34,12 +34,11 @@ class GroupEditDialog : BaseDialogFragment() {
private val viewModel by viewModels<GroupViewModel>() private val viewModel by viewModels<GroupViewModel>()
private var bookGroup: BookGroup? = null private var bookGroup: BookGroup? = null
val selectImage = registerForActivityResult(SelectImageContract()) { val selectImage = registerForActivityResult(SelectImageContract()) {
it?.second?.let { uri -> it?.second?.read(this) { name, bytes ->
if (uri.isContentScheme()) { var file = requireContext().externalFiles
binding.ivCover.load(uri.toString()) file = FileUtils.createFileIfNotExist(file, "covers", name)
} else { file.writeBytes(bytes)
binding.ivCover.load(uri.path) binding.ivCover.load(file.absolutePath)
}
} }
} }

@ -33,11 +33,11 @@ import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.ui.book.changesource.ChangeSourceDialog import io.legado.app.ui.book.changesource.ChangeSourceDialog
import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.book.group.GroupSelectDialog
import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.info.edit.BookInfoEditActivity
import io.legado.app.ui.book.login.SourceLoginActivity
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
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

@ -11,9 +11,9 @@ 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.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.model.ReadBook
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.utils.postEvent 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

@ -6,17 +6,16 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile
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.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.databinding.ActivityBookInfoEditBinding import io.legado.app.databinding.ActivityBookInfoEditBinding
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.utils.* import io.legado.app.utils.FileUtils
import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.externalFiles
import io.legado.app.utils.read
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import java.io.File
class BookInfoEditActivity : class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(), VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
@ -103,37 +102,11 @@ class BookInfoEditActivity :
} }
private fun coverChangeTo(uri: Uri) { private fun coverChangeTo(uri: Uri) {
if (uri.isContentScheme()) { uri.read(this) { name, bytes ->
val doc = DocumentFile.fromSingleUri(this, uri) var file = this.externalFiles
doc?.name?.let { file = FileUtils.createFileIfNotExist(file, "covers", name)
var file = this.externalFiles file.writeBytes(bytes)
file = FileUtils.createFileIfNotExist(file, "covers", it) coverChangeTo(file.absolutePath)
kotlin.runCatching {
DocumentUtils.readBytes(this, doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
coverChangeTo(file.absolutePath)
} ?: toastOnUi("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(this, uri)?.let { path ->
val imgFile = File(path)
if (imgFile.exists()) {
var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name)
file.writeBytes(imgFile.readBytes())
coverChangeTo(file.absolutePath)
}
}
}
.request()
} }
} }

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
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.service.help.ReadBook import io.legado.app.model.ReadBook
class BookInfoEditViewModel(application: Application) : BaseViewModel(application) { class BookInfoEditViewModel(application: Application) : BaseViewModel(application) {
var book: Book? = null var book: Book? = null

@ -1,4 +1,4 @@
package io.legado.app.ui.login package io.legado.app.ui.book.login
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Bitmap import android.graphics.Bitmap
@ -74,7 +74,7 @@ class SourceLoginActivity : BaseActivity<ActivitySourceLoginBinding>() {
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_success -> { R.id.menu_ok -> {
if (!checking) { if (!checking) {
checking = true checking = true
binding.titleBar.snackbar(R.string.check_host_cookie) binding.titleBar.snackbar(R.string.check_host_cookie)

@ -0,0 +1,83 @@
package io.legado.app.ui.book.login
import android.os.Bundle
import android.text.InputType
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.constant.AppConst
import io.legado.app.data.entities.rule.LoginRule
import io.legado.app.databinding.DialogLoginBinding
import io.legado.app.help.CacheManager
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.text.EditText
import io.legado.app.ui.widget.text.TextInputLayout
import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.GSON
import io.legado.app.utils.applyTint
import io.legado.app.utils.viewbindingdelegate.viewBinding
class SourceLoginDialog : BaseDialogFragment() {
private val binding by viewBinding(DialogLoginBinding::bind)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_login, container)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
binding.toolBar.setBackgroundColor(primaryColor)
val sourceUrl = arguments?.getString("sourceUrl")
val loginRule = arguments?.getParcelable<LoginRule>("loginRule")
loginRule?.ui?.forEachIndexed { index, rowUi ->
when (rowUi.type) {
"text" -> layoutInflater.inflate(R.layout.item_source_edit, binding.root)
.apply {
id = index
}
"password" -> layoutInflater.inflate(R.layout.item_source_edit, binding.root)
.apply {
id = index
findViewById<EditText>(R.id.editText)?.inputType =
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
}
}
}
binding.toolBar.inflateMenu(R.menu.source_login)
binding.toolBar.menu.applyTint(requireContext())
binding.toolBar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_ok -> {
val loginData = hashMapOf<String, String?>()
loginRule?.ui?.forEachIndexed { index, rowUi ->
when (rowUi.type) {
"text", "password" -> {
val value = binding.root.findViewById<TextInputLayout>(index)
.findViewById<EditText>(R.id.editText).text?.toString()
loginData[rowUi.name] = value
}
}
}
val data = Base64.encodeToString(
EncoderUtils.decryptAES(
GSON.toJson(loginData).toByteArray(),
AppConst.androidId.toByteArray()
),
Base64.DEFAULT
)
CacheManager.put("login_$sourceUrl", data)
}
}
return@setOnMenuItemClickListener true
}
}
}

@ -29,12 +29,13 @@ import io.legado.app.help.storage.Backup
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.model.ReadBook
import io.legado.app.receiver.TimeBatteryReceiver import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService 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.ui.book.changesource.ChangeSourceDialog import io.legado.app.ui.book.changesource.ChangeSourceDialog
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.login.SourceLoginActivity
import io.legado.app.ui.book.read.config.* import io.legado.app.ui.book.read.config.*
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR
import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR
@ -45,9 +46,9 @@ import io.legado.app.ui.book.read.page.entities.PageDirection
import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.ui.book.searchContent.SearchContentActivity import io.legado.app.ui.book.searchContent.SearchContentActivity
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.BookmarkDialog
import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.dict.DictDialog import io.legado.app.ui.dict.DictDialog
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.replace.edit.ReplaceEditActivity import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
@ -110,7 +111,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
override val isInitFinish: Boolean get() = viewModel.isInitFinish override val isInitFinish: Boolean get() = viewModel.isInitFinish
override val isScroll: Boolean get() = binding.readView.isScroll override val isScroll: Boolean get() = binding.readView.isScroll
private val mHandler = Handler(Looper.getMainLooper()) private val mHandler = Handler(Looper.getMainLooper())
private val keepScreenRunnable = Runnable { keepScreenOn(window, false) } private val keepScreenRunnable = Runnable { keepScreenOn(false) }
private val autoPageRunnable = Runnable { autoPagePlus() } private val autoPageRunnable = Runnable { autoPagePlus() }
private val backupRunnable = Runnable { private val backupRunnable = Runnable {
if (!BuildConfig.DEBUG) { if (!BuildConfig.DEBUG) {
@ -251,7 +252,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
chapterName = page.title chapterName = page.title
bookText = page.text.trim() bookText = page.text.trim()
} }
showBookMark(bookmark) BookmarkDialog.start(supportFragmentManager, bookmark)
} }
} }
R.id.menu_copy_text -> R.id.menu_copy_text ->
@ -513,7 +514,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
if (bookmark == null) { if (bookmark == null) {
toastOnUi(R.string.create_bookmark_error) toastOnUi(R.string.create_bookmark_error)
} else { } else {
showBookMark(bookmark) BookmarkDialog.start(supportFragmentManager, bookmark)
} }
return true return true
} }
@ -854,11 +855,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
private fun skipToSearch(index: Int, indexWithinChapter: Int) { private fun skipToSearch(index: Int, indexWithinChapter: Int) {
viewModel.openChapter(index) { viewModel.openChapter(index) {
val pages = ReadBook.curTextChapter?.pages ?: return@openChapter val pages = ReadBook.curTextChapter?.pages ?: return@openChapter
val positions = ReadBook.searchResultPositions( val positions = viewModel.searchResultPositions(pages, indexWithinChapter)
pages,
indexWithinChapter,
viewModel.searchContentQuery
)
ReadBook.skipToPage(positions[0]) { ReadBook.skipToPage(positions[0]) {
launch { launch {
binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2])
@ -983,16 +980,16 @@ class ReadBookActivity : ReadBookBaseActivity(),
*/ */
override fun screenOffTimerStart() { override fun screenOffTimerStart() {
if (screenTimeOut < 0) { if (screenTimeOut < 0) {
keepScreenOn(window, true) keepScreenOn(true)
return return
} }
val t = screenTimeOut - sysScreenOffTime val t = screenTimeOut - sysScreenOffTime
if (t > 0) { if (t > 0) {
mHandler.removeCallbacks(keepScreenRunnable) mHandler.removeCallbacks(keepScreenRunnable)
keepScreenOn(window, true) keepScreenOn(true)
mHandler.postDelayed(keepScreenRunnable, screenTimeOut) mHandler.postDelayed(keepScreenRunnable, screenTimeOut)
} else { } else {
keepScreenOn(window, false) keepScreenOn(false)
} }
} }
} }

@ -12,7 +12,6 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst.charsets import io.legado.app.constant.AppConst.charsets
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.ActivityBookReadBinding import io.legado.app.databinding.ActivityBookReadBinding
import io.legado.app.databinding.DialogDownloadChoiceBinding import io.legado.app.databinding.DialogDownloadChoiceBinding
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
@ -24,12 +23,11 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.ThemeStore import io.legado.app.lib.theme.ThemeStore
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.model.ReadBook
import io.legado.app.service.help.CacheBook import io.legado.app.service.help.CacheBook
import io.legado.app.service.help.ReadBook
import io.legado.app.ui.book.read.config.BgTextConfigDialog import io.legado.app.ui.book.read.config.BgTextConfigDialog
import io.legado.app.ui.book.read.config.ClickActionConfigDialog import io.legado.app.ui.book.read.config.ClickActionConfigDialog
import io.legado.app.ui.book.read.config.PaddingConfigDialog import io.legado.app.ui.book.read.config.PaddingConfigDialog
import io.legado.app.ui.book.toc.BookmarkDialog
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -152,7 +150,7 @@ abstract class ReadBookBaseActivity :
/** /**
* 保持亮屏 * 保持亮屏
*/ */
fun keepScreenOn(window: Window, on: Boolean) { fun keepScreenOn(on: Boolean) {
if (on) { if (on) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else { } else {
@ -194,11 +192,6 @@ abstract class ReadBookBaseActivity :
} }
} }
@SuppressLint("InflateParams")
fun showBookMark(bookmark: Bookmark) {
BookmarkDialog.start(supportFragmentManager, bookmark)
}
fun showCharsetConfig() { fun showCharsetConfig() {
alert(R.string.set_charset) { alert(R.string.set_charset) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {

@ -14,12 +14,13 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ContentProcessor import io.legado.app.help.ContentProcessor
import io.legado.app.help.storage.BookWebDav import io.legado.app.help.storage.BookWebDav
import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.PreciseSearch import io.legado.app.model.webBook.PreciseSearch
import io.legado.app.model.webBook.WebBook import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService 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.ui.book.read.page.entities.TextPage
import io.legado.app.utils.msg import io.legado.app.utils.msg
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@ -305,6 +306,68 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
/**
* 内容搜索跳转
*/
fun searchResultPositions(
pages: List<TextPage>,
indexWithinChapter: Int
): Array<Int> {
// calculate search result's pageIndex
var content = ""
pages.map {
content += it.text
}
var count = 1
var index = content.indexOf(searchContentQuery)
while (count != indexWithinChapter) {
index = content.indexOf(searchContentQuery, index + 1)
count += 1
}
val contentPosition = index
var pageIndex = 0
var length = pages[pageIndex].text.length
while (length < contentPosition) {
pageIndex += 1
if (pageIndex > pages.size) {
pageIndex = pages.size
break
}
length += pages[pageIndex].text.length
}
// calculate search result's lineIndex
val currentPage = pages[pageIndex]
var lineIndex = 0
length = length - currentPage.text.length + currentPage.textLines[lineIndex].text.length
while (length < contentPosition) {
lineIndex += 1
if (lineIndex > currentPage.textLines.size) {
lineIndex = currentPage.textLines.size
break
}
length += currentPage.textLines[lineIndex].text.length
}
// charIndex
val currentLine = currentPage.textLines[lineIndex]
length -= currentLine.text.length
val charIndex = contentPosition - length
var addLine = 0
var charIndex2 = 0
// change line
if ((charIndex + searchContentQuery.length) > currentLine.text.length) {
addLine = 1
charIndex2 = charIndex + searchContentQuery.length - currentLine.text.length - 1
}
// changePage
if ((lineIndex + addLine + 1) > currentPage.textLines.size) {
addLine = -1
charIndex2 = charIndex + searchContentQuery.length - currentLine.text.length - 1
}
return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2)
}
/** /**
* 替换规则变化 * 替换规则变化
*/ */

@ -18,7 +18,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.LocalConfig import io.legado.app.help.LocalConfig
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
import io.legado.app.lib.theme.* import io.legado.app.lib.theme.*
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.* import io.legado.app.utils.*
import splitties.views.onLongClick import splitties.views.onLongClick

@ -20,8 +20,6 @@ import io.legado.app.help.http.newCall
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.lib.theme.getSecondaryTextColor import io.legado.app.lib.theme.getSecondaryTextColor
@ -345,35 +343,13 @@ class BgTextConfigDialog : BaseDialogFragment() {
} }
private fun setBgFromUri(uri: Uri) { private fun setBgFromUri(uri: Uri) {
if (uri.toString().isContentScheme()) { uri.read(this) { name, bytes ->
val doc = DocumentFile.fromSingleUri(requireContext(), uri) var file = requireContext().externalFiles
doc?.name?.let { file = FileUtils.createFileIfNotExist(file, "bg", name)
val file = file.writeBytes(bytes)
FileUtils.createFileIfNotExist(requireContext().externalFiles, "bg", it) ReadBookConfig.durConfig.setCurBg(2, file.absolutePath)
kotlin.runCatching { ReadBookConfig.upBg()
DocumentUtils.readBytes(requireContext(), doc.uri) postEvent(EventBus.UP_CONFIG, false)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
ReadBookConfig.durConfig.setCurBg(2, file.absolutePath)
ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false)
} ?: toastOnUi("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
ReadBookConfig.durConfig.setCurBg(2, path)
ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false)
}
}
.request()
} }
} }
} }

@ -11,9 +11,9 @@ import io.legado.app.databinding.DialogReadAloudBinding
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.model.ReadBook
import io.legado.app.service.BaseReadAloudService 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.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils

@ -16,7 +16,7 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.lib.theme.bottomBackground import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.widget.font.FontSelectDialog import io.legado.app.ui.widget.font.FontSelectDialog
import io.legado.app.utils.ColorUtils import io.legado.app.utils.ColorUtils

@ -11,7 +11,7 @@ import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChar import io.legado.app.ui.book.read.page.entities.TextChar
import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage

@ -15,7 +15,7 @@ import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.ViewBookPageBinding import io.legado.app.databinding.ViewBookPageBinding
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.help.ReadTipConfig import io.legado.app.help.ReadTipConfig
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.BatteryView import io.legado.app.ui.widget.BatteryView

@ -15,7 +15,7 @@ import android.widget.FrameLayout
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.api.DataSource import io.legado.app.ui.book.read.page.api.DataSource
import io.legado.app.ui.book.read.page.delegate.* import io.legado.app.ui.book.read.page.delegate.*
import io.legado.app.ui.book.read.page.entities.PageDirection import io.legado.app.ui.book.read.page.entities.PageDirection

@ -1,6 +1,6 @@
package io.legado.app.ui.book.read.page.api package io.legado.app.ui.book.read.page.api
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.ui.book.read.page.entities.TextChapter
interface DataSource { interface DataSource {

@ -4,7 +4,7 @@ import android.text.Layout
import android.text.StaticLayout import android.text.StaticLayout
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.ReadBookConfig import io.legado.app.help.ReadBookConfig
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import splitties.init.appCtx import splitties.init.appCtx
import java.text.DecimalFormat import java.text.DecimalFormat

@ -1,6 +1,6 @@
package io.legado.app.ui.book.read.page.provider package io.legado.app.ui.book.read.page.provider
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.api.DataSource import io.legado.app.ui.book.read.page.api.DataSource
import io.legado.app.ui.book.read.page.api.PageFactory import io.legado.app.ui.book.read.page.api.PageFactory
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage

@ -24,10 +24,10 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.lib.theme.backgroundColor import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.book.login.SourceLoginActivity
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
import io.legado.app.ui.document.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.document.FilePickerParam import io.legado.app.ui.document.FilePickerParam
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.qrcode.QrCodeResult import io.legado.app.ui.qrcode.QrCodeResult
import io.legado.app.ui.widget.KeyboardToolPop import io.legado.app.ui.widget.KeyboardToolPop
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
@ -202,7 +202,13 @@ class BookSourceEditActivity :
add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url)) add(EditEntity("loginUrl", source?.loginUrl, R.string.login_url))
add(EditEntity("bookUrlPattern", source?.bookUrlPattern, R.string.book_url_pattern)) add(EditEntity("bookUrlPattern", source?.bookUrlPattern, R.string.book_url_pattern))
add(EditEntity("header", source?.header, R.string.source_http_header)) add(EditEntity("header", source?.header, R.string.source_http_header))
add(
EditEntity(
"concurrentRate",
source?.concurrentRate,
R.string.source_concurrent_rate
)
)
} }
//搜索 //搜索
val sr = source?.getSearchRule() val sr = source?.getSearchRule()
@ -294,6 +300,7 @@ class BookSourceEditActivity :
"bookUrlPattern" -> source.bookUrlPattern = it.value "bookUrlPattern" -> source.bookUrlPattern = it.value
"header" -> source.header = it.value "header" -> source.header = it.value
"bookSourceComment" -> source.bookSourceComment = it.value ?: "" "bookSourceComment" -> source.bookSourceComment = it.value ?: ""
"concurrentRate" -> source.concurrentRate = it.value
} }
} }
searchEntities.forEach { searchEntities.forEach {

@ -9,6 +9,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -30,7 +31,8 @@ import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.document.FilePicker import io.legado.app.ui.document.FilePicker
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.dialog.TextDialog
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
class BackupConfigFragment : BasePreferenceFragment(), class BackupConfigFragment : BasePreferenceFragment(),
@ -265,26 +267,38 @@ class BackupConfigFragment : BasePreferenceFragment(),
} }
fun restore() { fun restore() {
Coroutine.async(context = Dispatchers.Main) { Coroutine.async(context = Main) {
BookWebDav.showRestoreDialog(requireContext()) BookWebDav.showRestoreDialog(requireContext())
}.onError { }.onError {
longToast("WebDavError:${it.localizedMessage}\n将从本地备份恢复。") alert {
val backupPath = getPrefString(PreferKey.backupPath) setTitle(R.string.restore)
if (backupPath?.isNotEmpty() == true) { setMessage("WebDavError:${it.localizedMessage}\n将从本地备份恢复。")
if (backupPath.isContentScheme()) { okButton {
val uri = Uri.parse(backupPath) restoreFromLocal()
val doc = DocumentFile.fromTreeUri(requireContext(), uri) }
if (doc?.canWrite() == true) { cancelButton()
}
}
}
private fun restoreFromLocal() {
val backupPath = getPrefString(PreferKey.backupPath)
if (backupPath?.isNotEmpty() == true) {
if (backupPath.isContentScheme()) {
val uri = Uri.parse(backupPath)
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
if (doc?.canWrite() == true) {
lifecycleScope.launch {
Restore.restore(requireContext(), backupPath) Restore.restore(requireContext(), backupPath)
} else {
restoreDir.launch(null)
} }
} else { } else {
restoreUsePermission(backupPath) restoreDir.launch(null)
} }
} else { } else {
restoreDir.launch(null) restoreUsePermission(backupPath)
} }
} else {
restoreDir.launch(null)
} }
} }

@ -10,7 +10,6 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.SeekBar import android.widget.SeekBar
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BasePreferenceFragment import io.legado.app.base.BasePreferenceFragment
@ -24,15 +23,12 @@ import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.ThemeConfig import io.legado.app.help.ThemeConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ATH
import io.legado.app.ui.widget.image.CoverImageView import io.legado.app.ui.widget.image.CoverImageView
import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.ui.widget.prefs.ColorPreference import io.legado.app.ui.widget.prefs.ColorPreference
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.* import io.legado.app.utils.*
import java.io.File
@Suppress("SameParameterValue") @Suppress("SameParameterValue")
@ -306,76 +302,22 @@ class ThemeConfigFragment : BasePreferenceFragment(),
} }
private fun setBgFromUri(uri: Uri, preferenceKey: String, success: () -> Unit) { private fun setBgFromUri(uri: Uri, preferenceKey: String, success: () -> Unit) {
if (uri.isContentScheme()) { uri.read(this) { name, bytes ->
val doc = DocumentFile.fromSingleUri(requireContext(), uri) var file = requireContext().externalFiles
doc?.name?.let { file = FileUtils.createFileIfNotExist(file, preferenceKey, name)
var file = requireContext().externalFiles file.writeBytes(bytes)
file = FileUtils.createFileIfNotExist(file, preferenceKey, it) putPrefString(preferenceKey, file.absolutePath)
kotlin.runCatching { success()
DocumentUtils.readBytes(requireContext(), doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
putPrefString(preferenceKey, file.absolutePath)
success()
} ?: toastOnUi("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
val imgFile = File(path)
if (imgFile.exists()) {
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, preferenceKey, imgFile.name)
file.writeBytes(imgFile.readBytes())
putPrefString(preferenceKey, file.absolutePath)
success()
}
}
}
.request()
} }
} }
private fun setCoverFromUri(preferenceKey: String, uri: Uri) { private fun setCoverFromUri(preferenceKey: String, uri: Uri) {
if (uri.isContentScheme()) { uri.read(this) { name, bytes ->
val doc = DocumentFile.fromSingleUri(requireContext(), uri) var file = requireContext().externalFiles
doc?.name?.let { file = FileUtils.createFileIfNotExist(file, "covers", name)
var file = requireContext().externalFiles file.writeBytes(bytes)
file = FileUtils.createFileIfNotExist(file, "covers", it) putPrefString(preferenceKey, file.absolutePath)
kotlin.runCatching { CoverImageView.upDefaultCover()
DocumentUtils.readBytes(requireContext(), doc.uri)
}.getOrNull()?.let { byteArray ->
file.writeBytes(byteArray)
putPrefString(preferenceKey, file.absolutePath)
CoverImageView.upDefaultCover()
} ?: toastOnUi("获取文件出错")
}
} else {
PermissionsCompat.Builder(this)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
val imgFile = File(path)
if (imgFile.exists()) {
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", imgFile.name)
file.writeBytes(imgFile.readBytes())
putPrefString(PreferKey.defaultCover, file.absolutePath)
CoverImageView.upDefaultCover()
}
}
}
.request()
} }
} }

@ -31,10 +31,9 @@ class FilePickerActivity :
return@registerForActivityResult return@registerForActivityResult
} }
if (it.isContentScheme()) { if (it.isContentScheme()) {
contentResolver.takePersistableUriPermission( val modeFlags =
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) contentResolver.takePersistableUriPermission(it, modeFlags)
} }
onResult(Intent().setData(it)) onResult(Intent().setData(it))
} }

@ -104,6 +104,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
if (it != bookGroups) { if (it != bookGroups) {
bookGroups = it bookGroups = it
booksAdapter.notifyDataSetChanged() booksAdapter.notifyDataSetChanged()
binding.tvEmptyMsg.isGone = getItemCount() > 0
} }
} }
} }
@ -111,6 +112,15 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun initBooksData() { private fun initBooksData() {
if (groupId == AppConst.bookGroupNoneId) {
binding.titleBar.title = getString(R.string.bookshelf)
} else {
bookGroups.forEach {
if (groupId == it.groupId) {
binding.titleBar.title = "${getString(R.string.bookshelf)}(${it.groupName})"
}
}
}
booksFlowJob?.cancel() booksFlowJob?.cancel()
booksFlowJob = launch { booksFlowJob = launch {
when (groupId) { when (groupId) {

@ -5,13 +5,13 @@ import android.graphics.BitmapFactory
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 com.google.zxing.Result import com.google.zxing.Result
import com.king.zxing.CameraScan.OnScanResultCallback import com.king.zxing.CameraScan.OnScanResultCallback
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseActivity import io.legado.app.base.BaseActivity
import io.legado.app.databinding.ActivityQrcodeCaptureBinding import io.legado.app.databinding.ActivityQrcodeCaptureBinding
import io.legado.app.utils.QRCodeUtils import io.legado.app.utils.QRCodeUtils
import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.readBytes import io.legado.app.utils.readBytes
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -19,8 +19,8 @@ class QrCodeActivity : BaseActivity<ActivityQrcodeCaptureBinding>(), OnScanResul
override val binding by viewBinding(ActivityQrcodeCaptureBinding::inflate) override val binding by viewBinding(ActivityQrcodeCaptureBinding::inflate)
private val selectQrImage = registerForActivityResult(ActivityResultContracts.GetContent()) { private val selectQrImage = registerForActivityResult(SelectImageContract()) {
it?.readBytes(this)?.let { bytes -> it?.second?.readBytes(this)?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
onScanResultCallback(QRCodeUtils.parseCodeResult(bitmap)) onScanResultCallback(QRCodeUtils.parseCodeResult(bitmap))
} }
@ -41,7 +41,7 @@ class QrCodeActivity : BaseActivity<ActivityQrcodeCaptureBinding>(), OnScanResul
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_choose_from_gallery -> selectQrImage.launch("image/*") R.id.action_choose_from_gallery -> selectQrImage.launch(null)
} }
return super.onCompatOptionsItemSelected(item) return super.onCompatOptionsItemSelected(item)
} }

@ -8,7 +8,7 @@ import androidx.fragment.app.FragmentManager
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.databinding.DialogPhotoViewBinding import io.legado.app.databinding.DialogPhotoViewBinding
import io.legado.app.service.help.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.provider.ImageProvider import io.legado.app.ui.book.read.page.provider.ImageProvider
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding

@ -92,8 +92,8 @@ object EncoderUtils {
fun decryptBase64AES( fun decryptBase64AES(
data: ByteArray?, data: ByteArray?,
key: ByteArray?, key: ByteArray?,
transformation: String?, transformation: String = "DES/CBC/PKCS5Padding",
iv: ByteArray? iv: ByteArray? = null
): ByteArray? { ): ByteArray? {
return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv) return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
} }
@ -111,10 +111,10 @@ object EncoderUtils {
fun decryptAES( fun decryptAES(
data: ByteArray?, data: ByteArray?,
key: ByteArray?, key: ByteArray?,
transformation: String?, transformation: String = "DES/CBC/PKCS5Padding",
iv: ByteArray? iv: ByteArray? = null
): ByteArray? { ): ByteArray? {
return symmetricTemplate(data, key, "AES", transformation!!, iv, false) return symmetricTemplate(data, key, "AES", transformation, iv, false)
} }
@ -128,7 +128,7 @@ object EncoderUtils {
* @param isEncrypt True to encrypt, false otherwise. * @param isEncrypt True to encrypt, false otherwise.
* @return the bytes of symmetric encryption or decryption * @return the bytes of symmetric encryption or decryption
*/ */
@Suppress("SameParameterValue")
private fun symmetricTemplate( private fun symmetricTemplate(
data: ByteArray?, data: ByteArray?,
key: ByteArray?, key: ByteArray?,
@ -137,7 +137,8 @@ object EncoderUtils {
iv: ByteArray?, iv: ByteArray?,
isEncrypt: Boolean isEncrypt: Boolean
): ByteArray? { ): ByteArray? {
return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null else try { return if (data == null || data.isEmpty() || key == null || key.isEmpty()) null
else try {
val keySpec = SecretKeySpec(key, algorithm) val keySpec = SecretKeySpec(key, algorithm)
val cipher = Cipher.getInstance(transformation) val cipher = Cipher.getInstance(transformation)
if (iv == null || iv.isEmpty()) { if (iv == null || iv.isEmpty()) {

@ -2,11 +2,82 @@ package io.legado.app.utils
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import io.legado.app.R
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import java.io.File import java.io.File
fun Uri.isContentScheme() = this.scheme == "content" fun Uri.isContentScheme() = this.scheme == "content"
/**
* 读取URI
*/
fun Uri.read(activity: AppCompatActivity, success: (name: String, bytes: ByteArray) -> Unit) {
try {
if (isContentScheme()) {
val doc = DocumentFile.fromSingleUri(activity, this)
doc ?: error("未获取到文件")
val name = doc.name ?: error("未获取到文件名")
val fileBytes = DocumentUtils.readBytes(activity, doc.uri)
fileBytes ?: error("读取文件出错")
success.invoke(name, fileBytes)
} else {
PermissionsCompat.Builder(activity)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(activity, this)?.let { path ->
val imgFile = File(path)
success.invoke(imgFile.name, imgFile.readBytes())
}
}
.request()
}
} catch (e: Exception) {
e.printStackTrace()
activity.toastOnUi(e.localizedMessage ?: "read uri error")
}
}
/**
* 读取URI
*/
fun Uri.read(fragment: Fragment, success: (name: String, bytes: ByteArray) -> Unit) {
try {
if (isContentScheme()) {
val doc = DocumentFile.fromSingleUri(fragment.requireContext(), this)
doc ?: error("未获取到文件")
val name = doc.name ?: error("未获取到文件名")
val fileBytes = DocumentUtils.readBytes(fragment.requireContext(), doc.uri)
fileBytes ?: error("读取文件出错")
success.invoke(name, fileBytes)
} else {
PermissionsCompat.Builder(fragment)
.addPermissions(
Permissions.READ_EXTERNAL_STORAGE,
Permissions.WRITE_EXTERNAL_STORAGE
)
.rationale(R.string.bg_image_per)
.onGranted {
RealPathUtil.getPath(fragment.requireContext(), this)?.let { path ->
val imgFile = File(path)
success.invoke(imgFile.name, imgFile.readBytes())
}
}
.request()
}
} catch (e: Exception) {
e.printStackTrace()
fragment.toastOnUi(e.localizedMessage ?: "read uri error")
}
}
@Throws(Exception::class) @Throws(Exception::class)
fun Uri.readBytes(context: Context): ByteArray? { fun Uri.readBytes(context: Context): ByteArray? {
if (this.isContentScheme()) { if (this.isContentScheme()) {

@ -18,6 +18,8 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession): Response { override fun serve(session: IHTTPSession): Response {
var returnData: ReturnData? = null var returnData: ReturnData? = null
val ct = ContentType(session.headers["content-type"]).tryUTF8()
session.headers["content-type"] = ct.contentTypeHeader
var uri = session.uri var uri = session.uri
try { try {
@ -40,7 +42,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
"/saveSources" -> SourceController.saveSources(postData) "/saveSources" -> SourceController.saveSources(postData)
"/saveBook" -> BookController.saveBook(postData) "/saveBook" -> BookController.saveBook(postData)
"/deleteSources" -> SourceController.deleteSources(postData) "/deleteSources" -> SourceController.deleteSources(postData)
"/addLocalBook" -> BookController.addLocalBook(session, postData) "/addLocalBook" -> BookController.addLocalBook(session.parameters)
else -> null else -> null
} }
} }
@ -71,6 +73,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
(returnData.data as Bitmap).compress(Bitmap.CompressFormat.PNG, 100, outputStream) (returnData.data as Bitmap).compress(Bitmap.CompressFormat.PNG, 100, outputStream)
val byteArray = outputStream.toByteArray() val byteArray = outputStream.toByteArray()
outputStream.close()
val inputStream = ByteArrayInputStream(byteArray) val inputStream = ByteArrayInputStream(byteArray)
newFixedLengthResponse( newFixedLengthResponse(
Response.Status.OK, Response.Status.OK,

@ -49,7 +49,7 @@
android:id="@+id/tie_group_name" android:id="@+id/tie_group_name"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="2"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" /> tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
</io.legado.app.ui.widget.text.TextInputLayout> </io.legado.app.ui.widget.text.TextInputLayout>

@ -7,35 +7,47 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout <LinearLayout
android:id="@+id/ll_layout" android:id="@+id/ll_group_style"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="horizontal"
app:layout_constraintRight_toLeftOf="@+id/ll_sort" app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<LinearLayout <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"> android:layout_weight="1"
android:padding="6dp"
android:text="@string/group_style"
android:textColor="@color/primaryText" />
<io.legado.app.ui.widget.text.AccentTextView <androidx.appcompat.widget.AppCompatSpinner
android:layout_width="wrap_content" android:id="@+id/sp_group_style"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:padding="6dp" android:layout_height="wrap_content"
android:text="@string/group_style" android:entries="@array/group_style"
android:textSize="16sp" /> app:theme="@style/Spinner"
tools:ignore="TouchTargetSizeCheck" />
<androidx.appcompat.widget.AppCompatSpinner </LinearLayout>
android:id="@+id/sp_group_style"
android:layout_width="wrap_content" <io.legado.app.lib.theme.view.ATESwitch
android:layout_height="wrap_content" android:id="@+id/sw_show_unread"
android:entries="@array/group_style" android:layout_width="match_parent"
app:theme="@style/Spinner" android:layout_height="wrap_content"
tools:ignore="TouchTargetSizeCheck" /> android:padding="6dp"
android:text="@string/show_unread"
app:layout_constraintTop_toBottomOf="@+id/ll_group_style"
tools:ignore="TouchTargetSizeCheck" />
</LinearLayout> <LinearLayout
android:id="@+id/ll_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintRight_toLeftOf="@+id/ll_sort"
app:layout_constraintTop_toBottomOf="@+id/sw_show_unread"
app:layout_constraintLeft_toLeftOf="parent">
<io.legado.app.ui.widget.text.AccentTextView <io.legado.app.ui.widget.text.AccentTextView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -83,18 +95,6 @@
</LinearLayout> </LinearLayout>
<io.legado.app.lib.theme.view.ATESwitch
android:id="@+id/sw_show_unread"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="6dp"
android:text="@string/show_unread"
app:layout_constraintLeft_toRightOf="@+id/ll_layout"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="TouchTargetSizeCheck" />
<LinearLayout <LinearLayout
android:id="@+id/ll_sort" android:id="@+id/ll_sort"
android:layout_width="0dp" android:layout_width="0dp"

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/background"
android:gravity="center"
android:orientation="horizontal">
<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:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextAppearance="@style/ToolbarTitle" />
</LinearLayout>

@ -36,6 +36,11 @@
android:title="@string/export_to_web_dav" android:title="@string/export_to_web_dav"
android:checkable="true" android:checkable="true"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_export_no_chapter_name"
android:title="@string/export_no_chapter_name"
android:checkable="true"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_export_folder" android:id="@+id/menu_export_folder"

@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/menu_success" android:id="@+id/menu_ok"
android:icon="@drawable/ic_check" android:icon="@drawable/ic_check"
android:title="@string/success" android:title="@string/ok"
app:showAsAction="always" /> app:showAsAction="always" />
</menu> </menu>

@ -393,6 +393,7 @@
<string name="source_group">源分组(sourceGroup)</string> <string name="source_group">源分组(sourceGroup)</string>
<string name="diy_source_group">自定义源分组</string> <string name="diy_source_group">自定义源分组</string>
<string name="diy_edit_source_group">输入自定义源分组名称</string> <string name="diy_edit_source_group">输入自定义源分组名称</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string> <string name="comment">源注释(sourceComment)</string>
@ -849,4 +850,7 @@
<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> <string name="custom_group_summary">关闭替换分组/开启添加分组</string>
<string name="pref_media_button_per_next">媒体按钮•上一首|下一首</string>
<string name="pref_media_button_per_next_summary">上一段|下一段/上一章|下一章</string>
<string name="read_aloud_by_page_summary">及时翻页,翻页时会停顿一下</string>
</resources> </resources>

@ -393,6 +393,7 @@
<string name="source_group">源分组(sourceGroup)</string> <string name="source_group">源分组(sourceGroup)</string>
<string name="diy_source_group">自定义源分组</string> <string name="diy_source_group">自定义源分组</string>
<string name="diy_edit_source_group">输入自定义源分组名称</string> <string name="diy_edit_source_group">输入自定义源分组名称</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string> <string name="comment">源注释(sourceComment)</string>
@ -847,6 +848,10 @@
<string name="null_url">url为空</string> <string name="null_url">url为空</string>
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知错误</string> <string name="unknown_error">未知错误</string>
<string name="export_no_chapter_name">No export chapter names</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string> <string name="custom_group_summary">关闭替换分组/开启添加分组</string>
<string name="pref_media_button_per_next">媒体按钮•上一首|下一首</string>
<string name="pref_media_button_per_next_summary">上一段|下一段/上一章|下一章</string>
<string name="read_aloud_by_page_summary">及时翻页,翻页时会停顿一下</string>
</resources> </resources>

@ -393,6 +393,7 @@
<string name="source_group">源分组(fonteGrupo)</string> <string name="source_group">源分组(fonteGrupo)</string>
<string name="diy_source_group">自定义源分组</string> <string name="diy_source_group">自定义源分组</string>
<string name="diy_edit_source_group">输入自定义源分组名称</string> <string name="diy_edit_source_group">输入自定义源分组名称</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(fonteComentário)</string> <string name="comment">源注释(fonteComentário)</string>
@ -849,4 +850,7 @@
<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> <string name="custom_group_summary">关闭替换分组/开启添加分组</string>
<string name="pref_media_button_per_next">媒体按钮•上一首|下一首</string>
<string name="pref_media_button_per_next_summary">上一段|下一段/上一章|下一章</string>
<string name="read_aloud_by_page_summary">及时翻页,翻页时会停顿一下</string>
</resources> </resources>

@ -389,6 +389,7 @@
<string name="source_url">源URL (sourceUrl)</string> <string name="source_url">源URL (sourceUrl)</string>
<string name="source_group">源分組 (sourceGroup)</string> <string name="source_group">源分組 (sourceGroup)</string>
<string name="sort_url">分類 Url</string> <string name="sort_url">分類 Url</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="login_url">登錄 URL(loginUrl)</string> <string name="login_url">登錄 URL(loginUrl)</string>
<string name="comment">源注釋(sourceComment)</string> <string name="comment">源注釋(sourceComment)</string>
<string name="r_search_url">搜索地址 (url)</string> <string name="r_search_url">搜索地址 (url)</string>
@ -848,7 +849,11 @@
<string name="null_url">url為空</string> <string name="null_url">url為空</string>
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知錯誤</string> <string name="unknown_error">未知錯誤</string>
<string name="export_no_chapter_name">TXT不導出章節名</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string> <string name="custom_group_summary">关闭替换分组/开启添加分组</string>
<string name="pref_media_button_per_next">媒体按钮•上一首|下一首</string>
<string name="pref_media_button_per_next_summary">上一段|下一段/上一章|下一章</string>
<string name="read_aloud_by_page_summary">及时翻页,翻页时会停顿一下</string>
</resources> </resources>

@ -392,6 +392,7 @@
<string name="source_group">源分組(sourceGroup)</string> <string name="source_group">源分組(sourceGroup)</string>
<string name="diy_source_group">自訂源分組</string> <string name="diy_source_group">自訂源分組</string>
<string name="diy_edit_source_group">輸入自訂源分組名稱</string> <string name="diy_edit_source_group">輸入自訂源分組名稱</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="sort_url">分類Url</string> <string name="sort_url">分類Url</string>
<string name="login_url">登入URL(loginUrl)</string> <string name="login_url">登入URL(loginUrl)</string>
<string name="comment">源注釋(sourceComment)</string> <string name="comment">源注釋(sourceComment)</string>
@ -849,7 +850,11 @@
<string name="null_url">url為空</string> <string name="null_url">url為空</string>
<string name="dict">字典</string> <string name="dict">字典</string>
<string name="unknown_error">未知錯誤</string> <string name="unknown_error">未知錯誤</string>
<string name="export_no_chapter_name">TXT不匯出章節名</string>
<string name="end">end</string> <string name="end">end</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string> <string name="custom_group_summary">关闭替换分组/开启添加分组</string>
<string name="pref_media_button_per_next">媒体按钮•上一首|下一首</string>
<string name="pref_media_button_per_next_summary">上一段|下一段/上一章|下一章</string>
<string name="read_aloud_by_page_summary">及时翻页,翻页时会停顿一下</string>
</resources> </resources>

@ -392,6 +392,7 @@
<string name="source_group">源分组(sourceGroup)</string> <string name="source_group">源分组(sourceGroup)</string>
<string name="diy_source_group">自定义源分组</string> <string name="diy_source_group">自定义源分组</string>
<string name="diy_edit_source_group">输入自定义源分组名称</string> <string name="diy_edit_source_group">输入自定义源分组名称</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="sort_url">分类Url</string> <string name="sort_url">分类Url</string>
<string name="login_url">登录URL(loginUrl)</string> <string name="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string> <string name="comment">源注释(sourceComment)</string>
@ -812,6 +813,7 @@
<string name="background_image_hint">0为停用,启用范围1~25\n半径数值越大,虚化效果越高</string> <string name="background_image_hint">0为停用,启用范围1~25\n半径数值越大,虚化效果越高</string>
<string name="export_folder">导出文件夹</string> <string name="export_folder">导出文件夹</string>
<string name="export_charset">导出编码</string> <string name="export_charset">导出编码</string>
<string name="export_no_chapter_name">TXT不导出章节名</string>
<string name="export_to_web_dav">导出到WebDav</string> <string name="export_to_web_dav">导出到WebDav</string>
<string name="reverse_content">反转内容</string> <string name="reverse_content">反转内容</string>
<string name="debug">调试</string> <string name="debug">调试</string>
@ -852,5 +854,8 @@
<string name="autobackup_fail">自动备份失败</string> <string name="autobackup_fail">自动备份失败</string>
<string name="end">结束</string> <string name="end">结束</string>
<string name="custom_group_summary">关闭替换分组/开启添加分组</string> <string name="custom_group_summary">关闭替换分组/开启添加分组</string>
<string name="pref_media_button_per_next">媒体按钮•上一首|下一首</string>
<string name="pref_media_button_per_next_summary">上一段|下一段/上一章|下一章</string>
<string name="read_aloud_by_page_summary">及时翻页,翻页时会停顿一下</string>
</resources> </resources>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save