Kaynağa Gözat

Merge pull request #4021 from xuanyue202/simulated_reading

feat(app): 增加模拟追读功能
Horis 2 yıl önce
ebeveyn
işleme
983ec96158
24 değiştirilmiş dosya ile 388 ekleme ve 23 silme
  1. 60 4
      app/src/main/java/io/legado/app/data/entities/Book.kt
  2. 1 1
      app/src/main/java/io/legado/app/model/AudioPlay.kt
  3. 4 2
      app/src/main/java/io/legado/app/model/ReadBook.kt
  4. 3 2
      app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt
  5. 15 3
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt
  6. 1 1
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  7. 58 0
      app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt
  8. 1 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  9. 1 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  10. 2 1
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  11. 8 4
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt
  12. 1 1
      app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
  13. 1 1
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  14. 1 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt
  15. 185 0
      app/src/main/res/layout/dialog_simulated_reading.xml
  16. 5 0
      app/src/main/res/menu/book_read.xml
  17. 6 1
      app/src/main/res/values-es-rES/strings.xml
  18. 5 0
      app/src/main/res/values-ja-rJP/strings.xml
  19. 5 0
      app/src/main/res/values-pt-rBR/strings.xml
  20. 5 0
      app/src/main/res/values-vi/strings.xml
  21. 5 0
      app/src/main/res/values-zh-rHK/strings.xml
  22. 5 0
      app/src/main/res/values-zh-rTW/strings.xml
  23. 5 0
      app/src/main/res/values-zh/strings.xml
  24. 5 0
      app/src/main/res/values/strings.xml

+ 60 - 4
app/src/main/java/io/legado/app/data/entities/Book.kt

@@ -16,18 +16,18 @@ import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.isEpub
 import io.legado.app.help.book.isImage
-import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isPdf
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadBook
-import io.legado.app.model.localBook.LocalBook
 import io.legado.app.utils.GSON
 import io.legado.app.utils.MD5Utils
 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 kotlin.math.max
 import kotlin.math.min
 
@@ -160,9 +160,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 =
+                max(0, (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 +269,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 +437,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 {

+ 1 - 1
app/src/main/java/io/legado/app/model/AudioPlay.kt

@@ -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

+ 4 - 2
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -151,7 +151,7 @@ object ReadBook : CoroutineScope by MainScope() {
     fun setProgress(progress: BookProgress) {
         if (progress.durChapterIndex < chapterSize &&
             (durChapterIndex != progress.durChapterIndex
-                    || durChapterPos != progress.durChapterPos)
+                || durChapterPos != progress.durChapterPos)
         ) {
             durChapterIndex = progress.durChapterIndex
             durChapterPos = progress.durChapterPos
@@ -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()
@@ -590,6 +590,8 @@ object ReadBook : CoroutineScope by MainScope() {
                     }
                 }
             }
+
+            return@async
         }.onError {
             AppLog.put("ChapterProvider ERROR", it)
             appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")

+ 3 - 2
app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt

@@ -113,11 +113,11 @@ object BookChapterList {
         if (chapterList.isEmpty()) {
             throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
         }
-        //去重
         if (!reverse) {
             chapterList.reverse()
         }
         coroutineContext.ensureActive()
+        //去重
         val lh = LinkedHashSet(chapterList)
         val list = ArrayList(lh)
         if (!book.getReverseToc()) {
@@ -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) {

+ 15 - 3
app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt

@@ -36,13 +36,21 @@ 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.seekbar.SeekBarChangeListener
-import io.legado.app.utils.*
+import io.legado.app.utils.StartActivityContract
+import io.legado.app.utils.dpToPx
+import io.legado.app.utils.invisible
+import io.legado.app.utils.observeEvent
+import io.legado.app.utils.observeEventSticky
+import io.legado.app.utils.sendToClip
+import io.legado.app.utils.showDialogFragment
+import io.legado.app.utils.startActivity
 import io.legado.app.utils.viewbindingdelegate.viewBinding
+import io.legado.app.utils.visible
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import splitties.views.onLongClick
-import java.util.*
+import java.util.Locale
 
 /**
  * 音频播放
@@ -108,12 +116,14 @@ class AudioPlayActivity :
             R.id.menu_change_source -> AudioPlay.book?.let {
                 showDialogFragment(ChangeBookSourceDialog(it.name, it.author))
             }
+
             R.id.menu_login -> AudioPlay.bookSource?.let {
                 startActivity<SourceLoginActivity> {
                     putExtra("type", "bookSource")
                     putExtra("key", it.bookSourceUrl)
                 }
             }
+
             R.id.menu_wake_lock -> AppConfig.audioPlayUseWakeLock = !AppConfig.audioPlayUseWakeLock
             R.id.menu_copy_audio_url -> sendToClip(AudioPlayService.url)
             R.id.menu_edit_source -> AudioPlay.bookSource?.let {
@@ -121,6 +131,7 @@ class AudioPlayActivity :
                     putExtra("sourceUrl", it.bookSourceUrl)
                 }
             }
+
             R.id.menu_log -> showDialogFragment<AppLogDialog>()
         }
         return super.onCompatOptionsItemSelected(item)
@@ -258,7 +269,8 @@ 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) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -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)

+ 58 - 0
app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt

@@ -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
 
 /**
  * 阅读界面
@@ -57,6 +61,7 @@ abstract class BaseReadBookActivity :
 
     override val binding by viewBinding(ActivityBookReadBinding::inflate)
     override val viewModel by viewModels<ReadBookViewModel>()
+
     var bottomDialog = 0
         set(value) {
             if (field != value) {
@@ -304,6 +309,59 @@ 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 formattedDate = date.format(dateFormatter)
+                                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 {

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -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) {

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -233,7 +233,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
                         && progress.durChapterPos < book.durChapterPos)
             ) {
                 alertSync?.invoke(progress)
-            } else {
+            } else if (progress.durChapterIndex < book.simulatedTotalChapterNum()) {
                 ReadBook.setProgress(progress)
                 AppLog.put("自动同步阅读进度成功《${book.name}》 ${progress.durChapterTitle}")
             }

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -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 {

+ 8 - 4
app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt

@@ -118,7 +118,10 @@ 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 {
@@ -163,9 +166,10 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
 
     override fun openChapter(bookChapter: BookChapter) {
         activity?.run {
-            setResult(RESULT_OK, Intent()
-                .putExtra("index", bookChapter.index)
-                .putExtra("chapterChanged", bookChapter.index != durChapterIndex)
+            setResult(
+                RESULT_OK, Intent()
+                    .putExtra("index", bookChapter.index)
+                    .putExtra("chapterChanged", bookChapter.index != durChapterIndex)
             )
             finish()
         }

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt

@@ -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)
                 }

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -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 {

+ 1 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt

@@ -83,4 +83,4 @@ abstract class BaseBooksAdapter<VB : ViewBinding>(context: Context) :
         fun openBookInfo(book: Book)
         fun isUpdate(bookUrl: String): Boolean
     }
-}
+}

+ 185 - 0
app/src/main/res/layout/dialog_simulated_reading.xml

@@ -0,0 +1,185 @@
+<?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:gravity="center"
+    android:padding="20dp"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="60dp"
+        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: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_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 - 0
app/src/main/res/menu/book_read.xml

@@ -70,6 +70,11 @@
         android:title="@string/reverse_content"
         app:showAsAction="never" />
 
+    <item
+            android:id="@+id/menu_simulated_reading"
+            android:title="@string/simulated_reading"
+            app:showAsAction="never" />
+
     <item
         android:id="@+id/menu_enable_replace"
         android:checkable="true"

+ 6 - 1
app/src/main/res/values-es-rES/strings.xml

@@ -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,9 @@
     <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>
+    <string name="start_chapter">Cap. inicio</string>
 </resources>

+ 5 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -1151,4 +1151,9 @@
     <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>
+    <string name="start_chapter">開始の章</string>
 </resources>

+ 5 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -1151,4 +1151,9 @@
     <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>
+    <string name="start_chapter">Início</string>
 </resources>

+ 5 - 0
app/src/main/res/values-vi/strings.xml

@@ -1147,4 +1147,9 @@ 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>
+    <string name="start_chapter">Chương đầu</string>
 </resources>

+ 5 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1148,4 +1148,9 @@
     <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>
+    <string name="start_chapter">開始篇章</string>
 </resources>

+ 5 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1150,4 +1150,9 @@
     <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>
+    <string name="start_chapter">開始篇章</string>
 </resources>

+ 5 - 0
app/src/main/res/values-zh/strings.xml

@@ -1150,4 +1150,9 @@
     <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>
+    <string name="start_chapter">起始章节</string>
 </resources>

+ 5 - 0
app/src/main/res/values/strings.xml

@@ -1151,4 +1151,9 @@
     <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>
+    <string name="start_chapter">Start Chapter</string>
 </resources>