Compare commits

...

2 Commits

Author SHA1 Message Date
Ponshu Developer 2b90756417 chore: update download page to v1.0.25 2026-04-10 16:39:40 +09:00
Ponshu Developer fedfc6fa62 fix: code review fixes — data integrity, safety, architecture
C-2: draft_service — $i+1 → ${i+1} 文字列補間バグ修正
C-1: sake_item — setter内の unawaited save() を削除(呼び出し元で明示的に await)
H-2: sake_detail_screen — 再解析前に実ファイル存在チェック追加
M-4: gemini_exceptions.dart 新規作成、[CONGESTION]文字列マッチ→型チェックに変更
C-4: main.dart — migration_completed フラグ→ migration_version 番号管理に移行
     既存ユーザーのデータは migration_version=1 扱いで安全に互換維持

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 08:26:53 +09:00
8 changed files with 78 additions and 31 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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));

View File

@ -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,16 +385,16 @@ 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')) {
@ -383,7 +403,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
});
}
ScaffoldMessenger.of(context).showSnackBar(
messenger.showSnackBar(
SnackBar(content: Text('エラー: $e')),
);
}

View File

@ -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を解析

View File

@ -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';
}

View File

@ -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');
}

View File

@ -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"
}