From 9fba57621a39da3418d1de9e1d20ba029b1cb824 Mon Sep 17 00:00:00 2001 From: Ponshu Developer Date: Sat, 18 Apr 2026 14:18:27 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20quotaLockout=20Provider=E5=8C=96?= =?UTF-8?q?=E3=83=BB=E8=89=B2=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E6=95=B4?= =?UTF-8?q?=E5=82=99=E3=83=BB=E4=BE=9D=E5=AD=98=E3=83=90=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - quotaLockoutProvider (NotifierProvider) を新規作成し、カメラ・詳細画面で共有 - camera_analysis_mixin: quotaLockoutTime フィールドを削除、429時にProviderへ設定 - camera_screen: ref.watch(quotaLockoutProvider) でシャッターボタンUI更新 - sake_detail_screen: _quotaLockoutTime フィールドを削除、Providerに移行 - 画面遷移後もロックアウト状態が保持されるP1バグを解消 - camera_screen: Colors.red/grey → appColors.error/textTertiary に置換 - camera_screen: ギャラリー保存SnackBarから例外文字列 $e を除去 - camera_screen: SnackBarAction textColor Colors.yellow → appColors.brandAccent - pubspec.yaml: flutter_riverpod ^3.1.0, riverpod_annotation 3.0.0-dev.3, riverpod_generator 3.0.0-dev.11 を固定(バージョン未固定による意図しないアップグレードを防止) Co-Authored-By: Claude Sonnet 4.6 --- lib/providers/quota_lockout_provider.dart | 13 +++++++++++ lib/screens/camera_analysis_mixin.dart | 3 ++- lib/screens/camera_screen.dart | 27 ++++++++++++++--------- lib/screens/sake_detail_screen.dart | 13 +++++------ pubspec.yaml | 6 ++--- 5 files changed, 40 insertions(+), 22 deletions(-) create mode 100644 lib/providers/quota_lockout_provider.dart diff --git a/lib/providers/quota_lockout_provider.dart b/lib/providers/quota_lockout_provider.dart new file mode 100644 index 0000000..251d136 --- /dev/null +++ b/lib/providers/quota_lockout_provider.dart @@ -0,0 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class QuotaLockoutNotifier extends Notifier { + @override + DateTime? build() => null; + void set(DateTime? value) => state = value; +} + +/// Gemini API 429(レート制限)発生後の一時ロックアウト期限を管理するプロバイダー。 +/// +/// カメラ・詳細画面の両方で共有し、画面遷移をまたいで状態を保持する。 +/// null = ロックアウトなし、DateTime = その時刻まで再解析を禁止 +final quotaLockoutProvider = NotifierProvider(QuotaLockoutNotifier.new); diff --git a/lib/screens/camera_analysis_mixin.dart b/lib/screens/camera_analysis_mixin.dart index 154d564..5b7611b 100644 --- a/lib/screens/camera_analysis_mixin.dart +++ b/lib/screens/camera_analysis_mixin.dart @@ -6,6 +6,7 @@ import 'package:lucide_icons/lucide_icons.dart'; import '../models/sake_item.dart'; import '../providers/gemini_provider.dart'; +import '../providers/quota_lockout_provider.dart'; import '../providers/sakenowa_providers.dart'; import '../providers/theme_provider.dart'; import '../services/api_usage_service.dart'; @@ -24,7 +25,6 @@ import '../widgets/analyzing_dialog.dart'; /// - _performSakenowaMatching() : バックグラウンドさけのわ自動マッチング mixin CameraAnalysisMixin on ConsumerState { final List capturedImages = []; - DateTime? quotaLockoutTime; Future analyzeImages() async { if (capturedImages.isEmpty) return; @@ -331,6 +331,7 @@ mixin CameraAnalysisMixin on ConsumerState if (errStr.contains('Quota') || errStr.contains('429')) { try { await DraftService.saveDraft(capturedImages, reason: DraftReason.quotaLimit); + ref.read(quotaLockoutProvider.notifier).set(DateTime.now().add(const Duration(minutes: 1))); if (!mounted) return; navigator.pop(); // カメラ画面を閉じる final resetTime = ApiUsageService.getNextResetTime(); diff --git a/lib/screens/camera_screen.dart b/lib/screens/camera_screen.dart index 58f3641..6e3f023 100644 --- a/lib/screens/camera_screen.dart +++ b/lib/screens/camera_screen.dart @@ -10,6 +10,7 @@ import 'package:gal/gal.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:image_picker/image_picker.dart'; // Gallery & Camera +import '../providers/quota_lockout_provider.dart'; import '../services/image_compression_service.dart'; import '../theme/app_colors.dart'; import 'camera_analysis_mixin.dart'; @@ -165,10 +166,11 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr Future _takePicture() async { // Check Quota Lockout - if (quotaLockoutTime != null) { - final remaining = quotaLockoutTime!.difference(DateTime.now()); + final quotaLockout = ref.read(quotaLockoutProvider); + if (quotaLockout != null) { + final remaining = quotaLockout.difference(DateTime.now()); if (remaining.isNegative) { - setState(() => quotaLockoutTime = null); // Reset + ref.read(quotaLockoutProvider.notifier).set(null); } else { final appColors = Theme.of(context).extension()!; ScaffoldMessenger.of(context).showSnackBar( @@ -223,7 +225,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr final appColors = Theme.of(context).extension()!; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('ギャラリー保存に失敗しました: $e'), + content: const Text('ギャラリーへの保存に失敗しました'), duration: const Duration(seconds: 4), backgroundColor: appColors.warning, ), @@ -301,6 +303,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr // Batch handle - Notification only if (mounted) { + final appColors = Theme.of(context).extension()!; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${images.length}枚の画像を読み込みました。\n右下のボタンから解析を開始してください。'), @@ -308,7 +311,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr action: SnackBarAction( label: '解析する', onPressed: analyzeImages, - textColor: Colors.yellow, + textColor: appColors.brandAccent, ), ), ); @@ -367,6 +370,8 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr @override Widget build(BuildContext context) { + final quotaLockout = ref.watch(quotaLockoutProvider); + final appColors = Theme.of(context).extension()!; if (_cameraError != null) { return Scaffold( backgroundColor: Colors.black, @@ -564,22 +569,22 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: quotaLockoutTime != null ? Colors.red : Colors.white, + color: quotaLockout != null ? appColors.error : Colors.white, width: 4 ), - color: _isTakingPicture - ? Colors.white.withValues(alpha: 0.5) - : (quotaLockoutTime != null ? Colors.red.withValues(alpha: 0.2) : Colors.transparent), + color: _isTakingPicture + ? Colors.white.withValues(alpha: 0.5) + : (quotaLockout != null ? appColors.error.withValues(alpha: 0.2) : Colors.transparent), ), child: Center( - child: quotaLockoutTime != null + child: quotaLockout != null ? const Icon(LucideIcons.timer, color: Colors.white, size: 30) : Container( height: 60, width: 60, decoration: BoxDecoration( shape: BoxShape.circle, - color: quotaLockoutTime != null ? Colors.grey : Colors.white, + color: quotaLockout != null ? appColors.textTertiary : Colors.white, ), ), ), diff --git a/lib/screens/sake_detail_screen.dart b/lib/screens/sake_detail_screen.dart index 187f112..7928f04 100644 --- a/lib/screens/sake_detail_screen.dart +++ b/lib/screens/sake_detail_screen.dart @@ -19,6 +19,7 @@ import '../widgets/sake_detail/sake_detail_specs.dart'; import 'sake_detail/widgets/sake_photo_edit_modal.dart'; import 'sake_detail/sections/sake_mbti_stamp_section.dart'; import '../providers/license_provider.dart'; +import '../providers/quota_lockout_provider.dart'; import 'sake_detail/sections/sake_basic_info_section.dart'; import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart'; import '../services/mbti_compatibility_service.dart'; @@ -39,7 +40,6 @@ class _SakeDetailScreenState extends ConsumerState { late SakeItem _sake; int _currentImageIndex = 0; bool _isAnalyzing = false; - DateTime? _quotaLockoutTime; @override @@ -303,10 +303,11 @@ class _SakeDetailScreenState extends ConsumerState { if (_isAnalyzing) return; // 2. Check Quota Lockout - if (_quotaLockoutTime != null) { - final remaining = _quotaLockoutTime!.difference(DateTime.now()); + final quotaLockout = ref.read(quotaLockoutProvider); + if (quotaLockout != null) { + final remaining = quotaLockout.difference(DateTime.now()); if (remaining.isNegative) { - setState(() => _quotaLockoutTime = null); // Reset if time passed + ref.read(quotaLockoutProvider.notifier).set(null); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('AI利用制限中です。あと${remaining.inSeconds}秒お待ちください。')), @@ -389,9 +390,7 @@ class _SakeDetailScreenState extends ConsumerState { // Check for Quota Error to set Lockout if (e.toString().contains('Quota') || e.toString().contains('429')) { - setState(() { - _quotaLockoutTime = DateTime.now().add(const Duration(minutes: 1)); - }); + ref.read(quotaLockoutProvider.notifier).set(DateTime.now().add(const Duration(minutes: 1))); } debugPrint('Reanalyze error: $e'); diff --git a/pubspec.yaml b/pubspec.yaml index ae9e142..e16127b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,8 +35,8 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 google_fonts: ^6.3.3 - flutter_riverpod: - riverpod_annotation: + flutter_riverpod: ^3.1.0 + riverpod_annotation: 3.0.0-dev.3 hive: ^2.2.3 hive_flutter: ^1.1.0 google_generative_ai: ^0.4.7 @@ -85,7 +85,7 @@ dev_dependencies: flutter_lints: ^6.0.0 build_runner: hive_generator: - riverpod_generator: + riverpod_generator: 3.0.0-dev.11 flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.4.4