76 lines
2.8 KiB
Dart
76 lines
2.8 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
/// Gemini API 日次使用回数をローカルで追跡するサービス。
|
||
///
|
||
/// - 無料枠: 1プロジェクトあたり 20回/日
|
||
/// - リセット時刻: UTC 08:00(= 17:00 JST 冬時間 / 16:00 JST 夏時間)
|
||
/// ※ Gemini API は Pacific Time 基準でリセットされるため、UTC+9 の日本では
|
||
/// 冬(PST=UTC-8)は 17:00 JST、夏(PDT=UTC-7)は 16:00 JST となる。
|
||
class ApiUsageService {
|
||
static const int dailyLimit = 20;
|
||
|
||
static const _keyCount = 'gemini_usage_count';
|
||
static const _keyWindowStart = 'gemini_window_start';
|
||
|
||
/// 現在のクォータウィンドウ開始時刻(UTC 08:00)を返す
|
||
static DateTime getCurrentWindowStart() {
|
||
final now = DateTime.now().toUtc();
|
||
final todayReset = DateTime.utc(now.year, now.month, now.day, 8, 0, 0);
|
||
return now.isBefore(todayReset)
|
||
? todayReset.subtract(const Duration(days: 1))
|
||
: todayReset;
|
||
}
|
||
|
||
/// 次のリセット時刻(端末のローカル時間で返す)
|
||
static DateTime getNextResetTime() {
|
||
return getCurrentWindowStart().add(const Duration(days: 1)).toLocal();
|
||
}
|
||
|
||
/// 今日の使用回数(ウィンドウが変わっていれば自動リセット)
|
||
static Future<int> getCount() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final windowStart = getCurrentWindowStart();
|
||
final storedStr = prefs.getString(_keyWindowStart);
|
||
|
||
if (storedStr != null) {
|
||
final storedWindow = DateTime.parse(storedStr);
|
||
if (storedWindow.isBefore(windowStart)) {
|
||
// 新しいウィンドウ → リセット
|
||
await prefs.setInt(_keyCount, 0);
|
||
await prefs.setString(_keyWindowStart, windowStart.toIso8601String());
|
||
return 0;
|
||
}
|
||
} else {
|
||
// 初回起動 → ウィンドウ開始時刻を記録
|
||
await prefs.setString(_keyWindowStart, windowStart.toIso8601String());
|
||
}
|
||
|
||
return prefs.getInt(_keyCount) ?? 0;
|
||
}
|
||
|
||
/// 使用回数を 1 増やす
|
||
static Future<void> increment() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final current = await getCount();
|
||
await prefs.setInt(_keyCount, current + 1);
|
||
}
|
||
|
||
/// 残り回数(0 以上)
|
||
static Future<int> getRemaining() async {
|
||
final count = await getCount();
|
||
return (dailyLimit - count).clamp(0, dailyLimit);
|
||
}
|
||
|
||
/// 無料枠を使い切っているか
|
||
static Future<bool> isExhausted() async {
|
||
return await getCount() >= dailyLimit;
|
||
}
|
||
}
|
||
|
||
/// ActivityStats / カメラ画面で使う Riverpod プロバイダ。
|
||
/// increment() 後に ref.invalidate(apiUsageCountProvider) で UI を更新する。
|
||
final apiUsageCountProvider = FutureProvider.autoDispose<int>((ref) async {
|
||
return ApiUsageService.getCount();
|
||
});
|