Skip to content

Commit fd051e4

Browse files
Fix schedule widget theme sync
1 parent 9b974c8 commit fd051e4

4 files changed

Lines changed: 35 additions & 11 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
- 從 Widget 或 `scoreapp://schedule` deep link 進入 app 時,不得繞過生物識別鎖;若存在 biometric session,`MainActivity` 必須先顯示 `BiometricLockScreen`。課表頁網路 repository 要優先使用已解鎖的 in-memory active session;存在 biometric session 時不得 fallback 到一般 `SessionStore`,避免繞過鎖或誤顯示未登入。
8585
- Widget 本體不得讀取一般 session、biometric session、cookie 或 token;只能讀 `GradeCacheStore` 的 widget 專用課表快照。課表查詢成功或從舊的學生課表快取載入成功時,要同步寫入 widget 快照;登出、學生快取清除或生物識別資料失效時要清掉該快照並刷新 widget。
8686
- `ArchitectureBoundaryTest` 會防止 Widget 重新依賴登入狀態,並檢查 PIN 解鎖必須先 activate in-memory session 再解除鎖定;修改 widget、課表或生物識別流程時要保留這些邊界。
87-
- Widget 的設定由 Android 原生的 Widget 配置活動(Configuration Activity)即 `WidgetConfigurationActivity` 進行。它支援在新增 Widget 時跳出設定,且在 `schedule_widget_info.xml` 宣告為 `widgetFeatures="reconfigurable"`,使得使用者長按 Widget 時可以重新設定。設定項目會透過 `GradeCacheStore` 持久化,並呼叫 `ScheduleWidget().updateAll(...)` 即時更新
87+
- Widget 的設定由 Android 原生的 Widget 配置活動(Configuration Activity)即 `WidgetConfigurationActivity` 進行。它支援在新增 Widget 時跳出設定,且在 `schedule_widget_info.xml` 宣告為 `widgetFeatures="reconfigurable"`,使得使用者長按 Widget 時可以重新設定。設定項目會透過 `GradeCacheStore` 持久化,並透過 `syncAllScheduleWidgets(...)` / `syncScheduleWidget(...)` 同步課表資料、顯示偏好與 theme state 到 Glance state 後即時更新
8888

8989
## Android 成績匯出
9090

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ class MainActivity : androidx.fragment.app.FragmentActivity() {
160160
}
161161

162162
LaunchedEffect(appSettings.themeMode, appSettings.dynamicColor, appSettings.amoledBlack) {
163-
com.clhs.score.widget.syncAllScheduleWidgets(applicationContext)
163+
com.clhs.score.widget.syncAllScheduleWidgets(applicationContext, appSettings)
164164
}
165165

166166
val useFakeData = BuildConfig.USE_FAKE_DATA || appSettings.demoMode

android/app/src/main/java/com/clhs/score/widget/ScheduleWidget.kt

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ val WidgetShowTeacherKey = booleanPreferencesKey("widget_show_teacher")
6262
val WidgetShowClassroomKey = booleanPreferencesKey("widget_show_classroom")
6363
val WidgetShowTimeKey = booleanPreferencesKey("widget_show_time")
6464
val WidgetScheduleReportKey = stringPreferencesKey("widget_schedule_report")
65+
val WidgetThemeModeKey = stringPreferencesKey("widget_theme_mode")
66+
val WidgetDynamicColorKey = booleanPreferencesKey("widget_dynamic_color")
67+
val WidgetAmoledBlackKey = booleanPreferencesKey("widget_amoled_black")
6568
private val WidgetJson = Json { ignoreUnknownKeys = true }
6669

6770
fun getWidgetColorProviders(context: Context, settings: AppSettings) = run {
@@ -105,32 +108,32 @@ class ScheduleWidget : GlanceAppWidget() {
105108
val prefs = cacheStore.getWidgetPreferences()
106109
val report = cacheStore.loadWidgetScheduleReport()
107110
val reportStr = report?.let { WidgetJson.encodeToString(it) }
111+
val appSettings = settingsRepository.settings.first()
108112

109113
updateAppWidgetState(context, id) { state ->
110-
state.syncScheduleWidgetState(prefs, reportStr)
114+
state.syncScheduleWidgetState(prefs, reportStr, appSettings)
111115
}
112116

113-
val appSettings = settingsRepository.settings.first()
114-
115117
provideContent {
116-
val colors = getWidgetColorProviders(context, appSettings)
118+
val colors = getWidgetColorProviders(context, currentWidgetSettings(appSettings))
117119
GlanceTheme(colors = colors) {
118120
ScheduleWidgetContent()
119121
}
120122
}
121123
}
122124
}
123125

124-
suspend fun syncAllScheduleWidgets(context: Context) {
126+
suspend fun syncAllScheduleWidgets(context: Context, settings: AppSettings? = null) {
125127
val cacheStore = GradeCacheStore(context)
126128
val prefs = cacheStore.getWidgetPreferences()
127129
val report = cacheStore.loadWidgetScheduleReport()
128130
val reportStr = report?.let { WidgetJson.encodeToString(it) }
131+
val appSettings = settings ?: SettingsRepository(context).settings.first()
129132

130133
val glanceIds = GlanceAppWidgetManager(context).getGlanceIds(ScheduleWidget::class.java)
131134
glanceIds.forEach { glanceId ->
132135
updateAppWidgetState(context, glanceId) { state ->
133-
state.syncScheduleWidgetState(prefs, reportStr)
136+
state.syncScheduleWidgetState(prefs, reportStr, appSettings)
134137
}
135138
ScheduleWidget().update(context, glanceId)
136139
}
@@ -141,28 +144,45 @@ suspend fun syncScheduleWidget(context: Context, appWidgetId: Int) {
141144
val prefs = cacheStore.getWidgetPreferences()
142145
val report = cacheStore.loadWidgetScheduleReport()
143146
val reportStr = report?.let { WidgetJson.encodeToString(it) }
147+
val appSettings = SettingsRepository(context).settings.first()
144148
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
145149

146150
updateAppWidgetState(context, glanceId) { state ->
147-
state.syncScheduleWidgetState(prefs, reportStr)
151+
state.syncScheduleWidgetState(prefs, reportStr, appSettings)
148152
}
149153
ScheduleWidget().update(context, glanceId)
150154
}
151155

152156
private fun MutablePreferences.syncScheduleWidgetState(
153157
prefs: Triple<Boolean, Boolean, Boolean>,
154-
reportStr: String?
158+
reportStr: String?,
159+
settings: AppSettings,
155160
) {
156161
this[WidgetShowTeacherKey] = prefs.first
157162
this[WidgetShowClassroomKey] = prefs.second
158163
this[WidgetShowTimeKey] = prefs.third
164+
this[WidgetThemeModeKey] = settings.themeMode.name
165+
this[WidgetDynamicColorKey] = settings.dynamicColor
166+
this[WidgetAmoledBlackKey] = settings.amoledBlack
159167
if (reportStr != null) {
160168
this[WidgetScheduleReportKey] = reportStr
161169
} else {
162170
remove(WidgetScheduleReportKey)
163171
}
164172
}
165173

174+
@Composable
175+
private fun currentWidgetSettings(fallback: AppSettings): AppSettings {
176+
val themeMode = currentState(key = WidgetThemeModeKey)
177+
?.let { runCatching { ThemeMode.valueOf(it) }.getOrNull() }
178+
?: fallback.themeMode
179+
return fallback.copy(
180+
themeMode = themeMode,
181+
dynamicColor = currentState(key = WidgetDynamicColorKey) ?: fallback.dynamicColor,
182+
amoledBlack = currentState(key = WidgetAmoledBlackKey) ?: fallback.amoledBlack,
183+
)
184+
}
185+
166186
@Composable
167187
fun ScheduleWidgetContent() {
168188
val showTeacher = currentState(key = WidgetShowTeacherKey) ?: true

android/app/src/test/java/com/clhs/score/ArchitectureBoundaryTest.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class ArchitectureBoundaryTest {
191191
val source = readSource("app/src/main/java/com/clhs/score/MainActivity.kt")
192192

193193
assertTrue(source.contains("LaunchedEffect(appSettings.themeMode, appSettings.dynamicColor, appSettings.amoledBlack)"))
194-
assertTrue(source.contains("com.clhs.score.widget.syncAllScheduleWidgets(applicationContext)"))
194+
assertTrue(source.contains("com.clhs.score.widget.syncAllScheduleWidgets(applicationContext, appSettings)"))
195195
}
196196

197197
@Test
@@ -233,6 +233,10 @@ class ArchitectureBoundaryTest {
233233
assertTrue("Widget must read preferences via GradeCacheStore initially", source.contains("cacheStore.getWidgetPreferences()"))
234234
assertTrue("Widget must sync data to Glance State to support reactive updates", source.contains("updateAppWidgetState"))
235235
assertTrue("Widget content must read preferences using currentState", source.contains("currentState(key ="))
236+
assertTrue("Widget theme mode must be part of Glance state", source.contains("WidgetThemeModeKey"))
237+
assertTrue("Widget dynamic color setting must be part of Glance state", source.contains("WidgetDynamicColorKey"))
238+
assertTrue("Widget AMOLED setting must be part of Glance state", source.contains("WidgetAmoledBlackKey"))
239+
assertTrue("Widget colors must use the current Glance theme settings", source.contains("getWidgetColorProviders(context, currentWidgetSettings(appSettings))"))
236240
}
237241

238242
@Test

0 commit comments

Comments
 (0)