Merge remote-tracking branch 'upstream/master'

pull/1251/head
Jason Yao 3 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
/schemas

@ -14,6 +14,10 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec
android {
compileSdkVersion 30
buildToolsVersion '30.0.3'
kotlinOptions {
jvmTarget = "11"
}
signingConfigs {
if (project.hasProperty("RELEASE_STORE_FILE")) {
myConfig {
@ -102,10 +106,10 @@ android {
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
sourceSets {
// Adds exported schema location as test app assets.
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
buildToolsVersion '30.0.3'
tasks.withType(JavaCompile) {
//options.compilerArgs << "-Xlint:unchecked"
}
@ -166,7 +170,7 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
//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-ktx:$room_version")
kapt("androidx.room:room-compiler:$room_version")
@ -193,9 +197,9 @@ dependencies {
implementation('com.github.bumptech.glide:glide:4.12.0')
//webServer
implementation('org.nanohttpd:nanohttpd:2.3.1')
implementation('org.nanohttpd:nanohttpd-websocket:2.3.1')
implementation('org.nanohttpd:nanohttpd-apache-fileupload:2.3.1')
def nanoHttpdVersion = "2.3.1"
implementation("org.nanohttpd:nanohttpd:$nanoHttpdVersion")
implementation("org.nanohttpd:nanohttpd-websocket:$nanoHttpdVersion")
//
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" />
<!-- 书源登录 -->
<activity
android:name=".ui.login.SourceLoginActivity"
android:name="io.legado.app.ui.book.login.SourceLoginActivity"
android:configChanges="orientation|screenSize"
android:hardwareAccelerated="true" />
<!-- 阅读记录 -->

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

@ -123,42 +123,46 @@ try {
//正在上传
isUploading = true;
//设置上传的数据
var fd = new FormData();
fd.append("Filename", file.name);
fd.append("Filedata", file);
fd.append("Upload", "Submit Query");
//设置当前的上传对象
currUploadfile = file;
if (XHR.readyState > 0) {
XHR = new XMLHttpRequest();
var reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (e) {
var data = e.target.result;
var fd = new FormData();
fd.append("fileName", file.name);
fd.append("fileData", data);
//设置当前的上传对象
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
import android.util.Base64
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.api.ReturnData
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.ContentProcessor
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.LocalBook
import io.legado.app.model.localBook.UmdFile
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.utils.*
import kotlinx.coroutines.runBlocking
import org.apache.commons.fileupload.disk.DiskFileItemFactory
import splitties.init.appCtx
object BookController {
@ -193,32 +191,31 @@ object BookController {
}
}
private val uploader by lazy {
val dif = DiskFileItemFactory(0, LocalBook.cacheFolder)
NanoFileUpload(dif)
}
fun addLocalBook(session: NanoHTTPD.IHTTPSession, postData: String?): ReturnData {
fun addLocalBook(parameters: Map<String, List<String>>): ReturnData {
val returnData = ReturnData()
try {
uploader.parseRequest(session).forEach {
val path = FileUtils.getPath(LocalBook.cacheFolder, it.name)
val nameAuthor = LocalBook.analyzeNameAuthor(it.name)
val book = Book(
bookUrl = path,
name = nameAuthor.first,
author = nameAuthor.second,
originName = it.name,
coverUrl = FileUtils.getPath(
appCtx.externalFiles,
"covers",
"${MD5Utils.md5Encode16(path)}.jpg"
)
val fileName = parameters["fileName"]?.firstOrNull()
?: return returnData.setErrorMsg("fileName 不能为空")
val fileData = parameters["fileData"]?.firstOrNull()
?: return returnData.setErrorMsg("fileData 不能为空")
val file = FileUtils.createFileIfNotExist(LocalBook.cacheFolder, fileName)
val fileBytes = Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
file.writeBytes(fileBytes)
val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
val book = Book(
bookUrl = file.absolutePath,
name = nameAuthor.first,
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)
appDb.bookDao.insert(book)
}
)
if (book.isEpub()) EpubFile.upBookInfo(book)
if (book.isUmd()) UmdFile.upBookInfo(book)
appDb.bookDao.insert(book)
} catch (e: Exception) {
e.printStackTrace()
return returnData.setErrorMsg(

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

@ -4,13 +4,10 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppConst.androidId
import io.legado.app.data.dao.*
import io.legado.app.data.entities.*
import io.legado.app.help.AppConfig
import splitties.init.appCtx
import java.util.*
@ -19,13 +16,13 @@ val appDb by lazy {
}
@Database(
version = 35,
exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
RuleSub::class],
version = 34,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
@ -54,14 +51,7 @@ abstract class AppDatabase : RoomDatabase() {
fun createDatabase(context: Context) =
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
.fallbackToDestructiveMigration()
.addMigrations(
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
)
.addMigrations(*DatabaseMigrations.migrations)
.allowMainThreadQueries()
.addCallback(dbCallback)
.build()
@ -74,269 +64,28 @@ abstract class AppDatabase : RoomDatabase() {
override fun onOpen(db: SupportSQLiteDatabase) {
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})"""
)
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})"""
)
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})"""
)
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})"""
)
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.data.appDb
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.MD5Utils
import io.legado.app.utils.fromJsonObject

@ -29,6 +29,7 @@ data class BookSource(
var bookSourceUrl: String = "", // 地址,包括 http/https
var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频
var bookUrlPattern: String? = null, // 详情页url正则
var concurrentRate: String? = null, //并发率
var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现
@ -46,6 +47,17 @@ data class BookSource(
var ruleContent: ContentRule? = null // 正文页规则
) : 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:Ignore
@IgnoredOnParcel
@ -178,6 +190,19 @@ data class BookSource(
private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
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
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) {
appCtx.putPrefBoolean(PreferKey.exportToWebDav, value)
}
var exportNoChapterName: Boolean
get() = appCtx.getPrefBoolean(PreferKey.exportNoChapterName)
set(value) {
appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
}
var exportType: Int
get() = appCtx.getPrefInt(PreferKey.exportType)
set(value) {
@ -221,7 +225,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val autoChangeSource: Boolean
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)

@ -1,4 +1,4 @@
package io.legado.app.service.help
package io.legado.app.model
import androidx.lifecycle.MutableLiveData
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.model.webBook.WebBook
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.TextPage
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.book.read.page.provider.ImageProvider
import io.legado.app.utils.msg
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import splitties.init.appCtx
import kotlin.math.max
@ -44,7 +46,7 @@ object ReadBook {
var readStartTime: Long = System.currentTimeMillis()
fun resetData(book: Book) {
this.book = book
ReadBook.book = book
readRecord.bookName = book.name
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
durChapterIndex = book.durChapterIndex
@ -80,10 +82,14 @@ object ReadBook {
}
fun setProgress(progress: BookProgress) {
durChapterIndex = progress.durChapterIndex
durChapterPos = progress.durChapterPos
clearTextChapter()
loadContent(resetPageOffset = true)
if (durChapterIndex != progress.durChapterIndex
|| durChapterPos != progress.durChapterPos
) {
durChapterIndex = progress.durChapterIndex
durChapterPos = progress.durChapterPos
clearTextChapter()
loadContent(resetPageOffset = true)
}
}
fun clearTextChapter() {
@ -109,8 +115,8 @@ object ReadBook {
}
fun upMsg(msg: String?) {
if (this.msg != msg) {
this.msg = msg
if (ReadBook.msg != msg) {
ReadBook.msg = msg
callBack?.upContent()
}
}
@ -128,25 +134,39 @@ object ReadBook {
prevTextChapter = curTextChapter
curTextChapter = nextTextChapter
nextTextChapter = null
book?.let {
if (curTextChapter == null) {
loadContent(durChapterIndex, upContent, false)
} else if (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)
}
}
if (curTextChapter == null) {
loadContent(durChapterIndex, upContent, false)
} else if (upContent) {
callBack?.upContent()
}
loadContent(durChapterIndex.plus(1), upContent, false)
saveRead()
callBack?.upView()
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
} else {
return false
@ -163,24 +183,23 @@ object ReadBook {
nextTextChapter = curTextChapter
curTextChapter = prevTextChapter
prevTextChapter = null
book?.let {
if (curTextChapter == null) {
loadContent(durChapterIndex, upContent, false)
} else if (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)
}
}
if (curTextChapter == null) {
loadContent(durChapterIndex, upContent, false)
} else if (upContent) {
callBack?.upContent()
}
loadContent(durChapterIndex.minus(1), upContent, false)
saveRead()
callBack?.upView()
curPageChanged()
Coroutine.async {
//预下载
val minChapterIndex = max(0, durChapterIndex - 5)
for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
delay(1000)
download(i)
}
}
return true
} else {
return false
@ -202,6 +221,9 @@ object ReadBook {
curPageChanged()
}
/**
* 当前页面变化
*/
private fun curPageChanged() {
callBack?.pageChanged()
if (BaseReadAloudService.isRun) {
@ -257,7 +279,7 @@ object ReadBook {
fun loadContent(
index: Int,
upContent: Boolean = true,
resetPageOffset: Boolean,
resetPageOffset: Boolean = false,
success: (() -> Unit)? = null
) {
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.Evaluator
import org.seimicrawler.xpath.JXNode
import java.util.*
/**
* Created by GKF on 2018/1/25.
@ -71,7 +70,7 @@ class AnalyzeByJSoup(doc: Any) {
val results = ArrayList<List<String>>()
for (ruleStrX in ruleStrS) {
val temp: List<String>? =
val temp: ArrayList<String>? =
if (sourceRule.isCss) {
val lastIndex = ruleStrX.lastIndexOf('@')
getResultLast(
@ -83,11 +82,8 @@ class AnalyzeByJSoup(doc: Any) {
}
if (!temp.isNullOrEmpty()) {
results.add(temp) //!temp.isNullOrEmpty()时,results.isNotEmpty()为true
results.add(temp)
if (ruleAnalyzes.elementsType == "||") break
}
}
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
@ -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>()
try {
when (lastRule) {
"text" -> for (element in elements) {
textS.add(element.text())
val text = element.text()
if (text.isNotEmpty()) {
textS.add(text)
}
}
"textNodes" -> for (element in elements) {
val tn = arrayListOf<String>()
val contentEs = element.textNodes()
for (item in contentEs) {
val temp = item.text().trim { it <= ' ' }
if (temp.isNotEmpty()) {
tn.add(temp)
val text = item.text().trim { it <= ' ' }
if (text.isNotEmpty()) {
tn.add(text)
}
}
textS.add(tn.joinToString("\n"))
if (tn.isNotEmpty()) {
textS.add(tn.joinToString("\n"))
}
}
"ownText" -> for (element in elements) {
textS.add(element.ownText())
val text = element.ownText()
if (text.isNotEmpty()) {
textS.add(text)
}
}
"html" -> {
elements.select("script").remove()
elements.select("style").remove()
val html = elements.outerHtml()
textS.add(html)
if (html.isNotEmpty()) {
textS.add(html)
}
}
"all" -> textS.add(elements.outerHtml())
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.help.AppConfig
import io.legado.app.help.LifecycleHelp
import io.legado.app.model.ReadBook
import io.legado.app.service.AudioPlayService
import io.legado.app.service.BaseReadAloudService
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.read.ReadBookActivity
import io.legado.app.ui.main.MainActivity
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
@ -42,10 +44,18 @@ class MediaButtonReceiver : BroadcastReceiver() {
if (action == KeyEvent.ACTION_DOWN) {
when (keycode) {
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
ReadAloud.prevParagraph(context)
if (context.getPrefBoolean("mediaButtonPerNext", false)) {
ReadBook.moveToPrevChapter(true)
} else {
ReadAloud.prevParagraph(context)
}
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
ReadAloud.nextParagraph(context)
if (context.getPrefBoolean("mediaButtonPerNext", false)) {
ReadBook.moveToNextChapter(true)
} else {
ReadAloud.nextParagraph(context)
}
}
else -> readAloud(context)
}

@ -21,8 +21,8 @@ import io.legado.app.constant.*
import io.legado.app.help.IntentDataHelp
import io.legado.app.help.IntentHelp
import io.legado.app.help.MediaHelp
import io.legado.app.model.ReadBook
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.page.entities.TextChapter
import io.legado.app.utils.getPrefBoolean
@ -67,6 +67,7 @@ abstract class BaseReadAloudService : BaseService(),
initBroadcastReceiver()
upNotification()
upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING)
doDs()
}
override fun onDestroy() {
@ -137,6 +138,8 @@ abstract class BaseReadAloudService : BaseService(),
postEvent(EventBus.ALOUD_STATE, Status.PLAY)
}
abstract fun playStop()
@CallSuper
open fun pauseReadAloud(pause: Boolean) {
BaseReadAloudService.pause = pause
@ -157,9 +160,27 @@ abstract class BaseReadAloudService : BaseService(),
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) {
timeMinute = minute

@ -6,9 +6,9 @@ import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.ReadBook
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.service.help.ReadAloud
import io.legado.app.service.help.ReadBook
import io.legado.app.utils.*
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
@ -68,6 +68,10 @@ class HttpReadAloudService : BaseReadAloudService(),
}
}
override fun playStop() {
player.stop()
}
private fun downloadAudio() {
task?.cancel()
task = execute {
@ -216,30 +220,6 @@ class HttpReadAloudService : BaseReadAloudService(),
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?) {
super.play()
if (pause) return

@ -9,7 +9,7 @@ import io.legado.app.constant.EventBus
import io.legado.app.help.AppConfig
import io.legado.app.help.IntentHelp
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.postEvent
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.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.model.ReadBook
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.CacheBookService
import io.legado.app.utils.msg

@ -92,6 +92,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
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_type)?.title =
"${getString(R.string.export_type)}(${getTypeName()})"
@ -127,6 +128,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}
R.id.menu_export_all -> exportAll()
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_folder -> {
exportPosition = -1

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

@ -34,12 +34,11 @@ class GroupEditDialog : BaseDialogFragment() {
private val viewModel by viewModels<GroupViewModel>()
private var bookGroup: BookGroup? = null
val selectImage = registerForActivityResult(SelectImageContract()) {
it?.second?.let { uri ->
if (uri.isContentScheme()) {
binding.ivCover.load(uri.toString())
} else {
binding.ivCover.load(uri.path)
}
it?.second?.read(this) { name, bytes ->
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", name)
file.writeBytes(bytes)
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.group.GroupSelectDialog
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.search.SearchActivity
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
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.utils.*
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.BookSource
import io.legado.app.help.BookHelp
import io.legado.app.model.ReadBook
import io.legado.app.model.localBook.LocalBook
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.toastOnUi
import kotlinx.coroutines.Dispatchers.IO

@ -6,17 +6,16 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.documentfile.provider.DocumentFile
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.Book
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.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 java.io.File
class BookInfoEditActivity :
VMBaseActivity<ActivityBookInfoEditBinding, BookInfoEditViewModel>(),
@ -103,37 +102,11 @@ class BookInfoEditActivity :
}
private fun coverChangeTo(uri: Uri) {
if (uri.isContentScheme()) {
val doc = DocumentFile.fromSingleUri(this, uri)
doc?.name?.let {
var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", it)
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()
uri.read(this) { name, bytes ->
var file = this.externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", name)
file.writeBytes(bytes)
coverChangeTo(file.absolutePath)
}
}

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
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) {
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.graphics.Bitmap
@ -74,7 +74,7 @@ class SourceLoginActivity : BaseActivity<ActivitySourceLoginBinding>() {
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_success -> {
R.id.menu_ok -> {
if (!checking) {
checking = true
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.selector
import io.legado.app.lib.theme.accentColor
import io.legado.app.model.ReadBook
import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService
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.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.BgTextConfigDialog.Companion.BG_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.searchContent.SearchContentActivity
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.dict.DictDialog
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.dialog.TextDialog
@ -110,7 +111,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
override val isInitFinish: Boolean get() = viewModel.isInitFinish
override val isScroll: Boolean get() = binding.readView.isScroll
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 backupRunnable = Runnable {
if (!BuildConfig.DEBUG) {
@ -251,7 +252,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
chapterName = page.title
bookText = page.text.trim()
}
showBookMark(bookmark)
BookmarkDialog.start(supportFragmentManager, bookmark)
}
}
R.id.menu_copy_text ->
@ -513,7 +514,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
if (bookmark == null) {
toastOnUi(R.string.create_bookmark_error)
} else {
showBookMark(bookmark)
BookmarkDialog.start(supportFragmentManager, bookmark)
}
return true
}
@ -854,11 +855,7 @@ class ReadBookActivity : ReadBookBaseActivity(),
private fun skipToSearch(index: Int, indexWithinChapter: Int) {
viewModel.openChapter(index) {
val pages = ReadBook.curTextChapter?.pages ?: return@openChapter
val positions = ReadBook.searchResultPositions(
pages,
indexWithinChapter,
viewModel.searchContentQuery
)
val positions = viewModel.searchResultPositions(pages, indexWithinChapter)
ReadBook.skipToPage(positions[0]) {
launch {
binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2])
@ -983,16 +980,16 @@ class ReadBookActivity : ReadBookBaseActivity(),
*/
override fun screenOffTimerStart() {
if (screenTimeOut < 0) {
keepScreenOn(window, true)
keepScreenOn(true)
return
}
val t = screenTimeOut - sysScreenOffTime
if (t > 0) {
mHandler.removeCallbacks(keepScreenRunnable)
keepScreenOn(window, true)
keepScreenOn(true)
mHandler.postDelayed(keepScreenRunnable, screenTimeOut)
} 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.constant.AppConst.charsets
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.Bookmark
import io.legado.app.databinding.ActivityBookReadBinding
import io.legado.app.databinding.DialogDownloadChoiceBinding
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.ThemeStore
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.ReadBook
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.PaddingConfigDialog
import io.legado.app.ui.book.toc.BookmarkDialog
import io.legado.app.utils.getPrefString
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) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
@ -194,11 +192,6 @@ abstract class ReadBookBaseActivity :
}
}
@SuppressLint("InflateParams")
fun showBookMark(bookmark: Bookmark) {
BookmarkDialog.start(supportFragmentManager, bookmark)
}
fun showCharsetConfig() {
alert(R.string.set_charset) {
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.ContentProcessor
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.webBook.PreciseSearch
import io.legado.app.model.webBook.WebBook
import io.legado.app.service.BaseReadAloudService
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.postEvent
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.ThemeConfig
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.utils.*
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.lib.dialogs.alert
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.getPrimaryTextColor
import io.legado.app.lib.theme.getSecondaryTextColor
@ -345,35 +343,13 @@ class BgTextConfigDialog : BaseDialogFragment() {
}
private fun setBgFromUri(uri: Uri) {
if (uri.toString().isContentScheme()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri)
doc?.name?.let {
val file =
FileUtils.createFileIfNotExist(requireContext().externalFiles, "bg", it)
kotlin.runCatching {
DocumentUtils.readBytes(requireContext(), doc.uri)
}.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()
uri.read(this) { name, bytes ->
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "bg", name)
file.writeBytes(bytes)
ReadBookConfig.durConfig.setCurBg(2, file.absolutePath)
ReadBookConfig.upBg()
postEvent(EventBus.UP_CONFIG, false)
}
}
}

@ -11,9 +11,9 @@ import io.legado.app.databinding.DialogReadAloudBinding
import io.legado.app.help.AppConfig
import io.legado.app.lib.theme.bottomBackground
import io.legado.app.lib.theme.getPrimaryTextColor
import io.legado.app.model.ReadBook
import io.legado.app.service.BaseReadAloudService
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.widget.seekbar.SeekBarChangeListener
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.bottomBackground
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.widget.font.FontSelectDialog
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.help.ReadBookConfig
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.TextLine
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.help.ReadBookConfig
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.provider.ChapterProvider
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.ReadBookConfig
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.delegate.*
import io.legado.app.ui.book.read.page.entities.PageDirection

@ -1,6 +1,6 @@
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
interface DataSource {

@ -4,7 +4,7 @@ import android.text.Layout
import android.text.StaticLayout
import io.legado.app.R
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 splitties.init.appCtx
import java.text.DecimalFormat

@ -1,6 +1,6 @@
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.PageFactory
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.theme.ATH
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.document.FilePicker
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.widget.KeyboardToolPop
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("bookUrlPattern", source?.bookUrlPattern, R.string.book_url_pattern))
add(EditEntity("header", source?.header, R.string.source_http_header))
add(
EditEntity(
"concurrentRate",
source?.concurrentRate,
R.string.source_concurrent_rate
)
)
}
//搜索
val sr = source?.getSearchRule()
@ -294,6 +300,7 @@ class BookSourceEditActivity :
"bookUrlPattern" -> source.bookUrlPattern = it.value
"header" -> source.header = it.value
"bookSourceComment" -> source.bookSourceComment = it.value ?: ""
"concurrentRate" -> source.concurrentRate = it.value
}
}
searchEntities.forEach {

@ -9,6 +9,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
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.widget.dialog.TextDialog
import io.legado.app.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import splitties.init.appCtx
class BackupConfigFragment : BasePreferenceFragment(),
@ -265,26 +267,38 @@ class BackupConfigFragment : BasePreferenceFragment(),
}
fun restore() {
Coroutine.async(context = Dispatchers.Main) {
Coroutine.async(context = Main) {
BookWebDav.showRestoreDialog(requireContext())
}.onError {
longToast("WebDavError:${it.localizedMessage}\n将从本地备份恢复。")
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) {
alert {
setTitle(R.string.restore)
setMessage("WebDavError:${it.localizedMessage}\n将从本地备份恢复。")
okButton {
restoreFromLocal()
}
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)
} else {
restoreDir.launch(null)
}
} else {
restoreUsePermission(backupPath)
restoreDir.launch(null)
}
} 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.View
import android.widget.SeekBar
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference
import io.legado.app.R
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.lib.dialogs.alert
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.ui.widget.image.CoverImageView
import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.ui.widget.prefs.ColorPreference
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
import io.legado.app.utils.*
import java.io.File
@Suppress("SameParameterValue")
@ -306,76 +302,22 @@ class ThemeConfigFragment : BasePreferenceFragment(),
}
private fun setBgFromUri(uri: Uri, preferenceKey: String, success: () -> Unit) {
if (uri.isContentScheme()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri)
doc?.name?.let {
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, preferenceKey, it)
kotlin.runCatching {
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()
uri.read(this) { name, bytes ->
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, preferenceKey, name)
file.writeBytes(bytes)
putPrefString(preferenceKey, file.absolutePath)
success()
}
}
private fun setCoverFromUri(preferenceKey: String, uri: Uri) {
if (uri.isContentScheme()) {
val doc = DocumentFile.fromSingleUri(requireContext(), uri)
doc?.name?.let {
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", it)
kotlin.runCatching {
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()
uri.read(this) { name, bytes ->
var file = requireContext().externalFiles
file = FileUtils.createFileIfNotExist(file, "covers", name)
file.writeBytes(bytes)
putPrefString(preferenceKey, file.absolutePath)
CoverImageView.upDefaultCover()
}
}

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

@ -104,6 +104,7 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
if (it != bookGroups) {
bookGroups = it
booksAdapter.notifyDataSetChanged()
binding.tvEmptyMsg.isGone = getItemCount() > 0
}
}
}
@ -111,6 +112,15 @@ class BookshelfFragment2 : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
@SuppressLint("NotifyDataSetChanged")
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 = launch {
when (groupId) {

@ -5,13 +5,13 @@ import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import com.google.zxing.Result
import com.king.zxing.CameraScan.OnScanResultCallback
import io.legado.app.R
import io.legado.app.base.BaseActivity
import io.legado.app.databinding.ActivityQrcodeCaptureBinding
import io.legado.app.utils.QRCodeUtils
import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.readBytes
import io.legado.app.utils.viewbindingdelegate.viewBinding
@ -19,8 +19,8 @@ class QrCodeActivity : BaseActivity<ActivityQrcodeCaptureBinding>(), OnScanResul
override val binding by viewBinding(ActivityQrcodeCaptureBinding::inflate)
private val selectQrImage = registerForActivityResult(ActivityResultContracts.GetContent()) {
it?.readBytes(this)?.let { bytes ->
private val selectQrImage = registerForActivityResult(SelectImageContract()) {
it?.second?.readBytes(this)?.let { bytes ->
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
onScanResultCallback(QRCodeUtils.parseCodeResult(bitmap))
}
@ -41,7 +41,7 @@ class QrCodeActivity : BaseActivity<ActivityQrcodeCaptureBinding>(), OnScanResul
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
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)
}

@ -8,7 +8,7 @@ import androidx.fragment.app.FragmentManager
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
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.utils.viewbindingdelegate.viewBinding

@ -92,8 +92,8 @@ object EncoderUtils {
fun decryptBase64AES(
data: ByteArray?,
key: ByteArray?,
transformation: String?,
iv: ByteArray?
transformation: String = "DES/CBC/PKCS5Padding",
iv: ByteArray? = null
): ByteArray? {
return decryptAES(Base64.decode(data, Base64.NO_WRAP), key, transformation, iv)
}
@ -111,10 +111,10 @@ object EncoderUtils {
fun decryptAES(
data: ByteArray?,
key: ByteArray?,
transformation: String?,
iv: ByteArray?
transformation: String = "DES/CBC/PKCS5Padding",
iv: ByteArray? = null
): 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.
* @return the bytes of symmetric encryption or decryption
*/
@Suppress("SameParameterValue")
private fun symmetricTemplate(
data: ByteArray?,
key: ByteArray?,
@ -137,7 +137,8 @@ object EncoderUtils {
iv: ByteArray?,
isEncrypt: Boolean
): 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 cipher = Cipher.getInstance(transformation)
if (iv == null || iv.isEmpty()) {

@ -2,11 +2,82 @@ package io.legado.app.utils
import android.content.Context
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
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
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)
fun Uri.readBytes(context: Context): ByteArray? {
if (this.isContentScheme()) {

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

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

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

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

@ -393,6 +393,7 @@
<string name="source_group">源分组(sourceGroup)</string>
<string name="diy_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="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string>
@ -849,4 +850,7 @@
<string name="unknown_error">未知错误</string>
<string name="end">end</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>

@ -393,6 +393,7 @@
<string name="source_group">源分组(sourceGroup)</string>
<string name="diy_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="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string>
@ -847,6 +848,10 @@
<string name="null_url">url为空</string>
<string name="dict">字典</string>
<string name="unknown_error">未知错误</string>
<string name="export_no_chapter_name">No export chapter names</string>
<string name="end">end</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>

@ -393,6 +393,7 @@
<string name="source_group">源分组(fonteGrupo)</string>
<string name="diy_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="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(fonteComentário)</string>
@ -849,4 +850,7 @@
<string name="unknown_error">未知错误</string>
<string name="end">end</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>

@ -389,6 +389,7 @@
<string name="source_url">源URL (sourceUrl)</string>
<string name="source_group">源分組 (sourceGroup)</string>
<string name="sort_url">分類 Url</string>
<string name="source_concurrent_rate">并发率(concurrentRate)</string>
<string name="login_url">登錄 URL(loginUrl)</string>
<string name="comment">源注釋(sourceComment)</string>
<string name="r_search_url">搜索地址 (url)</string>
@ -848,7 +849,11 @@
<string name="null_url">url為空</string>
<string name="dict">字典</string>
<string name="unknown_error">未知錯誤</string>
<string name="export_no_chapter_name">TXT不導出章節名</string>
<string name="end">end</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>

@ -392,6 +392,7 @@
<string name="source_group">源分組(sourceGroup)</string>
<string name="diy_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="login_url">登入URL(loginUrl)</string>
<string name="comment">源注釋(sourceComment)</string>
@ -849,7 +850,11 @@
<string name="null_url">url為空</string>
<string name="dict">字典</string>
<string name="unknown_error">未知錯誤</string>
<string name="export_no_chapter_name">TXT不匯出章節名</string>
<string name="end">end</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>

@ -392,6 +392,7 @@
<string name="source_group">源分组(sourceGroup)</string>
<string name="diy_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="login_url">登录URL(loginUrl)</string>
<string name="comment">源注释(sourceComment)</string>
@ -812,6 +813,7 @@
<string name="background_image_hint">0为停用,启用范围1~25\n半径数值越大,虚化效果越高</string>
<string name="export_folder">导出文件夹</string>
<string name="export_charset">导出编码</string>
<string name="export_no_chapter_name">TXT不导出章节名</string>
<string name="export_to_web_dav">导出到WebDav</string>
<string name="reverse_content">反转内容</string>
<string name="debug">调试</string>
@ -852,5 +854,8 @@
<string name="autobackup_fail">自动备份失败</string>
<string name="end">结束</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>

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

Loading…
Cancel
Save