creek

The AI Image Editor of 2030

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}