creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
lib/ui/pages/image_analysis_page.dart (10804B)
1import 'dart:convert';
2import 'dart:io';
3import 'package:flutter/material.dart';
4import 'package:image_picker/image_picker.dart';
5import 'package:path_provider/path_provider.dart';
6import 'package:creekui/services/analyze/image_analyzer.dart';
7import 'package:creekui/ui/styles/variables.dart';
8import 'package:creekui/ui/widgets/app_bar.dart';
9import 'package:creekui/ui/widgets/tag_chip.dart';
10
11class ImageAnalysisPage extends StatefulWidget {
12 const ImageAnalysisPage({super.key});
13
14 @override
15 State<ImageAnalysisPage> createState() => _ImageAnalysisPageState();
16}
17
18class _ImageAnalysisPageState extends State<ImageAnalysisPage> {
19 File? _selectedImage;
20 bool _isAnalyzing = false;
21 Map<String, dynamic>? _analysisResult;
22 String? _errorMessage;
23
24 final ImagePicker _picker = ImagePicker();
25
26 final Set<String> _selectedTags = {};
27 final List<String> _availableTags = [
28 'Compositions',
29 'Colours',
30 'Texture',
31 'Style',
32 'Emotion',
33 'Lighting',
34 'Era',
35 'Fonts',
36 'Subject',
37 ];
38
39 Future<void> _pickImage(ImageSource source) async {
40 try {
41 final XFile? image = await _picker.pickImage(source: source);
42 if (image != null) {
43 setState(() {
44 _selectedImage = File(image.path);
45 _analysisResult = null;
46 _errorMessage = null;
47 });
48 }
49 } catch (e) {
50 if (mounted) {
51 setState(() => _errorMessage = 'Error picking image: $e');
52 ScaffoldMessenger.of(
53 context,
54 ).showSnackBar(SnackBar(content: Text('Error picking image: $e')));
55 }
56 }
57 }
58
59 Future<void> _runAnalysis() async {
60 if (_selectedImage == null) return;
61
62 setState(() {
63 _isAnalyzing = true;
64 _errorMessage = null;
65 });
66
67 try {
68 final appDir = await getApplicationDocumentsDirectory();
69 final fileName = _selectedImage!.path.split('/').last;
70 final targetPath = '${appDir.path}/$fileName';
71
72 final file = File(_selectedImage!.path);
73 await file.copy(targetPath);
74
75 Map<String, dynamic> result;
76 if (_selectedTags.isEmpty) {
77 result = await ImageAnalyzerService.analyzeFullSuite(targetPath);
78 } else {
79 result = await ImageAnalyzerService.analyzeSelected(
80 targetPath,
81 _selectedTags.toList(),
82 );
83 }
84
85 if (mounted) {
86 setState(() {
87 _analysisResult = result;
88 _isAnalyzing = false;
89 });
90 }
91 } catch (e) {
92 if (mounted) {
93 setState(() {
94 _errorMessage = e.toString();
95 _isAnalyzing = false;
96 });
97 }
98 }
99 }
100
101 void _showSourceSelector() {
102 showModalBottomSheet(
103 context: context,
104 backgroundColor: Colors.white,
105 shape: const RoundedRectangleBorder(
106 borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
107 ),
108 builder:
109 (ctx) => SafeArea(
110 child: Padding(
111 padding: const EdgeInsets.symmetric(vertical: 20),
112 child: Column(
113 mainAxisSize: MainAxisSize.min,
114 children: [
115 ListTile(
116 leading: const Icon(
117 Icons.camera_alt_outlined,
118 color: Colors.black,
119 ),
120 title: Text("Take Photo", style: Variables.bodyStyle),
121 onTap: () {
122 Navigator.pop(ctx);
123 _pickImage(ImageSource.camera);
124 },
125 ),
126 ListTile(
127 leading: const Icon(
128 Icons.image_outlined,
129 color: Colors.black,
130 ),
131 title: Text(
132 "Choose from Gallery",
133 style: Variables.bodyStyle,
134 ),
135 onTap: () {
136 Navigator.pop(ctx);
137 _pickImage(ImageSource.gallery);
138 },
139 ),
140 ],
141 ),
142 ),
143 ),
144 );
145 }
146
147 @override
148 Widget build(BuildContext context) {
149 final colorScheme = Theme.of(context).colorScheme;
150 return Scaffold(
151 backgroundColor: Variables.surfaceBackground,
152 appBar: CommonAppBar(
153 title: "Image Analysis",
154 showBack: true,
155 leading: IconButton(
156 icon: const Icon(Icons.arrow_back, color: Colors.black),
157 onPressed: () => Navigator.pop(context),
158 ),
159 actions: [
160 if (_selectedImage != null && !_isAnalyzing)
161 IconButton(
162 icon: const Icon(Icons.play_arrow, color: Variables.textPrimary),
163 tooltip: "Run Analysis",
164 onPressed: _runAnalysis,
165 ),
166 ],
167 ),
168 body: SingleChildScrollView(
169 padding: const EdgeInsets.all(20),
170 child: Column(
171 crossAxisAlignment: CrossAxisAlignment.start,
172 children: [
173 // Image Preview
174 Center(
175 child: Container(
176 width: double.infinity,
177 height: 250,
178 decoration: BoxDecoration(
179 color: Variables.surfaceSubtle,
180 borderRadius: BorderRadius.circular(16),
181 boxShadow: [
182 BoxShadow(
183 color: Colors.black.withOpacity(0.05),
184 blurRadius: 10,
185 offset: const Offset(0, 4),
186 ),
187 ],
188 ),
189 child: ClipRRect(
190 borderRadius: BorderRadius.circular(16),
191 child: Stack(
192 fit: StackFit.expand,
193 children: [
194 if (_selectedImage != null)
195 Image.file(_selectedImage!, fit: BoxFit.contain)
196 else
197 Column(
198 mainAxisAlignment: MainAxisAlignment.center,
199 children: [
200 Icon(
201 Icons.bug_report_outlined,
202 size: 48,
203 color: Colors.grey[400],
204 ),
205 const SizedBox(height: 12),
206 Text(
207 "Select image to analyze",
208 style: Variables.bodyStyle.copyWith(
209 color: Colors.grey[500],
210 ),
211 ),
212 ],
213 ),
214 if (_isAnalyzing)
215 Container(
216 color: Colors.black26,
217 child: const Center(
218 child: CircularProgressIndicator(
219 color: Colors.white,
220 ),
221 ),
222 ),
223 ],
224 ),
225 ),
226 ),
227 ),
228 const SizedBox(height: 24),
229
230 // Tag Selector
231 Text(
232 "Select Analysis Modules",
233 style: Variables.headerStyle.copyWith(fontSize: 16),
234 ),
235 const SizedBox(height: 12),
236 Wrap(
237 spacing: 8,
238 runSpacing: 8,
239 children:
240 _availableTags.map((tag) {
241 final isSelected = _selectedTags.contains(tag);
242 return GestureDetector(
243 onTap: () {
244 setState(() {
245 if (isSelected) {
246 _selectedTags.remove(tag);
247 } else {
248 _selectedTags.add(tag);
249 }
250 });
251 },
252 child: TagChip(
253 label: tag,
254 icon:
255 isSelected
256 ? const Icon(
257 Icons.check,
258 size: 14,
259 color: Variables.chipText,
260 )
261 : null,
262 ),
263 );
264 }).toList(),
265 ),
266 const SizedBox(height: 24),
267
268 // Error Message
269 if (_errorMessage != null)
270 Container(
271 width: double.infinity,
272 padding: const EdgeInsets.all(16),
273 decoration: BoxDecoration(
274 color: Colors.red[50],
275 borderRadius: BorderRadius.circular(12),
276 border: Border.all(color: Colors.red[100]!),
277 ),
278 child: Text(
279 _errorMessage!,
280 style: const TextStyle(
281 color: Colors.red,
282 fontFamily: 'GeneralSans',
283 ),
284 ),
285 ),
286
287 // Raw JSON Result
288 if (_analysisResult != null) ...[
289 const Text(
290 "Results",
291 style: TextStyle(
292 fontSize: 18,
293 fontWeight: FontWeight.bold,
294 fontFamily: 'GeneralSans',
295 ),
296 ),
297 const SizedBox(height: 12),
298 _buildJsonViewer(_analysisResult!),
299 ],
300 const SizedBox(height: 80),
301 ],
302 ),
303 ),
304 floatingActionButton: FloatingActionButton.extended(
305 onPressed: _showSourceSelector,
306 backgroundColor: Colors.grey[900],
307 foregroundColor: Colors.white,
308 icon: const Icon(Icons.add_photo_alternate),
309 label: Text(
310 _selectedImage == null ? "Select Image" : "Change Image",
311 style: const TextStyle(fontFamily: 'GeneralSans'),
312 ),
313 ),
314 );
315 }
316
317 Widget _buildJsonViewer(Map<String, dynamic> data) {
318 const encoder = JsonEncoder.withIndent(' ');
319 final String prettyJson = encoder.convert(data);
320 return Container(
321 width: double.infinity,
322 padding: const EdgeInsets.all(16),
323 decoration: BoxDecoration(
324 color: Variables.surfaceSubtle,
325 borderRadius: BorderRadius.circular(12),
326 border: Border.all(color: Variables.borderSubtle),
327 ),
328 child: SelectableText(
329 prettyJson,
330 style: TextStyle(
331 fontFamily: Platform.isIOS ? 'Courier' : 'monospace',
332 fontSize: 12,
333 color: Variables.textPrimary,
334 height: 1.3,
335 ),
336 ),
337 );
338 }
339}