fix(stability): エラーハンドリング強化・クラッシュ防止 (v1.0.45)
- backup_service: 復元ループを per-item try-catch に変更 一件のJSONパース失敗でも他のアイテムが正常に復元されるよう修正 rating/markup/score の num→double キャスト安全化も同時適用 - camera_analysis_mixin: cast<String>() を whereType<String>() に変更 旧Hiveデータや型不一致でも新規登録がクラッシュしなくなる - add_set_item_dialog: 空catchブロックにSnackBar通知を追加 画像選択失敗時にユーザーへフィードバックを表示するよう修正 - Image.file() errorBuilder を6ファイルに追加 sake_3d_carousel / sake_3d_carousel_with_reason / sake_search_delegate / sake_detail_sliver_app_bar (×2) / sake_photo_edit_modal / sakenowa_ranking_section — 画像ファイルが削除済みでも黒画面/クラッシュなし - gemini_service: Device ID の debugPrint を kDebugMode ガードに変更 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0fb4f6ea8b
commit
1bf59e02cc
|
|
@ -184,8 +184,10 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
||||||
|
|
||||||
// Prepend new item to sort order so it appears at the top
|
// Prepend new item to sort order so it appears at the top
|
||||||
final settingsBox = Hive.box('settings');
|
final settingsBox = Hive.box('settings');
|
||||||
final List<String> currentOrder = (settingsBox.get('sake_sort_order') as List<dynamic>?)
|
final rawOrder = settingsBox.get('sake_sort_order');
|
||||||
?.cast<String>() ?? [];
|
final List<String> currentOrder = (rawOrder is List)
|
||||||
|
? rawOrder.whereType<String>().toList()
|
||||||
|
: [];
|
||||||
currentOrder.insert(0, sakeItem.id);
|
currentOrder.insert(0, sakeItem.id);
|
||||||
await settingsBox.put('sake_sort_order', currentOrder);
|
await settingsBox.put('sake_sort_order', currentOrder);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,10 @@ class SakeDetailSliverAppBar extends StatelessWidget {
|
||||||
final imageWidget = Image.file(
|
final imageWidget = Image.file(
|
||||||
File(sake.displayData.imagePaths[index]),
|
File(sake.displayData.imagePaths[index]),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
child: const Icon(Icons.broken_image, size: 60, color: Colors.grey),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply Hero only to the first image for smooth transition from Grid/List
|
// Apply Hero only to the first image for smooth transition from Grid/List
|
||||||
|
|
@ -121,6 +125,10 @@ class SakeDetailSliverAppBar extends StatelessWidget {
|
||||||
? Image.file(
|
? Image.file(
|
||||||
File(sake.displayData.imagePaths.first),
|
File(sake.displayData.imagePaths.first),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
|
color: appColors.surfaceSubtle,
|
||||||
|
child: Icon(LucideIcons.imageOff, size: 80, color: appColors.iconSubtle),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
color: appColors.surfaceSubtle,
|
color: appColors.surfaceSubtle,
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,12 @@ class _SakePhotoEditModalState extends State<SakePhotoEditModal> {
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
child: const Icon(Icons.broken_image, size: 24, color: Colors.grey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,10 @@ class BackupService {
|
||||||
final sakeBox = Hive.box<SakeItem>('sake_items');
|
final sakeBox = Hive.box<SakeItem>('sake_items');
|
||||||
|
|
||||||
await sakeBox.clear();
|
await sakeBox.clear();
|
||||||
|
int restoredCount = 0;
|
||||||
|
int skippedCount = 0;
|
||||||
for (var itemData in sakeItemsJson) {
|
for (var itemData in sakeItemsJson) {
|
||||||
|
try {
|
||||||
final data = itemData as Map<String, dynamic>;
|
final data = itemData as Map<String, dynamic>;
|
||||||
|
|
||||||
// JSONからSakeItemオブジェクトを再構築
|
// JSONからSakeItemオブジェクトを再構築
|
||||||
|
|
@ -467,21 +470,21 @@ class BackupService {
|
||||||
prefecture: data['displayData']['prefecture'] as String,
|
prefecture: data['displayData']['prefecture'] as String,
|
||||||
catchCopy: data['displayData']['catchCopy'] as String?,
|
catchCopy: data['displayData']['catchCopy'] as String?,
|
||||||
imagePaths: List<String>.from(data['displayData']['imagePaths'] as List),
|
imagePaths: List<String>.from(data['displayData']['imagePaths'] as List),
|
||||||
rating: data['displayData']['rating'] as double?,
|
rating: (data['displayData']['rating'] as num?)?.toDouble(),
|
||||||
),
|
),
|
||||||
hiddenSpecs: HiddenSpecs(
|
hiddenSpecs: HiddenSpecs(
|
||||||
description: data['hiddenSpecs']['description'] as String?,
|
description: data['hiddenSpecs']['description'] as String?,
|
||||||
tasteStats: Map<String, int>.from(data['hiddenSpecs']['tasteStats'] as Map),
|
tasteStats: Map<String, int>.from(data['hiddenSpecs']['tasteStats'] as Map),
|
||||||
flavorTags: List<String>.from(data['hiddenSpecs']['flavorTags'] as List),
|
flavorTags: List<String>.from(data['hiddenSpecs']['flavorTags'] as List),
|
||||||
sweetnessScore: data['hiddenSpecs']['sweetnessScore'] as double?,
|
sweetnessScore: (data['hiddenSpecs']['sweetnessScore'] as num?)?.toDouble(),
|
||||||
bodyScore: data['hiddenSpecs']['bodyScore'] as double?,
|
bodyScore: (data['hiddenSpecs']['bodyScore'] as num?)?.toDouble(),
|
||||||
),
|
),
|
||||||
userData: UserData(
|
userData: UserData(
|
||||||
isFavorite: data['userData']['isFavorite'] as bool,
|
isFavorite: data['userData']['isFavorite'] as bool,
|
||||||
isUserEdited: data['userData']['isUserEdited'] as bool,
|
isUserEdited: data['userData']['isUserEdited'] as bool,
|
||||||
price: data['userData']['price'] as int?,
|
price: data['userData']['price'] as int?,
|
||||||
costPrice: data['userData']['costPrice'] as int?,
|
costPrice: data['userData']['costPrice'] as int?,
|
||||||
markup: data['userData']['markup'] as double,
|
markup: (data['userData']['markup'] as num).toDouble(),
|
||||||
priceVariants: data['userData']['priceVariants'] != null
|
priceVariants: data['userData']['priceVariants'] != null
|
||||||
? Map<String, int>.from(data['userData']['priceVariants'] as Map)
|
? Map<String, int>.from(data['userData']['priceVariants'] as Map)
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -498,8 +501,13 @@ class BackupService {
|
||||||
|
|
||||||
// IDを保持するためにput()を使用(add()は新しいキーを生成してしまう)
|
// IDを保持するためにput()を使用(add()は新しいキーを生成してしまう)
|
||||||
await sakeBox.put(item.id, item);
|
await sakeBox.put(item.id, item);
|
||||||
|
restoredCount++;
|
||||||
|
} catch (e) {
|
||||||
|
skippedCount++;
|
||||||
|
debugPrint('[RESTORE] Skipped malformed item: $e');
|
||||||
}
|
}
|
||||||
debugPrint('[RESTORE] SakeItems restored (${sakeItemsJson.length} items)');
|
}
|
||||||
|
debugPrint('[RESTORE] SakeItems restored ($restoredCount items, $skippedCount skipped)');
|
||||||
// UI更新のためにわずかに待機
|
// UI更新のためにわずかに待機
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ name・brand を出力する直前に以下を確認してください:
|
||||||
|
|
||||||
// 3. デバイスID取得
|
// 3. デバイスID取得
|
||||||
final deviceId = await DeviceService.getDeviceId();
|
final deviceId = await DeviceService.getDeviceId();
|
||||||
debugPrint('Device ID: $deviceId');
|
if (kDebugMode) debugPrint('Device ID: $deviceId');
|
||||||
|
|
||||||
// 4. リクエスト作成
|
// 4. リクエスト作成
|
||||||
final requestBody = jsonEncode({
|
final requestBody = jsonEncode({
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,12 @@ class _AddSetItemDialogState extends ConsumerState<AddSetItemDialog> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Handle error
|
debugPrint('Image pick error: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('画像の選択に失敗しました。再度お試しください。')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,10 @@ class _Sake3DCarouselState extends State<Sake3DCarousel> {
|
||||||
? Image.file(
|
? Image.file(
|
||||||
File(item.displayData.imagePaths.first),
|
File(item.displayData.imagePaths.first),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
child: const Icon(Icons.broken_image, size: 50, color: Colors.grey),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
color: Colors.grey[300],
|
color: Colors.grey[300],
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,10 @@ class _Sake3DCarouselWithReasonState extends State<Sake3DCarouselWithReason> {
|
||||||
? Image.file(
|
? Image.file(
|
||||||
File(rec.item.displayData.imagePaths.first),
|
File(rec.item.displayData.imagePaths.first),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => Container(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
child: Icon(LucideIcons.imageOff, size: 50, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
color: Colors.grey[300],
|
color: Colors.grey[300],
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,12 @@ class SakeSearchDelegate extends SearchDelegate {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: sake.displayData.imagePaths.isNotEmpty
|
child: sake.displayData.imagePaths.isNotEmpty
|
||||||
? Image.file(File(sake.displayData.imagePaths.first), fit: BoxFit.cover)
|
? Image.file(
|
||||||
|
File(sake.displayData.imagePaths.first),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) =>
|
||||||
|
const Icon(LucideIcons.imageOff),
|
||||||
|
)
|
||||||
: const Icon(LucideIcons.image),
|
: const Icon(LucideIcons.image),
|
||||||
),
|
),
|
||||||
title: Text(sake.displayData.displayName),
|
title: Text(sake.displayData.displayName),
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,7 @@ class _SakenowaRankingSectionState extends ConsumerState<SakenowaRankingSection>
|
||||||
Image.file(
|
Image.file(
|
||||||
File(item.userImagePath!),
|
File(item.userImagePath!),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
|
|
|
||||||
|
|
@ -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.44+51
|
version: 1.0.45+52
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.1
|
sdk: ^3.10.1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue