Skip to content

Commit 4e7c06e

Browse files
committed
feat: add customized settings widget on AppBar #27
This change adds a settingsWidget parameter to SolidScaffold, allowing developers to define their own settings widget (e.g., an IconButton) which is then displayed in the AppBar next to the About button. It also integrates this widget into the Navigation Rail (trailing) and the Navigation Drawer. Closes #27
1 parent fbe7302 commit 4e7c06e

13 files changed

Lines changed: 325 additions & 34 deletions

example/lib/app_scaffold.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,29 @@ class AppScaffold extends StatelessWidget {
181181

182182
onLogout: (context) => SolidAuthHandler.instance.handleLogout(context),
183183

184+
// SETTINGS.
185+
186+
settingsWidget: IconButton(
187+
icon: const Icon(Icons.settings),
188+
onPressed: () => _scaffoldController.navigateToSubpage(
189+
const _MySettings(),
190+
),
191+
tooltip: 'Settings',
192+
),
193+
184194
child: const Home(title: appTitle),
185195
);
186196
}
187197
}
198+
199+
class _MySettings extends StatelessWidget {
200+
const _MySettings();
201+
202+
@override
203+
Widget build(BuildContext context) {
204+
return Scaffold(
205+
appBar: AppBar(title: const Text('Custom Settings')),
206+
body: const Center(child: Text('This is a custom settings page.')),
207+
);
208+
}
209+
}

example/lib/home.dart

Lines changed: 210 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
2929
library;
3030

31+
import 'dart:convert';
3132
import 'package:flutter/material.dart';
33+
import 'package:solidpod/solidpod.dart';
34+
import 'package:solidui/solidui.dart';
3235

3336
class Home extends StatefulWidget {
3437
const Home({super.key, required this.title});
@@ -40,44 +43,220 @@ class Home extends StatefulWidget {
4043
}
4144

4245
class _HomeState extends State<Home> {
46+
final TextEditingController _nameController = TextEditingController();
47+
final TextEditingController _commentController = TextEditingController();
48+
bool _isLoading = false;
49+
50+
@override
51+
void initState() {
52+
super.initState();
53+
_loadData();
54+
}
55+
56+
@override
57+
void dispose() {
58+
_nameController.dispose();
59+
_commentController.dispose();
60+
super.dispose();
61+
}
62+
63+
Future<void> _loadData() async {
64+
if (!mounted) return;
65+
setState(() => _isLoading = true);
66+
67+
try {
68+
final appDataPath = await getDataDirPath();
69+
final filePath = PathUtils.combine(appDataPath, 'user_info.enc.ttl');
70+
71+
final content = await readPod(filePath, pathType: PathType.relativeToPod);
72+
73+
if (content != SolidFunctionCallStatus.fail.toString() &&
74+
content != SolidFunctionCallStatus.notLoggedIn.toString() &&
75+
content.isNotEmpty) {
76+
final data = jsonDecode(content);
77+
_nameController.text = data['name'] ?? '';
78+
_commentController.text = data['comment'] ?? '';
79+
}
80+
} catch (e) {
81+
debugPrint('Error loading data: $e');
82+
} finally {
83+
if (mounted) setState(() => _isLoading = false);
84+
}
85+
}
86+
87+
Future<void> _saveData() async {
88+
if (!mounted) return;
89+
setState(() => _isLoading = true);
90+
91+
try {
92+
// Ensure the security key is available before writing encrypted data.
93+
await getKeyFromUserIfRequired(
94+
context,
95+
const Text('Please enter your security key to save the data'),
96+
);
97+
98+
if (!mounted) return;
99+
100+
final data = {
101+
'name': _nameController.text,
102+
'comment': _commentController.text,
103+
'lastUpdated': DateTime.now().toIso8601String(),
104+
};
105+
106+
final appDataPath = await getDataDirPath();
107+
final filePath = PathUtils.combine(appDataPath, 'user_info.enc.ttl');
108+
109+
await writePod(
110+
filePath,
111+
jsonEncode(data),
112+
encrypted: true,
113+
pathType: PathType.relativeToPod,
114+
);
115+
116+
if (mounted) {
117+
ScaffoldMessenger.of(context).showSnackBar(
118+
const SnackBar(
119+
content: Text('Data saved to POD successfully!'),
120+
backgroundColor: Colors.green,
121+
),
122+
);
123+
}
124+
} catch (e) {
125+
if (mounted) {
126+
ScaffoldMessenger.of(context).showSnackBar(
127+
SnackBar(
128+
content: Text('Error saving data: $e'),
129+
backgroundColor: Colors.red,
130+
),
131+
);
132+
}
133+
} finally {
134+
if (mounted) setState(() => _isLoading = false);
135+
}
136+
}
137+
43138
@override
44139
Widget build(BuildContext context) {
45140
return SingleChildScrollView(
46141
padding: const EdgeInsets.all(24.0),
47142
child: Center(
48-
child: Card(
49-
child: Padding(
50-
padding: const EdgeInsets.all(32.0),
51-
child: Column(
52-
mainAxisSize: MainAxisSize.min,
53-
crossAxisAlignment: CrossAxisAlignment.start,
54-
children: [
55-
Icon(
56-
Icons.home,
57-
size: 64,
58-
color: Theme.of(context).primaryColor,
59-
),
60-
const SizedBox(height: 16),
61-
Text(
62-
widget.title,
63-
style: Theme.of(context).textTheme.headlineMedium,
143+
child: ConstrainedBox(
144+
constraints: const BoxConstraints(maxWidth: 800),
145+
child: Column(
146+
children: [
147+
Card(
148+
child: Padding(
149+
padding: const EdgeInsets.all(32.0),
150+
child: Column(
151+
mainAxisSize: MainAxisSize.min,
152+
crossAxisAlignment: CrossAxisAlignment.start,
153+
children: [
154+
Row(
155+
children: [
156+
Icon(
157+
Icons.home,
158+
size: 48,
159+
color: Theme.of(context).primaryColor,
160+
),
161+
const SizedBox(width: 16),
162+
Expanded(
163+
child: Text(
164+
widget.title,
165+
style: Theme.of(context).textTheme.headlineSmall,
166+
),
167+
),
168+
],
169+
),
170+
const SizedBox(height: 24),
171+
const Text(
172+
'POD Data Storage Example',
173+
style: TextStyle(
174+
fontWeight: FontWeight.bold,
175+
fontSize: 18,
176+
),
177+
),
178+
const SizedBox(height: 8),
179+
const Text(
180+
'This data is stored securely and encrypted on your POD.',
181+
style: TextStyle(color: Colors.grey),
182+
),
183+
const SizedBox(height: 24),
184+
TextFormField(
185+
controller: _nameController,
186+
decoration: const InputDecoration(
187+
labelText: 'NAME',
188+
hintText: 'Enter your name',
189+
border: OutlineInputBorder(),
190+
prefixIcon: Icon(Icons.person),
191+
),
192+
enabled: !_isLoading,
193+
),
194+
const SizedBox(height: 16),
195+
TextFormField(
196+
controller: _commentController,
197+
decoration: const InputDecoration(
198+
labelText: 'COMMENT',
199+
hintText: 'Enter a comment',
200+
border: OutlineInputBorder(),
201+
prefixIcon: Icon(Icons.comment),
202+
),
203+
maxLines: 3,
204+
enabled: !_isLoading,
205+
),
206+
const SizedBox(height: 24),
207+
SizedBox(
208+
width: double.infinity,
209+
child: ElevatedButton.icon(
210+
onPressed: _isLoading ? null : _saveData,
211+
icon: _isLoading
212+
? const SizedBox(
213+
width: 20,
214+
height: 20,
215+
child: CircularProgressIndicator(
216+
strokeWidth: 2,
217+
),
218+
)
219+
: const Icon(Icons.save),
220+
label: const Text('Save to POD'),
221+
style: ElevatedButton.styleFrom(
222+
padding: const EdgeInsets.symmetric(vertical: 16),
223+
),
224+
),
225+
),
226+
],
227+
),
64228
),
65-
const SizedBox(height: 24),
66-
Text(
67-
'Welcome to the SolidUI Template App!\n\n'
68-
'This template demonstrates the key features of SolidUI:\n\n'
69-
'• Responsive navigation (rail ↔ drawer)\n'
70-
'• Theme switching (light/dark/system)\n'
71-
'• Customisable About dialogues\n'
72-
'• Version information display\n'
73-
'• Security key management\n'
74-
'• Status bar integration\n'
75-
'• User information display\n\n'
76-
'Explore the different tabs to see these features in action!',
77-
style: Theme.of(context).textTheme.bodyLarge,
229+
),
230+
const SizedBox(height: 24),
231+
Card(
232+
child: Padding(
233+
padding: const EdgeInsets.all(32.0),
234+
child: Column(
235+
crossAxisAlignment: CrossAxisAlignment.start,
236+
children: [
237+
Text(
238+
'About SolidUI',
239+
style: Theme.of(context).textTheme.titleLarge,
240+
),
241+
const SizedBox(height: 16),
242+
Text(
243+
'Welcome to the SolidUI Template App!\n\n'
244+
'This template demonstrates the key features of SolidUI:\n\n'
245+
'• Responsive navigation (rail ↔ drawer)\n'
246+
'• Theme switching (light/dark/system)\n'
247+
'• Customisable About dialogues\n'
248+
'• Version information display\n'
249+
'• Security key management\n'
250+
'• Status bar integration\n'
251+
'• User information display\n\n'
252+
'Explore the different tabs to see these features in action!',
253+
style: Theme.of(context).textTheme.bodyLarge,
254+
),
255+
],
256+
),
78257
),
79-
],
80-
),
258+
),
259+
],
81260
),
82261
),
83262
),

lib/solidui.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export 'src/utils/solid_file_operations.dart';
102102
export 'src/utils/is_phone.dart';
103103
export 'src/utils/solid_alert.dart';
104104
export 'src/utils/solid_notifications.dart';
105+
export 'src/utils/path_utils.dart';
105106
export 'src/utils/solid_pod_helpers.dart'
106107
show loginIfRequired, getKeyFromUserIfRequired;
107108

lib/src/widgets/solid_nav_bar.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ class SolidNavBar extends StatelessWidget {
7575
7676
final double? labelFontSize;
7777

78+
/// Optional custom Settings widget.
79+
80+
final Widget? settingsWidget;
81+
7882
/// Creates a [SolidNavBar] with the specified configuration.
7983
8084
const SolidNavBar({
@@ -87,6 +91,7 @@ class SolidNavBar extends StatelessWidget {
8791
this.groupAlignment,
8892
this.iconSize,
8993
this.labelFontSize,
94+
this.settingsWidget,
9095
});
9196

9297
@override
@@ -165,6 +170,12 @@ class SolidNavBar extends StatelessWidget {
165170
letterSpacing: NavigationConstants.navLabelLetterSpacing,
166171
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
167172
),
173+
trailing: settingsWidget != null
174+
? Padding(
175+
padding: const EdgeInsets.only(bottom: 20),
176+
child: settingsWidget,
177+
)
178+
: null,
168179
),
169180
),
170181
),

lib/src/widgets/solid_nav_drawer.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ class SolidNavDrawer extends StatefulWidget {
8181
8282
final ShapeBorder? drawerShape;
8383

84+
/// Optional custom Settings widget.
85+
86+
final Widget? settingsWidget;
87+
8488
const SolidNavDrawer({
8589
super.key,
8690
this.userInfo,
@@ -93,6 +97,7 @@ class SolidNavDrawer extends StatefulWidget {
9397
this.showLogout = true,
9498
this.additionalMenuItems,
9599
this.drawerShape,
100+
this.settingsWidget,
96101
});
97102

98103
@override
@@ -175,6 +180,11 @@ class _SolidNavDrawerState extends State<SolidNavDrawer> {
175180
}),
176181
if (widget.additionalMenuItems != null)
177182
...widget.additionalMenuItems!,
183+
if (widget.settingsWidget != null)
184+
Padding(
185+
padding: const EdgeInsets.symmetric(vertical: 8.0),
186+
child: widget.settingsWidget,
187+
),
178188
if (widget.showLogout && widget.onLogout != null)
179189
..._buildLogoutSection(context, theme),
180190
],

lib/src/widgets/solid_scaffold.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ class SolidScaffold extends StatefulWidget {
231231
232232
final SolidAboutConfig? aboutConfig;
233233

234+
/// Optional custom Settings widget.
235+
236+
final Widget? settingsWidget;
237+
234238
/// Option to force the navigation rail to be hidden.
235239
236240
final bool hideNavRail;
@@ -278,6 +282,7 @@ class SolidScaffold extends StatefulWidget {
278282
this.selectedIndex,
279283
this.themeToggle,
280284
this.aboutConfig,
285+
this.settingsWidget,
281286
this.hideNavRail = false,
282287
});
283288

0 commit comments

Comments
 (0)