creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/widgets/canvas/layers_panel.dart (8212B)
1import 'dart:io';
2import 'package:flutter/material.dart';
3import 'package:creekui/data/models/canvas_models.dart';
4
5class LayersPanel extends StatefulWidget {
6 final List<CanvasLayer> layers;
7 final String? activeLayerId;
8 final bool isOpen;
9 final VoidCallback onToggle;
10 final Function(int oldIndex, int newIndex) onReorder;
11 final Function(String id) onDelete;
12 final Function(String id) onToggleVisibility;
13 final Function(String id) onLayerTap;
14 final VoidCallback? onAddLayer;
15
16 const LayersPanel({
17 Key? key,
18 required this.layers,
19 required this.activeLayerId,
20 required this.isOpen,
21 required this.onToggle,
22 required this.onReorder,
23 required this.onDelete,
24 required this.onToggleVisibility,
25 required this.onLayerTap,
26 this.onAddLayer,
27 }) : super(key: key);
28
29 @override
30 State<LayersPanel> createState() => _LayersPanelState();
31}
32
33class _LayersPanelState extends State<LayersPanel> {
34 @override
35 Widget build(BuildContext context) {
36 // Reverse list for UI so Top Layer is index 0
37 final displayLayers = widget.layers.reversed.toList();
38 return AnimatedContainer(
39 duration: const Duration(milliseconds: 300),
40 curve: Curves.easeOutCubic,
41 width: widget.isOpen ? 260 : 56,
42 // Use constrained height calculation based on state
43 height: widget.isOpen ? 400 : 56,
44 decoration: BoxDecoration(
45 color: Colors.white,
46 borderRadius: BorderRadius.circular(16),
47 boxShadow: [
48 BoxShadow(
49 color: Colors.black.withOpacity(0.15),
50 blurRadius: 10,
51 offset: const Offset(0, 4),
52 ),
53 ],
54 ),
55 clipBehavior: Clip.antiAlias,
56 child: Material(
57 color: Colors.transparent,
58 child:
59 widget.isOpen
60 ? _buildPanelContent(displayLayers)
61 : _buildCollapsedButton(),
62 ),
63 );
64 }
65
66 Widget _buildCollapsedButton() {
67 return InkWell(
68 onTap: widget.onToggle,
69 borderRadius: BorderRadius.circular(16),
70 child: const Center(
71 child: Icon(Icons.layers_outlined, color: Colors.black),
72 ),
73 );
74 }
75
76 Widget _buildPanelContent(List<CanvasLayer> displayLayers) {
77 return AnimatedSize(
78 duration: const Duration(milliseconds: 200),
79 curve: Curves.easeOutCubic,
80 alignment: Alignment.topRight,
81 child: Container(
82 constraints: const BoxConstraints(maxHeight: 400),
83 child: Column(
84 mainAxisSize: MainAxisSize.min, // Shrink to fit content
85 crossAxisAlignment: CrossAxisAlignment.start,
86 children: [
87 // Header
88 Padding(
89 padding: const EdgeInsets.all(12),
90 child: Row(
91 children: [
92 // Add Layer Button
93 if (widget.onAddLayer != null) ...[
94 InkWell(
95 onTap: widget.onAddLayer,
96 borderRadius: BorderRadius.circular(8),
97 child: Container(
98 padding: const EdgeInsets.all(6),
99 decoration: BoxDecoration(
100 color: Colors.blue.shade50,
101 borderRadius: BorderRadius.circular(8),
102 ),
103 child: const Icon(
104 Icons.add,
105 size: 20,
106 color: Colors.blue,
107 ),
108 ),
109 ),
110 ],
111
112 const Spacer(),
113
114 // Layers Icon
115 InkWell(
116 onTap: widget.onToggle,
117 borderRadius: BorderRadius.circular(8),
118 child: Container(
119 padding: const EdgeInsets.all(6),
120 decoration: BoxDecoration(
121 color: Colors.grey.shade100,
122 borderRadius: BorderRadius.circular(8),
123 ),
124 child: const Icon(
125 Icons.layers,
126 size: 20,
127 color: Colors.black87,
128 ),
129 ),
130 ),
131 ],
132 ),
133 ),
134 const Divider(height: 1, thickness: 0.5),
135
136 // List
137 Flexible(
138 child: ReorderableListView.builder(
139 shrinkWrap: true,
140 padding: EdgeInsets.zero,
141 buildDefaultDragHandles: false,
142 itemCount: displayLayers.length,
143 onReorder: widget.onReorder,
144 itemBuilder: (context, index) {
145 final layer = displayLayers[index];
146 return _buildLayerItem(context, layer, index);
147 },
148 ),
149 ),
150 ],
151 ),
152 ),
153 );
154 }
155
156 Widget _buildLayerItem(BuildContext context, CanvasLayer layer, int index) {
157 final bool isActive = layer.id == widget.activeLayerId;
158
159 Widget preview;
160 String label = "";
161
162 if (layer is ImageLayer) {
163 final type = layer.data['type'];
164 final content = layer.data['content'];
165
166 if (type == 'text') {
167 preview = const Icon(
168 Icons.text_fields,
169 size: 18,
170 color: Colors.black54,
171 );
172 label = content.toString();
173 } else {
174 preview = Container(
175 width: 28,
176 height: 28,
177 decoration: BoxDecoration(
178 borderRadius: BorderRadius.circular(4),
179 border: Border.all(color: Colors.grey[300]!),
180 image: DecorationImage(
181 image: FileImage(File(content)),
182 fit: BoxFit.cover,
183 ),
184 ),
185 );
186 label = "Image";
187 }
188 } else if (layer is SketchLayer) {
189 preview = const Icon(Icons.brush, size: 18, color: Colors.purpleAccent);
190 label = layer.isMagicDraw ? "Magic Draw" : "Sketch";
191 } else {
192 preview = const SizedBox();
193 }
194
195 return Dismissible(
196 key: ValueKey(layer.id),
197 direction: DismissDirection.endToStart,
198 onDismissed: (_) => widget.onDelete(layer.id),
199 background: Container(
200 color: Colors.redAccent,
201 alignment: Alignment.centerRight,
202 padding: const EdgeInsets.only(right: 12),
203 child: const Icon(Icons.delete, color: Colors.white, size: 20),
204 ),
205 child: Material(
206 color: isActive ? Colors.grey.withOpacity(0.1) : Colors.transparent,
207 child: InkWell(
208 onTap: () => widget.onLayerTap(layer.id),
209 child: Padding(
210 padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
211 child: Row(
212 children: [
213 const SizedBox(width: 12),
214 preview,
215 const SizedBox(width: 12),
216 Expanded(
217 child: Text(
218 label,
219 style: const TextStyle(fontSize: 14),
220 maxLines: 1,
221 overflow: TextOverflow.ellipsis,
222 ),
223 ),
224
225 IconButton(
226 padding: const EdgeInsets.all(4),
227 constraints: const BoxConstraints(),
228 icon: Icon(
229 layer.isVisible ? Icons.visibility : Icons.visibility_off,
230 size: 18,
231 color: layer.isVisible ? Colors.black54 : Colors.grey,
232 ),
233 onPressed: () => widget.onToggleVisibility(layer.id),
234 ),
235
236 const SizedBox(width: 4),
237
238 ReorderableDragStartListener(
239 index: index,
240 child: Container(
241 padding: const EdgeInsets.fromLTRB(12, 12, 16, 12),
242 color: Colors.transparent,
243 child: const Icon(
244 Icons.drag_handle,
245 size: 20,
246 color: Colors.grey,
247 ),
248 ),
249 ),
250 ],
251 ),
252 ),
253 ),
254 ),
255 );
256 }
257}