From c68923b7283a1f90dad6eba9dcb7cd45404a3194 Mon Sep 17 00:00:00 2001 From: gedoor Date: Sat, 24 Oct 2020 19:12:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96book=E5=AD=98=E5=82=A8,?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0ReadConfig,=E6=96=B9=E4=BE=BF=E4=B8=BA?= =?UTF-8?q?=E4=B9=A6=E7=B1=8D=E6=B7=BB=E5=8A=A0=E5=8D=95=E7=8B=AC=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/updateLog.md | 2 +- .../java/io/legado/app/data/AppDatabase.kt | 147 ++-- .../java/io/legado/app/data/entities/Book.kt | 55 +- .../java/io/legado/app/help/ContentHelp.kt | 627 ++++++++++++++++++ .../io/legado/app/help/storage/OldBook.kt | 2 +- .../io/legado/app/service/help/ReadBook.kt | 2 +- .../app/ui/book/read/ReadBookActivity.kt | 6 +- .../searchContent/SearchContentActivity.kt | 10 +- 8 files changed, 762 insertions(+), 89 deletions(-) create mode 100644 app/src/main/java/io/legado/app/help/ContentHelp.kt diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 430408e94..4ee1f9288 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -3,7 +3,7 @@ * 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 -**2020/10/23** +**2020/10/24** * 修复选择错误的bug * 修复长图最后一张不能滚动的bug * js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie by [AndyBernie](https://github.com/AndyBernie) diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt index ab989580e..997c698cc 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -18,15 +18,31 @@ import java.util.* ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class], - version = 21, + version = 22, exportSchema = true ) -abstract class AppDatabase: RoomDatabase() { - +abstract class AppDatabase : RoomDatabase() { + + abstract fun bookDao(): BookDao + abstract fun bookGroupDao(): BookGroupDao + abstract fun bookSourceDao(): BookSourceDao + abstract fun bookChapterDao(): BookChapterDao + abstract fun replaceRuleDao(): ReplaceRuleDao + abstract fun searchBookDao(): SearchBookDao + abstract fun searchKeywordDao(): SearchKeywordDao + abstract fun rssSourceDao(): RssSourceDao + abstract fun bookmarkDao(): BookmarkDao + abstract fun rssArticleDao(): RssArticleDao + abstract fun rssStarDao(): RssStarDao + abstract fun cookieDao(): CookieDao + abstract fun txtTocRule(): TxtTocRuleDao + abstract fun readRecordDao(): ReadRecordDao + abstract fun httpTTSDao(): HttpTTSDao + companion object { - + private const val DATABASE_NAME = "legado.db" - + fun createDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .fallbackToDestructiveMigration() @@ -40,7 +56,8 @@ abstract class AppDatabase: RoomDatabase() { migration_17_18, migration_18_19, migration_19_20, - migration_20_21 + migration_20_21, + migration_21_22 ) .allowMainThreadQueries() .addCallback(dbCallback) @@ -54,28 +71,20 @@ abstract class AppDatabase: RoomDatabase() { override fun onOpen(db: SupportSQLiteDatabase) { db.execSQL( - """ - insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAllId}, '全部', -10, 1 - where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId}) - """ + """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 - where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId}) - """ + """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 - where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId}) - """ + """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 - where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId}) - """ + """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1 + where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})""" ) } } @@ -84,67 +93,66 @@ abstract class AppDatabase: RoomDatabase() { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("DROP TABLE txtTocRules") database.execSQL( - """ - CREATE TABLE txtTocRules(id INTEGER NOT NULL, + """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)) - """ + enable INTEGER NOT NULL, PRIMARY KEY (id))""" ) } } - - private val migration_11_12 = object: Migration(11, 12) { + + 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) { + + 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) { + + 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, + """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`)) - """ + `order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))""" ) - database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books_new` (`name`, `author`) ") 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) { + + 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) { + + 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) { + + 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) { + + 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( + """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 '${App.androidId}' as androidId, bookName, readTime from readRecord") database.execSQL("DROP TABLE readRecord") database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord") @@ -161,21 +169,30 @@ abstract class AppDatabase: RoomDatabase() { 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`) ") + } + } } - - abstract fun bookDao(): BookDao - abstract fun bookGroupDao(): BookGroupDao - abstract fun bookSourceDao(): BookSourceDao - abstract fun bookChapterDao(): BookChapterDao - abstract fun replaceRuleDao(): ReplaceRuleDao - abstract fun searchBookDao(): SearchBookDao - abstract fun searchKeywordDao(): SearchKeywordDao - abstract fun rssSourceDao(): RssSourceDao - abstract fun bookmarkDao(): BookmarkDao - abstract fun rssArticleDao(): RssArticleDao - abstract fun rssStarDao(): RssStarDao - abstract fun cookieDao(): CookieDao - abstract fun txtTocRule(): TxtTocRuleDao - abstract fun readRecordDao(): ReadRecordDao - abstract fun httpTTSDao(): HttpTTSDao + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index 64748355a..a4d5d3c97 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -1,10 +1,7 @@ package io.legado.app.data.entities import android.os.Parcelable -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.Index -import androidx.room.PrimaryKey +import androidx.room.* import io.legado.app.App import io.legado.app.constant.AppPattern import io.legado.app.constant.BookType @@ -19,6 +16,7 @@ import java.nio.charset.Charset import kotlin.math.max @Parcelize +@TypeConverters(Book.Converters::class) @Entity( tableName = "books", indices = [Index(value = ["name", "author"], unique = true)] @@ -53,8 +51,8 @@ data class Book( var canUpdate: Boolean = true, // 刷新书架时更新书籍信息 var order: Int = 0, // 手动排序 var originOrder: Int = 0, //书源排序 - var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则 - var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息) + var variable: String? = null, // 自定义书籍变量信息(用于书源规则检索书籍信息) + var readConfig: ReadConfig? = null ): Parcelable, BaseBook { fun isLocalBook(): Boolean { @@ -105,21 +103,36 @@ data class Book( override var tocHtml: String? = null fun getRealAuthor() = author.replace(AppPattern.authorRegex, "") - + fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) - + fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl - + fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro - + fun fileCharset(): Charset { return charset(charset ?: "UTF-8") } - + + fun rConfig(): ReadConfig { + if (readConfig == null) { + readConfig = ReadConfig() + } + return readConfig!! + } + + fun setUseReplaceRule(useReplaceRule: Boolean) { + rConfig().useReplaceRule = useReplaceRule + } + + fun getUseReplaceRule(): Boolean { + return rConfig().useReplaceRule + } + fun getFolderName(): String { return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl) } - + fun toSearchBook() = SearchBook( name = name, author = author, @@ -147,7 +160,7 @@ data class Book( newBook.customIntro = customIntro newBook.customTag = customTag newBook.canUpdate = canUpdate - newBook.useReplaceRule = useReplaceRule + newBook.readConfig = readConfig delete() App.db.bookDao().insert(newBook) } @@ -173,4 +186,20 @@ data class Book( } } } + + @Parcelize + data class ReadConfig( + var pageAnim: Int = -1, + var reSegment: Boolean = false, + var useReplaceRule: Boolean = AppConfig.replaceEnableDefault, // 正文使用净化替换规则 + ) : Parcelable + + class Converters { + + @TypeConverter + fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config) + + @TypeConverter + fun stringToReadConfig(json: String?) = GSON.fromJsonObject(json) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ContentHelp.kt b/app/src/main/java/io/legado/app/help/ContentHelp.kt new file mode 100644 index 000000000..e3a88dd89 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ContentHelp.kt @@ -0,0 +1,627 @@ +package io.legado.app.help + +import java.util.* +import kotlin.math.max +import kotlin.math.min + +@Suppress("SameParameterValue", "RegExpRedundantEscape") +object ContentHelp { + + /** + * 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分 + * + * @param content 正文 + * @param chapterName 标题 + * @return + */ + fun lightNovelParagraph2(content: String, chapterName: String): String { + var content1 = content + if (true) { + val content2: String + val chapterNameLength = chapterName.trim { it <= ' ' }.length + content2 = if (chapterNameLength > 1) { + val regexp = + chapterName.trim { it <= ' ' }.replace("\\s+".toRegex(), "(\\\\s*)") + // 质量较低的页面,章节内可能重复出现章节标题 + if (chapterNameLength > 5) content1.replace(regexp.toRegex(), "") + .trim { it <= ' ' } else content1.replaceFirst( + "^\\s*" + regexp.toRegex(), + "" + ).trim { it <= ' ' } + } else { + content1 + } + var p = content2 + .replace(""".toRegex(), "“") + .replace("[::]['\"‘”“]+".toRegex(), ":“") + .replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“") + .split("\n(\\s*)".toRegex()).toTypedArray() + + //初始化StringBuffer的长度,在原content的长度基础上做冗余 + var buffer = StringBuffer((content1.length * 1.15).toInt()) + // 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。 + buffer.append(" ") + if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) { + // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 + buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), "")) + } + + //如果原文存在分段错误,需要把段落重新黏合 + for (i in 1 until p.size) { + if (match(MARK_SENTENCES_END, buffer[buffer.length - 1])) buffer.append("\n") + // 段落开头以外的地方不应该有空格 + // 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内 + buffer.append(p[i].replace("[\u3000\\s]".toRegex(), "")) + } + // 预分段预处理 + // ”“处理为”\n“。 + // ”。“处理为”。\n“。不考虑“?” “!”的情况。 + // ”。xxx处理为 ”。\n xxx + p = buffer.toString() + .replace("[\"”“]+[\\s]*[\"”“]+".toRegex(), "”\n“") + .replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "”$1\n“") + .replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "”$1\n$2") + .replace( + "([问说喊唱叫骂道着答])[\\.。]".toRegex(), + "$1。\n" + ) // .replaceAll("([\\.。\\!!??])([^\"”“]+)[::][\"”“]", "$1\n$2:“") + .split("\n".toRegex()).toTypedArray() + buffer = StringBuffer((content1.length * 1.15).toInt()) + for (s in p) { + buffer.append("\n") + buffer.append( + findNewLines(s) + ) + } + buffer = reduceLength(buffer) + content1 = ("$chapterName\n\n" + buffer.toString() // 处理章节头部空格和换行 + .replaceFirst("^\\s+".toRegex(), "") + .replace("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“") + .replace("[::][”“\"\\s]+".toRegex(), ":“") + .replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3") + .replace("\n(\\s*)".toRegex(), "\n")) + } + return content1 + } + + /** + * 强制切分,减少段落内的句子 + * 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落 + * 如果引号内的内容长于5句,可能引号状态有误,随机分段 + * 如果引号外的内容长于3句,随机分段 + * + * @param str + * @return + */ + private fun reduceLength(str: StringBuffer): StringBuffer { + val p = str.toString().split("\n".toRegex()).toTypedArray() + val l = p.size + val b = BooleanArray(l) + for (i in 0 until l) { + b[i] = p[i].matches(PARAGRAPH_DIAGLOG) + } + var dialogue = 0 + for (i in 0 until l) { + if (b[i]) { + if (dialogue < 0) dialogue = 1 else if (dialogue < 2) dialogue++ + } else { + if (dialogue > 1) { + p[i] = splitQuote(p[i]) + dialogue-- + } else if (dialogue > 0 && i < l - 2) { + if (b[i + 1]) p[i] = splitQuote(p[i]) + } + } + } + val string = StringBuffer() + for (i in 0 until l) { + string.append('\n') + string.append(p[i]) + // System.out.print(" "+b[i]); + } + // System.out.println(" " + str); + return string + } + + // 强制切分进入对话模式后,未构成 “xxx” 形式的段落 + private fun splitQuote(str: String): String { + val length = str.length + if (length < 3) return str + if (match(MARK_QUOTATION, str[0])) { + val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1 + if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) { + return "${str.substring(0, i)}\n${str.substring(i)}" + } + } else if (match(MARK_QUOTATION, str[length - 1])) { + val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false) + if (i > 1) { + if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) { + return "${str.substring(0, i)}\n${str.substring(i)}" + } + } + } + return str + } + + /** + * 计算随机插入换行符的位置。 + * @param str 字符串 + * @param offset 传回的结果需要叠加的偏移量 + * @param min 最低几个句子,随机插入换行 + * @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行 + * @return + */ + private fun forceSplit( + str: String, + offset: Int, + min: Int, + gain: Int, + tigger: Int + ): ArrayList { + val result = ArrayList() + val arrayEnd = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length - 2, true) + val arrayMid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length - 2, true) + if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result + var j = 0 + var i = min + while (i < arrayEnd.size) { + var k = 0 + while (j < arrayMid.size) { + if (arrayMid[j] < arrayEnd[i]) k++ + j++ + } + if (Math.random() * gain < 0.8 + k / 2.5) { + result.add(arrayEnd[i] + offset) + i = max(i + min, i) + } + i++ + } + return result + } + + // 对内容重新划分段落.输入参数str已经使用换行符预分割 + private fun findNewLines(str: String): String { + val string = StringBuffer(str) + // 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。 + val arrayQuote: MutableList = ArrayList() + // 标记插入换行符的位置,int为插入位置(str的char下标) + var insN = ArrayList() + +// mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。 +// 长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间 +// 0未知,正数引号内,负数引号外。 +// 如果相邻的两个标记都为+1,那么需要增加1个引号。 +// 引号内不进行断句 + val mod = IntArray(str.length) + var waitClose = false + for (i in str.indices) { + val c = str[i] + if (match(MARK_QUOTATION, c)) { + val size = arrayQuote.size + + // 把“xxx”、“yy”合并为“xxx_yy”进行处理 + if (size > 0) { + val quotePre = arrayQuote[size - 1] + if (i - quotePre == 2) { + if (match(",、,/", str[i - 1])) { + string.setCharAt(i, '“') + string.setCharAt(i - 2, '”') + arrayQuote.removeAt(size - 1) + mod[size - 1] = 1 + mod[size] = -1 + continue + } + } + } + arrayQuote.add(i) + + // 为xxx:“xxx”做标记 + if (i > 1) { + // 当前发言的正引号的前一个字符 + val charB1 = str[i - 1] + // 上次发言的正引号的前一个字符 + var charB2 = 0.toChar() + if (match(MARK_QUOTATION_BEFORE, charB1)) { + // 如果不是第一处引号,寻找上一处断句,进行分段 + if (arrayQuote.size > 1) { + val lastQuote = arrayQuote[arrayQuote.size - 2] + var p = 0 + if (charB1 == ',' || charB1 == ',') { + if (arrayQuote.size > 2) { + p = arrayQuote[arrayQuote.size - 3] + if (p > 0) { + charB2 = str[p - 1] + } + } + } + // if(char_b2=='.' || char_b2=='。') + if (match(MARK_SENTENCES_END_P, charB2)) insN.add(p - 1) else { + val lastEnd = seekLast(str, MARK_SENTENCES_END, i, lastQuote) + if (lastEnd > 0) insN.add(lastEnd) else insN.add(lastQuote) + } + } + waitClose = true + mod[size] = 1 + if (size > 0) { + mod[size - 1] = -1 + if (size > 1) { + mod[size - 2] = 1 + } + } + } else if (waitClose) { + run { + waitClose = false + insN.add(i) + } + } + } + } + } + val size = arrayQuote.size + + +// 标记循环状态,此位置前的引号是否已经配对 + var opend = false + if (size > 0) { + +// 第1次遍历array_quote,令其元素的值不为0 + for (i in 0 until size) { + if (mod[i] > 0) { + opend = true + } else if (mod[i] < 0) { +// 连续2个反引号表明存在冲突,强制把前一个设为正引号 + if (!opend) { + if (i > 0) mod[i] = 3 + } + opend = false + } else { + opend = !opend + if (opend) mod[i] = 2 else mod[i] = -2 + } + } + // 修正,断尾必须封闭引号 + if (opend) { + if (arrayQuote[size - 1] - string.length > -3) { +// if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){ + if (size > 1) mod[size - 2] = 4 + // 0<=i=1 + mod[size - 1] = -4 + } else if (!match(MARK_SENTENCES_SAY, string[string.length - 2])) string.append( + "”" + ) + } + + + //第2次循环,mod[i]由负变正时,前1字符如果是句末,需要插入换行 + var loop2Mod1 = -1 //上一个引号跟随内容的状态 + var loop2Mod2: Int //当前引号跟随内容的状态 + var i = 0 + var j = arrayQuote[0] - 1 //当前引号前一字符的序号 + if (j < 0) { + i = 1 + loop2Mod1 = 0 + } + while (i < size) { + j = arrayQuote[i] - 1 + loop2Mod2 = mod[i] + if (loop2Mod1 < 0 && loop2Mod2 > 0) { + if (match(MARK_SENTENCES_END, string[j])) insN.add(j) + } + /* + else if (mod[i - 1] > 0 && mod[i] < 0) { + if (j > 0) { + if (match(MARK_SENTENCES_END, string.charAt(j))) + ins_n.add(j); + } + } + */ + loop2Mod1 = loop2Mod2 + i++ + } + } + +// 第3次循环,匹配并插入换行。 +// "xxxx" xxxx。\n xxx“xxxx” +// 未实现 + +// 随机在句末插入换行符 + insN = ArrayList(HashSet(insN)) + insN.sort() + run { + var subs: String + var j = 0 + var progress = 0 + var nextLine = -1 + if (insN.size > 0) nextLine = insN[j] + var gain = 3 + var min = 0 + var trigger = 2 + for (i in arrayQuote.indices) { + val qutoe = arrayQuote[i] + if (qutoe > 0) { + gain = 4 + min = 2 + trigger = 4 + } else { + gain = 3 + min = 0 + trigger = 2 + } + +// 把引号前的换行符与内容相间插入 + while (j < insN.size) { + +// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 + if (nextLine >= qutoe) break + nextLine = insN[j] + if (progress < nextLine) { + subs = string.substring(progress, nextLine) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = nextLine + 1 + } + j++ + } + if (progress < qutoe) { + subs = string.substring(progress, qutoe + 1) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = qutoe + 1 + } + } + while (j < insN.size) { + nextLine = insN[j] + if (progress < nextLine) { + subs = string.substring(progress, nextLine) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + progress = nextLine + 1 + } + j++ + } + if (progress < string.length) { + subs = string.substring(progress, string.length) + insN.addAll(forceSplit(subs, progress, min, gain, trigger)) + } + } + +// 根据段落状态修正引号方向、计算需要插入引号的位置 +// ins_quote跟随array_quote ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' + val insQuote = BooleanArray(size) + opend = false + for (i in 0 until size) { + val p = arrayQuote[i] + if (mod[i] > 0) { + string.setCharAt(p, '“') + if (opend) insQuote[i] = true + opend = true + } else if (mod[i] < 0) { + string.setCharAt(p, '”') + opend = false + } else { + opend = !opend + if (opend) string.setCharAt(p, '“') else string.setCharAt(p, '”') + } + } + insN = ArrayList(HashSet(insN)) + insN.sort() + +// 输出log进行检验 +/* + System.out.println("quote[i]:position/mod\t" + string); + for (int i = 0; i < array_quote.size(); i++) { + System.out.print(" [" + i + "]" + array_quote.get(i) + "/" + mod[i]); + } + System.out.print("\n"); + + System.out.print("ins_q:"); + for (int i = 0; i < ins_quote.length; i++) { + System.out.print(" " + ins_quote[i]); + } + System.out.print("\n"); + + System.out.print("ins_n:"); + + for (int i : ins_n) { + System.out.print(" " + i); + } + System.out.print("\n"); +*/ + +// 完成字符串拼接(从string复制、插入引号和换行 +// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”' +// ins_n 插入换行。数组的值表示插入换行符的位置 + val buffer = StringBuffer((str.length * 1.15).toInt()) + var j = 0 + var progress = 0 + var nextLine = -1 + if (insN.size > 0) nextLine = insN[j] + for (i in arrayQuote.indices) { + val qutoe = arrayQuote[i] + +// 把引号前的换行符与内容相间插入 + while (j < insN.size) { + +// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况 + if (nextLine >= qutoe) break + nextLine = insN[j] + buffer.append(string, progress, nextLine + 1) + buffer.append('\n') + progress = nextLine + 1 + j++ + } + if (progress < qutoe) { + buffer.append(string, progress, qutoe + 1) + progress = qutoe + 1 + } + if (insQuote[i] && buffer.length > 2) { + if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert( + buffer.length - 1, + "”\n" + ) + } + } + while (j < insN.size) { + nextLine = insN[j] + if (progress <= nextLine) { + buffer.append(string, progress, nextLine + 1) + buffer.append('\n') + progress = nextLine + 1 + } + j++ + } + if (progress < string.length) { + buffer.append(string, progress, string.length) + } + return buffer.toString() + } + + /** + * 计算匹配到字典的每个字符的位置 + * + * @param str 待匹配的字符串 + * @param key 字典 + * @param from 从字符串的第几个字符开始匹配 + * @param to 匹配到第几个字符结束 + * @param inOrder 是否按照从前向后的顺序匹配 + * @return 返回距离构成的ArrayList + */ + private fun seekIndexs( + str: String, + key: String, + from: Int, + to: Int, + inOrder: Boolean + ): ArrayList { + val list = ArrayList() + if (str.length - from < 1) return list + var i = 0 + if (from > i) i = from + var t = str.length + if (to > 0) t = min(t, to) + var c: Char + while (i < t) { + c = if (inOrder) str[i] else str[str.length - i - 1] + if (key.indexOf(c) != -1) { + list.add(i) + } + i++ + } + return list + } + + /** + * 计算字符串最后出现与字典中字符匹配的位置 + * + * @param str 数据字符串 + * @param key 字典字符串 + * @param from 从哪个字符开始匹配,默认0 + * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位 + * @return 位置(正向计算) + */ + private fun seekLast(str: String, key: String, from: Int, to: Int): Int { + if (str.length - from < 1) return -1 + var i = 0 + if (from > i) i = from + var t = 0 + if (to > 0) t = to + var c: Char + while (i > t) { + c = str[i] + if (key.indexOf(c) != -1) { + return i + } + i-- + } + return -1 + } + + /** + * 计算字符串与字典中字符的最短距离 + * + * @param str 数据字符串 + * @param key 字典字符串 + * @param from 从哪个字符开始匹配,默认0 + * @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位 + * @param inOrder 是否从正向开始匹配 + * @return 返回最短距离, 注意不是str的char的下标 + */ + private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int { + if (str.length - from < 1) return -1 + var i = 0 + if (from > i) i = from + var t = str.length + if (to > 0) t = min(t, to) + var c: Char + while (i < t) { + c = if (inOrder) str[i] else str[str.length - i - 1] + if (key.indexOf(c) != -1) { + return i + } + i++ + } + return -1 + } + + /** + * 计算字符串与字典的距离。 + * + * @param str 数据字符串 + * @param form 从第几个字符开始匹配 + * @param to 匹配到第几个字符串结束 + * @param inOrder 是否从前向后匹配。 + * @param words 可变长参数构成的字典。每个字符串代表一个字符 + * @return 匹配结果。注意这个距离是使用第一个字符进行计算的 + */ + private fun seekWordsIndex( + str: String, + form: Int, + to: Int, + inOrder: Boolean, + vararg words: String + ): Int { + if (words.isEmpty()) return -2 + val i = seekIndex(str, words[0], form, to, inOrder) + if (i < 0) return i + for (j in 1 until words.size) { + val k = seekIndex(str, words[j], form, to, inOrder) + if (inOrder) { + if (i + j != k) return -3 + } else { + if (i - j != k) return -3 + } + } + return i + } + + /* 搜寻引号并进行分段。处理了一、二、五三类常见情况 + 参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。 + 一、完整引用说话内容,在反引号内侧有断句标点。例如: + 1) 丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!” + 2)“哎呀,真是美极了!”皇帝说,“我十分满意!” + 3)“怕什么!海的美就在这里!”我说道。 + 二、部分引用,在反引号外侧有断句标点: + 4)适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。 + 5)现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。 + 6)唐朝的张嘉贞说它“制造奇特,人不知其所为”。 + 三、一段接着一段地直接引用时,中间段落只在段首用起引号,该段段尾却不用引回号。但是正统文学不在考虑范围内。 + 四、引号里面又要用引号时,外面一层用双引号,里面一层用单引号。暂时不需要考虑 + 五、反语和强调,周围没有断句符号。 + */ + + // 句子结尾的标点。因为引号可能存在误判,不包含引号。 + private const val MARK_SENTENCES_END = "?。!?!~" + private const val MARK_SENTENCES_END_P = ".?。!?!~" + + // 句中标点,由于某些网站常把“,”写为".",故英文句点按照句中标点判断 + private const val MARK_SENTENCES_MID = ".,、,—…" + private const val MARK_SENTENCES_SAY = "问说喊唱叫骂道着答" + + // XXX说:“”的冒号 + private const val MARK_QUOTATION_BEFORE = ",:,:" + + // 引号 + private const val MARK_QUOTATION = "\"“”" + private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex() + + private fun match(rule: String, chr: Char): Boolean { + return rule.indexOf(chr) != -1 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/OldBook.kt b/app/src/main/java/io/legado/app/help/storage/OldBook.kt index a782cf5f5..724c43638 100644 --- a/app/src/main/java/io/legado/app/help/storage/OldBook.kt +++ b/app/src/main/java/io/legado/app/help/storage/OldBook.kt @@ -44,8 +44,8 @@ object OldBook { book.latestChapterTitle = jsonItem.readString("$.lastChapterName") book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 book.order = jsonItem.readInt("$.serialNumber") ?: 0 - book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true book.variable = jsonItem.readString("$.variable") + book.setUseReplaceRule(jsonItem.readBool("$.useReplaceRule") == true) books.add(book) } return books diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index 3d412a70b..a337f59ec 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -381,7 +381,7 @@ object ReadBook { book.name, webBook?.bookSource?.bookSourceUrl, content, - book.useReplaceRule + book.getUseReplaceRule() ) when (chapter.index) { durChapterIndex -> { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index fc73a041c..db2c1cfa2 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -229,7 +229,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo R.id.menu_group_login -> item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() else -> if (item.itemId == R.id.menu_enable_replace) { - item.isChecked = book.useReplaceRule + item.isChecked = book.getUseReplaceRule() } } } @@ -263,8 +263,8 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo loadChapterList(it) } R.id.menu_enable_replace -> ReadBook.book?.let { - it.useReplaceRule = !it.useReplaceRule - menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.useReplaceRule + it.setUseReplaceRule(!it.getUseReplaceRule()) + menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.getUseReplaceRule() onReplaceRuleSave() } R.id.menu_book_info -> ReadBook.book?.let { diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt index 3227a9a00..270c99f12 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt @@ -167,8 +167,8 @@ class SearchContentActivity : var replaceContents: List? = null var totalContents: String if (chapter != null) { - viewModel.book?.let { bookSource -> - val bookContent = BookHelp.getContent(bookSource, chapter) + viewModel.book?.let { book -> + val bookContent = BookHelp.getContent(book, chapter) if (bookContent != null) { //搜索替换后的正文 val job = async(Dispatchers.IO) { @@ -179,10 +179,10 @@ class SearchContentActivity : } replaceContents = BookHelp.disposeContent( chapter.title, - bookSource.name, - bookSource.bookUrl, + book.name, + book.bookUrl, bookContent, - bookSource.useReplaceRule + book.getUseReplaceRule() ) } job.await()