refactor: share settings page section shell
This commit is contained in:
parent
8539eab3b7
commit
f2ba2acbba
@ -0,0 +1,121 @@
|
||||
# Settings Section Shell Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Extract a shared Settings page shell so Desktop and Web reuse the same top-level layout, ordered section composition, and global apply bar behavior without changing the three-column workspace skeleton.
|
||||
|
||||
**Architecture:** Introduce a small shared shell widget layer under `lib/widgets/` that owns the common settings-page frame and ordered section assembly. Keep Desktop and Web-specific controllers, gateway sub-tabs, and section content builders in their existing feature modules.
|
||||
|
||||
**Tech Stack:** Flutter, Dart, existing `TopBar`/`SurfaceCard` widgets, Desktop/Web app controllers.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add a shared settings page shell module
|
||||
|
||||
**Files:**
|
||||
- Create: `lib/widgets/settings_page_shell.dart`
|
||||
|
||||
**Step 1: Add a shared apply-bar widget**
|
||||
|
||||
- Implement a widget that renders:
|
||||
- title `设置提交流程 / Settings Submission`
|
||||
- resolved draft/apply status copy
|
||||
- `settings-global-apply-button`
|
||||
- Pass behavior through parameters instead of depending on a specific controller type.
|
||||
|
||||
**Step 2: Add a shared page body shell**
|
||||
|
||||
- Implement a widget that renders:
|
||||
- `TopBar`
|
||||
- optional shared apply bar
|
||||
- page body children
|
||||
- Keep paddings configurable so Desktop and Web can preserve their current spacing.
|
||||
|
||||
**Step 3: Add a shared ordered-section helper**
|
||||
|
||||
- Implement a helper that:
|
||||
- takes `availableTabs`, `currentTab`
|
||||
- asks a callback for each tab’s content
|
||||
- interleaves `SizedBox(height: 24)` between non-empty sections
|
||||
|
||||
### Task 2: Move Desktop Settings page onto the shared shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/features/settings/settings_page_core.dart`
|
||||
- Modify: `lib/features/settings/settings_page_sections.dart`
|
||||
|
||||
**Step 1: Replace duplicated page frame markup**
|
||||
|
||||
- Swap the current top-level `SingleChildScrollView -> Column -> TopBar -> apply bar -> body` block to the shared shell widget.
|
||||
|
||||
**Step 2: Replace duplicated ordered overview assembly**
|
||||
|
||||
- Route overview ordering through the shared helper instead of local inline assembly.
|
||||
|
||||
**Step 3: Keep detail-mode behavior unchanged**
|
||||
|
||||
- Ensure the existing desktop-only detail flow still owns:
|
||||
- breadcrumbs
|
||||
- back button
|
||||
- detail intro cards
|
||||
- gateway navigation hints
|
||||
|
||||
### Task 3: Move Web Settings page onto the shared shell
|
||||
|
||||
**Files:**
|
||||
- Modify: `lib/web/web_settings_page_core.dart`
|
||||
- Modify: `lib/web/web_settings_page_sections.dart`
|
||||
|
||||
**Step 1: Reuse the shared page frame**
|
||||
|
||||
- Keep `DesktopWorkspaceScaffold` in Web.
|
||||
- Replace the inner duplicated page body with the shared shell widget.
|
||||
|
||||
**Step 2: Reuse the ordered-section helper where still needed**
|
||||
|
||||
- Keep Web tab availability constraints intact.
|
||||
- Use the same helper for any ordered overview logic that remains.
|
||||
|
||||
**Step 3: Preserve Web-specific gateway behavior**
|
||||
|
||||
- Keep:
|
||||
- web search field key
|
||||
- browser persistence copy
|
||||
- ACP-specific apply-bar gating
|
||||
|
||||
### Task 4: Verify the refactor
|
||||
|
||||
**Files:**
|
||||
- Modify as needed: `test/features/settings_page_suite.dart`
|
||||
- Modify as needed: `test/web/web_ui_browser_test.dart`
|
||||
|
||||
**Step 1: Run static analysis**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
flutter analyze lib/widgets/settings_page_shell.dart lib/features/settings/settings_page_core.dart lib/features/settings/settings_page_sections.dart lib/web/web_settings_page_core.dart lib/web/web_settings_page_sections.dart test/features/settings_page_suite.dart test/web/web_ui_browser_test.dart
|
||||
```
|
||||
|
||||
**Step 2: Run targeted Desktop settings tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
flutter test test/features/settings_page_suite.dart
|
||||
```
|
||||
|
||||
**Step 3: Run targeted Web browser regression**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
flutter test --platform chrome test/web/web_ui_browser_test.dart
|
||||
```
|
||||
|
||||
**Step 4: Record residual risk**
|
||||
|
||||
- Note whether any remaining Desktop/Web duplication still lives in:
|
||||
- gateway sub-tab section builders
|
||||
- detail-only desktop flows
|
||||
- web-only persistence cards
|
||||
@ -14,6 +14,7 @@ import '../../runtime/runtime_controllers.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
import 'codex_integration_card.dart';
|
||||
import 'skill_directory_authorization_card.dart';
|
||||
import '../../widgets/settings_page_shell.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
import 'settings_page_sections.dart';
|
||||
@ -218,62 +219,53 @@ class SettingsPageStateInternal extends State<SettingsPage> {
|
||||
(tabInternal != SettingsTab.gateway ||
|
||||
integrationSubTabInternal ==
|
||||
GatewayIntegrationSubTabInternal.acp);
|
||||
return SingleChildScrollView(
|
||||
return SettingsPageBodyShell(
|
||||
padding: const EdgeInsets.fromLTRB(32, 32, 32, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: buildSettingsBreadcrumbs(
|
||||
controller,
|
||||
tab: tabInternal,
|
||||
detail: detailInternal,
|
||||
navigationContext: navigationContextInternal,
|
||||
breadcrumbs: buildSettingsBreadcrumbs(
|
||||
controller,
|
||||
tab: tabInternal,
|
||||
detail: detailInternal,
|
||||
navigationContext: navigationContextInternal,
|
||||
),
|
||||
title: appText('设置', 'Settings'),
|
||||
subtitle: showingDetail
|
||||
? appText(
|
||||
'当前正在编辑详细设置参数,保存后会回写到对应状态页。',
|
||||
'You are editing detailed settings. Saved values flow back to the related status page.',
|
||||
)
|
||||
: appText(
|
||||
'配置 $kProductBrandName 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for $kProductBrandName.',
|
||||
),
|
||||
title: appText('设置', 'Settings'),
|
||||
subtitle: showingDetail
|
||||
? appText(
|
||||
'当前正在编辑详细设置参数,保存后会回写到对应状态页。',
|
||||
'You are editing detailed settings. Saved values flow back to the related status page.',
|
||||
)
|
||||
: appText(
|
||||
'配置 $kProductBrandName 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for $kProductBrandName.',
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: showingDetail ? 168 : 220,
|
||||
child: showingDetail
|
||||
? OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
controller.closeSettingsDetail();
|
||||
setState(() {
|
||||
detailInternal = null;
|
||||
navigationContextInternal = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
label: Text(appText('返回概览', 'Back to overview')),
|
||||
)
|
||||
: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (showGlobalApplyBar) ...[
|
||||
buildGlobalApplyBarInternal(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
...buildContentForCurrentStateInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
],
|
||||
trailing: SizedBox(
|
||||
width: showingDetail ? 168 : 220,
|
||||
child: showingDetail
|
||||
? OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
controller.closeSettingsDetail();
|
||||
setState(() {
|
||||
detailInternal = null;
|
||||
navigationContextInternal = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back_rounded),
|
||||
label: Text(appText('返回概览', 'Back to overview')),
|
||||
)
|
||||
: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
globalApplyBar: showGlobalApplyBar
|
||||
? buildGlobalApplyBarInternal(context, controller)
|
||||
: null,
|
||||
bodyChildren: buildContentForCurrentStateInternal(
|
||||
context,
|
||||
controller,
|
||||
settings,
|
||||
uiFeatures,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@ -15,6 +15,7 @@ import '../../runtime/runtime_models.dart';
|
||||
import 'codex_integration_card.dart';
|
||||
import 'skill_directory_authorization_card.dart';
|
||||
import '../../widgets/section_tabs.dart';
|
||||
import '../../widgets/settings_page_shell.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../widgets/top_bar.dart';
|
||||
import 'settings_page_core.dart';
|
||||
@ -46,10 +47,10 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
SettingsSnapshot settings,
|
||||
UiFeatureAccess uiFeatures,
|
||||
) {
|
||||
final orderedTabs = orderedOverviewTabsInternal(controller, uiFeatures);
|
||||
final sections = <Widget>[];
|
||||
for (final tab in orderedTabs) {
|
||||
final content = switch (tab) {
|
||||
return buildOrderedSettingsSections(
|
||||
availableTabs: uiFeatures.availableSettingsTabs,
|
||||
currentTab: uiFeatures.sanitizeSettingsTab(controller.settingsTab),
|
||||
buildTabContent: (tab) => switch (tab) {
|
||||
SettingsTab.general => buildGeneralInternal(
|
||||
context,
|
||||
controller,
|
||||
@ -77,16 +78,8 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
uiFeatures,
|
||||
),
|
||||
SettingsTab.about => buildAboutInternal(context, controller),
|
||||
};
|
||||
if (content.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (sections.isNotEmpty) {
|
||||
sections.add(const SizedBox(height: 24));
|
||||
}
|
||||
sections.addAll(content);
|
||||
}
|
||||
return sections;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> buildContentForCurrentStateInternal(
|
||||
@ -246,63 +239,31 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal {
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final hasDraft = controller.hasSettingsDraftChanges;
|
||||
final hasPendingApply = controller.hasPendingSettingsApply;
|
||||
final message = controller.settingsDraftStatusMessage;
|
||||
return SurfaceCard(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('设置提交流程', 'Settings Submission'),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
message.isNotEmpty
|
||||
? message
|
||||
: hasDraft
|
||||
? appText(
|
||||
'当前存在未保存草稿。保存并生效:按当前配置立即更新。',
|
||||
'There are unsaved drafts. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: hasPendingApply
|
||||
? appText(
|
||||
'当前存在待生效更改。保存并生效:立即按当前配置更新。',
|
||||
'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: (message.isEmpty
|
||||
? appText(
|
||||
'当前没有待提交更改。',
|
||||
'There are no pending settings changes.',
|
||||
)
|
||||
: message),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
return SettingsGlobalApplyCard(
|
||||
title: appText('设置提交流程', 'Settings Submission'),
|
||||
message: message.isNotEmpty
|
||||
? message
|
||||
: hasDraft
|
||||
? appText(
|
||||
'当前存在未保存草稿。保存并生效:按当前配置立即更新。',
|
||||
'There are unsaved drafts. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: hasPendingApply
|
||||
? appText(
|
||||
'当前存在待生效更改。保存并生效:立即按当前配置更新。',
|
||||
'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: appText(
|
||||
'当前没有待提交更改。',
|
||||
'There are no pending settings changes.',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('settings-global-apply-button'),
|
||||
onPressed: (!hasDraft && !hasPendingApply)
|
||||
? null
|
||||
: () => handleTopLevelApplyInternal(controller),
|
||||
child: Text(appText('保存并生效', 'Save & apply')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
applyLabel: appText('保存并生效', 'Save & apply'),
|
||||
onApply: (!hasDraft && !hasPendingApply)
|
||||
? null
|
||||
: () => handleTopLevelApplyInternal(controller),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import '../models/app_models.dart';
|
||||
import '../runtime/runtime_models.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import '../widgets/desktop_workspace_scaffold.dart';
|
||||
import '../widgets/settings_page_shell.dart';
|
||||
import '../widgets/surface_card.dart';
|
||||
import '../widgets/top_bar.dart';
|
||||
import 'web_settings_page_sections.dart';
|
||||
@ -249,52 +250,43 @@ class WebSettingsPageStateInternal extends State<WebSettingsPage> {
|
||||
currentTab != SettingsTab.gateway ||
|
||||
gatewaySubTabInternal == WebGatewaySettingsSubTabInternal.acp;
|
||||
return DesktopWorkspaceScaffold(
|
||||
child: SingleChildScrollView(
|
||||
child: SettingsPageBodyShell(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: <AppBreadcrumbItem>[
|
||||
AppBreadcrumbItem(
|
||||
label: appText('主页', 'Home'),
|
||||
icon: Icons.home_rounded,
|
||||
onTap: controller.navigateHome,
|
||||
),
|
||||
AppBreadcrumbItem(
|
||||
label: appText('设置', 'Settings'),
|
||||
onTap: () => controller.openSettings(tab: currentTab),
|
||||
),
|
||||
AppBreadcrumbItem(label: currentTab.label),
|
||||
],
|
||||
title: appText('设置', 'Settings'),
|
||||
subtitle: appText(
|
||||
'配置 XWorkmate Web 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for XWorkmate Web.',
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 260,
|
||||
child: TextField(
|
||||
key: const ValueKey('web-settings-search-field'),
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
),
|
||||
),
|
||||
),
|
||||
breadcrumbs: <AppBreadcrumbItem>[
|
||||
AppBreadcrumbItem(
|
||||
label: appText('主页', 'Home'),
|
||||
icon: Icons.home_rounded,
|
||||
onTap: controller.navigateHome,
|
||||
),
|
||||
AppBreadcrumbItem(
|
||||
label: appText('设置', 'Settings'),
|
||||
onTap: () => controller.openSettings(tab: currentTab),
|
||||
),
|
||||
AppBreadcrumbItem(label: currentTab.label),
|
||||
],
|
||||
title: appText('设置', 'Settings'),
|
||||
subtitle: appText(
|
||||
'配置 XWorkmate Web 工作区、网关默认项、界面与诊断选项',
|
||||
'Configure workspace, gateway defaults, appearance, and diagnostics for XWorkmate Web.',
|
||||
),
|
||||
trailing: SizedBox(
|
||||
width: 260,
|
||||
child: TextField(
|
||||
key: const ValueKey('web-settings-search-field'),
|
||||
decoration: InputDecoration(
|
||||
hintText: appText('搜索设置', 'Search settings'),
|
||||
prefixIcon: const Icon(Icons.search_rounded),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (showGlobalApplyBar) ...[
|
||||
buildGlobalApplyBarInternal(context, controller),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
...buildTabContentInternal(
|
||||
context,
|
||||
controller,
|
||||
controller.settingsDraft,
|
||||
currentTab,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
globalApplyBar: showGlobalApplyBar
|
||||
? buildGlobalApplyBarInternal(context, controller)
|
||||
: null,
|
||||
bodyChildren: buildTabContentInternal(
|
||||
context,
|
||||
controller,
|
||||
controller.settingsDraft,
|
||||
currentTab,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -11,6 +11,7 @@ import '../runtime/runtime_models.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import '../widgets/desktop_workspace_scaffold.dart';
|
||||
import '../widgets/section_tabs.dart';
|
||||
import '../widgets/settings_page_shell.dart';
|
||||
import '../widgets/surface_card.dart';
|
||||
import '../widgets/top_bar.dart';
|
||||
import 'web_settings_page_core.dart';
|
||||
@ -39,86 +40,45 @@ extension WebSettingsPageSectionsMixinInternal on WebSettingsPageStateInternal {
|
||||
List<SettingsTab> availableTabs,
|
||||
SettingsTab currentTab,
|
||||
) {
|
||||
final orderedTabs = <SettingsTab>[
|
||||
currentTab,
|
||||
...availableTabs.where((item) => item != currentTab),
|
||||
];
|
||||
final sections = <Widget>[];
|
||||
for (final tab in orderedTabs) {
|
||||
final content = buildTabContentInternal(context, controller, settings, tab);
|
||||
if (content.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (sections.isNotEmpty) {
|
||||
sections.add(const SizedBox(height: 24));
|
||||
}
|
||||
sections.addAll(content);
|
||||
}
|
||||
return sections;
|
||||
return buildOrderedSettingsSections(
|
||||
availableTabs: availableTabs,
|
||||
currentTab: currentTab,
|
||||
buildTabContent: (tab) =>
|
||||
buildTabContentInternal(context, controller, settings, tab),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildGlobalApplyBarInternal(
|
||||
BuildContext context,
|
||||
AppController controller,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
final hasDraft = controller.hasSettingsDraftChanges;
|
||||
final hasPendingApply = controller.hasPendingSettingsApply;
|
||||
final message = controller.settingsDraftStatusMessage;
|
||||
return SurfaceCard(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('设置提交流程', 'Settings Submission'),
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
message.isNotEmpty
|
||||
? message
|
||||
: hasDraft
|
||||
? appText(
|
||||
'当前存在未保存草稿。保存并生效:按当前配置立即更新。',
|
||||
'There are unsaved drafts. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: hasPendingApply
|
||||
? appText(
|
||||
'当前存在待生效更改。保存并生效:立即按当前配置更新。',
|
||||
'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: appText(
|
||||
'当前没有待提交更改。',
|
||||
'There are no pending settings changes.',
|
||||
),
|
||||
),
|
||||
],
|
||||
return SettingsGlobalApplyCard(
|
||||
title: appText('设置提交流程', 'Settings Submission'),
|
||||
message: message.isNotEmpty
|
||||
? message
|
||||
: hasDraft
|
||||
? appText(
|
||||
'当前存在未保存草稿。保存并生效:按当前配置立即更新。',
|
||||
'There are unsaved drafts. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: hasPendingApply
|
||||
? appText(
|
||||
'当前存在待生效更改。保存并生效:立即按当前配置更新。',
|
||||
'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.',
|
||||
)
|
||||
: appText(
|
||||
'当前没有待提交更改。',
|
||||
'There are no pending settings changes.',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('settings-global-apply-button'),
|
||||
onPressed:
|
||||
(hasDraft ||
|
||||
hasPendingApply ||
|
||||
gatewaySubTabInternal ==
|
||||
WebGatewaySettingsSubTabInternal.acp)
|
||||
? () => handleTopLevelApplyInternal(controller)
|
||||
: null,
|
||||
child: Text(appText('保存并生效', 'Save & apply')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
applyLabel: appText('保存并生效', 'Save & apply'),
|
||||
onApply: (hasDraft ||
|
||||
hasPendingApply ||
|
||||
gatewaySubTabInternal == WebGatewaySettingsSubTabInternal.acp)
|
||||
? () => handleTopLevelApplyInternal(controller)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
123
lib/widgets/settings_page_shell.dart
Normal file
123
lib/widgets/settings_page_shell.dart
Normal file
@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/app_models.dart';
|
||||
import 'surface_card.dart';
|
||||
import 'top_bar.dart';
|
||||
|
||||
List<Widget> buildOrderedSettingsSections({
|
||||
required List<SettingsTab> availableTabs,
|
||||
required SettingsTab currentTab,
|
||||
required List<Widget> Function(SettingsTab tab) buildTabContent,
|
||||
double gap = 24,
|
||||
}) {
|
||||
final orderedTabs = <SettingsTab>[
|
||||
currentTab,
|
||||
...availableTabs.where((item) => item != currentTab),
|
||||
];
|
||||
final sections = <Widget>[];
|
||||
for (final tab in orderedTabs) {
|
||||
final content = buildTabContent(tab);
|
||||
if (content.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (sections.isNotEmpty) {
|
||||
sections.add(SizedBox(height: gap));
|
||||
}
|
||||
sections.addAll(content);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
class SettingsGlobalApplyCard extends StatelessWidget {
|
||||
const SettingsGlobalApplyCard({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.onApply,
|
||||
this.applyLabel = 'Save & apply',
|
||||
this.title = 'Settings Submission',
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String message;
|
||||
final String applyLabel;
|
||||
final VoidCallback? onApply;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return SurfaceCard(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: theme.textTheme.titleMedium),
|
||||
const SizedBox(height: 6),
|
||||
Text(message, style: theme.textTheme.bodyMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
FilledButton.tonal(
|
||||
key: const ValueKey('settings-global-apply-button'),
|
||||
onPressed: onApply,
|
||||
child: Text(applyLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsPageBodyShell extends StatelessWidget {
|
||||
const SettingsPageBodyShell({
|
||||
super.key,
|
||||
required this.padding,
|
||||
required this.breadcrumbs,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.trailing,
|
||||
required this.bodyChildren,
|
||||
this.globalApplyBar,
|
||||
});
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
final List<AppBreadcrumbItem> breadcrumbs;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget trailing;
|
||||
final Widget? globalApplyBar;
|
||||
final List<Widget> bodyChildren;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: breadcrumbs,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (globalApplyBar != null) ...[
|
||||
globalApplyBar!,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
...bodyChildren,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user