Skip to content

Commit 419376b

Browse files
chore: release v1.1.4
1 parent 689b0f2 commit 419376b

13 files changed

Lines changed: 244 additions & 128 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [1.1.4] - 2026-05-26
4+
5+
### Features
6+
- 優化主頁排版:成績頁面向下滾動時,頂部標題與按鈕會自動滑出收起,最大化成績閱讀空間。
7+
- 重新設計子頁面導航:設定、開發者選項與成績模擬器全面改用「浮動圓形返回按鈕」,並加入頂部漸層遮罩,讓穿越狀態列的內容能自然淡出,騰出更多畫面高度。
8+
9+
### Bug Fixes
10+
- 修正成績模擬器中,卡片之間沒有保持標準間距的問題。
11+
12+
### Performance Improvements
13+
- 加快各子頁面切換的動畫速度,大幅提升整體操作的俐落感與流暢度。
14+
- 全面修復 Android 專案的 Lint 錯誤與警告,優化 Compose 狀態儲存機制,並升級 SDK 與核心相依套件至最新版本,進一步提升穩定性與效能。
15+
316
## [1.1.3] - 2026-05-25
417

518
### Features

android/app/build.gradle.kts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ plugins {
2828

2929
android {
3030
namespace = "com.clhs.score"
31-
compileSdk = 36
31+
compileSdk = 37
3232

3333
defaultConfig {
3434
applicationId = "com.clhs.score"
3535
minSdk = 29
36-
targetSdk = 36
36+
targetSdk = 37
3737
versionCode = releaseVersionCode.get()
3838
versionName = releaseVersionName.get()
3939

@@ -116,20 +116,20 @@ dependencies {
116116
implementation("androidx.datastore:datastore-preferences:1.2.1")
117117
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
118118
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
119-
implementation("androidx.navigation:navigation-compose:2.9.7")
119+
implementation("androidx.navigation:navigation-compose:2.9.8")
120120
implementation("androidx.security:security-crypto:1.1.0")
121121
implementation("com.squareup.okhttp3:okhttp:5.3.2")
122-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
122+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
123123
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
124-
implementation("org.jsoup:jsoup:1.22.1")
124+
implementation("org.jsoup:jsoup:1.22.2")
125125

126126
debugImplementation("androidx.compose.ui:ui-tooling")
127127
debugImplementation("androidx.compose.ui:ui-test-manifest")
128128

129129
testImplementation("app.cash.turbine:turbine:1.2.1")
130130
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
131131
testImplementation("junit:junit:4.13.2")
132-
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
132+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
133133

134134
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
135135
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")

android/app/src/main/java/com/clhs/score/data/SessionStore.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.clhs.score.data
22

33
import android.content.Context
4+
import androidx.core.content.edit
45
import androidx.security.crypto.EncryptedSharedPreferences
56
import androidx.security.crypto.MasterKey
67
import kotlinx.serialization.json.JsonObject
@@ -26,11 +27,11 @@ class SessionStore(context: Context) {
2627
val cookiesJson = buildJsonObject {
2728
session.cookies.forEach { (name, value) -> put(name, value) }
2829
}.toString()
29-
prefs.edit()
30-
.putString(KEY_STUDENT_NO, session.studentNo)
31-
.putString(KEY_API_TOKEN, session.apiToken)
32-
.putString(KEY_COOKIES, cookiesJson)
33-
.apply()
30+
prefs.edit {
31+
putString(KEY_STUDENT_NO, session.studentNo)
32+
putString(KEY_API_TOKEN, session.apiToken)
33+
putString(KEY_COOKIES, cookiesJson)
34+
}
3435
}
3536

3637
fun loadSession(): AuthenticatedSession? {
@@ -45,7 +46,7 @@ class SessionStore(context: Context) {
4546
}
4647

4748
fun clear() {
48-
prefs.edit().clear().apply()
49+
prefs.edit { clear() }
4950
}
5051

5152
private fun JsonObject.toStringMap(): Map<String, String> = entries.associate { (key, value) ->

android/app/src/main/java/com/clhs/score/ui/AdvancedComponents.kt

Lines changed: 97 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ import androidx.compose.material3.Text
4343
import androidx.compose.material3.TextButton
4444
import androidx.compose.material3.IconButtonDefaults
4545
import androidx.compose.runtime.Composable
46+
import androidx.compose.runtime.derivedStateOf
4647
import androidx.compose.runtime.getValue
48+
import androidx.compose.runtime.mutableIntStateOf
4749
import androidx.compose.runtime.mutableStateOf
4850
import androidx.compose.runtime.remember
4951
import androidx.compose.runtime.rememberCoroutineScope
@@ -71,9 +73,19 @@ import com.clhs.score.viewmodel.GradesUiState
7173
import kotlinx.coroutines.launch
7274
import kotlin.math.abs
7375
import kotlin.math.roundToInt
76+
import androidx.compose.foundation.layout.offset
77+
import androidx.compose.foundation.lazy.LazyColumn
78+
import androidx.compose.foundation.lazy.rememberLazyListState
79+
import androidx.compose.ui.draw.shadow
80+
import androidx.compose.ui.unit.IntOffset
7481
import androidx.compose.foundation.layout.WindowInsets
7582
import androidx.compose.foundation.layout.safeDrawing
7683
import androidx.compose.foundation.layout.windowInsetsPadding
84+
import androidx.compose.foundation.layout.statusBars
85+
import androidx.compose.foundation.layout.statusBarsPadding
86+
import androidx.compose.foundation.layout.navigationBarsPadding
87+
import androidx.compose.foundation.layout.windowInsetsTopHeight
88+
import androidx.compose.foundation.layout.Spacer
7789

7890
@Composable
7991
internal fun TrendChart(
@@ -185,18 +197,20 @@ internal fun ScoreSimulatorScreen(
185197
onBack: () -> Unit,
186198
) {
187199
val report = state.report
188-
Scaffold(
189-
snackbarHost = { SnackbarHost(snackbarHostState) },
190-
containerColor = MaterialTheme.colorScheme.surface,
191-
) { padding ->
192-
if (report == null) {
193-
Column(modifier = Modifier.padding(padding).padding(16.dp)) {
200+
if (report == null) {
201+
SubpageLayout(
202+
onBack = onBack,
203+
snackbarHost = { SnackbarHost(snackbarHostState) },
204+
containerColor = MaterialTheme.colorScheme.surface,
205+
) {
206+
Column(modifier = Modifier.padding(16.dp)) {
194207
EmptyAnalysisState("尚未載入成績資料。")
195208
}
196-
return@Scaffold
197209
}
210+
return
211+
}
198212

199-
val coroutineScope = rememberCoroutineScope()
213+
val coroutineScope = rememberCoroutineScope()
200214

201215
val initialScores = remember(report) {
202216
report.subjects.associate { cleanSubjectName(it.subjectName) to it.scoreValue }
@@ -226,38 +240,83 @@ internal fun ScoreSimulatorScreen(
226240
val allHistory = remember(state.trendReports, state.simulatorHistoryReports) {
227241
state.trendReports + state.simulatorHistoryReports
228242
}
229-
val historyMaxMin = remember(allHistory, report) {
230-
val map = mutableMapOf<String, Pair<Double, Double>>()
231-
report.subjects.forEach { subject ->
232-
val key = cleanSubjectName(subject.subjectName)
233-
val scores = allHistory.mapNotNull { r -> r.subjects.find { cleanSubjectName(it.subjectName) == key }?.scoreValue }
234-
if (scores.isNotEmpty()) {
235-
map[key] = scores.min() to scores.max()
236-
}
243+
244+
val historyMaxMin = remember(allHistory, report) {
245+
val map = mutableMapOf<String, Pair<Double, Double>>()
246+
report.subjects.forEach { subject ->
247+
val key = cleanSubjectName(subject.subjectName)
248+
val scores = allHistory.mapNotNull { r -> r.subjects.find { cleanSubjectName(it.subjectName) == key }?.scoreValue }
249+
if (scores.isNotEmpty()) {
250+
map[key] = scores.min() to scores.max()
237251
}
238-
map
239252
}
240-
val density = LocalDensity.current
241-
var summaryCardHeightPx by remember { mutableStateOf(0) }
242-
val summaryContentTopPadding = if (summaryCardHeightPx > 0) {
243-
with(density) { summaryCardHeightPx.toDp() } + 16.dp
244-
} else {
245-
156.dp
253+
map
254+
}
255+
256+
val density = LocalDensity.current
257+
val statusBars = WindowInsets.statusBars
258+
val statusBarHeightPx = remember(density, statusBars) { statusBars.getTop(density) }
259+
val topSpacerHeightPx = remember(density, statusBarHeightPx) { statusBarHeightPx + with(density) { 56.dp.toPx() }.roundToInt() }
260+
261+
val listState = rememberLazyListState()
262+
var cardHeightPx by remember { mutableIntStateOf(0) }
263+
264+
val cardYOffsetPx by remember(listState) {
265+
derivedStateOf {
266+
if (listState.firstVisibleItemIndex == 0) {
267+
val offset = listState.firstVisibleItemScrollOffset
268+
maxOf(statusBarHeightPx, topSpacerHeightPx - offset)
269+
} else {
270+
statusBarHeightPx
271+
}
272+
}
273+
}
274+
275+
val isStuck by remember(listState) {
276+
derivedStateOf {
277+
if (listState.firstVisibleItemIndex == 0) {
278+
(topSpacerHeightPx - listState.firstVisibleItemScrollOffset) <= statusBarHeightPx
279+
} else {
280+
true
281+
}
246282
}
283+
}
247284

248-
Box(
285+
SubpageLayout(
286+
onBack = onBack,
287+
snackbarHost = { SnackbarHost(snackbarHostState) },
288+
containerColor = MaterialTheme.colorScheme.surface,
289+
summaryContent = {
290+
SimulatorSummaryCard(
291+
currentAverage = currentAverage,
292+
currentTotal = currentTotal,
293+
adjustedAverage = adjustedAverage,
294+
adjustedTotal = adjustedTotal,
295+
modifier = Modifier
296+
.offset { IntOffset(0, cardYOffsetPx) }
297+
.padding(horizontal = 16.dp)
298+
.fillMaxWidth()
299+
.onGloballyPositioned { coordinates ->
300+
cardHeightPx = coordinates.size.height
301+
}
302+
)
303+
}
304+
) {
305+
LazyColumn(
249306
modifier = Modifier
250307
.fillMaxSize()
251-
.padding(padding),
308+
.padding(horizontal = 16.dp)
309+
.navigationBarsPadding(),
310+
verticalArrangement = Arrangement.spacedBy(16.dp),
311+
state = listState,
252312
) {
253-
Column(
254-
modifier = Modifier
255-
.fillMaxSize()
256-
.verticalScroll(rememberScrollState())
257-
.padding(horizontal = 16.dp)
258-
.padding(top = summaryContentTopPadding, bottom = 8.dp),
259-
verticalArrangement = Arrangement.spacedBy(16.dp),
260-
) {
313+
item {
314+
Spacer(modifier = Modifier.height(
315+
with(density) { (topSpacerHeightPx + cardHeightPx).toDp() }
316+
))
317+
}
318+
319+
item {
261320
Card(
262321
modifier = Modifier.fillMaxWidth(),
263322
shape = RoundedCornerShape(16.dp),
@@ -336,7 +395,9 @@ internal fun ScoreSimulatorScreen(
336395
}
337396
}
338397
}
398+
}
339399

400+
item {
340401
Card(
341402
modifier = Modifier.fillMaxWidth(),
342403
shape = RoundedCornerShape(16.dp),
@@ -389,33 +450,8 @@ internal fun ScoreSimulatorScreen(
389450
}
390451
}
391452
}
392-
393-
SimulatorSummaryCard(
394-
currentAverage = currentAverage,
395-
currentTotal = currentTotal,
396-
adjustedAverage = adjustedAverage,
397-
adjustedTotal = adjustedTotal,
398-
modifier = Modifier
399-
.padding(horizontal = 16.dp, vertical = 8.dp)
400-
.onGloballyPositioned { coordinates ->
401-
summaryCardHeightPx = coordinates.size.height
402-
},
403-
)
404-
405-
IconButton(
406-
onClick = onBack,
407-
modifier = Modifier
408-
.align(Alignment.TopStart)
409-
.padding(16.dp),
410-
colors = IconButtonDefaults.iconButtonColors(
411-
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f),
412-
contentColor = MaterialTheme.colorScheme.onSurface,
413-
),
414-
) {
415-
OutlinedRoundedSymbol(
416-
icon = "arrow_back",
417-
contentDescription = "返回",
418-
)
453+
item {
454+
Spacer(modifier = Modifier.height(16.dp))
419455
}
420456
}
421457
}
@@ -552,7 +588,7 @@ private fun SubjectScoreSlider(
552588
val diff = value - subject.scoreValue
553589
val isAboveMax = historyMax != null && value > historyMax
554590
var lastDispatchedScore by remember(subject.subjectName, value.roundToInt()) {
555-
mutableStateOf(value.roundToInt())
591+
mutableIntStateOf(value.roundToInt())
556592
}
557593
fun updateScore(score: Double, haptic: Boolean = false) {
558594
val roundedScore = score.coerceIn(0.0, 100.0).roundToInt()

android/app/src/main/java/com/clhs/score/ui/DeveloperSettingsScreen.kt

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ import androidx.compose.runtime.rememberCoroutineScope
4646
import androidx.compose.runtime.setValue
4747
import androidx.compose.ui.Alignment
4848
import androidx.compose.ui.Modifier
49-
import androidx.compose.foundation.layout.Box
49+
import androidx.compose.foundation.layout.Spacer
5050
import androidx.compose.foundation.layout.WindowInsets
5151
import androidx.compose.foundation.layout.safeDrawing
5252
import androidx.compose.foundation.layout.windowInsetsPadding
53+
import androidx.compose.foundation.layout.statusBars
54+
import androidx.compose.foundation.layout.statusBarsPadding
55+
import androidx.compose.foundation.layout.navigationBarsPadding
56+
import androidx.compose.foundation.layout.windowInsetsTopHeight
5357
import androidx.compose.ui.platform.LocalContext
5458
import androidx.compose.ui.text.font.FontFamily
5559
import androidx.compose.ui.text.font.FontWeight
@@ -149,21 +153,17 @@ fun DeveloperSettingsScreen(
149153
)
150154
}
151155

152-
Box(
153-
modifier = Modifier
154-
.fillMaxSize()
155-
.background(MaterialTheme.colorScheme.background)
156-
.windowInsetsPadding(WindowInsets.safeDrawing),
157-
) {
156+
SubpageLayout(onBack = onBack) {
158157
Column(
159158
modifier = Modifier
160159
.fillMaxSize()
161160
.verticalScroll(rememberScrollState())
162161
.padding(horizontal = 16.dp)
163-
.padding(top = 64.dp),
162+
.navigationBarsPadding(),
164163
verticalArrangement = Arrangement.spacedBy(12.dp),
165164
) {
166-
Spacer(modifier = Modifier.height(4.dp))
165+
Spacer(modifier = Modifier.windowInsetsTopHeight(WindowInsets.statusBars))
166+
Spacer(modifier = Modifier.height(56.dp))
167167

168168
DeveloperSwitchItem(
169169
icon = "science",
@@ -232,22 +232,6 @@ fun DeveloperSettingsScreen(
232232

233233
Spacer(modifier = Modifier.height(16.dp))
234234
}
235-
236-
IconButton(
237-
onClick = onBack,
238-
modifier = Modifier
239-
.align(Alignment.TopStart)
240-
.padding(16.dp),
241-
colors = IconButtonDefaults.iconButtonColors(
242-
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f),
243-
contentColor = MaterialTheme.colorScheme.onSurface,
244-
),
245-
) {
246-
OutlinedRoundedSymbol(
247-
icon = "arrow_back",
248-
contentDescription = "返回",
249-
)
250-
}
251235
}
252236
}
253237

android/app/src/main/java/com/clhs/score/ui/GradesScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ fun GradesScreen(
145145
},
146146
colors = TopAppBarDefaults.topAppBarColors(
147147
containerColor = MaterialTheme.colorScheme.surface,
148+
scrolledContainerColor = MaterialTheme.colorScheme.surface,
148149
),
149150
scrollBehavior = scrollBehavior,
150151
)

0 commit comments

Comments
 (0)