Compare commits
23
Commits
v1.0.10
...
v1.1.2-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235ab81efb | ||
|
|
12d7855ceb | ||
|
|
d1ab5206bd | ||
|
|
997e820be8 | ||
|
|
7fc78d5a83 | ||
|
|
818d4aa6c2 | ||
|
|
6aa74fcabc | ||
|
|
2a01d6f5b8 | ||
|
|
af920eda83 | ||
|
|
6b6802eaea | ||
|
|
e9d29db2ee | ||
|
|
9be5069aac | ||
|
|
cc9e2aaba1 | ||
|
|
19bb6459ed | ||
|
|
abf05b04fc | ||
|
|
849e94c942 | ||
|
|
d76c7496bb | ||
|
|
45a3925f65 | ||
|
|
113e91583d | ||
|
|
c60fb93e1a | ||
|
|
b16c24cb44 | ||
|
|
89a79c6f13 | ||
|
|
aa529835f7 |
@@ -0,0 +1,63 @@
|
|||||||
|
name: Release Beta
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*-beta*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-beta:
|
||||||
|
runs-on: macos-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
xcodebuild \
|
||||||
|
-project ClaudeChecker.xcodeproj \
|
||||||
|
-scheme ClaudeChecker \
|
||||||
|
-configuration Release \
|
||||||
|
-derivedDataPath build \
|
||||||
|
clean build
|
||||||
|
|
||||||
|
- name: Zip app
|
||||||
|
run: |
|
||||||
|
ditto -c -k --keepParent \
|
||||||
|
build/Build/Products/Release/ClaudeChecker.app \
|
||||||
|
ClaudeChecker.zip
|
||||||
|
|
||||||
|
- name: Create GitHub pre-release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \
|
||||||
|
--title "$GITHUB_REF_NAME" \
|
||||||
|
--prerelease \
|
||||||
|
--notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Beta release ${{ steps.version.outputs.version }}')"
|
||||||
|
|
||||||
|
- name: Update version-beta.json
|
||||||
|
run: |
|
||||||
|
notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "Beta release ${{ steps.version.outputs.version }}")
|
||||||
|
jq -n \
|
||||||
|
--arg version "${{ steps.version.outputs.version }}" \
|
||||||
|
--arg url "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/ClaudeChecker.zip" \
|
||||||
|
--arg notes "$notes" \
|
||||||
|
'{version: $version, url: $url, notes: $notes}' > version-beta.json
|
||||||
|
|
||||||
|
- name: Commit version-beta.json
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add version-beta.json
|
||||||
|
git commit -m "Beta release ${{ github.ref_name }}"
|
||||||
|
git push origin HEAD:main
|
||||||
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
- '!v*-*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
.sink { [weak self] _ in self?.updateStatusItem() }
|
.sink { [weak self] _ in self?.updateStatusItem() }
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
updateManager.$updateAvailable
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in self?.updateStatusItem() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// Refresh timer
|
// Refresh timer
|
||||||
scheduleTimer(interval: usageViewModel.refreshInterval)
|
scheduleTimer(interval: usageViewModel.refreshInterval)
|
||||||
|
|
||||||
@@ -166,7 +171,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
let sdStr = sd.map { "\(Int($0.usedPercent.rounded()))%" } ?? "—"
|
let sdStr = sd.map { "\(Int($0.usedPercent.rounded()))%" } ?? "—"
|
||||||
button.image = nil
|
button.image = nil
|
||||||
button.attributedTitle = NSAttributedString(string: "")
|
button.attributedTitle = NSAttributedString(string: "")
|
||||||
button.title = "◔ \(fhStr) \(sdStr)"
|
let updatePrefix = updateManager.updateAvailable ? "↑ Update " : "◔ "
|
||||||
|
button.title = "\(updatePrefix)\(fhStr) \(sdStr)"
|
||||||
|
button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium)
|
||||||
|
} else if updateManager.updateAvailable {
|
||||||
|
button.image = nil
|
||||||
|
button.attributedTitle = NSAttributedString(string: "")
|
||||||
|
button.title = "↑ Update"
|
||||||
button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium)
|
button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium)
|
||||||
} else {
|
} else {
|
||||||
button.title = ""
|
button.title = ""
|
||||||
|
|||||||
@@ -47,13 +47,29 @@ struct ContentView: View {
|
|||||||
EmptyStateView()
|
EmptyStateView()
|
||||||
.padding(.vertical, 40)
|
.padding(.vertical, 40)
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 1) {
|
VStack(spacing: 8) {
|
||||||
ForEach(vm.limits) { limit in
|
ForEach(vm.limits) { limit in
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack {
|
||||||
|
Text(limit.window == .fiveHour ? "5hr Limit" : "7 Day Limit")
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.top, 10)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
LimitCard(limit: limit)
|
LimitCard(limit: limit)
|
||||||
|
.background(Color(nsColor: .controlBackgroundColor))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.primary.opacity(0.08), lineWidth: 0.5))
|
||||||
|
.padding(.horizontal, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.primary.opacity(0.05))
|
}
|
||||||
.padding(.vertical, 1)
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
// Extra credits row
|
// Extra credits row
|
||||||
if let extra = vm.extraUsage, extra.isEnabled {
|
if let extra = vm.extraUsage, extra.isEnabled {
|
||||||
@@ -196,7 +212,7 @@ struct LimitCard: View {
|
|||||||
AgentIconView(agent: .claude, size: 14)
|
AgentIconView(agent: .claude, size: 14)
|
||||||
Text("Claude")
|
Text("Claude")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
Text("· \(vm.planLabel) · \(limit.window.label)")
|
Text("· \(vm.planLabel)")
|
||||||
.font(.system(size: 11))
|
.font(.system(size: 11))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -1032,12 +1048,6 @@ struct UpdateBanner: View {
|
|||||||
Button("Update") { showSheet = true }
|
Button("Update") { showSheet = true }
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
Button { updater.updateAvailable = false } label: {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.font(.system(size: 10, weight: .medium))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.10</string>
|
<string>1.1.2-beta.4</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>30</string>
|
<string>35</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>13.0</string>
|
<string>13.0</string>
|
||||||
<key>LSUIElement</key>
|
<key>LSUIElement</key>
|
||||||
|
|||||||
@@ -24,18 +24,25 @@ struct LoginWebView: NSViewRepresentable {
|
|||||||
|
|
||||||
class Coordinator: NSObject, WKNavigationDelegate {
|
class Coordinator: NSObject, WKNavigationDelegate {
|
||||||
let onAuthenticated: () -> Void
|
let onAuthenticated: () -> Void
|
||||||
|
var didAuthenticate = false
|
||||||
|
|
||||||
init(onAuthenticated: @escaping () -> Void) {
|
init(onAuthenticated: @escaping () -> Void) {
|
||||||
self.onAuthenticated = onAuthenticated
|
self.onAuthenticated = onAuthenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||||
|
guard !didAuthenticate else { return }
|
||||||
|
// Don't fire on the login/auth pages themselves
|
||||||
|
if let url = webView.url?.absoluteString,
|
||||||
|
url.contains("/login") || url.contains("/auth") { return }
|
||||||
|
|
||||||
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
|
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { cookies in
|
||||||
let hasSession = cookies.contains {
|
let hasSession = cookies.contains {
|
||||||
$0.domain.contains("claude.ai") &&
|
$0.domain.contains("claude.ai") &&
|
||||||
($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token")
|
($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token")
|
||||||
}
|
}
|
||||||
if hasSession {
|
guard hasSession, !self.didAuthenticate else { return }
|
||||||
|
self.didAuthenticate = true
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
self.onAuthenticated()
|
self.onAuthenticated()
|
||||||
}
|
}
|
||||||
@@ -43,7 +50,6 @@ struct LoginWebView: NSViewRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Login Sheet View
|
// MARK: - Login Sheet View
|
||||||
|
|
||||||
@@ -58,10 +64,19 @@ struct LoginSheetView: View {
|
|||||||
Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude")
|
Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
Spacer()
|
Spacer()
|
||||||
|
if authenticated {
|
||||||
|
Button("Done") {
|
||||||
|
isPresented = false
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.controlSize(.small)
|
||||||
|
} else {
|
||||||
Button("Cancel") { isPresented = false }
|
Button("Cancel") { isPresented = false }
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(.horizontal, 14)
|
.padding(.horizontal, 14)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.background(Color(nsColor: .windowBackgroundColor))
|
.background(Color(nsColor: .windowBackgroundColor))
|
||||||
|
|||||||
@@ -318,13 +318,31 @@ class UpdateManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func isNewer(_ version: String, than current: String) -> Bool {
|
private func isNewer(_ version: String, than current: String) -> Bool {
|
||||||
let toInts = { (v: String) in v.split(separator: ".").compactMap { Int($0) } }
|
func parse(_ v: String) -> (base: [Int], pre: [Int]?) {
|
||||||
let a = toInts(version), b = toInts(current)
|
guard !v.isEmpty else { return ([], nil) }
|
||||||
for i in 0..<max(a.count, b.count) {
|
let halves = v.split(separator: "-", maxSplits: 1)
|
||||||
let av = i < a.count ? a[i] : 0
|
let base = halves[0].split(separator: ".").compactMap { Int($0) }
|
||||||
let bv = i < b.count ? b[i] : 0
|
let pre = halves.count > 1 ? String(halves[1]).split(separator: ".").compactMap { Int($0) } : nil
|
||||||
|
return (base, pre)
|
||||||
|
}
|
||||||
|
let a = parse(version), b = parse(current)
|
||||||
|
// Compare base version numbers
|
||||||
|
for i in 0..<max(a.base.count, b.base.count) {
|
||||||
|
let av = i < a.base.count ? a.base[i] : 0
|
||||||
|
let bv = i < b.base.count ? b.base[i] : 0
|
||||||
if av != bv { return av > bv }
|
if av != bv { return av > bv }
|
||||||
}
|
}
|
||||||
|
// Base equal: stable beats pre-release
|
||||||
|
if a.pre == nil && b.pre != nil { return true }
|
||||||
|
if a.pre != nil && b.pre == nil { return false }
|
||||||
|
// Both pre-release: compare pre-release numbers
|
||||||
|
if let ap = a.pre, let bp = b.pre {
|
||||||
|
for i in 0..<max(ap.count, bp.count) {
|
||||||
|
let av = i < ap.count ? ap[i] : 0
|
||||||
|
let bv = i < bp.count ? bp[i] : 0
|
||||||
|
if av != bv { return av > bv }
|
||||||
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
[](https://www.apple.com/macos/)
|
[](https://www.apple.com/macos/)
|
||||||
[](https://swift.org)
|
[](https://swift.org)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases)
|
[](https://github.com/superdooper86/claudechecker/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
## What's new in v1.0.10
|
## What's new in v1.1.2
|
||||||
|
|
||||||
### Main window always opens on click
|
### Cleaner card headers
|
||||||
Closing the app while Settings was open would cause the next click to reopen straight to Settings. The app now always returns to the main window when reopened.
|
Removed the redundant "5h" / "7d" window labels from each card row — the section headers already make it clear which limit is which.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"version": "1.1.2-beta.3",
|
||||||
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.2-beta.3/ClaudeChecker.zip",
|
||||||
|
"notes": "## What's new in v1.1.2\n\n### Cleaner card headers\nRemoved the redundant \"5h\" / \"7d\" window labels from each card row — the section headers already make it clear which limit is which."
|
||||||
|
}
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.0/ClaudeChecker.zip",
|
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.1/ClaudeChecker.zip",
|
||||||
"notes": "## What's new in v1.1.0\n\n### Main window always opens on click\nClosing the app while Settings was open would cause the next click to reopen straight to Settings. The app now always returns to the main window when reopened."
|
"notes": "## What's new in v1.1.1\n\n### Clearer limit labels\nThe 5hr and 7 Day usage cards now have clear section headers so it's immediately obvious which limit is which.\n\n### Login window fix\nThe sign-in sheet now reliably closes after a successful login. A **Done** button also appears as a fallback once authentication is detected — no more being stuck with only a Cancel button."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user