Skip to content

Commit adfbbac

Browse files
committed
refactor: AppDelegate
1 parent 26e7b3d commit adfbbac

File tree

8 files changed

+316
-314
lines changed

8 files changed

+316
-314
lines changed

OpenAIAPIUsage.xcodeproj/project.pbxproj

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88

99
/* Begin PBXBuildFile section */
1010
EC16FD072B8497A300BDA8BC /* OpenAIAPIUsageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD062B8497A300BDA8BC /* OpenAIAPIUsageApp.swift */; };
11-
EC16FD092B8497A300BDA8BC /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD082B8497A300BDA8BC /* ContentView.swift */; };
12-
EC16FD0B2B8497A300BDA8BC /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD0A2B8497A300BDA8BC /* Item.swift */; };
1311
EC16FD0D2B8497A400BDA8BC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC16FD0C2B8497A400BDA8BC /* Assets.xcassets */; };
1412
EC16FD102B8497A400BDA8BC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC16FD0F2B8497A400BDA8BC /* Preview Assets.xcassets */; };
1513
EC16FD1B2B8497A500BDA8BC /* OpenAIAPIUsageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD1A2B8497A500BDA8BC /* OpenAIAPIUsageTests.swift */; };
1614
EC16FD252B8497A500BDA8BC /* OpenAIAPIUsageUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD242B8497A500BDA8BC /* OpenAIAPIUsageUITests.swift */; };
1715
EC16FD272B8497A500BDA8BC /* OpenAIAPIUsageUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD262B8497A500BDA8BC /* OpenAIAPIUsageUITestsLaunchTests.swift */; };
16+
EC16FD342B8499B600BDA8BC /* Usage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD332B8499B600BDA8BC /* Usage.swift */; };
17+
EC16FD362B8499D400BDA8BC /* WatchDog.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD352B8499D400BDA8BC /* WatchDog.swift */; };
18+
EC16FD382B849A0700BDA8BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD372B849A0700BDA8BC /* AppDelegate.swift */; };
19+
EC16FD3A2B849A8900BDA8BC /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC16FD392B849A8900BDA8BC /* API.swift */; };
1820
/* End PBXBuildFile section */
1921

2022
/* Begin PBXContainerItemProxy section */
@@ -37,8 +39,6 @@
3739
/* Begin PBXFileReference section */
3840
EC16FD032B8497A300BDA8BC /* OpenAIAPIUsage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenAIAPIUsage.app; sourceTree = BUILT_PRODUCTS_DIR; };
3941
EC16FD062B8497A300BDA8BC /* OpenAIAPIUsageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIAPIUsageApp.swift; sourceTree = "<group>"; };
40-
EC16FD082B8497A300BDA8BC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
41-
EC16FD0A2B8497A300BDA8BC /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
4242
EC16FD0C2B8497A400BDA8BC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
4343
EC16FD0F2B8497A400BDA8BC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
4444
EC16FD112B8497A400BDA8BC /* OpenAIAPIUsage.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OpenAIAPIUsage.entitlements; sourceTree = "<group>"; };
@@ -47,6 +47,10 @@
4747
EC16FD202B8497A500BDA8BC /* OpenAIAPIUsageUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenAIAPIUsageUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4848
EC16FD242B8497A500BDA8BC /* OpenAIAPIUsageUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIAPIUsageUITests.swift; sourceTree = "<group>"; };
4949
EC16FD262B8497A500BDA8BC /* OpenAIAPIUsageUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAIAPIUsageUITestsLaunchTests.swift; sourceTree = "<group>"; };
50+
EC16FD332B8499B600BDA8BC /* Usage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Usage.swift; sourceTree = "<group>"; };
51+
EC16FD352B8499D400BDA8BC /* WatchDog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchDog.swift; sourceTree = "<group>"; };
52+
EC16FD372B849A0700BDA8BC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
53+
EC16FD392B849A8900BDA8BC /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
5054
/* End PBXFileReference section */
5155

5256
/* Begin PBXFrameworksBuildPhase section */
@@ -98,8 +102,10 @@
98102
isa = PBXGroup;
99103
children = (
100104
EC16FD062B8497A300BDA8BC /* OpenAIAPIUsageApp.swift */,
101-
EC16FD082B8497A300BDA8BC /* ContentView.swift */,
102-
EC16FD0A2B8497A300BDA8BC /* Item.swift */,
105+
EC16FD372B849A0700BDA8BC /* AppDelegate.swift */,
106+
EC16FD352B8499D400BDA8BC /* WatchDog.swift */,
107+
EC16FD392B849A8900BDA8BC /* API.swift */,
108+
EC16FD332B8499B600BDA8BC /* Usage.swift */,
103109
EC16FD0C2B8497A400BDA8BC /* Assets.xcassets */,
104110
EC16FD112B8497A400BDA8BC /* OpenAIAPIUsage.entitlements */,
105111
EC16FD0E2B8497A400BDA8BC /* Preview Content */,
@@ -262,9 +268,11 @@
262268
isa = PBXSourcesBuildPhase;
263269
buildActionMask = 2147483647;
264270
files = (
265-
EC16FD092B8497A300BDA8BC /* ContentView.swift in Sources */,
266-
EC16FD0B2B8497A300BDA8BC /* Item.swift in Sources */,
271+
EC16FD382B849A0700BDA8BC /* AppDelegate.swift in Sources */,
267272
EC16FD072B8497A300BDA8BC /* OpenAIAPIUsageApp.swift in Sources */,
273+
EC16FD342B8499B600BDA8BC /* Usage.swift in Sources */,
274+
EC16FD362B8499D400BDA8BC /* WatchDog.swift in Sources */,
275+
EC16FD3A2B849A8900BDA8BC /* API.swift in Sources */,
268276
);
269277
runOnlyForDeploymentPostprocessing = 0;
270278
};

OpenAIAPIUsage/API.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// API.swift
3+
// OpenAIAPIUsage
4+
//
5+
// Created by Liang on 20/02/2024.
6+
//
7+
8+
import Foundation
9+
10+
struct API {
11+
static func getUsage(_ bearerToken: String) async -> Double? {
12+
let (startDate, endDate) = getFirstDaysOfCurrentAndNextMonth() //getTodaysAndNextMonthsFirstDate()
13+
print(startDate, endDate)
14+
let urlString = "https://api.openai.com/dashboard/billing/usage?end_date=\(endDate)&start_date=\(startDate)"
15+
guard let url = URL(string: urlString) else { return nil }
16+
var request = URLRequest(url: url)
17+
request.httpMethod = "GET"
18+
request.addValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
19+
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
20+
request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", forHTTPHeaderField: "User-Agent")
21+
22+
do {
23+
let (data, _) = try await URLSession.shared.data(for: request)
24+
let decoder = JSONDecoder()
25+
let usage = try decoder.decode(Usage.self, from: data)
26+
return usage.totalUsage
27+
} catch {
28+
print(error.localizedDescription)
29+
return nil
30+
}
31+
}
32+
33+
private static func getFirstDaysOfCurrentAndNextMonth() -> (String, String) {
34+
let dateFormatter = DateFormatter()
35+
dateFormatter.dateFormat = "yyyy-MM-dd"
36+
37+
let now = Date()
38+
let calendar = Calendar.current
39+
40+
let currentMonthComponents = calendar.dateComponents([.year, .month], from: now)
41+
let startOfCurrentMonth = calendar.date(from: currentMonthComponents)!
42+
43+
var nextMonthComponents = DateComponents()
44+
nextMonthComponents.month = 1
45+
let startOfNextMonth = calendar.date(byAdding: nextMonthComponents, to: startOfCurrentMonth)!
46+
47+
return (dateFormatter.string(from: startOfCurrentMonth), dateFormatter.string(from: startOfNextMonth))
48+
}
49+
50+
private static func getTodaysAndNextMonthsFirstDate() -> (today: String, firstDayOfNextMonth: String) {
51+
let dateFormatter = DateFormatter()
52+
dateFormatter.dateFormat = "yyyy-MM-dd"
53+
54+
let today = Date()
55+
var calendar = Calendar.current
56+
calendar.timeZone = TimeZone(identifier: "UTC") ?? .current
57+
58+
var components = DateComponents()
59+
components.month = 1
60+
components.day = -((calendar.component(.day, from: today) - 1))
61+
62+
let firstDayNextMonth = calendar.date(byAdding: components, to: today)!
63+
64+
// Reset to the first day of the next month
65+
let componentsForNextMonth = calendar.dateComponents([.year, .month], from: firstDayNextMonth)
66+
let firstDayOfNextMonth = calendar.date(from: componentsForNextMonth)!
67+
68+
return (dateFormatter.string(from: today), dateFormatter.string(from: firstDayOfNextMonth))
69+
}
70+
}

OpenAIAPIUsage/AppDelegate.swift

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
//
2+
// AppDelegate.swift
3+
// OpenAIAPIUsage
4+
//
5+
// Created by Liang on 20/02/2024.
6+
//
7+
8+
import Foundation
9+
import AppKit
10+
11+
12+
class AppDelegate: NSObject, NSApplicationDelegate {
13+
private var statusItem: NSStatusItem?
14+
private let bearerTokenKey = "BearerToken"
15+
private var websiteItem: NSMenuItem?
16+
private var textField: NSTextField!
17+
18+
func applicationDidFinishLaunching(_ notification: Notification) {
19+
setup()
20+
}
21+
}
22+
23+
//class MenuBar: NSObject {
24+
extension AppDelegate {
25+
// MARK: - Setup
26+
27+
func setup() {
28+
setupStatusBar()
29+
setupMenuItems()
30+
observeTokenUpdates()
31+
initiateWatchDog()
32+
}
33+
34+
private func setupStatusBar() {
35+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
36+
}
37+
38+
private func setupMenuItems() {
39+
let statusBarMenu = NSMenu(title: "Menu")
40+
setupWebsiteItem(in: statusBarMenu)
41+
statusBarMenu.addItem(.separator())
42+
setupTokenDisplay(in: statusBarMenu)
43+
setupTokenActions(in: statusBarMenu)
44+
statusBarMenu.addItem(.separator())
45+
setupQuitItem(in: statusBarMenu)
46+
statusItem?.menu = statusBarMenu
47+
}
48+
49+
private func setupWebsiteItem(in menu: NSMenu) {
50+
websiteItem = menu.addItem(
51+
withTitle: "3️⃣ Show Details",
52+
action: #selector(gotoWebsite),
53+
keyEquivalent: ""
54+
)
55+
}
56+
57+
private func setupTokenDisplay(in menu: NSMenu) {
58+
let customView = createTokenDisplayView()
59+
let tokenMenuItem = NSMenuItem()
60+
tokenMenuItem.view = customView
61+
menu.addItem(tokenMenuItem)
62+
}
63+
64+
private func setupTokenActions(in menu: NSMenu) {
65+
menu.addItem(
66+
withTitle: "1️⃣ Get Bearer Token",
67+
action: #selector(gotoWebsite),
68+
keyEquivalent: ""
69+
)
70+
71+
menu.addItem(
72+
withTitle: "2️⃣ Paste Bearer Token",
73+
action: #selector(pasteBearerToken),
74+
keyEquivalent: ""
75+
)
76+
}
77+
78+
private func setupQuitItem(in menu: NSMenu) {
79+
menu.addItem(
80+
withTitle: "Quit",
81+
action: #selector(quit),
82+
keyEquivalent: ""
83+
)
84+
}
85+
86+
private func createTokenDisplayView() -> NSView {
87+
let customView = NSView(frame: NSRect(x: 0, y: 0, width: 340, height: 20))
88+
let label = NSTextField(labelWithString: "Bearer Token")
89+
label.frame = NSRect(x: 12, y: 0, width: 100, height: 20)
90+
91+
textField = NSTextField(frame: NSRect(x: 105, y: 0, width: 230, height: 20))
92+
textField.isEditable = false
93+
textField.stringValue = getBearerToken()
94+
95+
customView.addSubview(label)
96+
customView.addSubview(textField)
97+
98+
return customView
99+
}
100+
101+
// MARK: - Token Management
102+
103+
private func getBearerToken() -> String {
104+
return UserDefaults.standard.string(forKey: bearerTokenKey) ?? ""
105+
}
106+
107+
private func setBearerToken(_ value: String) {
108+
UserDefaults.standard.set(value, forKey: bearerTokenKey)
109+
}
110+
111+
private func observeTokenUpdates() {
112+
let hasToken = !getBearerToken().isEmpty
113+
statusItem?.button?.title = hasToken ? "Updating..." : "No Bearer Token"
114+
update()
115+
}
116+
117+
private func initiateWatchDog() {
118+
WatchDog.shared.startRepeatingTimer(60) {
119+
self.update()
120+
}
121+
}
122+
123+
// MARK: - Actions
124+
125+
@objc private func gotoWebsite(sender: Any) {
126+
let urlString = "https://platform.openai.com/usage"
127+
if let url = URL(string: urlString) {
128+
NSWorkspace.shared.open(url)
129+
}
130+
}
131+
132+
@objc private func pasteBearerToken(_ sender: AnyObject) {
133+
if let token = fetchTokenFromPasteboard() {
134+
let value = token.trimmingCharacters(in: .whitespacesAndNewlines)
135+
textField.stringValue = value
136+
setBearerToken(value)
137+
update()
138+
}
139+
}
140+
141+
private func fetchTokenFromPasteboard() -> String? {
142+
let pasteboard = NSPasteboard.general
143+
if let text = pasteboard.string(forType: .string), text.hasPrefix("sess-") {
144+
return text
145+
}
146+
return nil
147+
}
148+
149+
@objc private func quit(sender: AnyObject) {
150+
NSApplication.shared.terminate(self)
151+
}
152+
153+
private func update() {
154+
let bearerToken = getBearerToken()
155+
guard !bearerToken.isEmpty else {
156+
return
157+
}
158+
159+
Task {
160+
let amount = await API.getUsage(bearerToken)
161+
DispatchQueue.main.async {
162+
self.updateBillAmount(amount)
163+
}
164+
}
165+
}
166+
167+
private func updateBillAmount(_ amount: Double?) {
168+
let title = amount != nil ? String(format: "$%.2f", amount!/100.0) : "USD?"
169+
statusItem?.button?.title = title
170+
}
171+
}

OpenAIAPIUsage/ContentView.swift

Lines changed: 0 additions & 59 deletions
This file was deleted.

OpenAIAPIUsage/Item.swift

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)