fix: quotaLockout Provider化・色トークン整備・依存バージョン固定
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
dd9b814174
commit
9fba57621a
|
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class QuotaLockoutNotifier extends Notifier<DateTime?> {
|
||||||
|
@override
|
||||||
|
DateTime? build() => null;
|
||||||
|
void set(DateTime? value) => state = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gemini API 429(レート制限)発生後の一時ロックアウト期限を管理するプロバイダー。
|
||||||
|
///
|
||||||
|
/// カメラ・詳細画面の両方で共有し、画面遷移をまたいで状態を保持する。
|
||||||
|
/// null = ロックアウトなし、DateTime = その時刻まで再解析を禁止
|
||||||
|
final quotaLockoutProvider = NotifierProvider<QuotaLockoutNotifier, DateTime?>(QuotaLockoutNotifier.new);
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
|
||||||
import '../models/sake_item.dart';
|
import '../models/sake_item.dart';
|
||||||
import '../providers/gemini_provider.dart';
|
import '../providers/gemini_provider.dart';
|
||||||
|
import '../providers/quota_lockout_provider.dart';
|
||||||
import '../providers/sakenowa_providers.dart';
|
import '../providers/sakenowa_providers.dart';
|
||||||
import '../providers/theme_provider.dart';
|
import '../providers/theme_provider.dart';
|
||||||
import '../services/api_usage_service.dart';
|
import '../services/api_usage_service.dart';
|
||||||
|
|
@ -24,7 +25,6 @@ import '../widgets/analyzing_dialog.dart';
|
||||||
/// - _performSakenowaMatching() : バックグラウンドさけのわ自動マッチング
|
/// - _performSakenowaMatching() : バックグラウンドさけのわ自動マッチング
|
||||||
mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
|
mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T> {
|
||||||
final List<String> capturedImages = [];
|
final List<String> capturedImages = [];
|
||||||
DateTime? quotaLockoutTime;
|
|
||||||
|
|
||||||
Future<void> analyzeImages() async {
|
Future<void> analyzeImages() async {
|
||||||
if (capturedImages.isEmpty) return;
|
if (capturedImages.isEmpty) return;
|
||||||
|
|
@ -331,6 +331,7 @@ mixin CameraAnalysisMixin<T extends ConsumerStatefulWidget> on ConsumerState<T>
|
||||||
if (errStr.contains('Quota') || errStr.contains('429')) {
|
if (errStr.contains('Quota') || errStr.contains('429')) {
|
||||||
try {
|
try {
|
||||||
await DraftService.saveDraft(capturedImages, reason: DraftReason.quotaLimit);
|
await DraftService.saveDraft(capturedImages, reason: DraftReason.quotaLimit);
|
||||||
|
ref.read(quotaLockoutProvider.notifier).set(DateTime.now().add(const Duration(minutes: 1)));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
navigator.pop(); // カメラ画面を閉じる
|
navigator.pop(); // カメラ画面を閉じる
|
||||||
final resetTime = ApiUsageService.getNextResetTime();
|
final resetTime = ApiUsageService.getNextResetTime();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'package:gal/gal.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
import 'package:image_picker/image_picker.dart'; // Gallery & Camera
|
import 'package:image_picker/image_picker.dart'; // Gallery & Camera
|
||||||
|
|
||||||
|
import '../providers/quota_lockout_provider.dart';
|
||||||
import '../services/image_compression_service.dart';
|
import '../services/image_compression_service.dart';
|
||||||
import '../theme/app_colors.dart';
|
import '../theme/app_colors.dart';
|
||||||
import 'camera_analysis_mixin.dart';
|
import 'camera_analysis_mixin.dart';
|
||||||
|
|
@ -165,10 +166,11 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
|
|
||||||
Future<void> _takePicture() async {
|
Future<void> _takePicture() async {
|
||||||
// Check Quota Lockout
|
// Check Quota Lockout
|
||||||
if (quotaLockoutTime != null) {
|
final quotaLockout = ref.read(quotaLockoutProvider);
|
||||||
final remaining = quotaLockoutTime!.difference(DateTime.now());
|
if (quotaLockout != null) {
|
||||||
|
final remaining = quotaLockout.difference(DateTime.now());
|
||||||
if (remaining.isNegative) {
|
if (remaining.isNegative) {
|
||||||
setState(() => quotaLockoutTime = null); // Reset
|
ref.read(quotaLockoutProvider.notifier).set(null);
|
||||||
} else {
|
} else {
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -223,7 +225,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
final appColors = Theme.of(context).extension<AppColors>()!;
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('ギャラリー保存に失敗しました: $e'),
|
content: const Text('ギャラリーへの保存に失敗しました'),
|
||||||
duration: const Duration(seconds: 4),
|
duration: const Duration(seconds: 4),
|
||||||
backgroundColor: appColors.warning,
|
backgroundColor: appColors.warning,
|
||||||
),
|
),
|
||||||
|
|
@ -301,6 +303,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
|
|
||||||
// Batch handle - Notification only
|
// Batch handle - Notification only
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('${images.length}枚の画像を読み込みました。\n右下のボタンから解析を開始してください。'),
|
content: Text('${images.length}枚の画像を読み込みました。\n右下のボタンから解析を開始してください。'),
|
||||||
|
|
@ -308,7 +311,7 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: '解析する',
|
label: '解析する',
|
||||||
onPressed: analyzeImages,
|
onPressed: analyzeImages,
|
||||||
textColor: Colors.yellow,
|
textColor: appColors.brandAccent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -367,6 +370,8 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final quotaLockout = ref.watch(quotaLockoutProvider);
|
||||||
|
final appColors = Theme.of(context).extension<AppColors>()!;
|
||||||
if (_cameraError != null) {
|
if (_cameraError != null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
|
|
@ -564,22 +569,22 @@ class _CameraScreenState extends ConsumerState<CameraScreen> with SingleTickerPr
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: quotaLockoutTime != null ? Colors.red : Colors.white,
|
color: quotaLockout != null ? appColors.error : Colors.white,
|
||||||
width: 4
|
width: 4
|
||||||
),
|
),
|
||||||
color: _isTakingPicture
|
color: _isTakingPicture
|
||||||
? Colors.white.withValues(alpha: 0.5)
|
? Colors.white.withValues(alpha: 0.5)
|
||||||
: (quotaLockoutTime != null ? Colors.red.withValues(alpha: 0.2) : Colors.transparent),
|
: (quotaLockout != null ? appColors.error.withValues(alpha: 0.2) : Colors.transparent),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: quotaLockoutTime != null
|
child: quotaLockout != null
|
||||||
? const Icon(LucideIcons.timer, color: Colors.white, size: 30)
|
? const Icon(LucideIcons.timer, color: Colors.white, size: 30)
|
||||||
: Container(
|
: Container(
|
||||||
height: 60,
|
height: 60,
|
||||||
width: 60,
|
width: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: quotaLockoutTime != null ? Colors.grey : Colors.white,
|
color: quotaLockout != null ? appColors.textTertiary : Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import '../widgets/sake_detail/sake_detail_specs.dart';
|
||||||
import 'sake_detail/widgets/sake_photo_edit_modal.dart';
|
import 'sake_detail/widgets/sake_photo_edit_modal.dart';
|
||||||
import 'sake_detail/sections/sake_mbti_stamp_section.dart';
|
import 'sake_detail/sections/sake_mbti_stamp_section.dart';
|
||||||
import '../providers/license_provider.dart';
|
import '../providers/license_provider.dart';
|
||||||
|
import '../providers/quota_lockout_provider.dart';
|
||||||
import 'sake_detail/sections/sake_basic_info_section.dart';
|
import 'sake_detail/sections/sake_basic_info_section.dart';
|
||||||
import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart';
|
import 'sake_detail/widgets/sake_detail_sliver_app_bar.dart';
|
||||||
import '../services/mbti_compatibility_service.dart';
|
import '../services/mbti_compatibility_service.dart';
|
||||||
|
|
@ -39,7 +40,6 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
late SakeItem _sake;
|
late SakeItem _sake;
|
||||||
int _currentImageIndex = 0;
|
int _currentImageIndex = 0;
|
||||||
bool _isAnalyzing = false;
|
bool _isAnalyzing = false;
|
||||||
DateTime? _quotaLockoutTime;
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -303,10 +303,11 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
if (_isAnalyzing) return;
|
if (_isAnalyzing) return;
|
||||||
|
|
||||||
// 2. Check Quota Lockout
|
// 2. Check Quota Lockout
|
||||||
if (_quotaLockoutTime != null) {
|
final quotaLockout = ref.read(quotaLockoutProvider);
|
||||||
final remaining = _quotaLockoutTime!.difference(DateTime.now());
|
if (quotaLockout != null) {
|
||||||
|
final remaining = quotaLockout.difference(DateTime.now());
|
||||||
if (remaining.isNegative) {
|
if (remaining.isNegative) {
|
||||||
setState(() => _quotaLockoutTime = null); // Reset if time passed
|
ref.read(quotaLockoutProvider.notifier).set(null);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('AI利用制限中です。あと${remaining.inSeconds}秒お待ちください。')),
|
SnackBar(content: Text('AI利用制限中です。あと${remaining.inSeconds}秒お待ちください。')),
|
||||||
|
|
@ -389,9 +390,7 @@ class _SakeDetailScreenState extends ConsumerState<SakeDetailScreen> {
|
||||||
|
|
||||||
// Check for Quota Error to set Lockout
|
// Check for Quota Error to set Lockout
|
||||||
if (e.toString().contains('Quota') || e.toString().contains('429')) {
|
if (e.toString().contains('Quota') || e.toString().contains('429')) {
|
||||||
setState(() {
|
ref.read(quotaLockoutProvider.notifier).set(DateTime.now().add(const Duration(minutes: 1)));
|
||||||
_quotaLockoutTime = DateTime.now().add(const Duration(minutes: 1));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Reanalyze error: $e');
|
debugPrint('Reanalyze error: $e');
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ dependencies:
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
google_fonts: ^6.3.3
|
google_fonts: ^6.3.3
|
||||||
flutter_riverpod:
|
flutter_riverpod: ^3.1.0
|
||||||
riverpod_annotation:
|
riverpod_annotation: 3.0.0-dev.3
|
||||||
hive: ^2.2.3
|
hive: ^2.2.3
|
||||||
hive_flutter: ^1.1.0
|
hive_flutter: ^1.1.0
|
||||||
google_generative_ai: ^0.4.7
|
google_generative_ai: ^0.4.7
|
||||||
|
|
@ -85,7 +85,7 @@ dev_dependencies:
|
||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
build_runner:
|
build_runner:
|
||||||
hive_generator:
|
hive_generator:
|
||||||
riverpod_generator:
|
riverpod_generator: 3.0.0-dev.11
|
||||||
flutter_launcher_icons: ^0.13.1
|
flutter_launcher_icons: ^0.13.1
|
||||||
flutter_native_splash: ^2.4.4
|
flutter_native_splash: ^2.4.4
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue