From 5fd7679aeaa18638c4d6f0956517f398ee819a00 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:41:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20granular=20Riverpod=20s?= =?UTF-8?q?elect=20in=20settings=20sub-pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 ++ .../aiSetting/ai_setting_page.dart | 26 +++++++--- .../focusSetting/focus_setting_page.dart | 43 +++++++++++----- .../notification_setting_page.dart | 40 ++++++++++----- .../planning_setting_page.dart | 34 +++++++++---- .../storageSetting/storage_setting_page.dart | 12 +++-- .../syncSetting/sync_setting_page.dart | 49 ++++++++++++++----- 7 files changed, 151 insertions(+), 56 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..5184da1 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +# Bolt Journal ⚡ + +## 2026-09-16 - Granular Riverpod `.select()` watching in Sub-Setting Pages **经验心得:** 在 Settings 界面各个子页面中,如果直接使用 `ref.watch(appSettingsProvider)` 监听整个 `AppSettings` 对象,任何设置的改变(例如修改 AI 接口地址或切换显示模式)都会触发所有已打开 Settings 子页面的重新构建(rebuild)。而通过使用 `ref.watch(appSettingsProvider.select(...))` 只监听子页面实际依赖的具体字段,能够在设置字段更新时仅重绘关心该字段的页面,避免无谓的全页重新构建。 **后续行动:** 在后续新增 Riverpod ConsumerWidget 或 Setting 页面时,优先使用 `.select()` 提取所需属性,减少全量 Model 监听引起的性能损耗。 diff --git a/lib/pages/SettingPage/subSettingPage/aiSetting/ai_setting_page.dart b/lib/pages/SettingPage/subSettingPage/aiSetting/ai_setting_page.dart index ad45027..b7d493f 100644 --- a/lib/pages/SettingPage/subSettingPage/aiSetting/ai_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/aiSetting/ai_setting_page.dart @@ -1,7 +1,6 @@ import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:janus/models/app_settings.dart'; import 'package:janus/providers/settings_provider.dart'; import 'package:janus/shared/custom_app_settings_tile.dart'; import 'package:janus/shared/custom_appbar.dart'; @@ -18,8 +17,21 @@ class AiSettingPage extends ConsumerStatefulWidget { class _AiSettingPageState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = - ref.watch(appSettingsProvider).value ?? const AppSettings(); + // Performance optimization: watch specific fields via granular .select() + // instead of watching the entire appSettingsProvider. This prevents + // unnecessary full-page rebuilds when unrelated settings change. + final aiDailySummary = ref.watch( + appSettingsProvider.select((s) => s.value?.aiDailySummary ?? false), + ); + final aiAnalyseReport = ref.watch( + appSettingsProvider.select((s) => s.value?.aiAnalyseReport ?? false), + ); + final aiTextToTask = ref.watch( + appSettingsProvider.select((s) => s.value?.aiTextToTask ?? false), + ); + final aiPicToTask = ref.watch( + appSettingsProvider.select((s) => s.value?.aiPicToTask ?? false), + ); final tt = Theme.of(context).textTheme; return GlassScaffold( @@ -103,7 +115,7 @@ class _AiSettingPageState extends ConsumerState { SettingsTile( title: Text('每日日报总结'), trailing: GlassSwitch( - value: settings.aiDailySummary, + value: aiDailySummary, onChanged: (val) { ref .read(appSettingsProvider.notifier) @@ -114,7 +126,7 @@ class _AiSettingPageState extends ConsumerState { SettingsTile( title: Text('更完善的分析报告'), trailing: GlassSwitch( - value: settings.aiAnalyseReport, + value: aiAnalyseReport, onChanged: (val) { ref .read(appSettingsProvider.notifier) @@ -126,7 +138,7 @@ class _AiSettingPageState extends ConsumerState { title: Text('智慧文本日程提取'), description: Text('指拥有更强的上下文理解能力和联想推理能力'), trailing: GlassSwitch( - value: settings.aiTextToTask, + value: aiTextToTask, onChanged: (val) { ref .read(appSettingsProvider.notifier) @@ -138,7 +150,7 @@ class _AiSettingPageState extends ConsumerState { title: Text('图片日程提取'), description: Text('需要对应模型支持图片输入和图像理解'), trailing: GlassSwitch( - value: settings.aiPicToTask, + value: aiPicToTask, onChanged: (val) { ref .read(appSettingsProvider.notifier) diff --git a/lib/pages/SettingPage/subSettingPage/focusSetting/focus_setting_page.dart b/lib/pages/SettingPage/subSettingPage/focusSetting/focus_setting_page.dart index 5912fb5..d57b7d4 100644 --- a/lib/pages/SettingPage/subSettingPage/focusSetting/focus_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/focusSetting/focus_setting_page.dart @@ -17,8 +17,30 @@ class FocusSettingPage extends ConsumerStatefulWidget { class _FocusSettingPageState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = - ref.watch(appSettingsProvider).value ?? const AppSettings(); + // Performance optimization: watch specific fields via granular .select() + // instead of watching the entire appSettingsProvider. This prevents + // unnecessary full-page rebuilds when unrelated settings change. + final tempLeaveDuration = ref.watch( + appSettingsProvider.select( + (s) => s.value?.tempLeaveDuration ?? TempLeaveDuration.tenM, + ), + ); + final tempLeaveTimes = ref.watch( + appSettingsProvider.select( + (s) => s.value?.tempLeaveTimes ?? TempLeaveTimes.twice, + ), + ); + final focusSceneRenderMode = ref.watch( + appSettingsProvider.select( + (s) => s.value?.focusSceneRenderMode ?? FocusSceneRenderMode.rive, + ), + ); + final focusSceneRenderQuality = ref.watch( + appSettingsProvider.select( + (s) => + s.value?.focusSceneRenderQuality ?? FocusSceneRenderQuality.medium, + ), + ); final tt = Theme.of(context).textTheme; return GlassScaffold( @@ -73,11 +95,11 @@ class _FocusSettingPageState extends ConsumerState { SettingsTile( title: Text('单次暂离时长'), trailing: GlassPullDownButton( - label: settings.tempLeaveDuration.label, + label: tempLeaveDuration.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: TempLeaveDuration.values.map((duration) { - final isSelected = settings.tempLeaveDuration == duration; + final isSelected = tempLeaveDuration == duration; return GlassMenuItem( title: duration.label, icon: Icon( @@ -97,11 +119,11 @@ class _FocusSettingPageState extends ConsumerState { SettingsTile( title: Text('单次专注最大暂离次数'), trailing: GlassPullDownButton( - label: settings.tempLeaveTimes.label, + label: tempLeaveTimes.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: TempLeaveTimes.values.map((times) { - final isSelected = settings.tempLeaveTimes == times; + final isSelected = tempLeaveTimes == times; return GlassMenuItem( title: times.label, icon: Icon( @@ -128,11 +150,11 @@ class _FocusSettingPageState extends ConsumerState { trailing: GlassPullDownButton( //TODO: 添加渲染引擎描述和建议 // TODO: 可以适当添加商标icon - label: settings.focusSceneRenderMode.label, + label: focusSceneRenderMode.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: FocusSceneRenderMode.values.map((mode) { - final isSelected = settings.focusSceneRenderMode == mode; + final isSelected = focusSceneRenderMode == mode; return GlassMenuItem( title: mode.label, icon: Icon( @@ -152,12 +174,11 @@ class _FocusSettingPageState extends ConsumerState { SettingsTile( title: Text('场景渲染质量'), trailing: GlassPullDownButton( - label: settings.focusSceneRenderQuality.label, + label: focusSceneRenderQuality.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: FocusSceneRenderQuality.values.map((quality) { - final isSelected = - settings.focusSceneRenderQuality == quality; + final isSelected = focusSceneRenderQuality == quality; return GlassMenuItem( title: quality.label, icon: Icon( diff --git a/lib/pages/SettingPage/subSettingPage/notificationSetting/notification_setting_page.dart b/lib/pages/SettingPage/subSettingPage/notificationSetting/notification_setting_page.dart index 679b404..ee03ff0 100644 --- a/lib/pages/SettingPage/subSettingPage/notificationSetting/notification_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/notificationSetting/notification_setting_page.dart @@ -19,8 +19,28 @@ class _NotificationSettingPageState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = - ref.watch(appSettingsProvider).value ?? const AppSettings(); + // Performance optimization: watch specific fields via granular .select() + // instead of watching the entire appSettingsProvider. This prevents + // unnecessary full-page rebuilds when unrelated settings change. + final isNotificationEnabled = ref.watch( + appSettingsProvider.select( + (s) => s.value?.isNotificationEnabled ?? false, + ), + ); + final urgentNotificationStyle = ref.watch( + appSettingsProvider.select( + (s) => + s.value?.urgentNotificationStyle ?? + UrgentNotificationStyle.notifier, + ), + ); + final approachingNotificationStyle = ref.watch( + appSettingsProvider.select( + (s) => + s.value?.approachingNotificationStyle ?? + ApproachingNotificationStyle.notifier, + ), + ); final tt = Theme.of(context).textTheme; return GlassScaffold( @@ -36,7 +56,7 @@ class _NotificationSettingPageState title: Text('是否通知'), leading: Icon(Icons.notifications_rounded), trailing: GlassSwitch( - value: settings.isNotificationEnabled, + value: isNotificationEnabled, onChanged: (status) { ref .read(appSettingsProvider.notifier) @@ -52,13 +72,12 @@ class _NotificationSettingPageState SettingsTile( title: Text('紧迫通知方式'), trailing: GlassPullDownButton( - icon: Icon(settings.urgentNotificationStyle.icon), - label: settings.urgentNotificationStyle.label, + icon: Icon(urgentNotificationStyle.icon), + label: urgentNotificationStyle.label, buttonWidth: 160, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: UrgentNotificationStyle.values.map((mode) { - final isSelected = - settings.urgentNotificationStyle == mode; + final isSelected = urgentNotificationStyle == mode; return GlassMenuItem( title: mode.label, icon: Icon( @@ -78,13 +97,12 @@ class _NotificationSettingPageState SettingsTile( title: Text('临近通知方式'), trailing: GlassPullDownButton( - icon: Icon(settings.approachingNotificationStyle.icon), - label: settings.approachingNotificationStyle.label, + icon: Icon(approachingNotificationStyle.icon), + label: approachingNotificationStyle.label, buttonWidth: 160, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: ApproachingNotificationStyle.values.map((mode) { - final isSelected = - settings.approachingNotificationStyle == mode; + final isSelected = approachingNotificationStyle == mode; return GlassMenuItem( title: mode.label, icon: Icon( diff --git a/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart b/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart index 50ab436..351a31c 100644 --- a/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart @@ -20,8 +20,25 @@ class PlanningSettingPage extends ConsumerStatefulWidget { class _PlanningSettingPageState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = - ref.watch(appSettingsProvider).value ?? const AppSettings(); + // Performance optimization: watch specific fields via granular .select() + // instead of watching the entire appSettingsProvider. This prevents + // unnecessary full-page rebuilds when unrelated settings change. + final workingDayTaskDensity = ref.watch( + appSettingsProvider.select( + (s) => + s.value?.workingDayTaskDensity ?? WorkingDayTaskDensity.medium, + ), + ); + final restDayTaskDensity = ref.watch( + appSettingsProvider.select( + (s) => s.value?.restDayTaskDensity ?? RestDayTaskDensity.loose, + ), + ); + final planningHorizon = ref.watch( + appSettingsProvider.select( + (s) => s.value?.planningHorizon ?? PlanningHorizon.weeks, + ), + ); final tt = Theme.of(context).textTheme; return GlassScaffold( topEdgeFade: false, @@ -81,12 +98,11 @@ class _PlanningSettingPageState extends ConsumerState { SettingsTile( title: Text('工作日'), trailing: GlassPullDownButton( - label: settings.workingDayTaskDensity.label, + label: workingDayTaskDensity.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: WorkingDayTaskDensity.values.map((density) { - final isSelected = - settings.workingDayTaskDensity == density; + final isSelected = workingDayTaskDensity == density; return GlassMenuItem( title: density.label, icon: Icon( @@ -106,11 +122,11 @@ class _PlanningSettingPageState extends ConsumerState { SettingsTile( title: Text('休息日'), trailing: GlassPullDownButton( - label: settings.restDayTaskDensity.label, + label: restDayTaskDensity.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: RestDayTaskDensity.values.map((density) { - final isSelected = settings.restDayTaskDensity == density; + final isSelected = restDayTaskDensity == density; return GlassMenuItem( title: density.label, icon: Icon( @@ -137,11 +153,11 @@ class _PlanningSettingPageState extends ConsumerState { title: Text('跨度'), description: Text('规划跨度越长,安排越详细,但用户需要更提前输入足够多的日程以供计算。'), trailing: GlassPullDownButton( - label: settings.planningHorizon.label, + label: planningHorizon.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle(borderRadius: 64), items: PlanningHorizon.values.map((horizon) { - final isSelected = settings.planningHorizon == horizon; + final isSelected = planningHorizon == horizon; return GlassMenuItem( title: horizon.label, icon: Icon( diff --git a/lib/pages/SettingPage/subSettingPage/storageSetting/storage_setting_page.dart b/lib/pages/SettingPage/subSettingPage/storageSetting/storage_setting_page.dart index b6da2fa..9f11993 100644 --- a/lib/pages/SettingPage/subSettingPage/storageSetting/storage_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/storageSetting/storage_setting_page.dart @@ -1,11 +1,9 @@ import 'package:card_settings_ui/card_settings_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:janus/models/app_settings.dart'; import 'package:janus/providers/settings_provider.dart'; import 'package:janus/theme/theme.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; -import 'package:liquid_glass_widgets/widgets/surfaces/glass_scaffold.dart'; import 'package:janus/shared/custom_appbar.dart'; import 'package:janus/shared/custom_app_settings_tile.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; @@ -21,8 +19,12 @@ class StorageSettingPage extends ConsumerStatefulWidget { class _StorageSettingPageState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = - ref.watch(appSettingsProvider).value ?? const AppSettings(); + // Performance optimization: watch specific fields via granular .select() + // instead of watching the entire appSettingsProvider. This prevents + // unnecessary full-page rebuilds when unrelated settings change. + final useLogToTrain = ref.watch( + appSettingsProvider.select((s) => s.value?.useLogToTrain ?? false), + ); final tt = Theme.of(context).textTheme; return GlassScaffold( topEdgeFade: false, @@ -89,7 +91,7 @@ class _StorageSettingPageState extends ConsumerState { title: Text('本地模型训练'), description: Text('是否允许用于本地小型神经网络训练?'), trailing: GlassSwitch( - value: settings.useLogToTrain, + value: useLogToTrain, onChanged: (status) { ref .read(appSettingsProvider.notifier) diff --git a/lib/pages/SettingPage/subSettingPage/syncSetting/sync_setting_page.dart b/lib/pages/SettingPage/subSettingPage/syncSetting/sync_setting_page.dart index 4f92621..e56a0ed 100644 --- a/lib/pages/SettingPage/subSettingPage/syncSetting/sync_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/syncSetting/sync_setting_page.dart @@ -36,8 +36,31 @@ class _SyncSettingPageState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = - ref.watch(appSettingsProvider).value ?? const AppSettings(); + // Performance optimization: watch specific fields via granular .select() + // instead of watching the entire appSettingsProvider. This prevents + // unnecessary full-page rebuilds when unrelated settings change. + final syncEnabled = ref.watch( + appSettingsProvider.select((s) => s.value?.syncEnabled ?? false), + ); + final syncMode = ref.watch( + appSettingsProvider.select((s) => s.value?.syncMode ?? SyncMode.auto), + ); + final syncTrigger = ref.watch( + appSettingsProvider.select( + (s) => s.value?.syncTrigger ?? SyncTrigger.onTime, + ), + ); + final syncDurationOnInterval = ref.watch( + appSettingsProvider.select( + (s) => s.value?.syncDurationOnInterval ?? const Duration(hours: 3), + ), + ); + final rsaType = ref.watch( + appSettingsProvider.select((s) => s.value?.rsaType ?? RsaType.rsa2048), + ); + final useAppLock = ref.watch( + appSettingsProvider.select((s) => s.value?.useAppLock ?? false), + ); final tt = Theme.of(context).textTheme; return GlassScaffold( topEdgeFade: false, @@ -61,7 +84,7 @@ class _SyncSettingPageState extends ConsumerState { title: Text('是否同步'), leading: Icon(Icons.upload_rounded), trailing: GlassSwitch( - value: settings.syncEnabled, + value: syncEnabled, onChanged: (val) { ref .read(appSettingsProvider.notifier) @@ -119,13 +142,13 @@ class _SyncSettingPageState extends ConsumerState { title: Text('同步模式'), trailing: Builder( builder: (ctx) => GlassPullDownButton( - label: settings.syncMode.label, + label: syncMode.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle( borderRadius: 64, ), items: SyncMode.values.map((mode) { - final isSelected = settings.syncMode == mode; + final isSelected = syncMode == mode; return GlassMenuItem( title: mode.label, icon: Icon( @@ -185,14 +208,14 @@ class _SyncSettingPageState extends ConsumerState { title: Text('触发间隔'), trailing: Builder( builder: (ctx) => GlassPullDownButton( - icon: Icon(settings.syncTrigger.icon), - label: settings.syncTrigger.label, + icon: Icon(syncTrigger.icon), + label: syncTrigger.label, buttonWidth: 120, buttonShape: const LiquidRoundedRectangle( borderRadius: 64, ), items: SyncTrigger.values.map((trigger) { - final isSelected = settings.syncTrigger == trigger; + final isSelected = syncTrigger == trigger; return GlassMenuItem( title: trigger.label, icon: Icon( @@ -210,7 +233,7 @@ class _SyncSettingPageState extends ConsumerState { ), ), ), - switch (settings.syncTrigger) { + switch (syncTrigger) { SyncTrigger.onChanged => InteractiveTile( leading: Icon(Icons.save_rounded), title: Text('自动保存'), @@ -249,7 +272,7 @@ class _SyncSettingPageState extends ConsumerState { DateTime.now() .day, //INFO: Wasted data, won't be stored into the [DataBase] or [SharedPreference]. Only to meet the need of a [DateTime] type data. //WARNING: Unsure whether it will cause vulnerability. - ).add(settings.syncDurationOnInterval), + ).add(syncDurationOnInterval), ), onCompleted: (timeOfDay) { if (timeOfDay == null) return; @@ -301,13 +324,13 @@ class _SyncSettingPageState extends ConsumerState { trailing: Builder( builder: (ctx) => GlassPullDownButton( icon: null, - label: settings.rsaType.label, + label: rsaType.label, buttonWidth: 170, buttonShape: const LiquidRoundedRectangle( borderRadius: 64, ), items: RsaType.values.map((type) { - final isSelected = settings.rsaType == type; + final isSelected = rsaType == type; return GlassMenuItem( title: type.label, icon: Icon( @@ -351,7 +374,7 @@ class _SyncSettingPageState extends ConsumerState { InteractiveTile( title: Text('应用锁'), trailing: GlassSwitch( - value: settings.useAppLock, + value: useAppLock, onChanged: (status) { ref .read(appSettingsProvider.notifier) From 9b85c2638e0cdc262b42779cb83d99ae37f2ab79 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:48:57 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20granular=20Riverpod=20s?= =?UTF-8?q?elect=20in=20settings=20sub-pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subSettingPage/planningSetting/planning_setting_page.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart b/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart index 351a31c..b44ee90 100644 --- a/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart +++ b/lib/pages/SettingPage/subSettingPage/planningSetting/planning_setting_page.dart @@ -25,8 +25,7 @@ class _PlanningSettingPageState extends ConsumerState { // unnecessary full-page rebuilds when unrelated settings change. final workingDayTaskDensity = ref.watch( appSettingsProvider.select( - (s) => - s.value?.workingDayTaskDensity ?? WorkingDayTaskDensity.medium, + (s) => s.value?.workingDayTaskDensity ?? WorkingDayTaskDensity.medium, ), ); final restDayTaskDensity = ref.watch(