Просмотр исходного кода

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

Yaoliang 2 лет назад
Родитель
Сommit
ddca205446
25 измененных файлов с 382 добавлено и 23 удалено
  1. 62 3
      app/src/main/java/io/legado/app/data/entities/Book.kt
  2. 2 2
      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. 1 1
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  6. 2 2
      app/src/main/java/io/legado/app/ui/book/audio/AudioPlayActivity.kt
  7. 1 1
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  8. 59 1
      app/src/main/java/io/legado/app/ui/book/read/BaseReadBookActivity.kt
  9. 1 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  10. 1 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  11. 2 1
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  12. 3 2
      app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt
  13. 2 2
      app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
  14. 2 2
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  15. 2 1
      app/src/main/java/io/legado/app/ui/main/bookshelf/style1/books/BaseBooksAdapter.kt
  16. 197 0
      app/src/main/res/layout/dialog_simulated_reading.xml
  17. 5 0
      app/src/main/res/menu/book_read.xml
  18. 5 1
      app/src/main/res/values-es-rES/strings.xml
  19. 4 0
      app/src/main/res/values-ja-rJP/strings.xml
  20. 4 0
      app/src/main/res/values-pt-rBR/strings.xml
  21. 4 0
      app/src/main/res/values-vi/strings.xml
  22. 4 0
      app/src/main/res/values-zh-rHK/strings.xml
  23. 4 0
      app/src/main/res/values-zh-rTW/strings.xml
  24. 4 0
      app/src/main/res/values-zh/strings.xml
  25. 4 0
      app/src/main/res/values/strings.xml

+ 62 - 3
app/src/main/java/io/legado/app/data/entities/Book.kt

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

+ 2 - 2
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
@@ -223,4 +223,4 @@ object AudioPlay {
             }
         }
     }
-}
+}

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

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

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

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

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

@@ -404,4 +404,4 @@ object WebBook {
         }
     }
 
-}
+}

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

@@ -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 :
         }
     }
 
-}
+}

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

+ 59 - 1
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
 
 /**
  * 阅读界面
@@ -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
     }
-}
+}

+ 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 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

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

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

+ 3 - 2
app/src/main/java/io/legado/app/ui/book/toc/ChapterListFragment.kt

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

+ 2 - 2
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)
                 }
@@ -224,4 +224,4 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
 
     }
 
-}
+}

+ 2 - 2
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 {
@@ -241,4 +241,4 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
-}
+}

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

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

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

@@ -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 - 0
app/src/main/res/menu/book_read.xml

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

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

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

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

@@ -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 - 0
app/src/main/res/values-vi/strings.xml

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

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

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

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

@@ -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 - 0
app/src/main/res/values-zh/strings.xml

@@ -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 - 0
app/src/main/res/values/strings.xml

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