ponshu-room-lite/lib/services/api_usage_service.dart

76 lines
2.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
});