From 4a61afd5a99f8d93d110bfacb9cbf2dec758cbea Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Mon, 31 Aug 2026 15:37:43 +0800 Subject: [PATCH] redesign course page: select course to enter training, guard tabs without course --- .../oraltrainer/sample/MainActivity.kt | 140 +++++++++++++++--- 1 file changed, 120 insertions(+), 20 deletions(-) diff --git a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt index b20a341..9b53a3b 100644 --- a/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt +++ b/android/sample-app/src/main/java/cn/learningpad/oraltrainer/sample/MainActivity.kt @@ -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() 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() @@ -612,13 +615,17 @@ class MainActivity : Activity() { } layoutParams = LinearLayout.LayoutParams(0, 44.dp, 1f).withMargins(0, 0, 8.dp, 0) setOnClickListener { - if (activeModule == module) { - return@setOnClickListener - } - if (module == Module.COURSE || module == Module.MINE) { - loadUserData() - } - activeModule = module + 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() + } + activeModule = module controller.setContinuousPlayback( if (activeModule == Module.TRAIN) continuousPlaybackEnabled else false ) @@ -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) { - catalogList = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - } + 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(