Skip to content

Commit bb5e23e

Browse files
committed
Add social share cards
1 parent 237aeb9 commit bb5e23e

12 files changed

Lines changed: 545 additions & 27 deletions

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Sub2API Status Bar is a macOS menu bar companion for Sub2API users. It keeps dai
1111
- Local proactive alerts for warning/error insights, with severity and quiet-period controls
1212
- Stale-data guardrail so the menu bar and local alerts warn when the last successful refresh is too old
1313
- Notification permission status inside Settings, including quick access to macOS notification settings
14+
- Copy Share Card for a social-ready anonymous usage card plus text fallback, with a one-click X draft
1415
- Copy Usage Report for a shareable, credential-free summary of balance, spend, tokens, quotas, trend, models, and insights
1516
- Custom insight thresholds for quota pressure, balance runway, monthly budget, token surge, model concentration, and latency
1617
- User dashboard cards for balance, API keys, requests, spend, blended cost per million tokens, token totals, RPM/TPM, and response time
@@ -84,6 +85,7 @@ Settings also includes:
8485
- **Notification status** to confirm alerts are ready or open macOS settings when permissions are blocked
8586
- **Insights thresholds** to tune when quota, balance, spend surge, token surge, model-share, and latency warnings appear
8687
- **Launch at login** so the monitor starts with macOS
88+
- **Copy Share Card** for an anonymous visual usage card and social post text without account details
8789
- **Copy Usage Report** for a shareable usage summary with credentials omitted
8890
- **Copy Diagnostics** for support-safe status details with tokens redacted
8991
- **Show Config** to reveal the local `config.json`

Sources/Sub2APIStatusBar/MonitorPanel.swift

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,34 @@ struct MonitorPanel: View {
2323
UsageTrendSection(state: UsageTrendDisplayState.make(points: model.snapshot.trend))
2424
}
2525

26-
HStack {
27-
Button {
28-
model.copyUsageReport()
29-
} label: {
30-
Label("Copy Usage Report", systemImage: "doc.on.doc")
26+
VStack(alignment: .leading, spacing: 6) {
27+
HStack {
28+
Button {
29+
model.copySocialShareCard()
30+
} label: {
31+
Label("Copy Share Card", systemImage: "square.and.arrow.up")
32+
}
33+
34+
Button {
35+
model.copyUsageReport()
36+
} label: {
37+
Label("Copy Usage Report", systemImage: "doc.on.doc")
38+
}
39+
40+
Button {
41+
model.openSocialShareDraft()
42+
} label: {
43+
Label("Post to X", systemImage: "paperplane")
44+
}
45+
46+
Spacer()
3147
}
3248

33-
if let message = model.updateStatusMessage, message == "Usage report copied." {
49+
if let message = model.updateStatusMessage, message.hasPrefix("Share") || message == "Usage report copied." {
3450
Text(message)
3551
.font(.caption)
3652
.foregroundStyle(.secondary)
3753
}
38-
39-
Spacer()
4054
}
4155
.buttonStyle(.borderless)
4256
}

Sources/Sub2APIStatusBar/MonitorViewModel.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,38 @@ final class MonitorViewModel: ObservableObject {
333333
updateStatusMessage = "Usage report copied."
334334
}
335335

336+
func copySocialShareCard() {
337+
let summary = SocialShareSummary.make(
338+
config: config,
339+
snapshot: snapshot,
340+
now: now
341+
)
342+
let pasteboard = NSPasteboard.general
343+
pasteboard.clearContents()
344+
if let image = SocialShareCardRenderer.image(for: summary) {
345+
pasteboard.writeObjects([image, NSString(string: summary.shareText)])
346+
updateStatusMessage = "Share card copied."
347+
} else {
348+
pasteboard.setString(summary.shareText, forType: .string)
349+
updateStatusMessage = "Share text copied."
350+
}
351+
}
352+
353+
func openSocialShareDraft() {
354+
let summary = SocialShareSummary.make(
355+
config: config,
356+
snapshot: snapshot,
357+
now: now
358+
)
359+
var components = URLComponents(string: "https://twitter.com/intent/tweet")
360+
components?.queryItems = [
361+
URLQueryItem(name: "text", value: summary.shareText),
362+
]
363+
if let url = components?.url {
364+
NSWorkspace.shared.open(url)
365+
}
366+
}
367+
336368
func revealConfigFile() {
337369
NSWorkspace.shared.activateFileViewerSelecting([store.configurationFileURL])
338370
}

Sources/Sub2APIStatusBar/SettingsView.swift

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -400,24 +400,35 @@ struct DiagnosticsSettingsSection: View {
400400
VStack(alignment: .leading, spacing: 8) {
401401
Text("Diagnostics")
402402
.font(.headline)
403-
HStack {
404-
Button {
405-
model.copyUsageReport()
406-
} label: {
407-
Label("Copy Usage Report", systemImage: "chart.line.text.clipboard")
408-
}
409-
.disabled(!model.snapshot.connected)
403+
VStack(alignment: .leading, spacing: 8) {
404+
HStack {
405+
Button {
406+
model.copySocialShareCard()
407+
} label: {
408+
Label("Copy Share Card", systemImage: "square.and.arrow.up")
409+
}
410+
.disabled(!model.snapshot.connected)
410411

411-
Button {
412-
model.copyDiagnostics()
413-
} label: {
414-
Label("Copy Diagnostics", systemImage: "doc.on.doc")
412+
Button {
413+
model.copyUsageReport()
414+
} label: {
415+
Label("Copy Usage Report", systemImage: "chart.line.text.clipboard")
416+
}
417+
.disabled(!model.snapshot.connected)
415418
}
416419

417-
Button {
418-
model.revealConfigFile()
419-
} label: {
420-
Label("Show Config", systemImage: "folder")
420+
HStack {
421+
Button {
422+
model.copyDiagnostics()
423+
} label: {
424+
Label("Copy Diagnostics", systemImage: "doc.on.doc")
425+
}
426+
427+
Button {
428+
model.revealConfigFile()
429+
} label: {
430+
Label("Show Config", systemImage: "folder")
431+
}
421432
}
422433
}
423434
.buttonStyle(.borderless)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import AppKit
2+
import SwiftUI
3+
import Sub2APIStatusCore
4+
5+
enum SocialShareCardRenderer {
6+
@MainActor
7+
static func image(for summary: SocialShareSummary) -> NSImage? {
8+
let view = SocialShareCard(summary: summary)
9+
.frame(width: 920, height: 520)
10+
let renderer = ImageRenderer(content: view)
11+
renderer.proposedSize = ProposedViewSize(width: 920, height: 520)
12+
renderer.scale = 2
13+
return renderer.nsImage
14+
}
15+
}
16+
17+
struct SocialShareCard: View {
18+
let summary: SocialShareSummary
19+
20+
var body: some View {
21+
ZStack {
22+
LinearGradient(
23+
colors: [
24+
Color(red: 0.04, green: 0.06, blue: 0.08),
25+
Color(red: 0.08, green: 0.14, blue: 0.16),
26+
Color(red: 0.04, green: 0.05, blue: 0.07),
27+
],
28+
startPoint: .topLeading,
29+
endPoint: .bottomTrailing
30+
)
31+
32+
VStack(alignment: .leading, spacing: 24) {
33+
HStack(alignment: .top) {
34+
VStack(alignment: .leading, spacing: 8) {
35+
Text(summary.title)
36+
.font(.system(size: 34, weight: .bold, design: .rounded))
37+
.foregroundStyle(.white)
38+
Text(summary.tagline)
39+
.font(.system(size: 18, weight: .medium, design: .rounded))
40+
.foregroundStyle(Color.white.opacity(0.68))
41+
}
42+
43+
Spacer()
44+
45+
Text("Sub2API Status Bar")
46+
.font(.system(size: 15, weight: .semibold, design: .rounded))
47+
.foregroundStyle(Color.white.opacity(0.86))
48+
.padding(.horizontal, 14)
49+
.padding(.vertical, 8)
50+
.background(Color.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
51+
}
52+
53+
HStack(alignment: .lastTextBaseline, spacing: 14) {
54+
Text(summary.primaryMetric)
55+
.font(.system(size: 90, weight: .black, design: .rounded))
56+
.foregroundStyle(.white)
57+
.lineLimit(1)
58+
.minimumScaleFactor(0.78)
59+
Text(summary.primaryLabel.uppercased())
60+
.font(.system(size: 18, weight: .bold, design: .rounded))
61+
.foregroundStyle(Color(red: 0.54, green: 0.92, blue: 0.76))
62+
.lineLimit(2)
63+
}
64+
65+
HStack(spacing: 14) {
66+
shareMetric(title: "Spend", value: summary.spendText, tint: Color(red: 0.54, green: 0.92, blue: 0.76))
67+
shareMetric(title: "Requests", value: summary.requestsText, tint: Color(red: 0.44, green: 0.72, blue: 1.0))
68+
shareMetric(title: "Cost / MTok", value: summary.unitCostText, tint: Color(red: 0.96, green: 0.72, blue: 0.33))
69+
}
70+
71+
VStack(alignment: .leading, spacing: 11) {
72+
detailRow(icon: "sparkles", label: "Top model", value: summary.topModelText)
73+
detailRow(icon: "gauge", label: "Quota", value: summary.quotaText)
74+
detailRow(icon: "chart.line.uptrend.xyaxis", label: "Trend", value: summary.trendText)
75+
}
76+
77+
Spacer(minLength: 0)
78+
79+
HStack {
80+
Text(summary.generatedText)
81+
.font(.system(size: 13, weight: .medium, design: .rounded))
82+
.foregroundStyle(Color.white.opacity(0.5))
83+
Spacer()
84+
Text("#AIUsage #BuildInPublic")
85+
.font(.system(size: 15, weight: .bold, design: .rounded))
86+
.foregroundStyle(Color(red: 0.54, green: 0.92, blue: 0.76))
87+
}
88+
}
89+
.padding(34)
90+
}
91+
}
92+
93+
private func shareMetric(title: String, value: String, tint: Color) -> some View {
94+
VStack(alignment: .leading, spacing: 7) {
95+
Text(title)
96+
.font(.system(size: 13, weight: .bold, design: .rounded))
97+
.foregroundStyle(Color.white.opacity(0.54))
98+
Text(value)
99+
.font(.system(size: 28, weight: .black, design: .rounded).monospacedDigit())
100+
.foregroundStyle(tint)
101+
.lineLimit(1)
102+
.minimumScaleFactor(0.72)
103+
}
104+
.frame(maxWidth: .infinity, alignment: .leading)
105+
.padding(16)
106+
.background(Color.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
107+
.overlay(
108+
RoundedRectangle(cornerRadius: 8)
109+
.stroke(Color.white.opacity(0.09), lineWidth: 1)
110+
)
111+
}
112+
113+
private func detailRow(icon: String, label: String, value: String) -> some View {
114+
HStack(spacing: 10) {
115+
SafeSystemImage(systemName: icon, fallbackName: "circle.fill")
116+
.font(.system(size: 15, weight: .bold))
117+
.foregroundStyle(Color(red: 0.54, green: 0.92, blue: 0.76))
118+
.frame(width: 26, height: 26)
119+
.background(Color.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 6))
120+
Text(label)
121+
.font(.system(size: 14, weight: .bold, design: .rounded))
122+
.foregroundStyle(Color.white.opacity(0.56))
123+
.frame(width: 82, alignment: .leading)
124+
Text(value)
125+
.font(.system(size: 17, weight: .semibold, design: .rounded))
126+
.foregroundStyle(Color.white.opacity(0.9))
127+
.lineLimit(1)
128+
.minimumScaleFactor(0.72)
129+
Spacer(minLength: 0)
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)