Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -8596,15 +8596,15 @@
"claudeResets": "{count, plural, =1{1 reset} other{{count} resets}}",
"@claudeResets": {"placeholders": {"count": {"type": "int"}}},
"claudeResetsTitle": "Claude limit resets",
"claudeResetsDetail": "Granted by Anthropic. Using one sets the limits it lists back to nothing used.",
"claudeResetsDetail": "Granted by Anthropic. Using one sets the limits it lists back to nothing used. With more than one, they are used one at a time, in the order Anthropic sets.",
"claudeResetClears": "Clears {windows}",
"@claudeResetClears": {"placeholders": {"windows": {"type": "String"}}},
"claudeResetWindowUsed": "{window} ({percent}% used)",
"@claudeResetWindowUsed": {"placeholders": {"window": {"type": "String"}, "percent": {"type": "int"}}},
"claudeResetWeeklyOf": "{model} weekly",
"@claudeResetWeeklyOf": {"placeholders": {"model": {"type": "String"}}},
"claudeResetNext": "Default",
"claudeResetNextHint": "The one the server would take first",
"claudeResetNext": "Usable now",
"claudeResetQueued": "Waits its turn: usable once the one above is used",
"claudeResetUse": "Use This One…",
"claudeResetConfirm": "Use a Claude limit reset?",
"claudeResetConfirmDetail": "“{label}” sets {windows} back to nothing used, at once. The reset can't be given back, and what is left of these limits now is lost.",
Expand All @@ -8619,6 +8619,10 @@
"@claudeResetDone": {"placeholders": {"windows": {"type": "String"}}},
"claudeResetNotDone": "Not reset: {reason}",
"@claudeResetNotDone": {"placeholders": {"reason": {"type": "String"}}},
"claudeResetNotNext": "Another reset is the one usable now; nothing was used. Refresh and try again.",
"claudeResetNotLimited": "This reset can only be used once a limit is reached; nothing was used.",
"claudeResetAlreadyUsed": "This reset was already used; nothing was used now.",
"claudeResetCoolingDown": "A reset was just used; wait a little before the next. Nothing was used.",
"claudeResetsRefresh": "Refresh",
"resetCreditsHint": "{count, plural, =1{A reset credit expires} other{{count} reset credits expire}} within a week. To get their worth, use Codex models on this plan, resetting once the quota runs out:",
"@resetCreditsHint": {"placeholders": {"count": {"type": "int"}}},
Expand Down
34 changes: 29 additions & 5 deletions lib/l10n/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21652,7 +21652,7 @@ abstract class AppLocalizations {
/// No description provided for @claudeResetsDetail.
///
/// In en, this message translates to:
/// **'Granted by Anthropic. Using one sets the limits it lists back to nothing used.'**
/// **'Granted by Anthropic. Using one sets the limits it lists back to nothing used. With more than one, they are used one at a time, in the order Anthropic sets.'**
String get claudeResetsDetail;

/// No description provided for @claudeResetClears.
Expand All @@ -21676,14 +21676,14 @@ abstract class AppLocalizations {
/// No description provided for @claudeResetNext.
///
/// In en, this message translates to:
/// **'Default'**
/// **'Usable now'**
String get claudeResetNext;

/// No description provided for @claudeResetNextHint.
/// No description provided for @claudeResetQueued.
///
/// In en, this message translates to:
/// **'The one the server would take first'**
String get claudeResetNextHint;
/// **'Waits its turn: usable once the one above is used'**
String get claudeResetQueued;

/// No description provided for @claudeResetUse.
///
Expand Down Expand Up @@ -21739,6 +21739,30 @@ abstract class AppLocalizations {
/// **'Not reset: {reason}'**
String claudeResetNotDone(String reason);

/// No description provided for @claudeResetNotNext.
///
/// In en, this message translates to:
/// **'Another reset is the one usable now; nothing was used. Refresh and try again.'**
String get claudeResetNotNext;

/// No description provided for @claudeResetNotLimited.
///
/// In en, this message translates to:
/// **'This reset can only be used once a limit is reached; nothing was used.'**
String get claudeResetNotLimited;

/// No description provided for @claudeResetAlreadyUsed.
///
/// In en, this message translates to:
/// **'This reset was already used; nothing was used now.'**
String get claudeResetAlreadyUsed;

/// No description provided for @claudeResetCoolingDown.
///
/// In en, this message translates to:
/// **'A reset was just used; wait a little before the next. Nothing was used.'**
String get claudeResetCoolingDown;

/// No description provided for @claudeResetsRefresh.
///
/// In en, this message translates to:
Expand Down
23 changes: 20 additions & 3 deletions lib/l10n/app_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13270,7 +13270,7 @@ class AppLocalizationsEn extends AppLocalizations {

@override
String get claudeResetsDetail =>
'Granted by Anthropic. Using one sets the limits it lists back to nothing used.';
'Granted by Anthropic. Using one sets the limits it lists back to nothing used. With more than one, they are used one at a time, in the order Anthropic sets.';

@override
String claudeResetClears(String windows) {
Expand All @@ -13288,10 +13288,11 @@ class AppLocalizationsEn extends AppLocalizations {
}

@override
String get claudeResetNext => 'Default';
String get claudeResetNext => 'Usable now';

@override
String get claudeResetNextHint => 'The one the server would take first';
String get claudeResetQueued =>
'Waits its turn: usable once the one above is used';

@override
String get claudeResetUse => 'Use This One…';
Expand Down Expand Up @@ -13331,6 +13332,22 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Not reset: $reason';
}

@override
String get claudeResetNotNext =>
'Another reset is the one usable now; nothing was used. Refresh and try again.';

@override
String get claudeResetNotLimited =>
'This reset can only be used once a limit is reached; nothing was used.';

@override
String get claudeResetAlreadyUsed =>
'This reset was already used; nothing was used now.';

@override
String get claudeResetCoolingDown =>
'A reset was just used; wait a little before the next. Nothing was used.';

@override
String get claudeResetsRefresh => 'Refresh';

Expand Down
19 changes: 16 additions & 3 deletions lib/l10n/app_localizations_zh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12555,7 +12555,8 @@ class AppLocalizationsZh extends AppLocalizations {
String get claudeResetsTitle => 'Claude 重置卡';

@override
String get claudeResetsDetail => 'Anthropic 赠送。用掉一张,它列出的额度立即清零。';
String get claudeResetsDetail =>
'Anthropic 赠送。用掉一张,它列出的额度立即清零。有多张时要按 Anthropic 排好的顺序一张一张用。';

@override
String claudeResetClears(String windows) {
Expand All @@ -12573,10 +12574,10 @@ class AppLocalizationsZh extends AppLocalizations {
}

@override
String get claudeResetNext => '默认';
String get claudeResetNext => '现在可用';

@override
String get claudeResetNextHint => '服务器默认先用这张';
String get claudeResetQueued => '排在后面,用掉上面那张后才能用';

@override
String get claudeResetUse => '使用这张…';
Expand Down Expand Up @@ -12615,6 +12616,18 @@ class AppLocalizationsZh extends AppLocalizations {
return '没有重置:$reason';
}

@override
String get claudeResetNotNext => '现在能用的换成了另一张,这次什么都没扣。刷新看看再用。';

@override
String get claudeResetNotLimited => '这张卡要等额度用满了才能用,这次什么都没扣。';

@override
String get claudeResetAlreadyUsed => '这张卡已经用过了,这次什么都没扣。';

@override
String get claudeResetCoolingDown => '刚用过一张,要稍等一会儿才能再用,这次什么都没扣。';

@override
String get claudeResetsRefresh => '刷新';

Expand Down
10 changes: 7 additions & 3 deletions lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3598,12 +3598,12 @@
"modelsResetCreditConfirmDetail": "额度会立即重置,卡用掉后不能退回;现在还没用完的额度也就作废了。",
"claudeResets": "重置卡 {count} 张",
"claudeResetsTitle": "Claude 重置卡",
"claudeResetsDetail": "Anthropic 赠送。用掉一张,它列出的额度立即清零。",
"claudeResetsDetail": "Anthropic 赠送。用掉一张,它列出的额度立即清零。有多张时要按 Anthropic 排好的顺序一张一张用。",
"claudeResetClears": "清零:{windows}",
"claudeResetWindowUsed": "{window}(已用 {percent}%)",
"claudeResetWeeklyOf": "{model} 每周",
"claudeResetNext": "默认",
"claudeResetNextHint": "服务器默认先用这张",
"claudeResetNext": "现在可用",
"claudeResetQueued": "排在后面,用掉上面那张后才能用",
"claudeResetUse": "使用这张…",
"claudeResetConfirm": "用掉一张 Claude 重置卡?",
"claudeResetConfirmDetail": "「{label}」会把 {windows} 立即清零。卡用掉后不能退回,这些额度现在还没用完的部分也就作废了。",
Expand All @@ -3613,6 +3613,10 @@
"claudeResetSignedOut": "Claude.ai 登录已过期,Claude Code 下次运行时会自动续上。",
"claudeResetDone": "已重置:{windows}",
"claudeResetNotDone": "没有重置:{reason}",
"claudeResetNotNext": "现在能用的换成了另一张,这次什么都没扣。刷新看看再用。",
"claudeResetNotLimited": "这张卡要等额度用满了才能用,这次什么都没扣。",
"claudeResetAlreadyUsed": "这张卡已经用过了,这次什么都没扣。",
"claudeResetCoolingDown": "刚用过一张,要稍等一会儿才能再用,这次什么都没扣。",
"claudeResetsRefresh": "刷新",
"resetCreditsHint": "有 {count} 张重置卡将在一周内过期。建议按这个计划用 Codex 模型,额度用满后再重置,别让它们白白过期:",
"resetCreditsHintDay": "{day}:重置 {count} 次",
Expand Down
59 changes: 32 additions & 27 deletions lib/models/claude_resets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ class ClaudeResets {
final String? reason;
final List<ClaudeResetGrant> grants;

/// The one the server would take first, of those the user may pick.
/// The one the server takes next: the only one it lets be used, others
/// refused as `not_next_grant`.
final String? nextGrantId;
final DateTime? cooldownUntil;

Expand All @@ -120,21 +121,16 @@ class ClaudeResets {

int get count => live.fold(0, (sum, grant) => sum + grant.left);

/// Whether a reset can be taken from [grant] now, whichever the user
/// picks: while the server names one it would take at all.
bool canUse(ClaudeResetGrant grant) =>
eligible &&
cooldownUntil?.isAfter(at) != true &&
nextGrantId != null &&
grant.liveAt(at) &&
!grant.paused &&
grant.usableNow;

/// Those a reset can be taken from now.
List<ClaudeResetGrant> get usable => [
for (final grant in live)
if (canUse(grant)) grant,
];
/// The grant a reset is taken from now, if one can be.
ClaudeResetGrant? get next {
if (!eligible || cooldownUntil?.isAfter(at) == true) return null;
for (final grant in live) {
if (grant.id == nextGrantId && !grant.paused && grant.usableNow) {
return grant;
}
}
return null;
}

/// From a `/api/oauth/usage?cedar_ember=1` answer; null when it has no
/// such block (not offered to this account, or not asked).
Expand Down Expand Up @@ -184,6 +180,10 @@ class ClaudeResetOutcome {

bool get reset => result == 'reset';

/// The server's [reason] when the grant asked for is not the next: only
/// that one can be used.
static const notNext = 'not_next_grant';

static ClaudeResetOutcome fromJson(Object? json) => switch (json) {
{'result': final String result} when result.isNotEmpty =>
ClaudeResetOutcome(
Expand Down Expand Up @@ -253,11 +253,8 @@ class ClaudeResetsService extends ChangeNotifier {
Future<void>? _loading;
DateTime? _askedAt;

bool get using => _usingId != null;

/// The grant one is being used of.
String? get usingId => _usingId;
String? _usingId;
bool get using => _using;
bool _using = false;

Future<void> refresh({bool force = false}) {
if (_loading case final loading?) return loading;
Expand Down Expand Up @@ -286,28 +283,36 @@ class ClaudeResetsService extends ChangeNotifier {
}
}

/// Uses one of [grant], as it still can be once asked again: what the
/// server answered. Never sent twice: a failure is told, not retried.
/// Uses one of [grant], as it still is the one next once asked again: what
/// the server answered. Never sent twice: a failure is told, not retried.
Future<ClaudeResetOutcome> use(ClaudeResetGrant grant) async {
if (using) return const ClaudeResetOutcome('busy');
_usingId = grant.id;
if (_using) return const ClaudeResetOutcome('busy');
_using = true;
notifyListeners();
try {
await refresh(force: true);
if (_resets?.usable.any((usable) => usable.id == grant.id) != true) {
final next = _resets?.next;
if (next == null) {
return ClaudeResetOutcome(
'unavailable',
reason: _error ?? 'no longer offered',
);
}
// Another one is next now: as the server would refuse it.
if (next.id != grant.id) {
return const ClaudeResetOutcome(
'ineligible',
reason: ClaudeResetOutcome.notNext,
);
}
final outcome = ClaudeResetOutcome.fromJson(
await _api.claim(grant.id, _requestId()),
);
return outcome;
} on ClaudeResetsException catch (error) {
return ClaudeResetOutcome('error', reason: error.message);
} finally {
_usingId = null;
_using = false;
// What is left of them, and of the windows, now.
unawaited(refresh(force: true));
}
Expand Down
Loading
Loading