Compare commits
2 Commits
30c15b553d
...
2b90756417
| Author | SHA1 | Date |
|---|---|---|
|
|
2b90756417 | |
|
|
fedfc6fa62 |
|
|
@ -51,15 +51,22 @@ void main() async {
|
|||
Hive.openBox<MenuSettings>('menu_settings'),
|
||||
]);
|
||||
|
||||
// Run Phase 0 Migration (Only once)
|
||||
// Migration — バージョン番号で管理(単一フラグだと将来の追加マイグレーションが走らないため)
|
||||
// migration_completed=true の旧ユーザーはバージョン 1 扱いとして互換性を維持する
|
||||
const int currentMigrationVersion = 1;
|
||||
final box = Hive.box('settings');
|
||||
final migrationCompleted = box.get('migration_completed', defaultValue: false);
|
||||
if (!migrationCompleted) {
|
||||
debugPrint('🚀 Running MigrationService...');
|
||||
final legacyCompleted = box.get('migration_completed', defaultValue: false) as bool;
|
||||
final storedVersion = legacyCompleted
|
||||
? box.get('migration_version', defaultValue: 1) as int // 旧ユーザー: 既に完了済み=v1
|
||||
: box.get('migration_version', defaultValue: 0) as int; // 新規ユーザー: 未実行=v0
|
||||
|
||||
if (storedVersion < currentMigrationVersion) {
|
||||
debugPrint('🚀 Running MigrationService (v$storedVersion → v$currentMigrationVersion)...');
|
||||
await MigrationService.runMigration();
|
||||
await box.put('migration_completed', true);
|
||||
await box.put('migration_version', currentMigrationVersion);
|
||||
await box.put('migration_completed', true); // 旧フラグも維持(後方互換)
|
||||
} else {
|
||||
debugPrint('✅ Migration already completed. Skipping.');
|
||||
debugPrint('✅ Migration up to date (v$storedVersion). Skipping.');
|
||||
}
|
||||
|
||||
// ✅ AI解析キャッシュは使うときに初期化する(Lazy initialization)
|
||||
|
|
|
|||
|
|
@ -157,11 +157,12 @@ class SakeItem extends HiveObject {
|
|||
);
|
||||
}
|
||||
|
||||
// Allow setting for UI updates
|
||||
// Allow setting for UI updates (呼び出し元で必ず await sakeItem.save() すること)
|
||||
set displayData(DisplayData val) {
|
||||
_displayData = val;
|
||||
save(); // Auto-save on set? Or just update memory. HiveObject usually requires save().
|
||||
// Better to just update memory here.
|
||||
// save() はここで呼ばない。setter は同期のため await できず、
|
||||
// unawaited save() はデータ消失リスクがある。
|
||||
// 呼び出し元(sakenowa_auto_matching_service.dart)で明示的に await save() する。
|
||||
}
|
||||
|
||||
HiddenSpecs get hiddenSpecs {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import 'package:uuid/uuid.dart';
|
|||
import 'package:gal/gal.dart';
|
||||
|
||||
import '../services/gemini_service.dart';
|
||||
import '../services/gemini_exceptions.dart';
|
||||
import '../services/image_compression_service.dart';
|
||||
import '../services/gamification_service.dart'; // Badge check
|
||||
import '../services/network_service.dart';
|
||||
|
|
@ -568,10 +569,8 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
|||
if (mounted) {
|
||||
Navigator.of(context).pop(); // Close AnalyzingDialog
|
||||
|
||||
final errStr = e.toString();
|
||||
|
||||
// AIサーバー混雑(503)→ ドラフト保存してオフライン時と同じ扱いに
|
||||
if (errStr.contains('[CONGESTION]')) {
|
||||
if (e is GeminiCongestionException) {
|
||||
try {
|
||||
await DraftService.saveDraft(_capturedImages);
|
||||
if (!mounted) return;
|
||||
|
|
@ -609,6 +608,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
|||
}
|
||||
|
||||
// Quota エラー(429)→ ロックアウト
|
||||
final errStr = e.toString();
|
||||
if (errStr.contains('Quota') || errStr.contains('429')) {
|
||||
setState(() {
|
||||
_quotaLockoutTime = DateTime.now().add(const Duration(minutes: 1));
|
||||
|
|
|
|||
|
|
@ -323,20 +323,40 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
|
||||
if (_sake.displayData.imagePaths.isEmpty) return;
|
||||
|
||||
// 実ファイルの存在確認(削除済み画像でAPIエラーになるのを防ぐ)
|
||||
final existingPaths = <String>[];
|
||||
for (final path in _sake.displayData.imagePaths) {
|
||||
if (await File(path).exists()) {
|
||||
existingPaths.add(path);
|
||||
}
|
||||
}
|
||||
// mounted チェック後に context 依存オブジェクトをキャプチャ(async gap 対策)
|
||||
if (!mounted) return;
|
||||
// ignore: use_build_context_synchronously
|
||||
final nav = Navigator.of(context);
|
||||
// ignore: use_build_context_synchronously
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
|
||||
if (existingPaths.isEmpty) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('画像ファイルが見つかりません。再解析できません。')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isAnalyzing = true);
|
||||
|
||||
try {
|
||||
// ignore: use_build_context_synchronously
|
||||
showDialog(
|
||||
context: context,
|
||||
context: context, // ignore: use_build_context_synchronously
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const AnalyzingDialog(),
|
||||
);
|
||||
|
||||
final geminiService = GeminiService();
|
||||
// 既存の画像パスを使用(すでに圧縮済みの想定)
|
||||
// 注: 既存のデータは未圧縮の可能性があるため、一括圧縮機能で対応
|
||||
// forceRefresh: true でキャッシュを無視して再解析
|
||||
final result = await geminiService.analyzeSakeLabel(_sake.displayData.imagePaths, forceRefresh: true);
|
||||
final result = await geminiService.analyzeSakeLabel(existingPaths, forceRefresh: true);
|
||||
|
||||
final newItem = _sake.copyWith(
|
||||
name: result.name ?? _sake.displayData.displayName,
|
||||
|
|
@ -365,17 +385,17 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
_sake = newItem;
|
||||
});
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // Close dialog
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
if (mounted) {
|
||||
nav.pop(); // Close dialog
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('再解析が完了しました')),
|
||||
);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context); // Close dialog (safely pops the top route, hopefully the dialog)
|
||||
|
||||
if (mounted) {
|
||||
nav.pop(); // Close dialog
|
||||
|
||||
// Check for Quota Error to set Lockout
|
||||
if (e.toString().contains('Quota') || e.toString().contains('429')) {
|
||||
setState(() {
|
||||
|
|
@ -383,7 +403,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
|||
});
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('エラー: $e')),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,10 +217,10 @@ class DraftService {
|
|||
onProgress?.call(i + 1, total);
|
||||
await analyzeDraft(itemKey);
|
||||
successCount++;
|
||||
debugPrint('✅ [$i+1/$total] Success: ${draft.displayData.displayName}');
|
||||
debugPrint('✅ [${i+1}/$total] Success: ${draft.displayData.displayName}');
|
||||
} catch (e) {
|
||||
failedCount++;
|
||||
final errorMsg = '[$i+1/$total] ${draft.displayData.displayName}: $e';
|
||||
final errorMsg = '[${i+1}/$total] ${draft.displayData.displayName}: $e';
|
||||
errors.add(errorMsg);
|
||||
debugPrint('❌ $errorMsg');
|
||||
// 失敗してもループは継続(他のDraftを解析)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// Gemini API 専用例外クラス
|
||||
// 文字列マッチングではなく型で分岐できるようにする
|
||||
|
||||
/// API サーバーの混雑(503 UNAVAILABLE)でリトライを使い切った
|
||||
class GeminiCongestionException implements Exception {
|
||||
const GeminiCongestionException();
|
||||
|
||||
@override
|
||||
String toString() => 'GeminiCongestionException: AIサーバーが混雑しています。しばらく待ってから再試行してください。';
|
||||
}
|
||||
|
||||
/// API キーが空または無効
|
||||
class GeminiApiKeyException implements Exception {
|
||||
const GeminiApiKeyException(this.message);
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => 'GeminiApiKeyException: $message';
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'device_service.dart';
|
|||
import 'package:google_generative_ai/google_generative_ai.dart';
|
||||
import '../secrets.dart';
|
||||
import 'analysis_cache_service.dart';
|
||||
import 'gemini_exceptions.dart';
|
||||
|
||||
|
||||
class GeminiService {
|
||||
|
|
@ -349,8 +350,7 @@ $extractedText
|
|||
if (isLastAttempt || !is503) {
|
||||
// 最終試行 or 503以外のエラーはそのまま投げる
|
||||
if (is503) {
|
||||
// [CONGESTION] マーカー付きで投げる → camera_screen でドラフト保存へ
|
||||
throw Exception('[CONGESTION] AIサーバーが混雑しています。解析待ちとして保存します。');
|
||||
throw const GeminiCongestionException();
|
||||
}
|
||||
throw Exception('AI解析エラー(Direct): $e');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"date": "2026-04-06",
|
||||
"name": "Ponshu Room 1.0.24 (2026-04-09)",
|
||||
"version": "v1.0.24",
|
||||
"name": "Ponshu Room 1.0.25 (2026-04-10)",
|
||||
"version": "v1.0.25",
|
||||
"apks": {
|
||||
"eiji": {
|
||||
"lite": {
|
||||
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.24/ponshu_room_consumer_eiji.apk",
|
||||
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.25/ponshu_room_consumer_eiji.apk",
|
||||
"size_mb": 89,
|
||||
"filename": "ponshu_room_consumer_eiji.apk"
|
||||
}
|
||||
},
|
||||
"maita": {
|
||||
"lite": {
|
||||
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.24/ponshu_room_consumer_maita.apk",
|
||||
"url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.25/ponshu_room_consumer_maita.apk",
|
||||
"size_mb": 89,
|
||||
"filename": "ponshu_room_consumer_maita.apk"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue