feat(app): 增加模拟追读功能

This commit is contained in:
Yaoliang
2024-07-10 09:28:10 +08:00
parent f48a87a948
commit 6d0a8699ce
25 changed files with 382 additions and 23 deletions
@@ -28,6 +28,9 @@ import io.legado.app.utils.fromJsonObject
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.nio.charset.Charset
import java.time.LocalDate
import java.time.Period.between
import java.time.chrono.Chronology
import kotlin.math.max
import kotlin.math.min
@@ -160,9 +163,21 @@ data class Book(
@IgnoredOnParcel
val lastChapterIndex get() = totalChapterNum - 1
// 根据当前日期计算章节总数
fun simulatedTotalChapterNum(): Int {
if (config.readSimulating) {
val currentDate = LocalDate.now()
val daysPassed = between(this.config.startDate, currentDate).days + 1
// 计算当前应该解锁到哪一章
val chaptersToUnlock = (config.startChapter?:0) + (daysPassed * config.dailyChapters)
return min(this.totalChapterNum, chaptersToUnlock);
} else return this.totalChapterNum;
}
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
fun getUnreadChapterNum() = max(simulatedTotalChapterNum() - durChapterIndex - 1, 0)
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
@@ -257,6 +272,46 @@ data class Book(
return config.splitLongChapter
}
// readSimulating 的 setter 和 getter
fun setReadSimulating(readSimulating: Boolean) {
config.readSimulating = readSimulating
}
fun getReadSimulating(): Boolean {
return config.readSimulating
}
// startDate 的 setter 和 getter
fun setStartDate(startDate: LocalDate?) {
config.startDate = startDate
}
fun getStartDate(): LocalDate? {
if (!config.readSimulating || config.startDate == null) {
return LocalDate.now()
}
return config.startDate
}
// startChapter 的 setter 和 getter
fun setStartChapter(startChapter: Int) {
config.startChapter = startChapter
}
fun getStartChapter(): Int {
if(config.readSimulating) return config.startChapter?:0
return this.durChapterIndex
}
// dailyChapters 的 setter 和 getter
fun setDailyChapters(dailyChapters: Int) {
config.dailyChapters = dailyChapters
}
fun getDailyChapters(): Int {
return config.dailyChapters
}
fun getDelTag(tag: Long): Boolean {
return config.delTag and tag == tag
}
@@ -385,7 +440,11 @@ data class Book(
var useReplaceRule: Boolean? = null,// 正文使用净化替换规则
var delTag: Long = 0L,//去除标签
var ttsEngine: String? = null,
var splitLongChapter: Boolean = true
var splitLongChapter: Boolean = true,
var readSimulating: Boolean = false,
var startDate: LocalDate? = null,
var startChapter: Int? = null, // 用户设置的起始章节
var dailyChapters: Int = 3 // 用户设置的每日更新章节数
) : Parcelable
class Converters {
@@ -396,4 +455,4 @@ data class Book(
@TypeConverter
fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json).getOrNull()
}
}
}
@@ -165,7 +165,7 @@ object AudioPlay {
fun next(context: Context) {
book?.let { book ->
if (book.durChapterIndex + 1 < book.totalChapterNum) {
if (book.durChapterIndex + 1 < book.simulatedTotalChapterNum()) {
book.durChapterIndex += 1
book.durChapterPos = 0
durChapterIndex = book.durChapterIndex
@@ -223,4 +223,4 @@ object AudioPlay {
}
}
}
}
}
@@ -229,7 +229,7 @@ object ReadBook : CoroutineScope by MainScope() {
}
fun moveToNextChapter(upContent: Boolean, upContentInPlace: Boolean = true): Boolean {
if (durChapterIndex < chapterSize - 1) {
if (durChapterIndex < (book?.simulatedTotalChapterNum()?: chapterSize) - 1) {
durChapterPos = 0
durChapterIndex++
prevTextChapter?.cancelLayout()
@@ -576,7 +576,7 @@ object ReadBook : CoroutineScope by MainScope() {
prevTextChapter?.cancelLayout()
prevTextChapter = textChapter
textChapter.layoutChannel.receiveAsFlow().collect()
if (upContent) callBack?.upContent(offset, resetPageOffset)
if (upContent) callBack?.upContent(offset, resetPageOffset) else TODO()
}
1 -> {
@@ -589,6 +589,8 @@ object ReadBook : CoroutineScope by MainScope() {
if (upContent) callBack?.upContent(offset, resetPageOffset)
}
}
else -> {TODO()}
}
}.onError {
AppLog.put("ChapterProvider ERROR", it)
@@ -145,7 +145,8 @@ object BookChapterList {
}
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
book.latestChapterTitle =
list.last().getDisplayTitle(replaceRules, book.getUseReplaceRule())
list.getOrElse(book.simulatedTotalChapterNum() - 1) {
list.last() }.getDisplayTitle(replaceRules, book.getUseReplaceRule())
book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
.getDisplayTitle(replaceRules, book.getUseReplaceRule())
if (book.totalChapterNum < list.size) {
@@ -260,4 +261,4 @@ object BookChapterList {
return Pair(chapterList, nextUrlList)
}
}
}
@@ -404,4 +404,4 @@ object WebBook {
}
}
}
}
@@ -258,7 +258,7 @@ class AudioPlayActivity :
binding.tvSubTitle.text = it
AudioPlay.book?.let { book ->
binding.ivSkipPrevious.isEnabled = book.durChapterIndex > 0
binding.ivSkipNext.isEnabled = book.durChapterIndex < book.totalChapterNum - 1
binding.ivSkipNext.isEnabled = book.durChapterIndex < book.simulatedTotalChapterNum() - 1
}
}
observeEventSticky<Int>(EventBus.AUDIO_SIZE) {
@@ -283,4 +283,4 @@ class AudioPlayActivity :
}
}
}
}
@@ -251,7 +251,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.insert(*it.toTypedArray())
if (book.isSameNameAuthor(ReadBook.book)) {
ReadBook.book = book
ReadBook.chapterSize = book.totalChapterNum
ReadBook.chapterSize = book.simulatedTotalChapterNum()
}
}
bookData.postValue(book)
@@ -1,6 +1,7 @@
package io.legado.app.ui.book.read
import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.content.pm.ActivityInfo
import android.os.Build
import android.os.Bundle
@@ -20,6 +21,7 @@ import io.legado.app.constant.PreferKey
import io.legado.app.databinding.ActivityBookReadBinding
import io.legado.app.databinding.DialogDownloadChoiceBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.databinding.DialogSimulatedReadingBinding
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig
import io.legado.app.help.config.ReadBookConfig
@@ -48,6 +50,8 @@ import io.legado.app.utils.setNavigationBarColorAuto
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
import java.time.LocalDate
import java.time.format.DateTimeFormatter
/**
* 阅读界面
@@ -304,6 +308,60 @@ abstract class BaseReadBookActivity :
}
}
fun showSimulatedReading() {
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
ReadBook.book?.let { book ->
alert(titleResource = R.string.simulated_reading) {
val alertBinding = DialogSimulatedReadingBinding.inflate(layoutInflater).apply {
root.setBackgroundColor(root.context.backgroundColor)
srEnabled.isChecked = book.getReadSimulating()
editStart.setText(book.getStartChapter().toString())
editNum.setText(book.getDailyChapters().toString())
startDate.setText(book.getStartDate()?.format(dateFormatter))
startDate.isFocusable = false; // 设置为false,不允许获得焦点
startDate.isCursorVisible = false; // 不显示光标
startDate.setOnClickListener {
// 获取当前日期
val localStartDate = LocalDate.parse(startDate.text)
// 创建 DatePickerDialog
val datePickerDialog = DatePickerDialog(
root.context,
{ _, yy, mm, dayOfMonth ->
// 使用Java 8的日期和时间API来格式化日期
val date = LocalDate.of(yy, mm + 1, dayOfMonth) // Java 8的LocalDate,月份从1开始
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val formattedDate = date.format(formatter)
startDate.setText(formattedDate)
}, localStartDate.year, localStartDate.monthValue - 1, localStartDate.dayOfMonth
)
datePickerDialog.show()
}
}
customView { alertBinding.root }
yesButton {
alertBinding.run {
val start = editStart.text!!.toString().let {
if (it.isEmpty()) 0 else it.toInt()
}
val num = editNum.text!!.toString().let {
if (it.isEmpty()) book.totalChapterNum else it.toInt()
}
val enabled = srEnabled.isChecked
val date = startDate.text!!.toString().let {
if (it.isEmpty()) LocalDate.now() else LocalDate.parse(it, dateFormatter)
}
book.setStartDate(date)
book.setDailyChapters(num)
book.setStartChapter(start)
book.setReadSimulating(enabled)
book.save()
}
}
noButton()
}
}
}
fun showCharsetConfig() {
alert(R.string.set_charset) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
@@ -350,4 +408,4 @@ abstract class BaseReadBookActivity :
val nextKeysStr = getPrefString(PreferKey.nextKeys)
return nextKeysStr?.split(",")?.contains(keyCode.toString()) ?: false
}
}
}
@@ -473,6 +473,7 @@ class ReadBookActivity : BaseReadBookActivity(),
R.id.menu_download -> showDownloadDialog()
R.id.menu_add_bookmark -> addBookmark()
R.id.menu_simulated_reading -> showSimulatedReading()
R.id.menu_edit_content -> showDialogFragment(ContentEditDialog())
R.id.menu_update_toc -> ReadBook.book?.let {
if (it.isEpub) {
@@ -229,6 +229,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
AppLog.put("拉取阅读进度失败《${book.name}\n${it.localizedMessage}", it)
}.onSuccess { progress ->
if (progress.durChapterIndex < book.durChapterIndex ||
progress.durChapterIndex >= book.simulatedTotalChapterNum() ||
(progress.durChapterIndex == book.durChapterIndex
&& progress.durChapterPos < book.durChapterPos)
) {
@@ -708,7 +708,8 @@ class ReadView(context: Context, attrs: AttributeSet) :
}
override fun hasNextChapter(): Boolean {
return ReadBook.durChapterIndex < ReadBook.chapterSize - 1
return ReadBook.durChapterIndex < ((ReadBook.book?.simulatedTotalChapterNum()
?: ReadBook.chapterSize) - 1)
}
override fun hasPrevChapter(): Boolean {
@@ -118,7 +118,8 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
lifecycleScope.launch {
withContext(IO) {
when {
searchKey.isNullOrBlank() -> appDb.bookChapterDao.getChapterList(viewModel.bookUrl)
searchKey.isNullOrBlank() -> appDb.bookChapterDao.getChapterList(
viewModel.bookUrl).take(book?.simulatedTotalChapterNum()?: Int.MAX_VALUE)
else -> appDb.bookChapterDao.search(viewModel.bookUrl, searchKey)
}
}.let {
@@ -171,4 +172,4 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
}
}
}
}
@@ -192,7 +192,7 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
ReadBook.book?.let { readBook ->
if (readBook == book) {
ReadBook.book = book
ReadBook.chapterSize = book.totalChapterNum
ReadBook.chapterSize = book.simulatedTotalChapterNum()
ReadBook.upMsg(null)
ReadBook.loadContent(resetPageOffset = true)
}
@@ -224,4 +224,4 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
}
}
}
@@ -163,7 +163,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
appDb.bookChapterDao.insert(*toc.toTypedArray())
if (book.isSameNameAuthor(ReadBook.book)) {
ReadBook.book = book
ReadBook.chapterSize = book.totalChapterNum
ReadBook.chapterSize = book.simulatedTotalChapterNum()
}
addDownload(source, book)
}.onFailure {
@@ -241,4 +241,4 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
}
}
}
}
@@ -28,6 +28,7 @@ abstract class BaseBooksAdapter<VB : ViewBinding>(context: Context) :
oldItem.lastCheckCount != newItem.lastCheckCount -> false
oldItem.getDisplayCover() != newItem.getDisplayCover() -> false
oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false
oldItem.getReadSimulating() -> false
else -> true
}
}
@@ -83,4 +84,4 @@ abstract class BaseBooksAdapter<VB : ViewBinding>(context: Context) :
fun openBookInfo(book: Book)
fun isUpdate(bookUrl: String): Boolean
}
}
}
@@ -0,0 +1,197 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background"
android:gravity="center"
android:padding="20dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/background"
android:gravity="center"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="2dp"
android:text="@string/switch_on"
android:textColor="@color/primaryText"
android:textSize="16sp"
tools:ignore="RtlHardcoded"
android:minWidth="88dp" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="53dp"
android:layout_height="match_parent"
android:gravity="center_vertical">
<androidx.appcompat.widget.SwitchCompat
android:layout_height="wrap_content"
android:id="@+id/sr_enabled"
android:layout_weight="1"
android:gravity="start|center_vertical"
style="@style/Widget.AppCompat.CompoundButton.Switch"
android:layout_width="match_parent"
android:width="20dp" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="65dp"
android:background="@color/background"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="start|center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/start_from"
android:textColor="@color/primaryText"
android:textSize="16sp"
android:gravity="center_vertical"
android:minWidth="100dp" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<EditText
android:layout_width="match_parent"
android:layout_height="49dp"
android:inputType="none"
android:ems="10"
android:id="@+id/start_date"
android:hint="Select date"
android:layout_weight="1"
android:clickable="true"
android:textSize="16sp"
android:layout_marginRight="68dp" />
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="63dp"
android:gravity="center_horizontal|left"
android:padding="16dp">
<LinearLayout
android:orientation="horizontal"
android:layout_width="100dp"
android:layout_height="match_parent"
android:minWidth="100dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="2dp"
android:text="@string/start"
android:textColor="@color/primaryText"
android:textSize="16sp"
tools:ignore="RtlHardcoded" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:text="@string/chapter"
android:textColor="@color/primaryText"
android:textSize="16sp"
tools:ignore="RtlHardcoded" />
</LinearLayout>
<EditText
android:id="@+id/edit_start"
android:layout_width="47dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_edit"
android:hint="@string/start"
android:importantForAutofill="no"
android:inputType="number"
android:lines="1"
android:maxLength="5"
android:minWidth="60dp"
android:paddingLeft="5dp"
android:paddingTop="4dp"
android:paddingRight="5dp"
android:paddingBottom="4dp"
android:textColor="@color/primaryText"
android:textCursorDrawable="@drawable/shape_text_cursor"
android:textSize="14sp"
tools:ignore="TouchTargetSizeCheck,TextContrastCheck" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="end">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="25dp"
android:layout_marginRight="5dp"
android:text="@string/daily_chapters"
android:textColor="@color/primaryText"
android:textSize="16sp" />
<EditText
android:id="@+id/edit_num"
android:layout_width="42dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_edit"
android:hint="3"
android:importantForAutofill="no"
android:inputType="number"
android:lines="1"
android:maxLength="5"
android:minWidth="60dp"
android:paddingLeft="5dp"
android:paddingTop="4dp"
android:paddingRight="5dp"
android:paddingBottom="4dp"
android:textColor="@color/primaryText"
android:textCursorDrawable="@drawable/shape_text_cursor"
android:textSize="14sp"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck,TextContrastCheck"
android:textAlignment="center" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
+5
View File
@@ -49,6 +49,11 @@
android:title="@string/bookmark_add"
app:showAsAction="never" />
<item
android:id="@+id/menu_simulated_reading"
android:title="@string/simulated_reading"
app:showAsAction="never" />
<item
android:id="@+id/menu_edit_content"
android:title="@string/edit_content"
+5 -1
View File
@@ -683,7 +683,7 @@
<string name="alouding_disable">The selected text cannot be spoken in full text speech</string>
<string name="read_body_to_lh">Extend to cutout</string>
<string name="toc_updateing">Updating Chapters</string>
<string name="media_button_on_exit_title">Headset buttons are always available</string>
<string name="media_button_on_exit_summary">Headset buttons are available even exit the app.</string>
<string name="contributors">Contributors</string>
@@ -1148,4 +1148,8 @@
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">Lectura Ficticia</string>
<string name="switch_on">Enc/Apag</string>
<string name="start_from">Comenzar</string>
<string name="daily_chapters">Cap. al día</string>
</resources>
@@ -1151,4 +1151,8 @@
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">模擬追読</string>
<string name="switch_on">スイッチ</string>
<string name="start_from">開始日</string>
<string name="daily_chapters">日更の章数</string>
</resources>
@@ -1151,4 +1151,8 @@
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">Leitura Fingida</string>
<string name="switch_on">Lig/Desl</string>
<string name="start_from">Começar</string>
<string name="daily_chapters">Cap. por dia</string>
</resources>
+4
View File
@@ -1147,4 +1147,8 @@ Còn </string>
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">Đọc giả lập</string>
<string name="switch_on">Bật</string>
<string name="start_from">Bắt đầu</string>
<string name="daily_chapters">Chương/ngày</string>
</resources>
@@ -1148,4 +1148,8 @@
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">模擬追讀</string>
<string name="switch_on">開關</string>
<string name="start_from">起始日期</string>
<string name="daily_chapters">日更章數</string>
</resources>
@@ -1150,4 +1150,8 @@
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">模擬追讀</string>
<string name="switch_on">開關</string>
<string name="start_from">起始日期</string>
<string name="daily_chapters">日更章數</string>
</resources>
+4
View File
@@ -1150,4 +1150,8 @@
<string name="keep_swipe_tip">继续滑动以加载下一章…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">模拟追读</string>
<string name="switch_on">开关</string>
<string name="start_from">开始日期</string>
<string name="daily_chapters">日更章数</string>
</resources>
+4
View File
@@ -1151,4 +1151,8 @@
<string name="keep_swipe_tip">Keep swiping to load the next chapter…</string>
<string name="enable_optimize_render">启用绘制优化</string>
<string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
<string name="simulated_reading">Simulated Reading</string>
<string name="switch_on">Switch</string>
<string name="start_from">Start from</string>
<string name="daily_chapters">Daily Chapters</string>
</resources>