2026-01-13 09:33:47 +00:00
|
|
|
|
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';
|
2026-01-29 15:54:22 +00:00
|
|
|
|
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';
|
2026-01-13 09:33:47 +00:00
|
|
|
|
|
|
|
|
|
|
class DevMenuScreen extends ConsumerWidget {
|
|
|
|
|
|
const DevMenuScreen({super.key});
|
|
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
|
|
|
|
final experiment = ref.watch(uiExperimentProvider);
|
2026-01-29 15:54:22 +00:00
|
|
|
|
|
2026-01-13 09:33:47 +00:00
|
|
|
|
return Scaffold(
|
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
|
title: const Text('🔬 開発者メニュー'),
|
|
|
|
|
|
),
|
|
|
|
|
|
body: ListView(
|
|
|
|
|
|
children: [
|
2026-02-15 15:13:12 +00:00
|
|
|
|
// NOTE: 言語・テーマ選択は設定画面(SoulScreen)に移動済み
|
|
|
|
|
|
// このメニューはUI実験・デバッグ機能のみ
|
|
|
|
|
|
|
2026-01-13 09:33:47 +00:00
|
|
|
|
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'),
|
|
|
|
|
|
),
|
2026-01-15 15:53:44 +00:00
|
|
|
|
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),
|
|
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
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),
|
|
|
|
|
|
),
|
2026-01-13 09:33:47 +00:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-01-29 15:54:22 +00:00
|
|
|
|
|
|
|
|
|
|
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('✅ キャッシュをクリアしました')),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
2026-01-13 09:33:47 +00:00
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-29 15:54:22 +00:00
|
|
|
|
|
|
|
|
|
|
Future<void> _runBatchAnalysis(BuildContext context, WidgetRef ref) async {
|
2026-02-15 15:13:12 +00:00
|
|
|
|
// v1.0.12: Dev Menu専用 - セット商品・Draft含む全データを解析するため rawSakeListItemsProvider を使用
|
|
|
|
|
|
// Phase D6設計: rawSakeListItemsProvider はフィルタリング前の生データを提供
|
|
|
|
|
|
// allSakeItemsProvider を使うと、セット商品・Draftが除外されてしまう
|
2026-01-29 15:54:22 +00:00
|
|
|
|
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(
|
2026-02-15 15:13:12 +00:00
|
|
|
|
name: result.name ?? item.displayData.displayName,
|
|
|
|
|
|
brand: result.brand ?? item.displayData.displayBrewery,
|
|
|
|
|
|
prefecture: result.prefecture ?? item.displayData.displayPrefecture,
|
2026-01-29 15:54:22 +00:00
|
|
|
|
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) {
|
2026-02-15 15:13:12 +00:00
|
|
|
|
debugPrint('Failed to analyze ${item.displayData.displayName}: $e');
|
2026-01-29 15:54:22 +00:00
|
|
|
|
failCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (context.mounted) {
|
|
|
|
|
|
Navigator.pop(context); // Close Progress
|
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
|
SnackBar(content: Text('完了: 成功 $successCount件 / 失敗 $failCount件')),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-13 09:33:47 +00:00
|
|
|
|
}
|