From 073e55cc51578d0488837d622952f965a908ed03 Mon Sep 17 00:00:00 2001 From: Ponshu Developer Date: Thu, 23 Apr 2026 22:32:46 +0900 Subject: [PATCH] chore: update download page to v1.0.49 --- lib/screens/pending_analysis_screen.dart | 9 ++ lib/screens/shop_settings_screen.dart | 21 ++--- lib/services/backup_service.dart | 68 ++++++++++++--- .../sake_detail/sake_detail_specs.dart | 6 +- .../settings/backup_settings_section.dart | 84 +++++++++++++++++-- web/download/releases.json | 8 +- 6 files changed, 161 insertions(+), 35 deletions(-) diff --git a/lib/screens/pending_analysis_screen.dart b/lib/screens/pending_analysis_screen.dart index 4e13d86..181efe3 100644 --- a/lib/screens/pending_analysis_screen.dart +++ b/lib/screens/pending_analysis_screen.dart @@ -358,6 +358,15 @@ class _PendingAnalysisScreenState extends ConsumerState { width: 60, height: 60, fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: appColors.divider, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(LucideIcons.image, color: appColors.iconSubtle), + ), ), ) : Container( diff --git a/lib/screens/shop_settings_screen.dart b/lib/screens/shop_settings_screen.dart index 7fccf60..0849b39 100644 --- a/lib/screens/shop_settings_screen.dart +++ b/lib/screens/shop_settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; import '../providers/theme_provider.dart'; +import '../theme/app_colors.dart'; import '../widgets/settings/display_settings_section.dart'; import '../widgets/settings/other_settings_section.dart'; import '../widgets/settings/backup_settings_section.dart'; @@ -22,7 +23,7 @@ class _ShopSettingsScreenState extends ConsumerState { @override Widget build(BuildContext context) { final userProfile = ref.watch(userProfileProvider); - final isDark = Theme.of(context).brightness == Brightness.dark; + final appColors = Theme.of(context).extension()!; return Scaffold( appBar: AppBar( @@ -35,25 +36,25 @@ class _ShopSettingsScreenState extends ConsumerState { // Business Config Section _buildSectionHeader(context, '価格設定', LucideIcons.briefcase), Card( - color: isDark ? const Color(0xFF1E1E1E) : null, + color: appColors.surfaceElevated, child: ListTile( - leading: Icon(LucideIcons.percent, color: isDark ? Colors.orange[300] : Theme.of(context).primaryColor), + leading: Icon(LucideIcons.percent, color: appColors.iconAccent), title: const Text('基本掛率'), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text('×', style: TextStyle( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.bold, fontSize: 16, - color: isDark ? Colors.grey[400] : Colors.grey[600], + color: appColors.textSecondary, )), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( - color: isDark ? Colors.grey[800] : Colors.grey[100], + color: appColors.surfaceSubtle, borderRadius: BorderRadius.circular(8), - border: Border.all(color: isDark ? Colors.grey[700]! : Colors.grey[300]!), + border: Border.all(color: appColors.divider), ), child: DropdownButton( value: userProfile.defaultMarkup, @@ -96,18 +97,18 @@ class _ShopSettingsScreenState extends ConsumerState { } Widget _buildSectionHeader(BuildContext context, String title, IconData icon) { - final isDark = Theme.of(context).brightness == Brightness.dark; + final appColors = Theme.of(context).extension()!; return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), child: Row( children: [ - Icon(icon, size: 20, color: isDark ? Colors.orange[300] : Theme.of(context).primaryColor), + Icon(icon, size: 20, color: appColors.iconAccent), const SizedBox(width: 8), Text( title, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, - color: isDark ? Colors.grey[300] : Theme.of(context).primaryColor, + color: appColors.textPrimary, ), ), ], diff --git a/lib/services/backup_service.dart b/lib/services/backup_service.dart index 7b2c5ad..682ee36 100644 --- a/lib/services/backup_service.dart +++ b/lib/services/backup_service.dart @@ -11,6 +11,15 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import '../models/sake_item.dart'; +/// 復元前の安全バックアップ作成に失敗したことを示す例外。 +/// UI 側でユーザーに中断/続行を選ばせるために使用する。 +class PreRestoreBackupException implements Exception { + const PreRestoreBackupException(); + + @override + String toString() => 'PreRestoreBackupException: pre-restore safety backup failed'; +} + /// Google Driveへのバックアップ・復元を管理するサービス /// /// 【主な機能】 @@ -325,8 +334,11 @@ class BackupService { final driveApi = drive.DriveApi(authClient); - // 3. 現在のデータを退避 - await _createPreRestoreBackup(); + // 3. 現在のデータを退避(失敗したら呼び出し元に通知して中断させる) + final preBackupOk = await _createPreRestoreBackup(); + if (!preBackupOk) { + throw const PreRestoreBackupException(); + } // 4. Google Driveからダウンロード final zipFile = await _downloadFromDrive(driveApi); @@ -342,26 +354,55 @@ class BackupService { await zipFile.delete(); return success; + } on PreRestoreBackupException { + rethrow; } catch (error) { debugPrint('[RESTORE] Restore error: $error'); return false; } } - /// 復元前に現在のデータを退避 - Future _createPreRestoreBackup() async { + /// 復元前に現在のデータを退避する。成功したら true、失敗したら false を返す。 + Future _createPreRestoreBackup() async { try { final tempDir = await getTemporaryDirectory(); final backupPath = path.join(tempDir.path, 'pre_restore_backup.zip'); final zipFile = await _createBackupZip(); - if (zipFile != null) { - await zipFile.copy(backupPath); - await zipFile.delete(); - debugPrint('[RESTORE] Pre-restore backup saved: $backupPath'); + if (zipFile == null) { + debugPrint('[RESTORE] Pre-restore backup: ZIP creation failed'); + return false; } + await zipFile.copy(backupPath); + await zipFile.delete(); + debugPrint('[RESTORE] Pre-restore backup saved: $backupPath'); + return true; } catch (error) { debugPrint('[RESTORE] Pre-restore backup error: $error'); + return false; + } + } + + /// 安全バックアップをスキップして復元を強行する(ユーザーが警告を承諾した場合)。 + Future restoreBackupSkippingPreBackup() async { + try { + final account = _googleSignIn.currentUser; + if (account == null) return false; + + final authClient = await _googleSignIn.authenticatedClient(); + if (authClient == null) return false; + + final driveApi = drive.DriveApi(authClient); + + final zipFile = await _downloadFromDrive(driveApi); + if (zipFile == null) return false; + + final success = await _restoreFromZip(zipFile); + await zipFile.delete(); + return success; + } catch (error) { + debugPrint('[RESTORE] Force restore error: $error'); + return false; } } @@ -393,7 +434,14 @@ class BackupService { // 3. ストリームをファイルに書き込み final sink = downloadFile.openWrite(); - await media.stream.pipe(sink); + await media.stream.pipe(sink).timeout( + const Duration(minutes: 3), + onTimeout: () { + sink.close(); + try { downloadFile.deleteSync(); } catch (_) {} + throw TimeoutException('Backup download timed out after 3 minutes'); + }, + ); debugPrint('[RESTORE] Download complete: $downloadPath'); return downloadFile; @@ -484,7 +532,7 @@ class BackupService { isUserEdited: data['userData']['isUserEdited'] as bool, price: data['userData']['price'] as int?, costPrice: data['userData']['costPrice'] as int?, - markup: (data['userData']['markup'] as num).toDouble(), + markup: (data['userData']['markup'] as num?)?.toDouble() ?? 3.0, priceVariants: data['userData']['priceVariants'] != null ? Map.from(data['userData']['priceVariants'] as Map) : null, diff --git a/lib/widgets/sake_detail/sake_detail_specs.dart b/lib/widgets/sake_detail/sake_detail_specs.dart index fc69194..bffb7e3 100644 --- a/lib/widgets/sake_detail/sake_detail_specs.dart +++ b/lib/widgets/sake_detail/sake_detail_specs.dart @@ -272,6 +272,7 @@ class _SakeDetailSpecsState extends State { _isEditing, suffixIcon: LucideIcons.calendar, onSuffixTap: () => _showDatePicker(context), + helperText: '例: 2023-10', ), ], ), @@ -303,10 +304,11 @@ class _SakeDetailSpecsState extends State { void _showDatePicker(BuildContext context) { if (!_isEditing) return; - // Parse current value or use now + // Parse current value or use now (AI出力 "2023.10" とユーザー入力 "2023-10" の両形式に対応) DateTime initialDate = DateTime.now(); try { - final parts = _manufacturingController.text.split('-'); + final normalized = _manufacturingController.text.replaceAll('.', '-'); + final parts = normalized.split('-'); if (parts.length >= 2) { final year = int.parse(parts[0]); final month = int.parse(parts[1]); diff --git a/lib/widgets/settings/backup_settings_section.dart b/lib/widgets/settings/backup_settings_section.dart index e19f079..54e8b8e 100644 --- a/lib/widgets/settings/backup_settings_section.dart +++ b/lib/widgets/settings/backup_settings_section.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lucide_icons/lucide_icons.dart'; +import '../../providers/sake_list_provider.dart'; +import '../../providers/theme_provider.dart'; import '../../services/backup_service.dart'; import '../../theme/app_colors.dart'; -class BackupSettingsSection extends StatefulWidget { +class BackupSettingsSection extends ConsumerStatefulWidget { final String title; const BackupSettingsSection({ @@ -12,12 +15,12 @@ class BackupSettingsSection extends StatefulWidget { }); @override - State createState() => _BackupSettingsSectionState(); + ConsumerState createState() => _BackupSettingsSectionState(); } enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } - - class _BackupSettingsSectionState extends State { + + class _BackupSettingsSectionState extends ConsumerState { final BackupService _backupService = BackupService(); _BackupState _state = _BackupState.idle; @@ -28,7 +31,11 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } } Future _initBackupService() async { - await _backupService.init(); + try { + await _backupService.init(); + } catch (e) { + debugPrint('[Backup] Init error (silent sign-in failed): $e'); + } if (mounted) { setState(() {}); } @@ -132,7 +139,7 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } Future _restoreBackup() async { final messenger = ScaffoldMessenger.of(context); final appColors = Theme.of(context).extension()!; - // Note: hasBackup check is async + final hasBackup = await _backupService.hasBackupOnDrive(); if (!hasBackup) { if (mounted) { @@ -173,11 +180,29 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } ), ); - if (confirmed == true && mounted) { - setState(() => _state = _BackupState.restoring); - final success = await _backupService.restoreBackup(); + if (confirmed != true || !mounted) return; + await _executeRestore(forceSkipPreBackup: false); + } + + /// 実際の復元処理。PreRestoreBackupException を受けた場合はダイアログで続行確認する。 + Future _executeRestore({required bool forceSkipPreBackup}) async { + final messenger = ScaffoldMessenger.of(context); + final appColors = Theme.of(context).extension()!; + + setState(() => _state = _BackupState.restoring); + + try { + final success = forceSkipPreBackup + ? await _backupService.restoreBackupSkippingPreBackup() + : await _backupService.restoreBackup(); + if (mounted) { setState(() => _state = _BackupState.idle); + if (success) { + ref.invalidate(rawSakeListItemsProvider); + ref.invalidate(sakeSortOrderProvider); + ref.invalidate(userProfileProvider); + } messenger.showSnackBar( SnackBar( content: Text(success ? '復元が完了しました' : '復元に失敗しました'), @@ -185,6 +210,47 @@ enum _BackupState { idle, signingIn, signingOut, backingUp, restoring } ), ); } + } on PreRestoreBackupException { + if (!mounted) return; + setState(() => _state = _BackupState.idle); + + // 事前バックアップ失敗 → ユーザーに警告して続行するか確認 + final continueAnyway = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Row( + children: [ + Icon(LucideIcons.alertTriangle, color: appColors.error, size: 24), + const SizedBox(width: 8), + const Text('安全バックアップに失敗'), + ], + ), + content: const Text( + '復元前の安全コピー作成に失敗しました。\n' + 'このまま続行すると、現在のデータが失われた場合に\n' + '元に戻せない可能性があります。\n\n' + '続行しますか?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('中断'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: appColors.error, + foregroundColor: appColors.surfaceSubtle, + ), + child: const Text('それでも続行'), + ), + ], + ), + ); + + if (continueAnyway == true && mounted) { + await _executeRestore(forceSkipPreBackup: true); + } } } diff --git a/web/download/releases.json b/web/download/releases.json index b7112e9..5fe031d 100644 --- a/web/download/releases.json +++ b/web/download/releases.json @@ -1,19 +1,19 @@ { - "version": "v1.0.47", - "name": "Ponshu Room 1.0.47 (2026-04-23)", + "version": "v1.0.49", + "name": "Ponshu Room 1.0.49 (2026-04-23)", "date": "2026-04-23", "apks": { "maita": { "lite": { "filename": "ponshu_room_consumer_maita.apk", - "url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.47/ponshu_room_consumer_maita.apk", + "url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.49/ponshu_room_consumer_maita.apk", "size_mb": 91 } }, "eiji": { "lite": { "filename": "ponshu_room_consumer_eiji.apk", - "url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.47/ponshu_room_consumer_eiji.apk", + "url": "https://posimai-lab.tail72e846.ts.net/mai/ponshu-room-lite/releases/download/v1.0.49/ponshu_room_consumer_eiji.apk", "size_mb": 91 } }