ponshu-room-lite/lib/screens/dev_menu_screen.dart

253 lines
10 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../providers/ui_experiment_provider.dart';
import '../services/analysis_cache_service.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/sake_item.dart';
import '../providers/sake_list_provider.dart';
import '../services/gemini_service.dart';
class DevMenuScreen extends ConsumerWidget {
const DevMenuScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final experiment = ref.watch(uiExperimentProvider);
return Scaffold(
appBar: AppBar(
title: const Text('🔬 開発者メニュー'),
),
body: ListView(
children: [
// NOTE: 言語・テーマ選択は設定画面(SoulScreen)に移動済み
// このメニューはUI実験・デバッグ機能のみ
const ListTile(
leading: Icon(LucideIcons.flaskConical),
title: Text('UI実験'),
subtitle: Text('新しいデザインのテスト'),
),
Card(
margin: const EdgeInsets.all(16),
child: Column(
children: [
SwitchListTile(
secondary: const Text('📱', style: TextStyle(fontSize: 24)),
title: const Text('グリッド3列表示'),
subtitle: Text('瓶型の縦長カード (現在: ${experiment.gridColumns}列)'),
value: experiment.gridColumns == 3,
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setGridColumns(val ? 3 : 2),
),
const Divider(),
SwitchListTile(
secondary: const Text('', style: TextStyle(fontSize: 24)),
title: const Text('FABバウンス'),
subtitle: Text('ぷるんとした動き (現在: ${experiment.fabAnimation == 'bounce' ? 'ON' : 'OFF'})'),
value: experiment.fabAnimation == 'bounce',
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setFabAnimation(val ? 'bounce' : 'rotate'),
),
const Divider(),
SwitchListTile(
secondary: const Text('🎨', style: TextStyle(fontSize: 24)),
title: const Text('地図お試しカラー'),
subtitle: Text('地方ごとに色分け (現在: ${experiment.isMapColorful ? 'ON' : 'OFF'})'),
value: experiment.isMapColorful,
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setMapColorful(val),
),
const Divider(),
SwitchListTile(
secondary: const Text('📝', style: TextStyle(fontSize: 24)),
title: const Text('一覧テキスト表示'),
subtitle: Text('写真のみ表示モード (現在: ${experiment.showGridText ? 'ON' : 'OFF'})'),
value: experiment.showGridText,
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setShowGridText(val),
),
const Divider(),
SwitchListTile(
secondary: const Text('🏅', style: TextStyle(fontSize: 24)),
title: const Text('バッジアイコン化'),
subtitle: Text('絵文字の代わりにアイコンを使用 (現在: ${experiment.useBadgeIcons ? 'ON' : 'OFF'})'),
value: experiment.useBadgeIcons,
onChanged: (val) => ref.read(uiExperimentProvider.notifier)
.setUseBadgeIcons(val),
),
],
),
),
const Divider(),
const Divider(),
// Batch Repair Section
ListTile(
leading: const Icon(LucideIcons.hammer, color: Colors.blue),
title: const Text('データ修復 (再解析)'),
subtitle: const Text('データの欠損があるアイテムをAIで再解析します'),
onTap: () => _runBatchAnalysis(context, ref),
),
ListTile(
leading: const Icon(LucideIcons.database, color: Colors.orange),
title: const Text('AI解析キャッシュをクリア'),
subtitle: FutureBuilder<int>(
future: AnalysisCacheService.getCacheSize(),
builder: (context, snapshot) {
final count = snapshot.data ?? 0;
return Text('$count件のキャッシュを削除します');
},
),
onTap: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('確認'),
content: const Text(
'AI解析キャッシュをクリアしますか\n'
'\n'
'同じ日本酒を再解析する場合、APIを再度呼び出します。',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('キャンセル'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('クリア'),
),
],
),
);
if (confirmed == true) {
await AnalysisCacheService.clearAll();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('✅ キャッシュをクリアしました')),
);
}
}
},
),
],
),
);
}
Future<void> _runBatchAnalysis(BuildContext context, WidgetRef ref) async {
// v1.0.12: Dev Menu専用 - セット商品・Draft含む全データを解析するため rawSakeListItemsProvider を使用
// Phase D6設計: rawSakeListItemsProvider はフィルタリング前の生データを提供
// allSakeItemsProvider を使うと、セット商品・Draftが除外されてしまう
final allItems = ref.read(rawSakeListItemsProvider).asData?.value ?? [];
if (allItems.isEmpty) return;
// Filter items that need repair (e.g., empty stats)
final targets = allItems.where((item) {
final stats = item.hiddenSpecs.sakeTasteStats;
final isStatsEmpty = stats.aroma == 0 && stats.bitterness == 0 && stats.sweetness == 0 &&
stats.acidity == 0 && stats.body == 0;
// Also check for mismatch/missing new keys if possible, but empty check is safest proxy for now.
return isStatsEmpty || item.metadata.aiConfidence == null;
}).toList();
if (targets.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('修復が必要なデータは見つかりませんでした')),
);
return;
}
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('データ修復の実行'),
content: Text(
'${targets.length}件のデータの再解析を行います。\n'
'時間がかかりますが、実行しますか?\n(API制限に注意してください)',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('キャンセル'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('実行'),
),
],
),
);
if (confirmed != true) return;
// Show Progress Dialog
if (!context.mounted) return;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => const Center(child: CircularProgressIndicator()),
);
int successCount = 0;
int failCount = 0;
final box = Hive.box<SakeItem>('sake_items');
try {
final gemini = GeminiService();
for (final item in targets) {
if (item.displayData.imagePaths.isEmpty) continue;
try {
// Force Refresh Analysis
final result = await gemini.analyzeSakeLabel(item.displayData.imagePaths, forceRefresh: true);
final updated = item.copyWith(
name: result.name ?? item.displayData.displayName,
brand: result.brand ?? item.displayData.displayBrewery,
prefecture: result.prefecture ?? item.displayData.displayPrefecture,
description: result.description ?? item.hiddenSpecs.description,
catchCopy: result.catchCopy ?? item.displayData.catchCopy,
confidenceScore: result.confidenceScore,
flavorTags: result.flavorTags.isNotEmpty ? result.flavorTags : item.hiddenSpecs.flavorTags,
tasteStats: result.tasteStats.isNotEmpty ? result.tasteStats : item.hiddenSpecs.tasteStats,
// New Fields
specificDesignation: result.type ?? item.hiddenSpecs.type,
alcoholContent: result.alcoholContent ?? item.hiddenSpecs.alcoholContent,
polishingRatio: result.polishingRatio ?? item.hiddenSpecs.polishingRatio,
sakeMeterValue: result.sakeMeterValue ?? item.hiddenSpecs.sakeMeterValue,
riceVariety: result.riceVariety ?? item.hiddenSpecs.riceVariety,
yeast: result.yeast ?? item.hiddenSpecs.yeast,
manufacturingYearMonth: result.manufacturingYearMonth ?? item.hiddenSpecs.manufacturingYearMonth,
);
await box.put(item.key, updated);
successCount++;
// Wait to prevent rate limit
await Future.delayed(const Duration(seconds: 2));
} catch (e) {
debugPrint('Failed to analyze ${item.displayData.displayName}: $e');
failCount++;
}
}
} finally {
if (context.mounted) {
Navigator.pop(context); // Close Progress
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('完了: 成功 $successCount件 / 失敗 $failCount件')),
);
}
}
}
}