fixed some bugs

This commit is contained in:
2026-10-26 20:23:57 +08:00
parent ed8df744f9
commit 03e7987c7c
6 changed files with 245 additions and 126 deletions

View File

@@ -263,6 +263,9 @@ class MainActivity : Activity() {
if (screenState.activeModule == Module.MINE) {
screenFactory.refreshMyContent(sdk.userApi, authSessionStore.session?.token.orEmpty())
}
if (screenState.activeModule == Module.READING) {
screenFactory.scrollToReadingCurrentSentence()
}
}
private fun refreshCurrentUi() {
@@ -552,6 +555,17 @@ class MainActivity : Activity() {
return sentenceFavoriteStore.toggle(favoriteUserKey(), item.id, sentence.index)
}
override fun onSentenceDoubleTapped(sentence: SentenceBoundary) {
screenState.continuousPlaybackEnabled = false
switchModule(Module.TRAIN)
controller.setContinuousPlayback(false)
controller.seekTo(sentence.startMs)
controller.play()
if (bindings.hasPlayerChrome) {
bindings.statusText.text = "已定位到第 ${sentence.index + 1} 句,单句循环播放中"
}
}
override fun onExplainCurrentSentence() {
val sentence = controller.currentSentence() ?: return
if (sentence.text.isNullOrBlank()) return

View File

@@ -29,6 +29,7 @@ internal interface ReadingScreenActions {
fun onReadingTranslationVisible(visible: Boolean)
fun isSentenceFavorite(sentence: SentenceBoundary): Boolean
fun toggleSentenceFavorite(sentence: SentenceBoundary): Boolean
fun onSentenceDoubleTapped(sentence: SentenceBoundary)
}
internal interface TestScreenActions :

View File

@@ -85,6 +85,12 @@ internal class SampleScreenFactory(
}
}
fun scrollToReadingCurrentSentence() {
if (screenState.activeModule == Module.READING) {
readingScreen.scrollToCurrentSentence()
}
}
fun renderCatalog() {
if (screenState.activeModule == Module.COURSE) {
courseScreen.renderCatalog()

View File

@@ -19,6 +19,7 @@ internal object SampleTheme {
val textDark = Color.rgb(18, 24, 31)
val textMuted = Color.rgb(151, 162, 174)
val textSubtle = Color.rgb(88, 98, 108)
val readingTranslation = Color.rgb(147, 197, 253)
val danger = Color.rgb(220, 38, 38)
val textOverlay = Color.rgb(163, 172, 182)
}

View File

@@ -3,14 +3,16 @@ package cn.learningpad.oraltrainer.sample.ui
import android.app.Activity
import android.graphics.Color
import android.graphics.Typeface
import android.os.SystemClock
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Switch
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import cn.learningpad.oraltrainer.sample.ReadingScreenActions
import cn.learningpad.oraltrainer.sample.SampleTheme
import cn.learningpad.oraltrainer.sample.TrainingScreenState
@@ -18,7 +20,7 @@ import cn.learningpad.oraltrainer.sample.UiStyled
import cn.learningpad.oraltrainer.sdk.OralTrainerController
import cn.learningpad.oraltrainer.sdk.SentenceBoundary
/** Scrollable full-text reading view for the selected course. */
/** Lazy-loading full-text reading view for the selected course. */
internal class ReadingScreen(
private val activity: Activity,
private val screenState: TrainingScreenState,
@@ -28,90 +30,81 @@ internal class ReadingScreen(
) : UiStyled {
override val uiContext get() = activity
private data class SentenceRow(
val translation: TextView,
val favoriteButton: Button,
)
private val sentenceRows = mutableMapOf<Int, SentenceRow>()
private var sentenceList: LinearLayout? = null
private var readingAdapter: ReadingSentenceAdapter? = null
private var titleText: TextView? = null
private var sentenceListView: RecyclerView? = null
private var lastClickedSentenceIndex = Int.MIN_VALUE
private var lastClickedAtMs = 0L
fun createSection(): View {
val content = LinearLayout(activity).apply {
val root = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
setPadding(16.dp, 12.dp, 16.dp, 16.dp)
}
val list = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
}
sentenceList = list
val title = TextView(activity).apply {
setTextColor(Color.WHITE)
textSize = 24f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, 0, 0, 6.dp)
}
titleText = title
content.addView(title)
content.addView(createTranslationToggle())
content.addView(list)
renderSentences()
return ScrollView(activity).apply {
val adapter = ReadingSentenceAdapter()
readingAdapter = adapter
val list = RecyclerView(activity).apply {
layoutManager = LinearLayoutManager(activity)
this.adapter = adapter
itemAnimator = null
setItemViewCacheSize(12)
clipToPadding = false
setPadding(0, 0, 0, 16.dp)
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
0,
1f,
)
addView(content)
}
sentenceListView = list
val title = TextView(activity).apply {
setTextColor(Color.WHITE)
textSize = 15f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, 0, 0, 6.dp)
}
titleText = title
root.addView(title)
root.addView(createTranslationToggle())
root.addView(list)
renderSentences()
return root
}
fun renderSentences() {
val list = sentenceList ?: return
val item = controller.currentTrainingItem()
titleText?.text = screenState.currentCourse?.title.orEmpty()
val baseTitle = screenState.currentCourse?.title.orEmpty()
.ifBlank { item?.title ?: "阅读模式" }
sentenceRows.clear()
list.removeAllViews()
if (item == null || item.sentences.isEmpty()) {
list.addView(widgets.infoCard("当前课程还没有可阅读的句子文本"))
return
}
val currentTitle = titleText?.text?.toString().orEmpty()
titleText?.text = "$currentTitle · ${item.sentences.size} 句"
item.sentences.forEach { sentence ->
list.addView(sentenceRow(sentence))
val sentences = item?.sentences.orEmpty()
titleText?.text = if (sentences.isEmpty()) {
baseTitle
} else {
"$baseTitle · ${sentences.size} 句"
}
readingAdapter?.submitSentences(sentences)
}
fun refreshTranslations() {
val item = controller.currentTrainingItem() ?: return
item.sentences.forEach { sentence ->
val row = sentenceRows[sentence.index] ?: return@forEach
row.translation.visibility = if (screenState.readingTranslationVisible) {
View.VISIBLE
} else {
View.GONE
}
row.translation.text = translationText(sentence)
}
readingAdapter?.notifyDataSetChanged()
}
fun refreshFavorites() {
readingAdapter?.notifyDataSetChanged()
}
fun scrollToCurrentSentence() {
val list = sentenceListView ?: return
val currentSentence = controller.currentSentence() ?: return
val item = controller.currentTrainingItem() ?: return
item.sentences.forEach { sentence ->
sentenceRows[sentence.index]?.favoriteButton?.apply {
val isFavorite = actions.isSentenceFavorite(sentence)
setText(if (isFavorite) "已收藏" else "收藏")
background = rounded(
if (isFavorite) SampleTheme.accent else SampleTheme.button,
8f,
)
}
val position = item.sentences.indexOfFirst { it.index == currentSentence.index }
if (position < 0) return
list.post {
if (sentenceListView !== list) return@post
val layoutManager = list.layoutManager as? LinearLayoutManager ?: return@post
val offset = (list.height * 0.25f).toInt().coerceAtLeast(0)
layoutManager.scrollToPositionWithOffset(position, offset)
}
}
@@ -136,75 +129,178 @@ internal class ReadingScreen(
})
}
private fun sentenceRow(sentence: SentenceBoundary): View {
val translation = TextView(activity).apply {
text = translationText(sentence)
setTextColor(SampleTheme.textSubtle)
textSize = 14f
setPadding(0, 6.dp, 0, 0)
visibility = if (screenState.readingTranslationVisible) View.VISIBLE else View.GONE
}
val favoriteButton = Button(activity).apply {
setText("收藏")
isAllCaps = false
setTextColor(Color.WHITE)
textSize = 12f
typeface = Typeface.DEFAULT_BOLD
minWidth = 0
minHeight = 0
minimumWidth = 0
minimumHeight = 0
setPadding(12.dp, 0, 12.dp, 0)
background = rounded(SampleTheme.button, 8f)
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
32.dp,
).withMargins(10.dp, 0, 0, 0)
setOnClickListener {
val isFavorite = actions.toggleSentenceFavorite(sentence)
setText(if (isFavorite) "已收藏" else "收藏")
background = rounded(
if (isFavorite) SampleTheme.accent else SampleTheme.button,
8f,
)
}
}
val isFavorite = actions.isSentenceFavorite(sentence)
favoriteButton.setText(if (isFavorite) "已收藏" else "收藏")
favoriteButton.background = rounded(
if (isFavorite) SampleTheme.accent else SampleTheme.button,
8f,
)
val textColumn = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
addView(TextView(activity).apply {
text = "${sentence.index + 1}. ${sentence.text?.takeIf { it.isNotBlank() } ?: "未提供文本"}"
setTextColor(Color.WHITE)
textSize = 16f
})
addView(translation)
}
return LinearLayout(activity).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
background = rounded(SampleTheme.surface, 10f, SampleTheme.border)
setPadding(14.dp, 12.dp, 10.dp, 12.dp)
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).withMargins(0, 0, 0, 10.dp)
addView(textColumn)
addView(favoriteButton)
}
}
private fun translationText(sentence: SentenceBoundary): String = when {
!sentence.zhText.isNullOrBlank() -> sentence.zhText.orEmpty()
screenState.chineseTranslationLoading -> "正在翻译中文…"
screenState.chineseTranslationError.isNotBlank() -> screenState.chineseTranslationError
else -> "暂无中文翻译"
}
private inner class ReadingSentenceAdapter :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var sentences: List<SentenceBoundary> = emptyList()
init {
setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
return if (sentences.isEmpty()) EMPTY_ID else sentences[position].index.toLong()
}
override fun getItemViewType(position: Int): Int {
return if (sentences.isEmpty()) TYPE_EMPTY else TYPE_SENTENCE
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == TYPE_EMPTY) {
val empty = TextView(activity).apply {
text = "当前课程还没有可阅读的句子文本"
setTextColor(SampleTheme.textDark)
textSize = 13f
background = rounded(SampleTheme.selected, 8f)
setPadding(12.dp, 10.dp, 12.dp, 10.dp)
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
EmptyViewHolder(empty)
} else {
SentenceViewHolder(createSentenceView())
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder !is SentenceViewHolder || sentences.isEmpty()) return
val sentence = sentences[position]
attachDoubleTap(holder.itemView, sentence)
holder.text.text =
"${sentence.index + 1}. ${sentence.text?.takeIf { it.isNotBlank() } ?: "未提供文本"}"
holder.translation.visibility = if (screenState.readingTranslationVisible) {
View.VISIBLE
} else {
View.GONE
}
holder.translation.text = translationText(sentence)
bindFavoriteButton(holder.favoriteButton, sentence)
}
override fun getItemCount(): Int {
return if (sentences.isEmpty()) 1 else sentences.size
}
fun submitSentences(updated: List<SentenceBoundary>) {
sentences = updated
notifyDataSetChanged()
}
private fun createSentenceView(): View {
val text = TextView(activity).apply {
setTextColor(Color.WHITE)
textSize = 16f
}
val translation = TextView(activity).apply {
setTextColor(SampleTheme.readingTranslation)
textSize = 14f
setPadding(0, 6.dp, 0, 0)
}
val favoriteButton = Button(activity).apply {
isAllCaps = false
setTextColor(Color.WHITE)
textSize = 12f
typeface = Typeface.DEFAULT_BOLD
minWidth = 0
minHeight = 0
minimumWidth = 0
minimumHeight = 0
setPadding(12.dp, 0, 12.dp, 0)
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
32.dp,
).withMargins(10.dp, 0, 0, 0)
}
val textColumn = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)
addView(text)
addView(translation)
}
return LinearLayout(activity).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
background = rounded(SampleTheme.surface, 10f, SampleTheme.border)
setPadding(14.dp, 12.dp, 10.dp, 12.dp)
layoutParams = RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
bottomMargin = 10.dp
}
addView(textColumn)
addView(favoriteButton)
}
}
private fun bindFavoriteButton(button: Button, sentence: SentenceBoundary) {
val isFavorite = actions.isSentenceFavorite(sentence)
button.setText(if (isFavorite) "已收藏" else "收藏")
button.background = rounded(
if (isFavorite) SampleTheme.accent else SampleTheme.button,
8f,
)
button.setOnClickListener {
val favorite = actions.toggleSentenceFavorite(sentence)
button.setText(if (favorite) "已收藏" else "收藏")
button.background = rounded(
if (favorite) SampleTheme.accent else SampleTheme.button,
8f,
)
}
}
private fun attachDoubleTap(view: View, sentence: SentenceBoundary) {
view.isClickable = true
view.setOnClickListener {
val now = SystemClock.elapsedRealtime()
val isDoubleTap = lastClickedSentenceIndex == sentence.index &&
now - lastClickedAtMs <= DOUBLE_TAP_INTERVAL_MS
if (isDoubleTap) {
lastClickedSentenceIndex = Int.MIN_VALUE
lastClickedAtMs = 0L
actions.onSentenceDoubleTapped(sentence)
} else {
lastClickedSentenceIndex = sentence.index
lastClickedAtMs = now
}
}
}
private inner class EmptyViewHolder(root: View) : RecyclerView.ViewHolder(root)
private inner class SentenceViewHolder(
root: View,
) : RecyclerView.ViewHolder(root) {
val text: TextView
val translation: TextView
val favoriteButton: Button
init {
val textColumn = root as ViewGroup
val content = textColumn.getChildAt(0) as ViewGroup
text = content.getChildAt(0) as TextView
translation = content.getChildAt(1) as TextView
favoriteButton = textColumn.getChildAt(1) as Button
}
}
}
private companion object {
private const val TYPE_EMPTY = 0
private const val TYPE_SENTENCE = 1
private const val EMPTY_ID = Long.MIN_VALUE
private const val DOUBLE_TAP_INTERVAL_MS = 300L
}
}