优化book存储,添加ReadConfig,方便为书籍添加单独阅读配置

pull/443/head
gedoor 4 years ago
parent cdbfe6d1b0
commit c68923b728
  1. 2
      app/src/main/assets/updateLog.md
  2. 107
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  3. 43
      app/src/main/java/io/legado/app/data/entities/Book.kt
  4. 627
      app/src/main/java/io/legado/app/help/ContentHelp.kt
  5. 2
      app/src/main/java/io/legado/app/help/storage/OldBook.kt
  6. 2
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  7. 6
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  8. 10
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt

@ -3,7 +3,7 @@
* 关注合作公众号 **[小说拾遗]()** 获取好看的小说。 * 关注合作公众号 **[小说拾遗]()** 获取好看的小说。
* 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。
**2020/10/23** **2020/10/24**
* 修复选择错误的bug * 修复选择错误的bug
* 修复长图最后一张不能滚动的bug * 修复长图最后一张不能滚动的bug
* js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie by [AndyBernie](https://github.com/AndyBernie) * js添加java.getCookie(sourceUrl:String, key:String? = null)来获取登录后的cookie by [AndyBernie](https://github.com/AndyBernie)

@ -18,11 +18,27 @@ import java.util.*
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class], RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class],
version = 21, version = 22,
exportSchema = true 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 { companion object {
private const val DATABASE_NAME = "legado.db" private const val DATABASE_NAME = "legado.db"
@ -40,7 +56,8 @@ abstract class AppDatabase: RoomDatabase() {
migration_17_18, migration_17_18,
migration_18_19, migration_18_19,
migration_19_20, migration_19_20,
migration_20_21 migration_20_21,
migration_21_22
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.addCallback(dbCallback) .addCallback(dbCallback)
@ -54,28 +71,20 @@ abstract class AppDatabase: RoomDatabase() {
override fun onOpen(db: SupportSQLiteDatabase) { override fun onOpen(db: SupportSQLiteDatabase) {
db.execSQL( db.execSQL(
""" """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAllId}, '全部', -10, 1
insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAllId}, '全部', -10, 1 where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId})"""
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId})
"""
) )
db.execSQL( db.execSQL(
""" """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupLocalId}, '本地', -9, 1
insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupLocalId}, '本地', -9, 1 where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId})"""
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId})
"""
) )
db.execSQL( db.execSQL(
""" """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAudioId}, '音频', -8, 1
insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupAudioId}, '音频', -8, 1 where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId})"""
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId})
"""
) )
db.execSQL( db.execSQL(
""" """insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
insert into book_groups(groupId, groupName, 'order', show) select ${AppConst.bookGroupNoneId}, '未分组', -7, 1 where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})"""
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})
"""
) )
} }
} }
@ -84,11 +93,9 @@ abstract class AppDatabase: RoomDatabase() {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE txtTocRules") database.execSQL("DROP TABLE txtTocRules")
database.execSQL( 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, 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))"""
"""
) )
} }
} }
@ -108,19 +115,17 @@ abstract class AppDatabase: RoomDatabase() {
private val migration_13_14 = object : Migration(13, 14) { private val migration_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL( 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,
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, `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, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime`
`lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, 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, `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("INSERT INTO books_new select * from books ")
database.execSQL("DROP TABLE books") database.execSQL("DROP TABLE books")
database.execSQL("ALTER TABLE books_new RENAME TO 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`) ")
} }
} }
@ -144,7 +149,10 @@ abstract class AppDatabase: RoomDatabase() {
private val migration_18_19 = object : Migration(18, 19) { private val migration_18_19 = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) { 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("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${App.androidId}' as androidId, bookName, readTime from readRecord")
database.execSQL("DROP TABLE readRecord") database.execSQL("DROP TABLE readRecord")
database.execSQL("ALTER TABLE readRecordNew RENAME TO 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") 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
} }

@ -1,10 +1,7 @@
package io.legado.app.data.entities package io.legado.app.data.entities
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity import androidx.room.*
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legado.app.App import io.legado.app.App
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
@ -19,6 +16,7 @@ import java.nio.charset.Charset
import kotlin.math.max import kotlin.math.max
@Parcelize @Parcelize
@TypeConverters(Book.Converters::class)
@Entity( @Entity(
tableName = "books", tableName = "books",
indices = [Index(value = ["name", "author"], unique = true)] indices = [Index(value = ["name", "author"], unique = true)]
@ -53,8 +51,8 @@ data class Book(
var canUpdate: Boolean = true, // 刷新书架时更新书籍信息 var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
var order: Int = 0, // 手动排序 var order: Int = 0, // 手动排序
var originOrder: 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 { ): Parcelable, BaseBook {
fun isLocalBook(): Boolean { fun isLocalBook(): Boolean {
@ -116,6 +114,21 @@ data class Book(
return charset(charset ?: "UTF-8") 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 { fun getFolderName(): String {
return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl) return name.replace(AppPattern.fileNameRegex, "") + MD5Utils.md5Encode16(bookUrl)
} }
@ -147,7 +160,7 @@ data class Book(
newBook.customIntro = customIntro newBook.customIntro = customIntro
newBook.customTag = customTag newBook.customTag = customTag
newBook.canUpdate = canUpdate newBook.canUpdate = canUpdate
newBook.useReplaceRule = useReplaceRule newBook.readConfig = readConfig
delete() delete()
App.db.bookDao().insert(newBook) 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<ReadConfig>(json)
}
} }

@ -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("&quot;".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<Int> {
val result = ArrayList<Int>()
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<Int> = ArrayList()
// 标记插入换行符的位置,int为插入位置(str的char下标)
var insN = ArrayList<Int>()
// 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<size,故无需判断size>=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<Integer>
</Integer> */
private fun seekIndexs(
str: String,
key: String,
from: Int,
to: Int,
inOrder: Boolean
): ArrayList<Int> {
val list = ArrayList<Int>()
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
}
}

@ -44,8 +44,8 @@ object OldBook {
book.latestChapterTitle = jsonItem.readString("$.lastChapterName") book.latestChapterTitle = jsonItem.readString("$.lastChapterName")
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0
book.order = jsonItem.readInt("$.serialNumber") ?: 0 book.order = jsonItem.readInt("$.serialNumber") ?: 0
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true
book.variable = jsonItem.readString("$.variable") book.variable = jsonItem.readString("$.variable")
book.setUseReplaceRule(jsonItem.readBool("$.useReplaceRule") == true)
books.add(book) books.add(book)
} }
return books return books

@ -381,7 +381,7 @@ object ReadBook {
book.name, book.name,
webBook?.bookSource?.bookSourceUrl, webBook?.bookSource?.bookSourceUrl,
content, content,
book.useReplaceRule book.getUseReplaceRule()
) )
when (chapter.index) { when (chapter.index) {
durChapterIndex -> { durChapterIndex -> {

@ -229,7 +229,7 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
R.id.menu_group_login -> R.id.menu_group_login ->
item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty()
else -> if (item.itemId == R.id.menu_enable_replace) { else -> if (item.itemId == R.id.menu_enable_replace) {
item.isChecked = book.useReplaceRule item.isChecked = book.getUseReplaceRule()
} }
} }
} }
@ -263,8 +263,8 @@ class ReadBookActivity : VMBaseActivity<ReadBookViewModel>(R.layout.activity_boo
loadChapterList(it) loadChapterList(it)
} }
R.id.menu_enable_replace -> ReadBook.book?.let { R.id.menu_enable_replace -> ReadBook.book?.let {
it.useReplaceRule = !it.useReplaceRule it.setUseReplaceRule(!it.getUseReplaceRule())
menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.useReplaceRule menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.getUseReplaceRule()
onReplaceRuleSave() onReplaceRuleSave()
} }
R.id.menu_book_info -> ReadBook.book?.let { R.id.menu_book_info -> ReadBook.book?.let {

@ -167,8 +167,8 @@ class SearchContentActivity :
var replaceContents: List<String>? = null var replaceContents: List<String>? = null
var totalContents: String var totalContents: String
if (chapter != null) { if (chapter != null) {
viewModel.book?.let { bookSource -> viewModel.book?.let { book ->
val bookContent = BookHelp.getContent(bookSource, chapter) val bookContent = BookHelp.getContent(book, chapter)
if (bookContent != null) { if (bookContent != null) {
//搜索替换后的正文 //搜索替换后的正文
val job = async(Dispatchers.IO) { val job = async(Dispatchers.IO) {
@ -179,10 +179,10 @@ class SearchContentActivity :
} }
replaceContents = BookHelp.disposeContent( replaceContents = BookHelp.disposeContent(
chapter.title, chapter.title,
bookSource.name, book.name,
bookSource.bookUrl, book.bookUrl,
bookContent, bookContent,
bookSource.useReplaceRule book.getUseReplaceRule()
) )
} }
job.await() job.await()

Loading…
Cancel
Save