Skip to content

Commit 95a8c8b

Browse files
committed
feat: Add user guidance for top menu (v1.0.3)
1 parent 3535776 commit 95a8c8b

File tree

15 files changed

+1071
-25
lines changed

15 files changed

+1071
-25
lines changed

.vscode/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
"java.import.gradle.wrapper.enabled": true,
1212
"java.import.gradle.enabled": true,
1313
"gradle.nestedProjects": true,
14-
"kotlin.languageServer.enabled": true
15-
}
14+
"kotlin.languageServer.enabled": true,
15+
"java.configuration.updateBuildConfiguration": "automatic"
16+
}

TODO/bug_fix_read_aloud.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
- [x] Identify why highlighting is missing on fresh start until page turn (Background thread wiping sentence data in `PageEngine`)
1010
- [x] Refactor `PageEngine.paginate` to not clear sentence ranges
1111
- [x] Verify fix (manual compilation and deploy as per user rules)
12-
- [ ] Build Release APK
13-
- [ ] Create Git Commit and Tag
14-
- [ ] Publish GitHub Release
12+
- [x] Build Release APK
13+
- [x] Create Git Commit and Tag
14+
- [x] Publish GitHub Release (Uploaded binary via gh CLI)

TODO/monkey_high_pressure.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# High Pressure Monkey Test Plan
2+
3+
- [ ] Configuration
4+
- Package: `com.xuyutech.hongbaoshu`
5+
- Events: 10,000
6+
- Throttle: 50ms
7+
- Log File: `monkey_logs/monkey_high_pressure.log`
8+
- [x] Execution
9+
- [x] Run Command
10+
- [ ] Monitor for crashes
11+
- [ ] Reporting
12+
- [ ] Check log for "Monkey finished"
13+
- [ ] Check for exceptions/ANRs

TODO/monkey_test_execution.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Monkey Test Execution Todo List
2+
3+
- [x] Verify Android Package Name (com.xuyutech.hongbaoshu)
4+
- [x] Check ADB Connection (Device: ONNZ95CAEMMZSKTS)
5+
- [x] Build and Install APK
6+
- [x] Construct Monkey Command (adb shell monkey -p com.xuyutech.hongbaoshu --throttle 200 -v 1000)
7+
- [x] Execute Monkey Test
8+
- [x] Capture and Save Logs (Saved to monkey_logs/monkey_test_latest.log)

TODO/user_guidance_todo.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# User Guidance Implementation TODO
2+
3+
- [ ] Explore codebase to identify Reader component and page detection logic
4+
- [ ] Design guidance UI and state management
5+
- [ ] Add multi-language strings (English, Chinese)
6+
- [ ] Implement state tracking (Shown/Not Shown)
7+
- [ ] Implement Guidance UI in Reader Screen
8+
- [ ] Verify logic: Show only on first content page, not cover
9+
- [ ] Build and Deploy

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ android {
1515
applicationId = "com.xuyutech.hongbaoshu"
1616
minSdk = 28
1717
targetSdk = 36
18-
versionCode = 3
19-
versionName = "1.0.2"
18+
versionCode = 4
19+
versionName = "1.0.3"
2020

2121
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2222
}

app/src/main/java/com/xuyutech/hongbaoshu/reader/ReaderScreen.kt

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ import androidx.compose.material.icons.filled.ArrowForward
77
import androidx.compose.material.icons.filled.Settings
88
import androidx.compose.material.icons.filled.Info
99

10+
import androidx.compose.ui.draw.scale
11+
12+
1013
import androidx.compose.animation.AnimatedVisibility
1114
import androidx.compose.animation.slideInVertically
1215
import androidx.compose.animation.slideOutVertically
1316
import androidx.compose.foundation.clickable
17+
import androidx.compose.animation.core.animateFloat
18+
import androidx.compose.animation.core.infiniteRepeatable
19+
import androidx.compose.animation.core.rememberInfiniteTransition
20+
import androidx.compose.animation.core.tween
1421
import androidx.compose.foundation.background
1522
import androidx.compose.foundation.gestures.detectTapGestures
1623
import androidx.compose.foundation.gestures.detectVerticalDragGestures
@@ -319,6 +326,7 @@ fun ReaderScreen(
319326
onTopDoubleTap = {
320327
if (!showMenu.value) {
321328
showMenu.value = true
329+
viewModel.dismissMenuGuide()
322330
}
323331
}
324332
) { pageIndexToRender ->
@@ -350,6 +358,20 @@ fun ReaderScreen(
350358
}
351359
}
352360

361+
362+
363+
// 用户引导层
364+
if (!state.value.hasShownMenuGuide &&
365+
state.value.currentChapterIndex == 0 &&
366+
state.value.pageIndex == 0 &&
367+
!state.value.isLoading &&
368+
state.value.error == null
369+
) {
370+
MenuGuideOverlay(
371+
onDismiss = { viewModel.dismissMenuGuide() }
372+
)
373+
}
374+
353375
// 下拉菜单(带遮罩层)
354376
if (showMenu.value) {
355377
// 遮罩层:点击或滑动菜单以外区域关闭菜单
@@ -375,7 +397,11 @@ fun ReaderScreen(
375397
onBgmToggle = { viewModel.playBgm(it) },
376398
onBgmNext = { viewModel.nextBgm() },
377399
onBgmVolumeChange = { viewModel.setBgmVolume(it) },
378-
onShowToc = { showToc.value = true },
400+
onShowToc = {
401+
showToc.value = true
402+
// 如果在引导显示时打开了目录(也是通过菜单打开的),也应该关闭引导
403+
viewModel.dismissMenuGuide()
404+
},
379405
narrationEnabled = state.value.narrationEnabled,
380406
onNarrationToggle = { enabled ->
381407
viewModel.toggleNarration(enabled)
@@ -419,6 +445,140 @@ fun ReaderScreen(
419445
}
420446
}
421447

448+
/**
449+
* 用户引导层
450+
*/
451+
/**
452+
* 用户引导层 (Ripple Animation)
453+
*/
454+
@Composable
455+
private fun MenuGuideOverlay(
456+
onDismiss: () -> Unit
457+
) {
458+
// 动画状态
459+
val infiniteTransition = androidx.compose.animation.core.rememberInfiniteTransition(label = "GuidePulse")
460+
461+
// 两个波纹动画,错开播放,形成连贯感
462+
val scale1 by infiniteTransition.animateFloat(
463+
initialValue = 0.5f,
464+
targetValue = 1.5f,
465+
animationSpec = androidx.compose.animation.core.infiniteRepeatable(
466+
animation = androidx.compose.animation.core.tween(2000),
467+
repeatMode = androidx.compose.animation.core.RepeatMode.Restart
468+
),
469+
label = "Scale1"
470+
)
471+
val alpha1 by infiniteTransition.animateFloat(
472+
initialValue = 0.6f,
473+
targetValue = 0f,
474+
animationSpec = androidx.compose.animation.core.infiniteRepeatable(
475+
animation = androidx.compose.animation.core.tween(2000),
476+
repeatMode = androidx.compose.animation.core.RepeatMode.Restart
477+
),
478+
label = "Alpha1"
479+
)
480+
481+
val scale2 by infiniteTransition.animateFloat(
482+
initialValue = 0.5f,
483+
targetValue = 1.5f,
484+
animationSpec = androidx.compose.animation.core.infiniteRepeatable(
485+
animation = androidx.compose.animation.core.tween(2000, delayMillis = 1000),
486+
repeatMode = androidx.compose.animation.core.RepeatMode.Restart
487+
),
488+
label = "Scale2"
489+
)
490+
val alpha2 by infiniteTransition.animateFloat(
491+
initialValue = 0.6f,
492+
targetValue = 0f,
493+
animationSpec = androidx.compose.animation.core.infiniteRepeatable(
494+
animation = androidx.compose.animation.core.tween(2000, delayMillis = 1000),
495+
repeatMode = androidx.compose.animation.core.RepeatMode.Restart
496+
),
497+
label = "Alpha2"
498+
)
499+
500+
Box(
501+
modifier = Modifier
502+
.fillMaxSize()
503+
// 渐变背景,顶部深色,底部透明,不再全屏遮挡
504+
.background(
505+
androidx.compose.ui.graphics.Brush.verticalGradient(
506+
colors = listOf(
507+
Color.Black.copy(alpha = 0.7f),
508+
Color.Transparent
509+
),
510+
endY = 600f // 只遮挡顶部一部分
511+
)
512+
)
513+
.clickable(
514+
interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() },
515+
indication = null
516+
) { onDismiss() }
517+
) {
518+
Column(
519+
modifier = Modifier
520+
.align(Alignment.TopCenter)
521+
.padding(top = 60.dp), // 避开状态栏,定位到顶部交互区
522+
horizontalAlignment = Alignment.CenterHorizontally
523+
) {
524+
// 波纹动画区域
525+
Box(
526+
contentAlignment = Alignment.Center,
527+
modifier = Modifier.size(80.dp)
528+
) {
529+
// 波纹 1
530+
Box(
531+
modifier = Modifier
532+
.size(80.dp)
533+
.scale(scale1)
534+
.background(MaterialTheme.colorScheme.primary.copy(alpha = alpha1), androidx.compose.foundation.shape.CircleShape)
535+
)
536+
// 波纹 2
537+
Box(
538+
modifier = Modifier
539+
.size(80.dp)
540+
.scale(scale2)
541+
.background(MaterialTheme.colorScheme.primary.copy(alpha = alpha2), androidx.compose.foundation.shape.CircleShape)
542+
)
543+
544+
// 中心手势图标
545+
androidx.compose.material3.Icon(
546+
imageVector = androidx.compose.material.icons.Icons.Default.Info,
547+
contentDescription = null,
548+
tint = MaterialTheme.colorScheme.onPrimary,
549+
modifier = Modifier
550+
.size(32.dp)
551+
.background(MaterialTheme.colorScheme.primary, androidx.compose.foundation.shape.CircleShape)
552+
.padding(6.dp)
553+
)
554+
}
555+
556+
Spacer(modifier = Modifier.height(16.dp))
557+
558+
// 提示卡片
559+
androidx.compose.material3.Surface(
560+
shape = RoundedCornerShape(percent = 50),
561+
color = MaterialTheme.colorScheme.surfaceContainerHigh,
562+
tonalElevation = 6.dp,
563+
shadowElevation = 6.dp,
564+
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
565+
) {
566+
Row(
567+
modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
568+
verticalAlignment = Alignment.CenterVertically
569+
) {
570+
Text(
571+
text = androidx.compose.ui.res.stringResource(id = com.xuyutech.hongbaoshu.R.string.guide_double_tap_menu),
572+
style = MaterialTheme.typography.labelLarge,
573+
color = MaterialTheme.colorScheme.onSurface
574+
)
575+
}
576+
}
577+
}
578+
}
579+
}
580+
581+
422582
/**
423583
* 根据页码索引获取页面数据(支持跨章节预览)
424584
* 返回:(章节, 页面, 全书页码, 全书总页数)

app/src/main/java/com/xuyutech/hongbaoshu/reader/ReaderState.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ data class ReaderState(
2424
val narrationEnabled: Boolean = false, // 朗读模式是否开启
2525
val currentPageSentenceIds: List<String> = emptyList(), // 当前页的句子 ID 列表
2626
val lastPlayedSentenceId: String? = null, // 上一个播放完成的句子 ID(用于查找下一句)
27-
val isNightMode: Boolean = false // 夜间模式
27+
val isNightMode: Boolean = false, // 夜间模式
28+
val hasShownMenuGuide: Boolean = false // 是否已显示过菜单引导
2829
)

app/src/main/java/com/xuyutech/hongbaoshu/reader/ReaderViewModel.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,8 @@ class ReaderViewModel(
343343
missingAudio = bookResult.missingSentenceAudioIds,
344344
currentChapterIndex = cappedChapterIndex,
345345
pageIndex = saved.pageIndex, // 由 UI 层校验
346-
isNightMode = saved.isNightMode
346+
isNightMode = saved.isNightMode,
347+
hasShownMenuGuide = saved.hasShownMenuGuide
347348
)
348349
restoreAudioState(saved)
349350
}.onFailure { e ->
@@ -501,7 +502,8 @@ class ReaderViewModel(
501502
bgmIndex = audioManager.state.value.bgmIndex,
502503
bgmEnabled = audioManager.state.value.bgmEnabled,
503504
bgmVolume = audioManager.state.value.bgmVolume,
504-
isNightMode = current.isNightMode
505+
isNightMode = current.isNightMode,
506+
hasShownMenuGuide = current.hasShownMenuGuide
505507
)
506508
)
507509
}
@@ -605,4 +607,12 @@ class ReaderViewModel(
605607
persistState(pageIndex = index)
606608
}
607609
}
610+
611+
fun dismissMenuGuide() {
612+
val current = _state.value ?: return
613+
if (!current.hasShownMenuGuide) {
614+
_state.value = current.copy(hasShownMenuGuide = true)
615+
persistState()
616+
}
617+
}
608618
}

app/src/main/java/com/xuyutech/hongbaoshu/storage/ProgressStore.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ data class ProgressState(
2525
val bgmIndex: Int = 0,
2626
val bgmEnabled: Boolean = false,
2727
val bgmVolume: Float = 1.0f,
28-
val isNightMode: Boolean = false
28+
val isNightMode: Boolean = false,
29+
val hasShownMenuGuide: Boolean = false
2930
)
3031

3132
class ProgressStore(private val context: Context) {
@@ -38,6 +39,7 @@ class ProgressStore(private val context: Context) {
3839
private val keyBgmEnabled = booleanPreferencesKey("bgm_enabled")
3940
private val keyBgmVolume = floatPreferencesKey("bgm_volume")
4041
private val keyIsNightMode = booleanPreferencesKey("is_night_mode")
42+
private val keyHasShownMenuGuide = booleanPreferencesKey("has_shown_menu_guide")
4143

4244
val progress: Flow<ProgressState> = context.progressDataStore.data.map { prefs ->
4345
ProgressState(
@@ -48,7 +50,8 @@ class ProgressStore(private val context: Context) {
4850
bgmIndex = prefs[keyBgmIndex] ?: 0,
4951
bgmEnabled = prefs[keyBgmEnabled] ?: false,
5052
bgmVolume = prefs[keyBgmVolume] ?: 1.0f,
51-
isNightMode = prefs[keyIsNightMode] ?: false
53+
isNightMode = prefs[keyIsNightMode] ?: false,
54+
hasShownMenuGuide = prefs[keyHasShownMenuGuide] ?: false
5255
)
5356
}
5457

@@ -67,6 +70,7 @@ class ProgressStore(private val context: Context) {
6770
prefs[keyBgmEnabled] = state.bgmEnabled
6871
prefs[keyBgmVolume] = state.bgmVolume
6972
prefs[keyIsNightMode] = state.isNightMode
73+
prefs[keyHasShownMenuGuide] = state.hasShownMenuGuide
7074
}
7175
}
7276

0 commit comments

Comments
 (0)