creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/widgets/top_bar.dart (15046B)
1import 'package:flutter/material.dart';
2import 'package:flutter_svg/flutter_svg.dart';
3import 'package:creekui/data/models/project_model.dart';
4import 'package:creekui/data/repos/project_repo.dart';
5import 'package:creekui/ui/styles/variables.dart';
6import 'package:creekui/ui/pages/settings_page.dart';
7import 'package:creekui/ui/pages/home_page.dart';
8
9class TopBar extends StatefulWidget implements PreferredSizeWidget {
10 final int currentProjectId;
11 final VoidCallback? onBack;
12 final Function(ProjectModel)? onProjectChanged;
13 final VoidCallback? onSettingsPressed;
14 final String? titleOverride;
15 final VoidCallback? onLayoutPressed;
16 final VoidCallback? onFilterPressed;
17 final VoidCallback? onAIPressed;
18 final bool? isAlternateView;
19 final VoidCallback? onLayoutToggle;
20 final bool hideSecondRow;
21 final bool hideSelector;
22 final bool showSettings;
23
24 const TopBar({
25 super.key,
26 required this.currentProjectId,
27 this.onBack,
28 this.onProjectChanged,
29 this.onSettingsPressed,
30 this.titleOverride,
31 this.onLayoutPressed,
32 this.onFilterPressed,
33 this.onAIPressed,
34 this.isAlternateView,
35 this.onLayoutToggle,
36 this.hideSecondRow = false,
37 this.hideSelector = false,
38 this.showSettings = true,
39 });
40
41 @override
42 State<TopBar> createState() => _TopBarState();
43
44 @override
45 Size get preferredSize =>
46 hideSecondRow ? const Size.fromHeight(48.0) : const Size.fromHeight(88.0);
47}
48
49class _TopBarState extends State<TopBar> {
50 final ProjectRepo _projectRepo = ProjectRepo();
51
52 bool _isLoading = true;
53 ProjectModel? _currentProject;
54 ProjectModel? _rootProject;
55 List<ProjectModel> _contextList = [];
56
57 @override
58 void initState() {
59 super.initState();
60 _loadData();
61 }
62
63 @override
64 void didUpdateWidget(covariant TopBar oldWidget) {
65 super.didUpdateWidget(oldWidget);
66 if (oldWidget.currentProjectId != widget.currentProjectId) {
67 _loadData();
68 }
69 }
70
71 Future<void> _loadData() async {
72 try {
73 final current = await _projectRepo.getProjectById(
74 widget.currentProjectId,
75 );
76 if (current == null) {
77 if (mounted) setState(() => _isLoading = false);
78 return;
79 }
80
81 ProjectModel? root;
82 List<ProjectModel> events = [];
83
84 if (current.parentId == null) {
85 root = current;
86 events = await _projectRepo.getEvents(current.id!);
87 } else {
88 root = await _projectRepo.getProjectById(current.parentId!);
89 if (root != null) {
90 events = await _projectRepo.getEvents(root.id!);
91 }
92 }
93 root ??= current;
94 final List<ProjectModel> fullList = [root, ...events];
95 if (mounted) {
96 setState(() {
97 _currentProject = current;
98 _rootProject = root;
99 _contextList = fullList;
100 _isLoading = false;
101 });
102 }
103 } catch (e) {
104 debugPrint("TopBar Error: $e");
105 if (mounted) setState(() => _isLoading = false);
106 }
107 }
108
109 void _handleSafeBack() {
110 if (widget.onBack != null) {
111 widget.onBack!();
112 return;
113 }
114
115 if (Navigator.canPop(context)) {
116 Navigator.pop(context);
117 } else {
118 // Fallback: Go to HomePage to prevent app exit/crash
119 Navigator.pushReplacement(
120 context,
121 MaterialPageRoute(builder: (_) => const HomePage()),
122 );
123 }
124 }
125
126 void _openSettings() {
127 if (widget.onSettingsPressed != null) {
128 widget.onSettingsPressed!();
129 } else {
130 Navigator.push(
131 context,
132 MaterialPageRoute(builder: (_) => const SettingsPage()),
133 );
134 }
135 }
136
137 @override
138 Widget build(BuildContext context) {
139 return Container(
140 color: Colors.white,
141 child: SafeArea(
142 bottom: false,
143 child: Column(
144 mainAxisSize: MainAxisSize.min,
145 children: [
146 // Row 1: Title and Settings
147 Container(
148 height: 48.0,
149 padding: const EdgeInsets.symmetric(horizontal: 16.0),
150 child: Row(
151 children: [
152 // Back Button
153 GestureDetector(
154 onTap: _handleSafeBack,
155 behavior: HitTestBehavior.opaque,
156 child: Container(
157 width: 24,
158 height: 24,
159 alignment: Alignment.center,
160 child: const Icon(
161 Icons.arrow_back,
162 size: 20,
163 color: Variables.textPrimary,
164 ),
165 ),
166 ),
167 const SizedBox(width: 10),
168
169 // Title
170 Expanded(
171 child:
172 widget.titleOverride != null
173 ? Text(
174 widget.titleOverride!,
175 style: Variables.headerStyle.copyWith(
176 fontSize: 20,
177 height: 24 / 20,
178 ),
179 overflow: TextOverflow.ellipsis,
180 )
181 : (!_isLoading &&
182 _currentProject != null &&
183 _rootProject != null)
184 ? Text(
185 _rootProject!.title,
186 style: Variables.headerStyle.copyWith(
187 fontSize: 20,
188 height: 24 / 20,
189 ),
190 overflow: TextOverflow.ellipsis,
191 )
192 : const SizedBox(),
193 ),
194
195 // Settings Icon
196 if (widget.showSettings)
197 Material(
198 color: Colors.transparent,
199 child: InkWell(
200 onTap: _openSettings,
201 borderRadius: BorderRadius.circular(24),
202 child: Container(
203 width: 48,
204 height: 48,
205 alignment: Alignment.center,
206 child: SvgPicture.asset(
207 'assets/icons/settings-line.svg',
208 width: 24,
209 height: 24,
210 colorFilter: const ColorFilter.mode(
211 Variables.textPrimary,
212 BlendMode.srcIn,
213 ),
214 ),
215 ),
216 ),
217 ),
218 ],
219 ),
220 ),
221 // Row 2: Global Dropdown and Action Buttons
222 if (!widget.hideSecondRow)
223 Container(
224 height: 40.0,
225 padding: const EdgeInsets.symmetric(horizontal: 16.0),
226 child: Row(
227 children: [
228 // Left group: Global Dropdown and Layout Button
229 Row(
230 children: [
231 // Global Dropdown
232 if (!widget.hideSelector &&
233 !_isLoading &&
234 _currentProject != null &&
235 _rootProject != null)
236 PopupMenuButton<ProjectModel>(
237 padding: EdgeInsets.zero,
238 onSelected:
239 (project) =>
240 widget.onProjectChanged?.call(project),
241 color: Colors.white,
242 shape: RoundedRectangleBorder(
243 borderRadius: BorderRadius.circular(8),
244 ),
245 offset: const Offset(0, 42),
246 child: Container(
247 padding: const EdgeInsets.symmetric(
248 horizontal: 16,
249 vertical: 10,
250 ),
251 decoration: BoxDecoration(
252 color: Variables.borderSubtle,
253 borderRadius: BorderRadius.circular(1000),
254 ),
255 child: Row(
256 mainAxisSize: MainAxisSize.min,
257 children: [
258 Text(
259 _currentProject!.id == _rootProject!.id
260 ? "Global"
261 : _currentProject!.title,
262 style: const TextStyle(
263 fontFamily: 'GeneralSans',
264 fontSize: 14,
265 fontWeight: FontWeight.w500,
266 height: 20 / 14,
267 color: Variables.textPrimary,
268 ),
269 ),
270 const SizedBox(width: 8),
271 const Icon(
272 Icons.keyboard_arrow_down_rounded,
273 size: 20,
274 color: Variables.textPrimary,
275 ),
276 ],
277 ),
278 ),
279 itemBuilder: (context) {
280 return _contextList.map((project) {
281 final isSelected =
282 project.id == _currentProject!.id;
283 final isRoot = project.id == _rootProject!.id;
284 return PopupMenuItem<ProjectModel>(
285 value: project,
286 child: Text(
287 isRoot ? "Global" : project.title,
288 style: Variables.bodyStyle.copyWith(
289 fontWeight:
290 isSelected
291 ? FontWeight.bold
292 : FontWeight.normal,
293 color:
294 isSelected
295 ? Variables.textPrimary
296 : Variables.textSecondary,
297 ),
298 ),
299 );
300 }).toList();
301 },
302 ),
303 if (widget.onLayoutToggle != null ||
304 widget.onLayoutPressed != null) ...[
305 const SizedBox(width: 8),
306 GestureDetector(
307 onTap:
308 widget.onLayoutToggle ?? widget.onLayoutPressed,
309 child: Container(
310 padding: const EdgeInsets.symmetric(
311 horizontal: 16,
312 vertical: 10,
313 ),
314 decoration: BoxDecoration(
315 color: Variables.borderSubtle,
316 borderRadius: BorderRadius.circular(1000),
317 ),
318 child: Icon(
319 widget.onLayoutToggle != null
320 ? (widget.isAlternateView == true
321 ? Icons.dashboard
322 : Icons.view_agenda_outlined)
323 : Icons.edit,
324 size: 20,
325 color: Variables.textPrimary,
326 ),
327 ),
328 ),
329 ],
330 ],
331 ),
332 const Spacer(),
333 // Right group: Filter and AI Buttons
334 if (widget.onFilterPressed != null ||
335 widget.onAIPressed != null)
336 Row(
337 children: [
338 // Filter Button
339 if (widget.onFilterPressed != null)
340 GestureDetector(
341 onTap: widget.onFilterPressed,
342 child: Container(
343 padding: const EdgeInsets.symmetric(
344 horizontal: 16,
345 vertical: 10,
346 ),
347 decoration: BoxDecoration(
348 color: Variables.borderSubtle,
349 borderRadius: BorderRadius.circular(1000),
350 ),
351 child: const Icon(
352 Icons.tune,
353 size: 20,
354 color: Variables.textPrimary,
355 ),
356 ),
357 ),
358 if (widget.onFilterPressed != null &&
359 widget.onAIPressed != null)
360 const SizedBox(width: 8),
361 // AI Button
362 if (widget.onAIPressed != null)
363 GestureDetector(
364 onTap: widget.onAIPressed,
365 child: Container(
366 padding: const EdgeInsets.symmetric(
367 horizontal: 16,
368 vertical: 10,
369 ),
370 decoration: BoxDecoration(
371 color: Variables.borderSubtle,
372 borderRadius: BorderRadius.circular(1000),
373 ),
374 child: const Icon(
375 Icons.auto_awesome,
376 size: 20,
377 color: Variables.textPrimary,
378 ),
379 ),
380 ),
381 ],
382 ),
383 ],
384 ),
385 ),
386 ],
387 ),
388 ),
389 );
390 }
391}