Skip to content

Commit 8be8f38

Browse files
chore: release v1.1.5
1 parent 24c711c commit 8be8f38

43 files changed

Lines changed: 1774 additions & 413 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agent/skills/taste-skill

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 3c7017d636c3a4aad378433ea6d0cfa6c921da4a

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.5] - 2026-05-30
4+
5+
### Features
6+
- 新增「我的課表」頁面,提供方便且流暢的查詢課表體驗,並支援下載課表圖片。
7+
- 新增桌面課表小工具,可快速查看今日課程。
8+
9+
### Bug Fixes
10+
- 修正快速切換考試時,較慢的舊成績請求可能覆蓋新選擇的問題。
11+
12+
### Performance Improvements
13+
- 移除未使用的依賴,降低建置與封包負擔。
14+
- 優化成績與課表快取解析流程。
15+
316
## [1.1.4] - 2026-05-26
417

518
### Features

android/app/build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ android {
9595
testOptions {
9696
unitTests.isIncludeAndroidResources = true
9797
}
98+
99+
lint {
100+
disable += "ChromeOsAbiSupport"
101+
}
98102
}
99103

100104
dependencies {
@@ -107,13 +111,14 @@ dependencies {
107111
implementation("androidx.compose.foundation:foundation")
108112

109113
implementation("androidx.compose.material:material-icons-core")
110-
implementation("androidx.compose.material:material-icons-extended")
111114
implementation("androidx.compose.material3:material3")
112115
implementation("androidx.compose.runtime:runtime")
113116
implementation("androidx.compose.ui:ui")
114117
implementation("androidx.compose.ui:ui-graphics")
115118
implementation("androidx.compose.ui:ui-tooling-preview")
116119
implementation("androidx.datastore:datastore-preferences:1.2.1")
120+
implementation("androidx.glance:glance-appwidget:1.1.1")
121+
implementation("androidx.glance:glance-material3:1.1.1")
117122
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
118123
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
119124
implementation("androidx.navigation:navigation-compose:2.9.8")

android/app/src/androidTest/java/com/clhs/score/ui/ScoreUiTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class ScoreUiTest {
7373
composeRule.onNodeWithContentDescription("進階").assertIsDisplayed()
7474
composeRule.onAllNodesWithText("全部科目").assertCountEquals(0)
7575
composeRule.onAllNodesWithText("圖表").assertCountEquals(0)
76-
composeRule.onNodeWithText("展示學生", substring = true).assertIsDisplayed()
76+
composeRule.onNodeWithText("範例學生", substring = true).assertIsDisplayed()
7777
composeRule.onNodeWithText("加權平均").assertIsDisplayed()
7878
composeRule.onNodeWithText("班排 15/38(前 39%)").assertIsDisplayed()
7979
composeRule.onNodeWithText("類排 88/226(前 38%)").assertIsDisplayed()
@@ -185,7 +185,7 @@ class ScoreUiTest {
185185
GradesScreen(
186186
state = GradesUiState(
187187
isLoggedIn = true,
188-
studentNo = "110000",
188+
studentNo = "DEMO-000",
189189
structure = listOf(
190190
YearTermOption(
191191
text = "114學年度 上學期",

android/app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@
2222
<category android:name="android.intent.category.LAUNCHER" />
2323
</intent-filter>
2424
</activity>
25+
26+
<receiver
27+
android:name=".widget.ScheduleWidgetReceiver"
28+
android:exported="true">
29+
<intent-filter>
30+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
31+
</intent-filter>
32+
<meta-data
33+
android:name="android.appwidget.provider"
34+
android:resource="@xml/schedule_widget_info" />
35+
</receiver>
2536
</application>
2637

2738
</manifest>

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ data class ErrorDiagnosticContext(
5050
val gradesErrorMessage: String?,
5151
)
5252

53+
data class DiagnosticEvent(
54+
val timestamp: String,
55+
val area: String,
56+
val message: String,
57+
)
58+
5359
class DeveloperDiagnostics(private val context: Context) {
5460
private val appContext = context.applicationContext
5561

@@ -107,6 +113,16 @@ class DeveloperDiagnostics(private val context: Context) {
107113
}
108114
appendLine("- 合計: ${storage.totalBytes.toReadableSize()}")
109115
appendLine()
116+
appendLine("近期診斷事件")
117+
val events = recentEvents(appContext)
118+
if (events.isEmpty()) {
119+
appendLine("- 無")
120+
} else {
121+
events.forEach { event ->
122+
appendLine("- ${event.timestamp} [${event.area}] ${event.message}")
123+
}
124+
}
125+
appendLine()
110126
appendLine("隱私: 此診斷包不包含帳密、cookie、token、學生姓名或成績內容。")
111127
}
112128
}
@@ -156,6 +172,51 @@ class DeveloperDiagnostics(private val context: Context) {
156172
.filter { it.isFile }
157173
.sumOf { file -> runCatching { file.length() }.getOrDefault(0L) }
158174
}
175+
176+
companion object {
177+
private const val DiagnosticsPrefs = "developer_diagnostics"
178+
private const val EventsKey = "recent_events"
179+
private const val MaxEvents = 20
180+
private const val Separator = "\u001F"
181+
182+
fun recordEvent(context: Context, area: String, message: String) {
183+
val appContext = context.applicationContext
184+
val nextEvent = DiagnosticEvent(
185+
timestamp = Instant.now().toString(),
186+
area = area.sanitizeEventField(),
187+
message = message.sanitizeEventField(),
188+
)
189+
val prefs = appContext.getSharedPreferences(DiagnosticsPrefs, Context.MODE_PRIVATE)
190+
val updated = (recentEvents(appContext) + nextEvent).takeLast(MaxEvents)
191+
prefs.edit()
192+
.putString(EventsKey, updated.joinToString("\n") { it.serialize() })
193+
.apply()
194+
}
195+
196+
fun recentEvents(context: Context): List<DiagnosticEvent> {
197+
val prefs = context.applicationContext.getSharedPreferences(DiagnosticsPrefs, Context.MODE_PRIVATE)
198+
return prefs.getString(EventsKey, null)
199+
?.lineSequence()
200+
?.mapNotNull { it.deserializeEvent() }
201+
?.toList()
202+
.orEmpty()
203+
}
204+
205+
private fun DiagnosticEvent.serialize(): String =
206+
listOf(timestamp, area, message).joinToString(Separator)
207+
208+
private fun String.deserializeEvent(): DiagnosticEvent? {
209+
val parts = split(Separator)
210+
if (parts.size != 3) return null
211+
return DiagnosticEvent(parts[0], parts[1], parts[2])
212+
}
213+
214+
private fun String.sanitizeEventField(): String =
215+
replace("\n", " ")
216+
.replace("\r", " ")
217+
.replace(Separator, " ")
218+
.take(240)
219+
}
159220
}
160221

161222
fun defaultClearableLocalDataCategories(): Set<LocalDataCategory> =

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

Lines changed: 69 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ enum class StudentScenario {
77
}
88

99
object MockGradeSystem {
10+
private const val DemoStudentNo = "DEMO-000"
11+
private const val DemoStudentName = "範例學生"
12+
private const val DemoClassName = "示範班級"
13+
private const val DemoSeatNo = "00"
14+
1015
data class SubjectSpec(
1116
val name: String,
1217
val classAverage: Double,
@@ -120,10 +125,10 @@ object MockGradeSystem {
120125
return GradeReport(
121126
message = "fake mock data",
122127
studentInfo = StudentInfo(
123-
studentNo = "110000",
124-
studentName = "展示學生",
125-
className = "高二 3 班",
126-
seatNo = "18",
128+
studentNo = DemoStudentNo,
129+
studentName = DemoStudentName,
130+
className = DemoClassName,
131+
seatNo = DemoSeatNo,
127132
updatedAt = "2026-05-20 08:00",
128133
showClassRank = true,
129134
showClassRankCount = true,
@@ -162,7 +167,7 @@ object MockGradeSystem {
162167

163168
object FakeData {
164169
val session = AuthenticatedSession(
165-
studentNo = "demo-student",
170+
studentNo = "DEMO-000",
166171
apiToken = "fake-token",
167172
cookies = emptyMap(),
168173
)
@@ -247,22 +252,6 @@ class FakeGradeRepository : GradeRepository {
247252

248253
override fun restoreSession(): AuthenticatedSession? = activeSession
249254

250-
override suspend fun refreshCaptcha(): CaptchaChallenge = CaptchaChallenge(
251-
loginToken = "fake-login-token",
252-
shCaptchaGenCode = "fake-captcha",
253-
deviceToken = "fake-device",
254-
cookies = emptyMap(),
255-
imageBytes = ByteArray(0),
256-
contentType = "image/png",
257-
)
258-
259-
override suspend fun login(
260-
username: String,
261-
password: String,
262-
captchaCode: String,
263-
challenge: CaptchaChallenge,
264-
): AuthenticatedSession = FakeData.session.also { activeSession = it }
265-
266255
override suspend fun loadStructure(session: AuthenticatedSession, forceRefresh: Boolean): List<YearTermOption> = FakeData.structure
267256

268257
override suspend fun fetchGrades(
@@ -281,3 +270,62 @@ class FakeGradeRepository : GradeRepository {
281270
cookies: Map<String, String>,
282271
): AuthenticatedSession = FakeData.session.also { activeSession = it }
283272
}
273+
274+
object FakeScheduleData {
275+
val years = listOf(
276+
ScheduleYearTermOption("113學年度第1學期", "1131"),
277+
ScheduleYearTermOption("113學年度第2學期", "1132")
278+
)
279+
280+
val classes = listOf(
281+
ScheduleClassOption("高二 30 班", "230"),
282+
ScheduleClassOption("高二 31 班", "231"),
283+
)
284+
285+
fun report(yearValue: String, classNo: String): ScheduleReport {
286+
return ScheduleReport(
287+
yearTermValue = yearValue,
288+
items = listOf(
289+
ScheduleItem(dayOfWeek = 1, period = 1, subjectName = "國語文", teacherName = "張三", classroom = "高二30"),
290+
ScheduleItem(dayOfWeek = 1, period = 2, subjectName = "數學", teacherName = "李四", classroom = "高二30"),
291+
ScheduleItem(dayOfWeek = 1, period = 3, subjectName = "英語文", teacherName = "王五", classroom = "高二30"),
292+
ScheduleItem(dayOfWeek = 1, period = 4, subjectName = "體育", teacherName = "趙六", classroom = "操場"),
293+
ScheduleItem(dayOfWeek = 1, period = 5, subjectName = "物理", teacherName = "孫七", classroom = "高二30"),
294+
ScheduleItem(dayOfWeek = 1, period = 6, subjectName = "化學", teacherName = "周八", classroom = "實驗室"),
295+
ScheduleItem(dayOfWeek = 1, period = 7, subjectName = "歷史", teacherName = "吳九", classroom = "高二30"),
296+
297+
ScheduleItem(dayOfWeek = 2, period = 1, subjectName = "數學", teacherName = "李四", classroom = "高二30"),
298+
ScheduleItem(dayOfWeek = 2, period = 2, subjectName = "英語文", teacherName = "王五", classroom = "高二30"),
299+
ScheduleItem(dayOfWeek = 2, period = 3, subjectName = "地理", teacherName = "鄭十", classroom = "高二30"),
300+
ScheduleItem(dayOfWeek = 2, period = 4, subjectName = "國語文", teacherName = "張三", classroom = "高二30"),
301+
ScheduleItem(dayOfWeek = 2, period = 5, subjectName = "美術", teacherName = "林一", classroom = "美術教室"),
302+
ScheduleItem(dayOfWeek = 2, period = 6, subjectName = "美術", teacherName = "林一", classroom = "美術教室"),
303+
ScheduleItem(dayOfWeek = 2, period = 7, subjectName = "班會", teacherName = "導師", classroom = "高二30"),
304+
305+
ScheduleItem(dayOfWeek = 3, period = 1, subjectName = "英語文", teacherName = "王五", classroom = "高二30"),
306+
ScheduleItem(dayOfWeek = 3, period = 2, subjectName = "國語文", teacherName = "張三", classroom = "高二30"),
307+
ScheduleItem(dayOfWeek = 3, period = 3, subjectName = "數學", teacherName = "李四", classroom = "高二30"),
308+
ScheduleItem(dayOfWeek = 3, period = 4, subjectName = "公民", teacherName = "陳二", classroom = "高二30"),
309+
ScheduleItem(dayOfWeek = 3, period = 5, subjectName = "物理", teacherName = "孫七", classroom = "高二30"),
310+
ScheduleItem(dayOfWeek = 3, period = 6, subjectName = "音樂", teacherName = "黃三", classroom = "音樂教室"),
311+
ScheduleItem(dayOfWeek = 3, period = 7, subjectName = "體育", teacherName = "趙六", classroom = "體育館"),
312+
313+
ScheduleItem(dayOfWeek = 4, period = 1, subjectName = "化學", teacherName = "周八", classroom = "高二30"),
314+
ScheduleItem(dayOfWeek = 4, period = 2, subjectName = "歷史", teacherName = "吳九", classroom = "高二30"),
315+
ScheduleItem(dayOfWeek = 4, period = 3, subjectName = "國語文", teacherName = "張三", classroom = "高二30"),
316+
ScheduleItem(dayOfWeek = 4, period = 4, subjectName = "數學", teacherName = "李四", classroom = "高二30"),
317+
ScheduleItem(dayOfWeek = 4, period = 5, subjectName = "英語文", teacherName = "王五", classroom = "高二30"),
318+
ScheduleItem(dayOfWeek = 4, period = 6, subjectName = "地科", teacherName = "何四", classroom = "高二30"),
319+
ScheduleItem(dayOfWeek = 4, period = 7, subjectName = "社團", teacherName = "各社團指導", classroom = "各處"),
320+
321+
ScheduleItem(dayOfWeek = 5, period = 1, subjectName = "物理", teacherName = "孫七", classroom = "高二30"),
322+
ScheduleItem(dayOfWeek = 5, period = 2, subjectName = "化學", teacherName = "周八", classroom = "高二30"),
323+
ScheduleItem(dayOfWeek = 5, period = 3, subjectName = "英語文", teacherName = "王五", classroom = "高二30"),
324+
ScheduleItem(dayOfWeek = 5, period = 4, subjectName = "數學", teacherName = "李四", classroom = "高二30"),
325+
ScheduleItem(dayOfWeek = 5, period = 5, subjectName = "國語文", teacherName = "張三", classroom = "高二30"),
326+
ScheduleItem(dayOfWeek = 5, period = 6, subjectName = "地理", teacherName = "鄭十", classroom = "高二30"),
327+
ScheduleItem(dayOfWeek = 5, period = 7, subjectName = "公民", teacherName = "陳二", classroom = "高二30")
328+
)
329+
)
330+
}
331+
}

0 commit comments

Comments
 (0)