redesign course page: select course to enter training, guard tabs without course

This commit is contained in:
2026-08-31 15:37:43 +08:00
parent e80a770847
commit 4a61afd5a9

View File

@@ -42,6 +42,7 @@ import android.widget.ScrollView
import android.widget.SeekBar
import android.widget.Switch
import android.widget.TextView
import android.widget.Toast
import cn.learningpad.oraltrainer.sdk.GestureEvent
import cn.learningpad.oraltrainer.sdk.GestureKind
import cn.learningpad.oraltrainer.sdk.AuthSession
@@ -127,6 +128,7 @@ class MainActivity : Activity() {
private val courseEnrollment = mutableMapOf<String, Boolean>()
private var catalogStatusTextValue = "正在同步"
private var activeModule = Module.TRAIN
private var currentCourse: TrainingVideoSummary? = null
private var continuousPlaybackEnabled = false
private var testSubtitleVisible = false
private var landscapeSubtitleVisible = true
@@ -550,6 +552,7 @@ class MainActivity : Activity() {
authToken = ""
authNickname = ""
sdk.config.userToken = null
currentCourse = null
controller.pause()
activeModule = Module.TRAIN
setRootView()
@@ -615,6 +618,10 @@ class MainActivity : Activity() {
if (activeModule == module) {
return@setOnClickListener
}
if (module != Module.COURSE && currentCourse == null) {
Toast.makeText(this@MainActivity, "请先选择课程", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (module == Module.COURSE || module == Module.MINE) {
loadUserData()
}
@@ -948,7 +955,7 @@ class MainActivity : Activity() {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 10.dp, 0, 4.dp)
}
// Fresh instances each time — safe to addView.
val header = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
@@ -1757,28 +1764,35 @@ class MainActivity : Activity() {
}
private fun createCourseSection(): View {
if (!::catalogList.isInitialized) {
catalogStatusText = TextView(this).apply {
setTextColor(COLOR_TEXT_MUTED)
textSize = 13f
text = catalogStatusTextValue
setPadding(16.dp, 0.dp, 16.dp, 8.dp)
}
catalogList = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
}
setPadding(16.dp, 8.dp, 16.dp, 16.dp)
}
return ScrollView(this).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
addView(LinearLayout(this@MainActivity).apply {
orientation = LinearLayout.VERTICAL
setPadding(16.dp, 12.dp, 16.dp, 16.dp)
setPadding(0.dp, 12.dp, 0.dp, 16.dp)
addView(TextView(this@MainActivity).apply {
text = "课程首页"
textSize = 26f
typeface = Typeface.DEFAULT_BOLD
setTextColor(Color.WHITE)
setPadding(16.dp, 0.dp, 16.dp, 0.dp)
})
addView(TextView(this@MainActivity).apply {
text = "选择一门课程开始训练"
textSize = 13f
setTextColor(COLOR_TEXT_MUTED)
setPadding(0, 4.dp, 0, 16.dp)
setPadding(16.dp, 4.dp, 16.dp, 8.dp)
})
addView(catalogStatusText)
addView(catalogList)
})
}
@@ -1818,15 +1832,35 @@ class MainActivity : Activity() {
private fun refreshMyContent() {
val list = myContentList ?: return
list.removeAllViews()
val course = currentCourse
if (course == null) {
list.addView(infoCard("请先在「课程」中选择一门课程"))
return
}
list.addView(TextView(this).apply {
text = "当前课程:${course.title}"
setTextColor(Color.WHITE)
textSize = 14f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, 0, 0, 8.dp)
})
list.addView(infoCard("加载中..."))
sdk.userApi.results(authToken) { result ->
list.removeAllViews()
list.addView(TextView(this).apply {
text = "当前课程:${course.title}"
setTextColor(Color.WHITE)
textSize = 14f
typeface = Typeface.DEFAULT_BOLD
setPadding(0, 0, 0, 8.dp)
})
result.fold(
onSuccess = { rows ->
if (rows.isEmpty()) list.addView(infoCard("还没有测试记录"))
rows.forEach { row ->
val filtered = rows.filter { it.videoHash == course.videoHash }
if (filtered.isEmpty()) list.addView(infoCard("该课程还没有测试记录"))
filtered.forEach { row ->
list.addView(infoCard(
"${row.courseTitle} · ${row.sentenceIndex + 1}\n" +
"${row.sentenceIndex + 1}\n" +
"得分 ${row.overallScore.toInt()} · ${if (row.passed) "通过" else "未通过"}"
))
}
@@ -1838,10 +1872,11 @@ class MainActivity : Activity() {
list.addView(titleDivider("我的配音分享"))
shareResult.fold(
onSuccess = { shares ->
if (shares.isEmpty()) list.addView(infoCard("还没有分享配音"))
shares.forEach { row ->
val filtered = shares.filter { it.videoHash == course.videoHash }
if (filtered.isEmpty()) list.addView(infoCard("该课程还没有分享配音"))
filtered.forEach { row ->
list.addView(infoCard(
"${row.title}\n${row.courseTitle} · ${row.segmentCount} 句 · 平均 ${row.averageScore.toInt()}"
"${row.title}\n${row.segmentCount} 句 · 平均 ${row.averageScore.toInt()}"
))
}
},
@@ -1881,6 +1916,18 @@ class MainActivity : Activity() {
return
}
catalogList.removeAllViews()
if (activeModule == Module.COURSE) {
if (catalogVideos.isEmpty()) {
catalogList.addView(infoCard("暂无课程"))
return
}
catalogVideos.forEach { video ->
catalogList.addView(courseListCard(video) {
selectCourse(video)
})
}
return
}
catalogVideos.forEach { video ->
val enrolled = courseEnrollment[video.videoHash] == true
catalogList.addView(
@@ -1899,6 +1946,59 @@ class MainActivity : Activity() {
}
}
private fun courseListCard(
video: TrainingVideoSummary,
onClick: () -> Unit,
): View {
val enrolled = courseEnrollment[video.videoHash] == true
val isSelected = currentCourse?.videoHash == video.videoHash
return LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
isClickable = true
background = if (isSelected) {
rounded(COLOR_SELECTED, 10f, COLOR_ACCENT)
} else {
rounded(COLOR_SURFACE, 10f, COLOR_BORDER)
}
setPadding(16.dp, 14.dp, 16.dp, 14.dp)
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).withMargins(0, 0, 0, 10.dp)
setOnClickListener { onClick() }
addView(TextView(this@MainActivity).apply {
text = video.title
setTextColor(if (isSelected) COLOR_TEXT_DARK else Color.WHITE)
textSize = 16f
typeface = Typeface.DEFAULT_BOLD
maxLines = 2
})
addView(TextView(this@MainActivity).apply {
text = buildString {
append(if (enrolled) "已选修" else "未选修")
append(" · ${video.sentenceCount}")
if (isSelected) append(" · 当前课程")
}
setTextColor(if (isSelected) COLOR_ACCENT_DEEP else COLOR_TEXT_MUTED)
textSize = 13f
setPadding(0, 6.dp, 0, 0)
})
}
}
private fun selectCourse(video: TrainingVideoSummary) {
currentCourse = video
if (courseEnrollment[video.videoHash] != true) {
enrollCourse(video)
}
activeModule = Module.TRAIN
landscapeSubtitleVisible = true
setContentView(createContentView())
renderCatalog()
refreshCurrentUi()
loadRemoteVideo(video)
}
private fun enrollCourse(video: TrainingVideoSummary) {
sdk.userApi.enroll(authToken, video.videoHash) { result ->
result.fold(