import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../models/sake_item.dart'; import '../services/draft_service.dart'; import '../services/network_service.dart'; import '../theme/app_colors.dart'; /// 未解析Draft一覧・一括解析画面 /// /// Phase 1: オフライン対応機能 /// オフライン時に撮影した写真(Draft)の一覧を表示し、 /// オンライン復帰後に一括解析できます。 class PendingAnalysisScreen extends ConsumerStatefulWidget { const PendingAnalysisScreen({super.key}); @override ConsumerState createState() => _PendingAnalysisScreenState(); } class _PendingAnalysisScreenState extends ConsumerState { List _pendingDrafts = []; bool _isLoading = true; bool _isAnalyzing = false; int _analyzedCount = 0; int _totalCount = 0; String? _errorMessage; @override void initState() { super.initState(); _loadPendingDrafts(); } Future _loadPendingDrafts() async { setState(() { _isLoading = true; _errorMessage = null; }); try { final drafts = await DraftService.getPendingDrafts(); setState(() { _pendingDrafts = drafts; _isLoading = false; }); } catch (e) { setState(() { _errorMessage = 'Draft読み込みエラー: $e'; _isLoading = false; }); } } Future _analyzeAllDrafts() async { final appColors = Theme.of(context).extension()!; // オンライン確認 final isOnline = await NetworkService.isOnline(); if (!isOnline) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('オフライン状態です。インターネット接続を確認してください。'), backgroundColor: appColors.error, duration: const Duration(seconds: 4), ), ); return; } setState(() { _isAnalyzing = true; _analyzedCount = 0; _totalCount = _pendingDrafts.length; _errorMessage = null; }); try { final result = await DraftService.analyzeAllDrafts( onProgress: (progress, total) { if (mounted) { setState(() { _analyzedCount = progress; }); } }, ); if (!mounted) return; // 解析完了後、リスト再読み込み await _loadPendingDrafts(); // 結果通知 final successCount = result['success'] as int; final failedCount = result['failed'] as int; final errors = result['errors'] as List; if (failedCount > 0) { // 一部失敗 setState(() { _errorMessage = '解析完了: $successCount成功, $failedCount失敗\n\n失敗詳細:\n${errors.join('\n')}'; }); } else { // 全成功 if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('すべてのDraft($successCount件)を解析しました'), backgroundColor: appColors.success, duration: const Duration(seconds: 3), ), ); // 全件解析完了なら画面を閉じる if (_pendingDrafts.isEmpty) { if (!mounted) return; Navigator.of(context).pop(); } } } catch (e) { setState(() { _errorMessage = '一括解析エラー: $e'; }); } finally { setState(() { _isAnalyzing = false; }); } } Future _deleteDraft(SakeItem draft) async { final appColors = Theme.of(context).extension()!; final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Draft削除確認'), content: const Text('この写真を削除しますか?\n(ギャラリーには残ります)'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('キャンセル'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom(foregroundColor: appColors.error), child: const Text('削除'), ), ], ), ); if (confirm != true) return; try { await DraftService.deleteDraft(draft.key); await _loadPendingDrafts(); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Draftを削除しました'), duration: Duration(seconds: 3), ), ); // 全件削除された場合は画面を閉じる if (_pendingDrafts.isEmpty) { Navigator.of(context).pop(); } } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('削除エラー: $e'), duration: const Duration(seconds: 5), ), ); } } @override Widget build(BuildContext context) { final appColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( title: const Text('未解析の写真', style: TextStyle(fontWeight: FontWeight.bold)), actions: [ if (_pendingDrafts.isNotEmpty && !_isAnalyzing) IconButton( icon: const Icon(LucideIcons.trash2), tooltip: 'すべて削除', onPressed: () async { // BuildContextキャプチャ(async gapの前) if (!mounted) return; final navigator = Navigator.of(context); final messenger = ScaffoldMessenger.of(context); final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('全件削除確認'), content: Text('${_pendingDrafts.length}件のDraftをすべて削除しますか?'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('キャンセル'), ), TextButton( onPressed: () => Navigator.of(context).pop(true), style: TextButton.styleFrom(foregroundColor: appColors.error), child: const Text('すべて削除'), ), ], ), ); if (!mounted) return; if (confirm != true) return; final count = await DraftService.deleteAllDrafts(); await _loadPendingDrafts(); // async gap後はキャプチャした変数を使用 if (!mounted) return; messenger.showSnackBar( SnackBar( content: Text('$count件のDraftを削除しました'), duration: const Duration(seconds: 3), ), ); if (!mounted) return; navigator.pop(); }, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _errorMessage != null ? Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.alertCircle, size: 60, color: appColors.error), const SizedBox(height: 16), Text( _errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: appColors.error), ), const SizedBox(height: 24), ElevatedButton( onPressed: _loadPendingDrafts, child: const Text('再読み込み'), ), ], ), ), ) : _pendingDrafts.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(LucideIcons.checkCircle, size: 60, color: appColors.success), const SizedBox(height: 16), const Text( '未解析の写真はありません', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), Text( 'すべての写真の解析が完了しています', style: TextStyle(color: appColors.textSecondary), ), ], ), ) : Column( children: [ // 進捗表示(解析中のみ) if (_isAnalyzing) Container( padding: const EdgeInsets.all(16), color: appColors.brandPrimary.withValues(alpha: 0.1), child: Column( children: [ Row( children: [ const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 3), ), const SizedBox(width: 12), Text( '解析中... $_analyzedCount / $_totalCount', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 8), LinearProgressIndicator( value: _totalCount > 0 ? _analyzedCount / _totalCount : 0, ), ], ), ), // Draft一覧 Expanded( child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _pendingDrafts.length, itemBuilder: (context, index) { final draft = _pendingDrafts[index]; final photoPath = draft.draftPhotoPath; return Card( margin: const EdgeInsets.only(bottom: 12), child: ListTile( contentPadding: const EdgeInsets.all(12), leading: photoPath != null && File(photoPath).existsSync() ? ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.file( File(photoPath), width: 60, height: 60, fit: BoxFit.cover, ), ) : Container( width: 60, height: 60, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(8), ), child: const Icon(LucideIcons.image, color: Colors.grey), ), title: const Text( '解析待ち', style: TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( '撮影日時: ${draft.metadata.createdAt.toString().substring(0, 16)}', style: TextStyle(fontSize: 12, color: appColors.textSecondary), ), trailing: IconButton( icon: Icon(LucideIcons.trash2, color: appColors.error), onPressed: () => _deleteDraft(draft), ), ), ); }, ), ), // 一括解析ボタン if (_pendingDrafts.isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: SizedBox( width: double.infinity, height: 56, child: ElevatedButton( onPressed: _isAnalyzing ? null : _analyzeAllDrafts, style: ElevatedButton.styleFrom( backgroundColor: appColors.brandPrimary, foregroundColor: Colors.white, disabledBackgroundColor: Colors.grey.shade400, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: _isAnalyzing ? const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ), SizedBox(width: 12), Text('解析中...', style: TextStyle(fontSize: 16)), ], ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(LucideIcons.sparkles, size: 20), const SizedBox(width: 8), Text( 'すべて解析(${_pendingDrafts.length}件)', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ), ), ), ), ], ), ); } }