Skip to content

Commit cb3e620

Browse files
chore(release): v1.0.1 WebView login revamp
1 parent ec123d3 commit cb3e620

12 files changed

Lines changed: 579 additions & 455 deletions

File tree

CHANGELOG.md

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

3+
## [1.0.1] - 2026-05-20
4+
5+
## 更新內容
6+
- 全新登入體驗:內嵌學校網頁登入,並提供密碼自動填入(Autofill)支援
7+
- 新增首頁介面,點擊後平滑切換至全螢幕 WebView 登入
8+
- 徹底移除不穩定的傳統驗證碼登入機制
9+
- 最佳化成績總覽畫面上方的操作圖示與樣式
10+
311
## [1.0.0] - 2026-05-20
412

513
## 更新內容

android/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
## 功能
2727

28-
- 登入頁:帳號、密碼、驗證碼圖片、刷新驗證碼、登入狀態與錯誤提示
28+
- 登入頁:內嵌學校系統 WebView 登入,具備浮動控制按鈕並支援密碼自動填入
2929
- 成績查詢:年學期與考試選擇,登入後直連學校 API 取得成績
3030
- 總覽:摘要卡、重點解讀、本地推估洞察、快速入口、強弱科摘要
3131
- 科目:精簡科目卡,點擊後展開五標落點、分佈摘要與上一考比較
@@ -54,7 +54,7 @@ Compose UI
5454

5555
主要模組:
5656

57-
- `data/SchoolGradeClient.kt`: 集中處理學校登入、captcha、token、成績 API 流程
57+
- `data/SchoolGradeClient.kt`: 集中處理 Cookie 擷取、登入與成績 API 流程
5858
- `data/GradeRepository.kt`: session restore、login、logout、structure loading、grade fetching
5959
- `data/GradeAnalysis.kt`: 成績分析、上一考比較、近三次趨勢、本地洞察與排名粗估
6060
- `viewmodel/ScoreViewModel.kt`: UI state、背景比較與趨勢載入

android/app/src/main/java/com/clhs/score/MainActivity.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ class MainActivity : ComponentActivity() {
2323
ScoreApp(
2424
loginState = loginState,
2525
gradesState = gradesState,
26-
onUsernameChange = viewModel::updateUsername,
27-
onPasswordChange = viewModel::updatePassword,
28-
onCaptchaChange = viewModel::updateCaptchaCode,
29-
onRefreshCaptcha = viewModel::refreshCaptcha,
30-
onLogin = viewModel::login,
26+
onWebViewLoginSuccess = viewModel::loginWithWebViewCookies,
3127
onSelectYear = viewModel::selectYear,
3228
onSelectExam = viewModel::selectExam,
3329
onReload = viewModel::reloadStructure,
34-
onLogout = viewModel::logout,
30+
onLogout = {
31+
android.webkit.CookieManager.getInstance().removeAllCookies(null)
32+
android.webkit.CookieManager.getInstance().flush()
33+
viewModel.logout()
34+
},
3535
onToggleSubject = viewModel::toggleSubjectExpanded,
3636
onDismissLoginError = viewModel::clearLoginError,
3737
onDismissGradesError = viewModel::clearGradesError,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,13 @@ class GradeRepository(
3636
sessionStore.clear()
3737
client.clearSession()
3838
}
39+
40+
suspend fun loginWithCookies(
41+
studentNo: String,
42+
cookies: Map<String, String>,
43+
): AuthenticatedSession {
44+
val session = client.loginWithCookies(studentNo, cookies)
45+
sessionStore.saveSession(session)
46+
return session
47+
}
3948
}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,27 @@ class SchoolGradeClient(
199199
cookieJar.replace(session.cookies, domain = baseUrl.host)
200200
}
201201

202+
suspend fun loginWithCookies(
203+
studentNo: String,
204+
cookies: Map<String, String>,
205+
): AuthenticatedSession = withContext(Dispatchers.IO) {
206+
cookieJar.replace(cookies, domain = baseUrl.host)
207+
val gradesPage = execute(
208+
Request.Builder()
209+
.url(gradesPageUrl())
210+
.headers(defaultHeaders(referer = loginPageUrl().toString()))
211+
.get()
212+
.build(),
213+
).body.string()
214+
val apiToken = hiddenInput(gradesPage, "__RequestVerificationToken")
215+
?: throw SchoolException("找不到成績 API token,登入狀態可能無效")
216+
AuthenticatedSession(
217+
studentNo = studentNo,
218+
apiToken = apiToken,
219+
cookies = cookieJar.snapshot(),
220+
)
221+
}
222+
202223
fun clearSession() {
203224
cookieJar.clear()
204225
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ internal fun TrendChart(
8585
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
8686
Text(point.examName, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
8787
Text(
88-
text = "加權 ${"%.1f".format(point.weightedAverage)} | 班排 ${point.classRank ?: "--"}",
88+
text = "平均 ${"%.1f".format(point.weightedAverage)} | 班排 ${point.classRank ?: "--"}",
8989
style = MaterialTheme.typography.bodyMedium.copy(fontFeatureSettings = "tnum"),
9090
color = MaterialTheme.colorScheme.onSurfaceVariant,
9191
)
@@ -124,7 +124,7 @@ internal fun ScoreSimulatorEntryCard(
124124
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
125125
) {
126126
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
127-
Text("成績模擬器", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
127+
Text("成績模擬器(beta)", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
128128
Text(
129129
text = "調整 ${report.subjects.size} 個科目分數,依歷次考試估算平均與班排。",
130130
style = MaterialTheme.typography.bodyMedium,

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

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import androidx.compose.foundation.rememberScrollState
1818
import androidx.compose.foundation.shape.RoundedCornerShape
1919
import androidx.compose.foundation.verticalScroll
2020
import androidx.compose.material.icons.Icons
21+
import androidx.compose.material.icons.automirrored.filled.ExitToApp
2122
import androidx.compose.material.icons.automirrored.filled.List
22-
import androidx.compose.material.icons.filled.Star
2323
import androidx.compose.material.icons.filled.Home
24+
import androidx.compose.material.icons.filled.Refresh
25+
import androidx.compose.material.icons.filled.Star
2426
import androidx.compose.material3.Button
2527
import androidx.compose.material3.ButtonDefaults
2628
import androidx.compose.material3.Card
@@ -30,6 +32,7 @@ import androidx.compose.material3.DropdownMenuItem
3032
import androidx.compose.material3.ExperimentalMaterial3Api
3133
import androidx.compose.material3.HorizontalDivider
3234
import androidx.compose.material3.Icon
35+
import androidx.compose.material3.IconButton
3336
import androidx.compose.material3.LinearProgressIndicator
3437
import androidx.compose.material3.MaterialTheme
3538
import androidx.compose.material3.NavigationBar
@@ -110,7 +113,7 @@ fun GradesScreen(
110113
title = {
111114
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
112115
Text(
113-
text = "成績查詢",
116+
text = "壢中成績",
114117
style = MaterialTheme.typography.titleLarge,
115118
fontWeight = FontWeight.Bold,
116119
)
@@ -124,23 +127,26 @@ fun GradesScreen(
124127
}
125128
},
126129
actions = {
127-
TextButton(
130+
IconButton(
128131
enabled = !state.isLoadingStructure && !state.isLoadingGrades,
129132
onClick = onReload,
130-
colors = ButtonDefaults.textButtonColors(
131-
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
132-
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.42f),
133-
),
134133
) {
135-
Text("重新整理")
134+
Icon(
135+
imageVector = Icons.Filled.Refresh,
136+
contentDescription = "重新整理",
137+
tint = if (!state.isLoadingStructure && !state.isLoadingGrades) {
138+
MaterialTheme.colorScheme.onSurfaceVariant
139+
} else {
140+
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.42f)
141+
}
142+
)
136143
}
137-
TextButton(
138-
onClick = onLogout,
139-
colors = ButtonDefaults.textButtonColors(
140-
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
141-
),
142-
) {
143-
Text("登出")
144+
IconButton(onClick = onLogout) {
145+
Icon(
146+
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
147+
contentDescription = "登出",
148+
tint = MaterialTheme.colorScheme.error
149+
)
144150
}
145151
},
146152
colors = TopAppBarDefaults.topAppBarColors(
@@ -232,8 +238,8 @@ private fun headerStudentText(state: GradesUiState): String {
232238
if (student != null) {
233239
return listOf(
234240
student.studentName.takeIf { it.isNotBlank() },
235-
"${student.className.ifBlank { "--" }} 座號 ${student.seatNo.ifBlank { "--" }}",
236-
"學號 ${student.studentNo.ifBlank { state.studentNo.ifBlank { "--" } }}",
241+
"${student.className.ifBlank { "--" }} ${student.seatNo.ifBlank { "--" }}",
242+
"${student.studentNo.ifBlank { state.studentNo.ifBlank { "--" } }}",
237243
).filterNotNull().joinToString("")
238244
}
239245
return state.studentNo.takeIf { it.isNotBlank() }?.let { "學號 $it" } ?: "登入後顯示學生資訊"
@@ -514,7 +520,7 @@ private fun InsightCard(
514520
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
515521
) {
516522
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
517-
Text("AI 分析摘要", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
523+
Text("分析", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
518524
DashboardInsightRow(
519525
label = "風險",
520526
text = riskInsightText(analysis, insights),
@@ -590,7 +596,7 @@ private fun StrengthWeaknessCard(analysis: GradeAnalysis) {
590596
modifier = Modifier.fillMaxWidth(),
591597
verticalArrangement = Arrangement.spacedBy(16.dp),
592598
) {
593-
Text("強弱摘要", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
599+
Text("摘要", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
594600
SubjectHighlightRow(title = "優勢科目", subjects = analysis.strengths, color = PositiveColor, emptyText = "尚無明顯高於平均的科目")
595601
SubjectHighlightRow(title = "待加強科目", subjects = analysis.weaknesses, color = NegativeColor, emptyText = "尚無明顯低於平均的科目")
596602
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.clhs.score.ui
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.material.icons.Icons
15+
import androidx.compose.material.icons.filled.Person
16+
import androidx.compose.material3.Button
17+
import androidx.compose.material3.ButtonDefaults
18+
import androidx.compose.material3.Icon
19+
import androidx.compose.material3.MaterialTheme
20+
import androidx.compose.material3.Surface
21+
import androidx.compose.material3.Text
22+
import androidx.compose.runtime.Composable
23+
import androidx.compose.ui.Alignment
24+
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.graphics.Color
26+
import androidx.compose.ui.text.font.FontWeight
27+
import androidx.compose.ui.unit.dp
28+
29+
@Composable
30+
fun IntroScreen(
31+
onLoginClick: () -> Unit,
32+
) {
33+
Surface(
34+
modifier = Modifier.fillMaxSize(),
35+
color = MaterialTheme.colorScheme.background
36+
) {
37+
Column(
38+
modifier = Modifier
39+
.fillMaxSize()
40+
.padding(32.dp),
41+
verticalArrangement = Arrangement.Center,
42+
horizontalAlignment = Alignment.CenterHorizontally
43+
) {
44+
Icon(
45+
imageVector = Icons.Filled.Person,
46+
contentDescription = null,
47+
modifier = Modifier.size(100.dp),
48+
tint = MaterialTheme.colorScheme.primary
49+
)
50+
51+
Spacer(modifier = Modifier.height(32.dp))
52+
53+
Text(
54+
text = "壢中成績查詢",
55+
style = MaterialTheme.typography.displaySmall,
56+
fontWeight = FontWeight.Bold,
57+
color = MaterialTheme.colorScheme.onBackground
58+
)
59+
60+
Spacer(modifier = Modifier.height(16.dp))
61+
62+
Text(
63+
text = "請使用學校雲端系統登入以查詢您的成績",
64+
style = MaterialTheme.typography.bodyLarge,
65+
color = MaterialTheme.colorScheme.onSurfaceVariant
66+
)
67+
68+
Spacer(modifier = Modifier.height(48.dp))
69+
70+
Button(
71+
onClick = onLoginClick,
72+
modifier = Modifier
73+
.fillMaxWidth()
74+
.height(56.dp),
75+
shape = RoundedCornerShape(16.dp),
76+
colors = ButtonDefaults.buttonColors(
77+
containerColor = MaterialTheme.colorScheme.primary
78+
)
79+
) {
80+
Text(
81+
text = "使用欣河系統登入",
82+
style = MaterialTheme.typography.titleMedium,
83+
fontWeight = FontWeight.Bold
84+
)
85+
}
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)