Compare commits
50
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02440c5340 | ||
|
|
6fe3e9722b | ||
|
|
f5114a31f8 | ||
|
|
4f260a3d4b | ||
|
|
3d1bae3d0a | ||
|
|
b95102cb1b | ||
|
|
2b48114996 | ||
|
|
6a4bb73c4f | ||
|
|
d2815d65ce | ||
|
|
a1c3c0b314 | ||
|
|
d3ec3f5dd7 | ||
|
|
5fee0af18b | ||
|
|
4edca2962c | ||
|
|
ef2cdd8bf3 | ||
|
|
2da3eeaf72 | ||
|
|
d229581de1 | ||
|
|
169cfd8950 | ||
|
|
a5218fa44f | ||
|
|
1f7afab61c | ||
|
|
1507c6490f | ||
|
|
ccf3468621 | ||
|
|
3199b228a8 | ||
|
|
255f3f6cd5 | ||
|
|
773fcb1a46 | ||
|
|
464884e88f | ||
|
|
edd8d42c8d | ||
|
|
e123b48812 | ||
|
|
603d405c87 | ||
|
|
f976a90875 | ||
|
|
c79bd41504 | ||
|
|
dfbd276616 | ||
|
|
dbf14bdd70 | ||
|
|
6c894c32a4 | ||
|
|
345150e894 | ||
|
|
d724622533 | ||
|
|
8ecb5f3670 | ||
|
|
cf6f9184e3 | ||
|
|
d734fec836 | ||
|
|
b6542e099a | ||
|
|
c4e798b484 | ||
|
|
0dd1ac3a8f | ||
|
|
2d8a3629da | ||
|
|
e07af519a6 | ||
|
|
6b1f85dbaf | ||
|
|
74fe785898 | ||
|
|
3de4e09876 | ||
|
|
43dbb155f5 | ||
|
|
79209eee6f | ||
|
|
d9598a70ea | ||
|
|
c8cae1827f |
@@ -1,63 +0,0 @@
|
||||
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
|
||||
@@ -1,96 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- '!v*-*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
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: Compute SHA256
|
||||
id: sha
|
||||
run: echo "sha256=$(shasum -a 256 ClaudeChecker.zip | awk '{print $1}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create GitHub release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "$GITHUB_REF_NAME" ClaudeChecker.zip \
|
||||
--title "$GITHUB_REF_NAME" \
|
||||
--notes "$(cat RELEASE_NOTES.md 2>/dev/null || echo 'Release ${{ steps.version.outputs.version }}')"
|
||||
|
||||
- name: Update version.json
|
||||
run: |
|
||||
notes=$(cat RELEASE_NOTES.md 2>/dev/null || echo "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.json
|
||||
|
||||
- name: Update Homebrew cask
|
||||
run: |
|
||||
sed -i '' \
|
||||
-e 's/version "[^"]*"/version "${{ steps.version.outputs.version }}"/' \
|
||||
-e 's/sha256 "[^"]*"/sha256 "${{ steps.sha.outputs.sha256 }}"/' \
|
||||
../homebrew-tap/Casks/claudechecker.rb 2>/dev/null || true
|
||||
|
||||
- name: Update README badge
|
||||
run: |
|
||||
sed -i '' \
|
||||
's/version-[0-9][^-]*-orange/version-${{ steps.version.outputs.version }}-orange/' \
|
||||
README.md
|
||||
|
||||
- name: Commit version.json and README
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add version.json README.md
|
||||
git commit -m "Release ${{ github.ref_name }}"
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Update Homebrew tap
|
||||
env:
|
||||
TAP_TOKEN: ${{ secrets.TAP_TOKEN }}
|
||||
run: |
|
||||
git clone https://x-access-token:${{ secrets.TAP_TOKEN }}@github.com/${{ github.repository_owner }}/homebrew-tap /tmp/homebrew-tap
|
||||
sed -i '' \
|
||||
-e 's/version "[^"]*"/version "${{ steps.version.outputs.version }}"/' \
|
||||
-e 's/sha256 "[^"]*"/sha256 "${{ steps.sha.outputs.sha256 }}"/' \
|
||||
/tmp/homebrew-tap/Casks/claudechecker.rb
|
||||
cd /tmp/homebrew-tap
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add Casks/claudechecker.rb
|
||||
git commit -m "Update claudechecker to ${{ github.ref_name }}"
|
||||
git push
|
||||
@@ -60,11 +60,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
.sink { [weak self] _ in self?.updateStatusItem() }
|
||||
.store(in: &cancellables)
|
||||
|
||||
updateManager.$updateAvailable
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in self?.updateStatusItem() }
|
||||
.store(in: &cancellables)
|
||||
|
||||
// Refresh timer
|
||||
scheduleTimer(interval: usageViewModel.refreshInterval)
|
||||
|
||||
@@ -171,13 +166,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
let sdStr = sd.map { "\(Int($0.usedPercent.rounded()))%" } ?? "—"
|
||||
button.image = nil
|
||||
button.attributedTitle = NSAttributedString(string: "")
|
||||
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.title = "◔ \(fhStr) \(sdStr)"
|
||||
button.font = NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .medium)
|
||||
} else {
|
||||
button.title = ""
|
||||
@@ -210,7 +199,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@objc func showPopover() {
|
||||
guard let button = statusItem?.button, let popover else { return }
|
||||
if !popover.isShown {
|
||||
NotificationCenter.default.post(name: .popoverWillOpen, object: nil)
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
@@ -221,7 +209,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
if popover.isShown {
|
||||
popover.performClose(nil)
|
||||
} else {
|
||||
NotificationCenter.default.post(name: .popoverWillOpen, object: nil)
|
||||
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
|
||||
@@ -47,29 +47,13 @@ struct ContentView: View {
|
||||
EmptyStateView()
|
||||
.padding(.vertical, 40)
|
||||
} else {
|
||||
VStack(spacing: 8) {
|
||||
VStack(spacing: 1) {
|
||||
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)
|
||||
.background(Color(nsColor: .controlBackgroundColor))
|
||||
.cornerRadius(8)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.primary.opacity(0.08), lineWidth: 0.5))
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
LimitCard(limit: limit)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
.background(Color.primary.opacity(0.05))
|
||||
.padding(.vertical, 1)
|
||||
|
||||
// Extra credits row
|
||||
if let extra = vm.extraUsage, extra.isEnabled {
|
||||
@@ -110,13 +94,6 @@ struct ContentView: View {
|
||||
}
|
||||
.frame(width: 480)
|
||||
.animation(.easeInOut(duration: 0.2), value: showSettings)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .popoverWillOpen)) { _ in
|
||||
showSettings = false
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .openUpdateSheet)) { _ in
|
||||
showSettings = false
|
||||
showUpdateSheet = true
|
||||
}
|
||||
.sheet(isPresented: $showLogin) {
|
||||
LoginSheetView(isPresented: $showLogin) {
|
||||
Task { await vm.refresh() }
|
||||
@@ -212,7 +189,7 @@ struct LimitCard: View {
|
||||
AgentIconView(agent: .claude, size: 14)
|
||||
Text("Claude")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text("· \(vm.planLabel)")
|
||||
Text("· \(vm.planLabel) · \(limit.window.label)")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
@@ -797,16 +774,14 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(4)
|
||||
.fixedSize()
|
||||
} else if updater.betaAvailable {
|
||||
Text("v\(updater.latestBetaVersion) available")
|
||||
Text("v\(updater.latestBetaVersion) beta")
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange)
|
||||
.cornerRadius(4)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
Text("Built by superdooper86 & claude")
|
||||
@@ -996,36 +971,6 @@ struct UpdateSuccessBanner: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Markdown notes renderer
|
||||
|
||||
struct ReleaseNotesText: View {
|
||||
let text: String
|
||||
var font: Font = .system(size: 12)
|
||||
|
||||
var body: some View {
|
||||
let processed = text
|
||||
.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
.map { line -> String in
|
||||
let s = String(line)
|
||||
if s.hasPrefix("### ") { return "**\(s.dropFirst(4))**" }
|
||||
if s.hasPrefix("## ") { return "**\(s.dropFirst(3))**" }
|
||||
if s.hasPrefix("# ") { return "**\(s.dropFirst(2))**" }
|
||||
if s.hasPrefix("- ") { return "• \(s.dropFirst(2))" }
|
||||
return s
|
||||
}
|
||||
.joined(separator: "\n")
|
||||
|
||||
if let attr = try? AttributedString(markdown: processed,
|
||||
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) {
|
||||
Text(attr).font(font).foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text(text).font(font).foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Update Banner
|
||||
|
||||
struct UpdateBanner: View {
|
||||
@@ -1041,7 +986,10 @@ struct UpdateBanner: View {
|
||||
Text("Update available — v\(updater.latestVersion)")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
if !updater.releaseNotes.isEmpty {
|
||||
ReleaseNotesText(text: updater.releaseNotes, font: .system(size: 11))
|
||||
Text(updater.releaseNotes)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
@@ -1101,7 +1049,9 @@ struct UpdateSheet: View {
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
if !updater.releaseNotes.isEmpty {
|
||||
ReleaseNotesText(text: updater.releaseNotes)
|
||||
Text(updater.releaseNotes)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.2-beta.2</string>
|
||||
<string>3.0.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>35</string>
|
||||
<string>18</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>LSUIElement</key>
|
||||
|
||||
@@ -24,27 +24,21 @@ struct LoginWebView: NSViewRepresentable {
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
let onAuthenticated: () -> Void
|
||||
var didAuthenticate = false
|
||||
|
||||
init(onAuthenticated: @escaping () -> Void) {
|
||||
self.onAuthenticated = onAuthenticated
|
||||
}
|
||||
|
||||
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
|
||||
let hasSession = cookies.contains {
|
||||
$0.domain.contains("claude.ai") &&
|
||||
($0.name == "sessionKey" || $0.name == "__Secure-next-auth.session-token")
|
||||
}
|
||||
guard hasSession, !self.didAuthenticate else { return }
|
||||
self.didAuthenticate = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.onAuthenticated()
|
||||
if hasSession {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.onAuthenticated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,18 +58,9 @@ struct LoginSheetView: View {
|
||||
Text(authenticated ? "✓ Signed in — loading data…" : "Sign in to Claude")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Spacer()
|
||||
if authenticated {
|
||||
Button("Done") {
|
||||
isPresented = false
|
||||
onDone()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
Button("Cancel") { isPresented = false }
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
} else {
|
||||
Button("Cancel") { isPresented = false }
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 10)
|
||||
|
||||
@@ -46,8 +46,6 @@ class UpdateManager: ObservableObject {
|
||||
@Published var updateComplete = false
|
||||
@Published var justUpdated = false
|
||||
|
||||
private var notifiedVersion: String = ""
|
||||
|
||||
@Published var betaChannel: Bool = false {
|
||||
didSet {
|
||||
UserDefaults.standard.set(betaChannel, forKey: "beta_channel")
|
||||
@@ -72,13 +70,14 @@ class UpdateManager: ObservableObject {
|
||||
if let stable = await stableResult {
|
||||
print("[UpdateManager] Stable remote=\(stable.version), isNewer=\(isNewer(stable.version, than: currentVersion))")
|
||||
if isNewer(stable.version, than: currentVersion) {
|
||||
let wasAvailable = updateAvailable
|
||||
latestVersion = stable.version
|
||||
releaseNotes = stable.notes ?? ""
|
||||
downloadURL = stable.url
|
||||
updateAvailable = true
|
||||
print("[UpdateManager] updateAvailable set to true")
|
||||
if notifiedVersion != stable.version {
|
||||
notifiedVersion = stable.version
|
||||
// Only notify once — when first detected
|
||||
if !wasAvailable {
|
||||
NotificationCenter.default.post(name: .updateDetected, object: stable.version)
|
||||
}
|
||||
} else {
|
||||
@@ -87,8 +86,6 @@ class UpdateManager: ObservableObject {
|
||||
}
|
||||
} else {
|
||||
print("[UpdateManager] Stable fetch returned nil")
|
||||
updateAvailable = false
|
||||
latestVersion = ""
|
||||
}
|
||||
|
||||
if let beta = await betaResult {
|
||||
@@ -101,19 +98,10 @@ class UpdateManager: ObservableObject {
|
||||
releaseNotes = beta.notes ?? ""
|
||||
downloadURL = beta.url
|
||||
updateAvailable = true
|
||||
if notifiedVersion != beta.version {
|
||||
notifiedVersion = beta.version
|
||||
NotificationCenter.default.post(name: .updateDetected, object: beta.version)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
betaAvailable = false
|
||||
latestBetaVersion = ""
|
||||
}
|
||||
} else {
|
||||
print("[UpdateManager] Beta fetch returned nil")
|
||||
betaAvailable = false
|
||||
latestBetaVersion = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,30 +306,13 @@ class UpdateManager: ObservableObject {
|
||||
}
|
||||
|
||||
private func isNewer(_ version: String, than current: String) -> Bool {
|
||||
func parse(_ v: String) -> (base: [Int], pre: [Int]?) {
|
||||
let halves = v.split(separator: "-", maxSplits: 1)
|
||||
let base = halves[0].split(separator: ".").compactMap { Int($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
|
||||
let toInts = { (v: String) in v.split(separator: ".").compactMap { Int($0) } }
|
||||
let a = toInts(version), b = toInts(current)
|
||||
for i in 0..<max(a.count, b.count) {
|
||||
let av = i < a.count ? a[i] : 0
|
||||
let bv = i < b.count ? b[i] : 0
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class UpdateNotificationWindowController: NSWindowController {
|
||||
|
||||
// Called when a new update is detected while running
|
||||
static func showUpdate(version: String, notes: String, near statusItem: NSStatusItem?) {
|
||||
showWindow(version: version, subtitle: "Tap to view release notes & update now!", near: statusItem, isUpdate: true)
|
||||
showWindow(version: version, subtitle: notes.isEmpty ? "Tap to update now." : notes, near: statusItem, isUpdate: true)
|
||||
}
|
||||
|
||||
static func showLimitWarning(windowName: String, percent: Int, near statusItem: NSStatusItem?) {
|
||||
@@ -39,12 +39,7 @@ class UpdateNotificationWindowController: NSWindowController {
|
||||
sharedLimit?.close()
|
||||
let content = UpdateNotificationView(
|
||||
version: "", subtitle: subtitle, isUpdate: false,
|
||||
titleText: title, badgeIcon: badgeIcon, badgeColor: badgeColor,
|
||||
onTap: {
|
||||
sharedLimit?.close()
|
||||
sharedLimit = nil
|
||||
NotificationCenter.default.post(name: .showPopover, object: nil)
|
||||
}
|
||||
titleText: title, badgeIcon: badgeIcon, badgeColor: badgeColor
|
||||
) {
|
||||
sharedLimit?.close()
|
||||
sharedLimit = nil
|
||||
@@ -58,16 +53,7 @@ class UpdateNotificationWindowController: NSWindowController {
|
||||
|
||||
private static func showWindow(version: String, subtitle: String, near statusItem: NSStatusItem?, isUpdate: Bool) {
|
||||
shared?.close()
|
||||
let content = UpdateNotificationView(version: version, subtitle: subtitle, isUpdate: isUpdate,
|
||||
onTap: {
|
||||
shared?.close()
|
||||
shared = nil
|
||||
NotificationCenter.default.post(name: .showPopover, object: nil)
|
||||
if isUpdate {
|
||||
NotificationCenter.default.post(name: .openUpdateSheet, object: nil)
|
||||
}
|
||||
}
|
||||
) {
|
||||
let content = UpdateNotificationView(version: version, subtitle: subtitle, isUpdate: isUpdate) {
|
||||
shared?.close()
|
||||
shared = nil
|
||||
}
|
||||
@@ -143,7 +129,6 @@ struct UpdateNotificationView: View {
|
||||
var titleText: String? = nil
|
||||
var badgeIcon: String? = nil
|
||||
var badgeColor: Color? = nil
|
||||
var onTap: (() -> Void)? = nil
|
||||
let onDismiss: () -> Void
|
||||
|
||||
private var effectiveTitle: String {
|
||||
@@ -157,40 +142,30 @@ struct UpdateNotificationView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Button(action: { onTap?() }) {
|
||||
HStack(spacing: 12) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Image(nsImage: NSImage(named: "AppIcon") ?? NSImage())
|
||||
.resizable()
|
||||
.frame(width: 44, height: 44)
|
||||
.cornerRadius(10)
|
||||
HStack(spacing: 12) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
Image(nsImage: NSImage(named: "AppIcon") ?? NSImage())
|
||||
.resizable()
|
||||
.frame(width: 44, height: 44)
|
||||
.cornerRadius(10)
|
||||
|
||||
Image(systemName: effectiveBadgeIcon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(effectiveBadgeColor)
|
||||
.background(Color(nsColor: .windowBackgroundColor).clipShape(Circle()))
|
||||
.offset(x: 4, y: 4)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(effectiveTitle)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
Text(subtitle)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Spacer(minLength: 24)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 14)
|
||||
.frame(width: 340, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
Image(systemName: effectiveBadgeIcon)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(effectiveBadgeColor)
|
||||
.background(Color(nsColor: .windowBackgroundColor).clipShape(Circle()))
|
||||
.offset(x: 4, y: 4)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(effectiveTitle)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text(subtitle)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Spacer(minLength: 8)
|
||||
|
||||
Button(action: onDismiss) {
|
||||
Image(systemName: "xmark")
|
||||
@@ -198,8 +173,10 @@ struct UpdateNotificationView: View {
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(14)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 14)
|
||||
.frame(width: 340, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 13)
|
||||
.fill(Color(nsColor: .windowBackgroundColor))
|
||||
|
||||
@@ -307,9 +307,7 @@ extension Notification.Name {
|
||||
static let refreshIntervalChanged = Notification.Name("refreshIntervalChanged")
|
||||
static let closePopover = Notification.Name("closePopover")
|
||||
static let showPopover = Notification.Name("showPopover")
|
||||
static let popoverWillOpen = Notification.Name("popoverWillOpen")
|
||||
static let updateDetected = Notification.Name("updateDetected")
|
||||
static let limitWarning = Notification.Name("limitWarning")
|
||||
static let limitReset = Notification.Name("limitReset")
|
||||
static let openUpdateSheet = Notification.Name("openUpdateSheet")
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
[](https://www.apple.com/macos/)
|
||||
[](https://swift.org)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](https://github.com/superdooper86/claudechecker/releases)
|
||||
[](LICENSE)
|
||||
|
||||
</div>
|
||||
@@ -113,29 +113,25 @@ No API key required. It reads directly from your `claude.ai` session using a bui
|
||||
## Requirements
|
||||
|
||||
- macOS 13 Ventura or later
|
||||
- Xcode 15+ (to build from source)
|
||||
- A claude.ai account
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Homebrew (easiest)
|
||||
```bash
|
||||
brew tap superdooper86/tap
|
||||
brew install --cask claudechecker
|
||||
```
|
||||
|
||||
### Download
|
||||
### Download (recommended)
|
||||
1. Go to [Releases](https://github.com/superdooper86/claudechecker/releases)
|
||||
2. Download `ClaudeChecker.zip` from the latest release
|
||||
3. Unzip and drag `ClaudeChecker.app` to `/Applications`
|
||||
4. Open from Applications — it will appear in your menubar
|
||||
|
||||
> **macOS security prompt?** If macOS blocks the app on first launch, run this in Terminal:
|
||||
> ```bash
|
||||
> xattr -cr /Applications/ClaudeChecker.app
|
||||
> ```
|
||||
> This is not needed when installing via Homebrew.
|
||||
### Build from source
|
||||
```bash
|
||||
git clone https://github.com/superdooper86/claudechecker.git
|
||||
open ClaudeChecker/ClaudeChecker.xcodeproj
|
||||
```
|
||||
Then in Xcode: set your Team under Signing & Capabilities, then **⌘R** to run.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
## What's new in v1.1.2
|
||||
|
||||
### Cleaner card headers
|
||||
Removed 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.2-beta.1",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.2-beta.1/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."
|
||||
"version": "3.1.0-beta",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v3.0.0/ClaudeChecker.zip",
|
||||
"notes": "Beta test build."
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.1.1",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v1.1.1/ClaudeChecker.zip",
|
||||
"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."
|
||||
"version": "3.0.0",
|
||||
"url": "https://github.com/superdooper86/claudechecker/releases/download/v3.0.0/ClaudeChecker.zip",
|
||||
"notes": "Limit notifications for 80% and 95% usage, and on quota reset."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user