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
|
||||
final settingsBox = Hive.box('settings');
|
||||
final List<String> currentOrder = (settingsBox.get('sake_sort_order') as List<dynamic>?)
|
||||
?.cast<String>() ?? [];
|
||||
final rawOrder = settingsBox.get('sake_sort_order');
|
||||
final List<String> currentOrder = (rawOrder is List)
|
||||
? rawOrder.whereType<String>().toList()
|
||||
: [];
|
||||
currentOrder.insert(0, sakeItem.id);
|
||||
await settingsBox.put('sake_sort_order', currentOrder);
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ class SakeDetailSliverAppBar extends StatelessWidget {
|
|||
final imageWidget = Image.file(
|
||||
File(sake.displayData.imagePaths[index]),
|
||||
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
|
||||
|
|
@ -121,6 +125,10 @@ class SakeDetailSliverAppBar extends StatelessWidget {
|
|||
? Image.file(
|
||||
File(sake.displayData.imagePaths.first),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: appColors.surfaceSubtle,
|
||||
child: Icon(LucideIcons.imageOff, size: 80, color: appColors.iconSubtle),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: appColors.surfaceSubtle,
|
||||
|
|
|
|||
|
|
@ -112,6 +112,12 @@ class _SakePhotoEditModalState extends State<SakePhotoEditModal> {
|
|||
width: 60,
|
||||
height: 60,
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -455,7 +455,10 @@ class BackupService {
|
|||
final sakeBox = Hive.box<SakeItem>('sake_items');
|
||||
|
||||
await sakeBox.clear();
|
||||
int restoredCount = 0;
|
||||
int skippedCount = 0;
|
||||
for (var itemData in sakeItemsJson) {
|
||||
try {
|
||||
final data = itemData as Map<String, dynamic>;
|
||||
|
||||
// JSONからSakeItemオブジェクトを再構築
|
||||
|
|
@ -467,21 +470,21 @@ class BackupService {
|
|||
prefecture: data['displayData']['prefecture'] as String,
|
||||
catchCopy: data['displayData']['catchCopy'] as String?,
|
||||
imagePaths: List<String>.from(data['displayData']['imagePaths'] as List),
|
||||
rating: data['displayData']['rating'] as double?,
|
||||
rating: (data['displayData']['rating'] as num?)?.toDouble(),
|
||||
),
|
||||
hiddenSpecs: HiddenSpecs(
|
||||
description: data['hiddenSpecs']['description'] as String?,
|
||||
tasteStats: Map<String, int>.from(data['hiddenSpecs']['tasteStats'] as Map),
|
||||
flavorTags: List<String>.from(data['hiddenSpecs']['flavorTags'] as List),
|
||||
sweetnessScore: data['hiddenSpecs']['sweetnessScore'] as double?,
|
||||
bodyScore: data['hiddenSpecs']['bodyScore'] as double?,
|
||||
sweetnessScore: (data['hiddenSpecs']['sweetnessScore'] as num?)?.toDouble(),
|
||||
bodyScore: (data['hiddenSpecs']['bodyScore'] as num?)?.toDouble(),
|
||||
),
|
||||
userData: UserData(
|
||||
isFavorite: data['userData']['isFavorite'] as bool,
|
||||
isUserEdited: data['userData']['isUserEdited'] as bool,
|
||||
price: data['userData']['price'] 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
|
||||
? Map<String, int>.from(data['userData']['priceVariants'] as Map)
|
||||
: null,
|
||||
|
|
@ -498,8 +501,13 @@ class BackupService {
|
|||
|
||||
// IDを保持するためにput()を使用(add()は新しいキーを生成してしまう)
|
||||
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更新のためにわずかに待機
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ name・brand を出力する直前に以下を確認してください:
|
|||
|
||||
// 3. デバイスID取得
|
||||
final deviceId = await DeviceService.getDeviceId();
|
||||
debugPrint('Device ID: $deviceId');
|
||||
if (kDebugMode) debugPrint('Device ID: $deviceId');
|
||||
|
||||
// 4. リクエスト作成
|
||||
final requestBody = jsonEncode({
|
||||
|
|
|
|||
|
|
@ -52,7 +52,12 @@ class _AddSetItemDialogState extends ConsumerState<AddSetItemDialog> {
|
|||
});
|
||||
}
|
||||
} 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(
|
||||
File(item.displayData.imagePaths.first),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Colors.grey[300],
|
||||
child: const Icon(Icons.broken_image, size: 50, color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: Colors.grey[300],
|
||||
|
|
|
|||
|
|
@ -164,6 +164,10 @@ class _Sake3DCarouselWithReasonState extends State<Sake3DCarouselWithReason> {
|
|||
? Image.file(
|
||||
File(rec.item.displayData.imagePaths.first),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Container(
|
||||
color: Colors.grey[300],
|
||||
child: Icon(LucideIcons.imageOff, size: 50, color: Colors.grey[600]),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
color: Colors.grey[300],
|
||||
|
|
|
|||
|
|
@ -70,7 +70,12 @@ class SakeSearchDelegate extends SearchDelegate {
|
|||
width: 40,
|
||||
height: 40,
|
||||
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),
|
||||
),
|
||||
title: Text(sake.displayData.displayName),
|
||||
|
|
|
|||
|
|
@ -286,6 +286,7 @@ class _SakenowaRankingSectionState extends ConsumerState<SakenowaRankingSection>
|
|||
Image.file(
|
||||
File(item.userImagePath!),
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(),
|
||||
)
|
||||
else
|
||||
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
|
||||
# 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.
|
||||
version: 1.0.44+51
|
||||
version: 1.0.45+52
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue