refactor: code quality improvements based on critical review
Made-with: Cursor
This commit is contained in:
parent
2074f85da8
commit
94f7ee20ea
|
|
@ -78,6 +78,7 @@ Desktop.ini
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
analyze_output*.txt
|
analyze_output*.txt
|
||||||
|
build_log_*.txt
|
||||||
|
|
||||||
# Deprecated build scripts (API keys hardcoded - use build_consumer_apks.ps1 instead)
|
# Deprecated build scripts (API keys hardcoded - use build_consumer_apks.ps1 instead)
|
||||||
build_all_apks.ps1
|
build_all_apks.ps1
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,6 @@ import 'screens/main_screen.dart';
|
||||||
import 'screens/license_screen.dart';
|
import 'screens/license_screen.dart';
|
||||||
import 'services/migration_service.dart';
|
import 'services/migration_service.dart';
|
||||||
|
|
||||||
/// ビルド時Pro解放フラグ(現在未使用 — 実行時ライセンスはisProProviderで管理)
|
|
||||||
/// 将来的に削除予定。isProProvider (license_provider.dart) を使うこと。
|
|
||||||
const bool isProVersion = bool.fromEnvironment('IS_PRO_VERSION', defaultValue: false);
|
|
||||||
|
|
||||||
/// 店舗向けビルドかどうかを判定するビルド時フラグ
|
/// 店舗向けビルドかどうかを判定するビルド時フラグ
|
||||||
///
|
///
|
||||||
/// ビルドコマンド:
|
/// ビルドコマンド:
|
||||||
|
|
@ -57,12 +53,9 @@ void main() async {
|
||||||
: box.get('migration_version', defaultValue: 0) as int; // 新規ユーザー: 未実行=v0
|
: box.get('migration_version', defaultValue: 0) as int; // 新規ユーザー: 未実行=v0
|
||||||
|
|
||||||
if (storedVersion < currentMigrationVersion) {
|
if (storedVersion < currentMigrationVersion) {
|
||||||
debugPrint('🚀 Running MigrationService (v$storedVersion → v$currentMigrationVersion)...');
|
|
||||||
await MigrationService.runMigration();
|
await MigrationService.runMigration();
|
||||||
await box.put('migration_version', currentMigrationVersion);
|
await box.put('migration_version', currentMigrationVersion);
|
||||||
await box.put('migration_completed', true); // 旧フラグも維持(後方互換)
|
await box.put('migration_completed', true); // 旧フラグも維持(後方互換)
|
||||||
} else {
|
|
||||||
debugPrint('✅ Migration up to date (v$storedVersion). Skipping.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ AI解析キャッシュは使うときに初期化する(Lazy initialization)
|
// ✅ AI解析キャッシュは使うときに初期化する(Lazy initialization)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import '../services/gemini_service.dart';
|
||||||
|
|
||||||
|
/// GeminiService のシングルトンプロバイダー
|
||||||
|
///
|
||||||
|
/// アプリ全体で同一インスタンスを共有する。
|
||||||
|
/// レート制限の状態(_lastApiCallTime)がインスタンス間で共有されるため、
|
||||||
|
/// 複数画面から同時に呼び出しても連打防止が正しく機能する。
|
||||||
|
///
|
||||||
|
/// 使用例:
|
||||||
|
/// ```dart
|
||||||
|
/// final geminiService = ref.read(geminiServiceProvider);
|
||||||
|
/// final result = await geminiService.analyzeSakeLabel(paths);
|
||||||
|
/// ```
|
||||||
|
final geminiServiceProvider = Provider<GeminiService>((ref) {
|
||||||
|
return GeminiService();
|
||||||
|
});
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:uuid/uuid.dart';
|
||||||
import 'package:gal/gal.dart';
|
import 'package:gal/gal.dart';
|
||||||
|
|
||||||
import '../services/gemini_service.dart';
|
import '../services/gemini_service.dart';
|
||||||
|
import '../providers/gemini_provider.dart';
|
||||||
import '../services/gemini_exceptions.dart';
|
import '../services/gemini_exceptions.dart';
|
||||||
import '../services/image_compression_service.dart';
|
import '../services/image_compression_service.dart';
|
||||||
import '../services/gamification_service.dart'; // Badge check
|
import '../services/gamification_service.dart'; // Badge check
|
||||||
|
|
@ -372,19 +373,21 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
Future<void> _analyzeImages() async {
|
Future<void> _analyzeImages() async {
|
||||||
if (_capturedImages.isEmpty) return;
|
if (_capturedImages.isEmpty) return;
|
||||||
|
|
||||||
|
// async gap 前に context 依存オブジェクトをキャプチャ
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
|
||||||
final isOnline = await NetworkService.isOnline();
|
final isOnline = await NetworkService.isOnline();
|
||||||
if (!isOnline) {
|
if (!isOnline) {
|
||||||
// オフライン時: Draft として保存
|
// オフライン時: Draft として保存
|
||||||
debugPrint('📴 Offline detected: Saving as draft...');
|
debugPrint('Offline detected: Saving as draft...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🔧 FIX: 複数画像をすべて保存
|
|
||||||
await DraftService.saveDraft(_capturedImages);
|
await DraftService.saveDraft(_capturedImages);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// ユーザーに通知
|
messenger.showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -407,13 +410,12 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// カメラ画面を閉じる
|
navigator.pop();
|
||||||
Navigator.of(context).pop();
|
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ Draft save error: $e');
|
debugPrint('Draft save error: $e');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(content: Text('Draft保存エラー: $e')),
|
SnackBar(content: Text('Draft保存エラー: $e')),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
@ -421,7 +423,6 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
// オンライン時: 通常の解析フロー
|
// オンライン時: 通常の解析フロー
|
||||||
// Show AnalyzingDialog
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -432,7 +433,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
try {
|
try {
|
||||||
// Direct Gemini Vision Analysis (OCR removed for app size reduction)
|
// Direct Gemini Vision Analysis (OCR removed for app size reduction)
|
||||||
debugPrint('📸 Starting Gemini Vision Direct Analysis for ${_capturedImages.length} images');
|
debugPrint('📸 Starting Gemini Vision Direct Analysis for ${_capturedImages.length} images');
|
||||||
final geminiService = GeminiService();
|
final geminiService = ref.read(geminiServiceProvider);
|
||||||
final result = await geminiService.analyzeSakeLabel(_capturedImages);
|
final result = await geminiService.analyzeSakeLabel(_capturedImages);
|
||||||
|
|
||||||
// Create SakeItem (Schema v2.0)
|
// Create SakeItem (Schema v2.0)
|
||||||
|
|
@ -504,11 +505,8 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
// Close Dialog
|
navigator.pop(); // Close AnalyzingDialog
|
||||||
Navigator.of(context).pop();
|
navigator.pop(); // Close Camera Screen (Return to Home)
|
||||||
|
|
||||||
// Close Camera Screen (Return to Home)
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
|
|
||||||
// Success Message (with EXP/Level Up/Badge info)
|
// Success Message (with EXP/Level Up/Badge info)
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
@ -554,28 +552,28 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: messageWidgets,
|
children: messageWidgets,
|
||||||
),
|
),
|
||||||
duration: Duration(seconds: newBadges.isNotEmpty ? 6 : 4), // Longer for badges
|
duration: Duration(seconds: newBadges.isNotEmpty ? 6 : 4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop(); // Close AnalyzingDialog
|
navigator.pop(); // Close AnalyzingDialog
|
||||||
|
|
||||||
// AIサーバー混雑(503)→ ドラフト保存してオフライン時と同じ扱いに
|
// AIサーバー混雑(503)→ ドラフト保存してオフライン時と同じ扱いに
|
||||||
if (e is GeminiCongestionException) {
|
if (e is GeminiCongestionException) {
|
||||||
try {
|
try {
|
||||||
await DraftService.saveDraft(_capturedImages);
|
await DraftService.saveDraft(_capturedImages);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(context).pop(); // Close camera screen
|
navigator.pop(); // Close camera screen
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
messenger.showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
@ -598,9 +596,8 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// ドラフト保存も失敗した場合のみエラー表示
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
messenger.showSnackBar(
|
||||||
const SnackBar(content: Text('解析もドラフト保存も失敗しました。再試行してください。')),
|
const SnackBar(content: Text('解析もドラフト保存も失敗しました。再試行してください。')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -616,7 +613,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
}
|
}
|
||||||
|
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
messenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('解析エラー: $e'),
|
content: Text('解析エラー: $e'),
|
||||||
duration: const Duration(seconds: 5),
|
duration: const Duration(seconds: 5),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import '../models/sake_item.dart';
|
import '../models/sake_item.dart';
|
||||||
import '../providers/sake_list_provider.dart';
|
import '../providers/sake_list_provider.dart';
|
||||||
import '../services/gemini_service.dart';
|
import '../services/gemini_service.dart';
|
||||||
|
import '../providers/gemini_provider.dart';
|
||||||
|
|
||||||
class DevMenuScreen extends ConsumerWidget {
|
class DevMenuScreen extends ConsumerWidget {
|
||||||
const DevMenuScreen({super.key});
|
const DevMenuScreen({super.key});
|
||||||
|
|
@ -198,7 +199,7 @@ class DevMenuScreen extends ConsumerWidget {
|
||||||
final box = Hive.box<SakeItem>('sake_items');
|
final box = Hive.box<SakeItem>('sake_items');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final gemini = GeminiService();
|
final gemini = ref.read(geminiServiceProvider);
|
||||||
|
|
||||||
for (final item in targets) {
|
for (final item in targets) {
|
||||||
if (item.displayData.imagePaths.isEmpty) continue;
|
if (item.displayData.imagePaths.isEmpty) continue;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import '../models/sake_item.dart';
|
import '../models/sake_item.dart';
|
||||||
import '../services/gemini_service.dart';
|
import '../services/gemini_service.dart';
|
||||||
|
import '../providers/gemini_provider.dart';
|
||||||
import '../services/sake_recommendation_service.dart';
|
import '../services/sake_recommendation_service.dart';
|
||||||
import '../widgets/analyzing_dialog.dart';
|
import '../widgets/analyzing_dialog.dart';
|
||||||
import '../widgets/sake_3d_carousel_with_reason.dart';
|
import '../widgets/sake_3d_carousel_with_reason.dart';
|
||||||
|
|
@ -331,11 +332,8 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
existingPaths.add(path);
|
existingPaths.add(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// mounted チェック後に context 依存オブジェクトをキャプチャ(async gap 対策)
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
final nav = Navigator.of(context);
|
final nav = Navigator.of(context);
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
|
||||||
if (existingPaths.isEmpty) {
|
if (existingPaths.isEmpty) {
|
||||||
|
|
@ -348,15 +346,13 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
setState(() => _isAnalyzing = true);
|
setState(() => _isAnalyzing = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context, // ignore: use_build_context_synchronously
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => const AnalyzingDialog(),
|
builder: (context) => const AnalyzingDialog(),
|
||||||
);
|
);
|
||||||
|
|
||||||
final geminiService = GeminiService();
|
final geminiService = ref.read(geminiServiceProvider);
|
||||||
// forceRefresh: true でキャッシュを無視して再解析
|
|
||||||
final result = await geminiService.analyzeSakeLabel(existingPaths, forceRefresh: true);
|
final result = await geminiService.analyzeSakeLabel(existingPaths, forceRefresh: true);
|
||||||
|
|
||||||
final newItem = _sake.copyWith(
|
final newItem = _sake.copyWith(
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import '../models/sake_item.dart';
|
|
||||||
import '../models/user_profile.dart';
|
|
||||||
import '../models/menu_settings.dart';
|
|
||||||
import '../services/migration_service.dart';
|
|
||||||
import 'main_screen.dart';
|
|
||||||
|
|
||||||
class SplashScreen extends ConsumerStatefulWidget {
|
|
||||||
const SplashScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<SplashScreen> createState() => _SplashScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SplashScreenState extends ConsumerState<SplashScreen> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_initializeData();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _initializeData() async {
|
|
||||||
// Artificial delay for branding (optional, keep it minimal)
|
|
||||||
// await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Initialize Hive
|
|
||||||
await Hive.initFlutter();
|
|
||||||
|
|
||||||
// Register Adapters
|
|
||||||
Hive.registerAdapter(SakeItemAdapter());
|
|
||||||
Hive.registerAdapter(UserProfileAdapter());
|
|
||||||
Hive.registerAdapter(MenuSettingsAdapter());
|
|
||||||
// Phase 0 New Adapters
|
|
||||||
Hive.registerAdapter(DisplayDataAdapter());
|
|
||||||
Hive.registerAdapter(HiddenSpecsAdapter());
|
|
||||||
Hive.registerAdapter(UserDataAdapter());
|
|
||||||
Hive.registerAdapter(GamificationAdapter());
|
|
||||||
Hive.registerAdapter(MetadataAdapter());
|
|
||||||
Hive.registerAdapter(ItemTypeAdapter());
|
|
||||||
|
|
||||||
// Open all boxes (Parallel Execution)
|
|
||||||
final results = await Future.wait([
|
|
||||||
Hive.openBox('settings'),
|
|
||||||
Hive.openBox<UserProfile>('user_profile'),
|
|
||||||
Hive.openBox<SakeItem>('sake_items'),
|
|
||||||
Hive.openBox<MenuSettings>('menu_settings'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
final settingsBox = results[0];
|
|
||||||
|
|
||||||
// Run Phase 0 Migration (Only once)
|
|
||||||
final migrationCompleted = settingsBox.get('migration_completed', defaultValue: false);
|
|
||||||
if (!migrationCompleted) {
|
|
||||||
debugPrint('🚀 Running MigrationService...');
|
|
||||||
await MigrationService.runMigration();
|
|
||||||
await settingsBox.put('migration_completed', true);
|
|
||||||
} else {
|
|
||||||
debugPrint('✅ Migration already completed. Skipping.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
// Navigate to MainScreen
|
|
||||||
Navigator.of(context).pushReplacement(
|
|
||||||
PageRouteBuilder(
|
|
||||||
pageBuilder: (context, animation, secondaryAnimation) => const MainScreen(),
|
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
|
||||||
return FadeTransition(opacity: animation, child: child);
|
|
||||||
},
|
|
||||||
transitionDuration: const Duration(milliseconds: 500),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('❌ Initialization Error: $e');
|
|
||||||
// Show error UI or retry logic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
backgroundColor: Colors.white, // Match brand color
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// App Logo
|
|
||||||
const Text(
|
|
||||||
'🍶',
|
|
||||||
style: TextStyle(fontSize: 80),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
'Ponshu Room',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.grey[800],
|
|
||||||
letterSpacing: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 48),
|
|
||||||
// Subtle loading indicator
|
|
||||||
SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -69,6 +69,7 @@ class AnalysisCacheService {
|
||||||
/// キャッシュから取得
|
/// キャッシュから取得
|
||||||
///
|
///
|
||||||
/// 戻り値: キャッシュがあれば解析結果、なければ null
|
/// 戻り値: キャッシュがあれば解析結果、なければ null
|
||||||
|
/// タイムスタンプ付き新形式と旧形式(直接 JSON)の両方に対応
|
||||||
static Future<SakeAnalysisResult?> getCached(String imageHash) async {
|
static Future<SakeAnalysisResult?> getCached(String imageHash) async {
|
||||||
await init();
|
await init();
|
||||||
if (_box == null) return null;
|
if (_box == null) return null;
|
||||||
|
|
@ -76,33 +77,43 @@ class AnalysisCacheService {
|
||||||
try {
|
try {
|
||||||
final jsonString = _box!.get(imageHash);
|
final jsonString = _box!.get(imageHash);
|
||||||
if (jsonString == null) {
|
if (jsonString == null) {
|
||||||
debugPrint('🔍 Cache MISS: $imageHash');
|
debugPrint('Cache MISS: $imageHash');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final jsonMap = jsonDecode(jsonString) as Map<String, dynamic>;
|
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||||
debugPrint('✅ Cache HIT: ${jsonMap['name'] ?? 'Unknown'} ($imageHash)');
|
|
||||||
return SakeAnalysisResult.fromJson(jsonMap);
|
// 新形式: { data: {...}, savedAt: "..." }
|
||||||
|
if (decoded.containsKey('data') && decoded.containsKey('savedAt')) {
|
||||||
|
debugPrint('Cache HIT: ${decoded['data']['name'] ?? 'Unknown'}');
|
||||||
|
return SakeAnalysisResult.fromJson(decoded['data'] as Map<String, dynamic>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 旧形式(後方互換): 直接 SakeAnalysisResult の JSON
|
||||||
|
debugPrint('Cache HIT (legacy): ${decoded['name'] ?? 'Unknown'}');
|
||||||
|
return SakeAnalysisResult.fromJson(decoded);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ Cache read error: $e');
|
debugPrint('Cache read error: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// キャッシュに保存
|
/// キャッシュに保存
|
||||||
///
|
///
|
||||||
/// 解析結果をJSON化してHiveに永続化
|
/// 解析結果を JSON 化してタイムスタンプ付きで Hive に永続化
|
||||||
static Future<void> saveCache(String imageHash, SakeAnalysisResult result) async {
|
static Future<void> saveCache(String imageHash, SakeAnalysisResult result) async {
|
||||||
await init();
|
await init();
|
||||||
if (_box == null) return;
|
if (_box == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final jsonMap = result.toJson();
|
final entry = {
|
||||||
final jsonString = jsonEncode(jsonMap);
|
'data': result.toJson(),
|
||||||
await _box!.put(imageHash, jsonString);
|
'savedAt': DateTime.now().toIso8601String(),
|
||||||
debugPrint('💾 Cache SAVED: ${result.name ?? 'Unknown'} ($imageHash)');
|
};
|
||||||
|
await _box!.put(imageHash, jsonEncode(entry));
|
||||||
|
debugPrint('Cache saved: ${result.name ?? 'Unknown'}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ Cache save error: $e');
|
debugPrint('Cache save error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,13 +133,37 @@ class AnalysisCacheService {
|
||||||
return _box?.length ?? 0;
|
return _box?.length ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// キャッシュの有効期限チェック(将来実装)
|
/// 30日以上経過したキャッシュエントリを削除
|
||||||
///
|
///
|
||||||
/// 現在は永続キャッシュだが、将来的に有効期限を設定する場合:
|
/// アプリ起動時またはバックグラウンドで呼び出す。
|
||||||
/// - 30日経過したキャッシュは削除
|
/// 旧形式エントリ(タイムスタンプなし)は対象外として保持する。
|
||||||
/// - 日本酒の仕様変更(リニューアル)に対応
|
static Future<void> cleanupExpired({int ttlDays = 30}) async {
|
||||||
static Future<void> cleanupExpired() async {
|
await init();
|
||||||
// TODO: キャッシュにタイムスタンプを追加し、30日以上古いエントリを削除する
|
if (_box == null) return;
|
||||||
|
|
||||||
|
final cutoff = DateTime.now().subtract(Duration(days: ttlDays));
|
||||||
|
final keysToDelete = <dynamic>[];
|
||||||
|
|
||||||
|
for (final key in _box!.keys) {
|
||||||
|
try {
|
||||||
|
final jsonString = _box!.get(key as String);
|
||||||
|
if (jsonString == null) continue;
|
||||||
|
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
|
||||||
|
if (!decoded.containsKey('savedAt')) continue; // 旧形式は対象外
|
||||||
|
|
||||||
|
final savedAt = DateTime.tryParse(decoded['savedAt'] as String? ?? '');
|
||||||
|
if (savedAt != null && savedAt.isBefore(cutoff)) {
|
||||||
|
keysToDelete.add(key);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// 読み込みエラーのエントリは無視
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keysToDelete.isNotEmpty) {
|
||||||
|
await _box!.deleteAll(keysToDelete);
|
||||||
|
debugPrint('Cache cleanup: ${keysToDelete.length} expired entries removed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
@ -162,25 +197,29 @@ class AnalysisCacheService {
|
||||||
|
|
||||||
/// 銘柄名をインデックスに登録
|
/// 銘柄名をインデックスに登録
|
||||||
///
|
///
|
||||||
/// 解析結果のキャッシュ保存後に呼び出す
|
/// [forceUpdate] = true のとき(再解析など)は既存エントリを上書きする。
|
||||||
/// 既にインデックスがある場合は上書きしない(最初の結果を優先)
|
/// false のときは最初の結果を優先し、上書きしない。
|
||||||
static Future<void> registerBrandIndex(String? brandName, String imageHash) async {
|
static Future<void> registerBrandIndex(
|
||||||
|
String? brandName,
|
||||||
|
String imageHash, {
|
||||||
|
bool forceUpdate = false,
|
||||||
|
}) async {
|
||||||
if (brandName == null || brandName.isEmpty) return;
|
if (brandName == null || brandName.isEmpty) return;
|
||||||
await init();
|
await init();
|
||||||
if (_brandIndexBox == null) return;
|
if (_brandIndexBox == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final normalized = _normalizeBrandName(brandName);
|
final normalized = _normalizeBrandName(brandName);
|
||||||
|
final exists = _brandIndexBox!.containsKey(normalized);
|
||||||
|
|
||||||
// 既にインデックスがある場合は上書きしない(最初の結果を優先)
|
if (!exists || forceUpdate) {
|
||||||
if (!_brandIndexBox!.containsKey(normalized)) {
|
|
||||||
await _brandIndexBox!.put(normalized, imageHash);
|
await _brandIndexBox!.put(normalized, imageHash);
|
||||||
debugPrint('📝 Brand index registered: $brandName → $imageHash');
|
debugPrint('Brand index ${exists ? "updated" : "registered"}: $brandName');
|
||||||
} else {
|
} else {
|
||||||
debugPrint('ℹ️ Brand index already exists: $brandName (skipped)');
|
debugPrint('Brand index already exists: $brandName (skipped)');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ Brand index registration error: $e');
|
debugPrint('Brand index registration error: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -284,8 +284,12 @@ class GeminiService {
|
||||||
if (imagePaths.isNotEmpty) {
|
if (imagePaths.isNotEmpty) {
|
||||||
final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths);
|
final imageHash = await AnalysisCacheService.computeCombinedHash(imagePaths);
|
||||||
await AnalysisCacheService.saveCache(imageHash, result);
|
await AnalysisCacheService.saveCache(imageHash, result);
|
||||||
// 4. 銘柄名インデックスに登録(v1.0.15: チャート一貫性向上)
|
// 4. 銘柄名インデックスに登録(forceRefresh 時は誤認識結果を上書き)
|
||||||
await AnalysisCacheService.registerBrandIndex(result.name, imageHash);
|
await AnalysisCacheService.registerBrandIndex(
|
||||||
|
result.name,
|
||||||
|
imageHash,
|
||||||
|
forceUpdate: forceRefresh,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt > 0) debugPrint('Succeeded on attempt $attempt (model: $modelName)');
|
if (attempt > 0) debugPrint('Succeeded on attempt $attempt (model: $modelName)');
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.30+37
|
version: 1.0.31+38
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.1
|
sdk: ^3.10.1
|
||||||
|
|
@ -97,8 +97,8 @@ flutter_launcher_icons:
|
||||||
web:
|
web:
|
||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/images/app_icon.png"
|
image_path: "assets/images/app_icon.png"
|
||||||
background_color: "#hex_code"
|
background_color: "#FDFAF5"
|
||||||
theme_color: "#hex_code"
|
theme_color: "#4A3B32"
|
||||||
windows:
|
windows:
|
||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/images/app_icon.png"
|
image_path: "assets/images/app_icon.png"
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,24 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <gal/gal_plugin_c_api.h>
|
#include <gal/gal_plugin_c_api.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
GalPluginCApiRegisterWithRegistrar(
|
GalPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
registry->GetRegistrarForPlugin("GalPluginCApi"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
connectivity_plus
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
gal
|
gal
|
||||||
printing
|
printing
|
||||||
|
share_plus
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue