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
25 changes: 25 additions & 0 deletions mac/Sources/TokenomicsMenubar/MinuteClock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Combine
import Foundation

/// A single refresh source shared by the status item and its popover.
/// `TimelineView` inside a `MenuBarExtra` label causes continuous status-item rendering on macOS.
@MainActor
public final class MinuteClock: ObservableObject {
@Published public private(set) var now: Date

private var cancellable: AnyCancellable?

public convenience init(now: Date = .now) {
let updates = Timer.publish(every: 60, tolerance: 1, on: .main, in: .common)
.autoconnect()
.eraseToAnyPublisher()
self.init(now: now, updates: updates)
}

init(now: Date, updates: AnyPublisher<Date, Never>) {
self.now = now
cancellable = updates.sink { [weak self] date in
self?.now = date
}
}
}
5 changes: 4 additions & 1 deletion mac/Sources/TokenomicsMenubar/TokenomicsMenubarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
struct TokenomicsMenubar: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@StateObject private var coordinator: ConnectionCoordinator
@StateObject private var clock: MinuteClock
private let settingsWindowController: SettingsWindowController

init() {
let prefs = PreferencesStore()
_coordinator = StateObject(wrappedValue: ConnectionCoordinator(preferences: prefs))
_clock = StateObject(wrappedValue: MinuteClock())
settingsWindowController = SettingsWindowController(preferences: prefs)
}

var body: some Scene {
MenuBarExtra {
PopoverView(
coordinator: coordinator,
clock: clock,
settingsWindowController: settingsWindowController
)
} label: {
MenuBarLabelView(coordinator: coordinator)
MenuBarLabelView(coordinator: coordinator, clock: clock)
}
.menuBarExtraStyle(.window)
}
Expand Down
41 changes: 21 additions & 20 deletions mac/Sources/TokenomicsMenubar/Views.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,27 +146,27 @@ enum MenuBarPresentation {

public struct MenuBarLabelView: View {
@ObservedObject var coordinator: ConnectionCoordinator
@ObservedObject private var clock: MinuteClock
@ObservedObject private var preferences: PreferencesStore

public init(coordinator: ConnectionCoordinator) {
public init(coordinator: ConnectionCoordinator, clock: MinuteClock) {
self.coordinator = coordinator
self.clock = clock
_preferences = ObservedObject(wrappedValue: coordinator.preferences)
}

public var body: some View {
TimelineView(.periodic(from: .now, by: 60)) { context in
HStack(spacing: 4) {
if let text = labelText(now: context.date) {
if coordinator.state.isUsable == false { Image(systemName: iconName).accessibilityHidden(true) }
Text(text)
} else {
Image(systemName: iconName).accessibilityHidden(true)
Text(stateLabel)
}
HStack(spacing: 4) {
if let text = labelText(now: clock.now) {
if coordinator.state.isUsable == false { Image(systemName: iconName).accessibilityHidden(true) }
Text(text)
} else {
Image(systemName: iconName).accessibilityHidden(true)
Text(stateLabel)
}
.accessibilityLabel(accessibilityText(now: context.date))
.help(accessibilityText(now: context.date))
}
.accessibilityLabel(accessibilityText(now: clock.now))
.help(accessibilityText(now: clock.now))
.font(.system(size: 12, weight: .medium, design: .rounded))
.monospacedDigit()
.foregroundStyle(labelColor)
Expand Down Expand Up @@ -222,14 +222,17 @@ public struct MenuBarLabelView: View {

public struct PopoverView: View {
@ObservedObject var coordinator: ConnectionCoordinator
@ObservedObject private var clock: MinuteClock
private let settingsWindowController: SettingsWindowController
@State private var showDetails = false

public init(
coordinator: ConnectionCoordinator,
clock: MinuteClock,
settingsWindowController: SettingsWindowController
) {
self.coordinator = coordinator
self.clock = clock
self.settingsWindowController = settingsWindowController
}

Expand Down Expand Up @@ -319,15 +322,13 @@ public struct PopoverView: View {
private var quotaSection: some View {
let windows = QuotaPresentation.orderedWindows(payload?.subscriptionWindows ?? [])
if !windows.isEmpty {
TimelineView(.periodic(from: .now, by: 60)) { context in
if windows.count > 4 {
ScrollView {
QuotaCockpitView(windows: windows, now: context.date)
}
.frame(maxHeight: 180)
} else {
QuotaCockpitView(windows: windows, now: context.date)
if windows.count > 4 {
ScrollView {
QuotaCockpitView(windows: windows, now: clock.now)
}
.frame(maxHeight: 180)
} else {
QuotaCockpitView(windows: windows, now: clock.now)
}
Divider()
}
Expand Down
24 changes: 24 additions & 0 deletions mac/Tests/TokenomicsMenubarTests/TokenomicsMenubarTests.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import AppKit
import Combine
import Darwin
import Foundation
import SwiftUI
import XCTest
@testable import TokenomicsMenubar

@MainActor
final class MinuteClockTests: XCTestCase {
func testClockPublishesInjectedMinuteTicks() {
let start = Date(timeIntervalSince1970: 1_000)
let tick = Date(timeIntervalSince1970: 1_060)
let updates = PassthroughSubject<Date, Never>()
let clock = MinuteClock(now: start, updates: updates.eraseToAnyPublisher())

var observed: [Date] = []
let observation = clock.$now.sink { observed.append($0) }
defer { observation.cancel() }

updates.send(tick)

XCTAssertEqual(clock.now, tick)
XCTAssertEqual(observed, [start, tick])
}
}

final class BudgetUsageLevelTests: XCTestCase {
func testUsageLevelThresholds() {
XCTAssertEqual(BudgetUsageLevel(amount: 79.99, limit: 100), .normal)
Expand Down Expand Up @@ -121,6 +141,10 @@ final class SettingsWindowControllerTests: XCTestCase {
let view = NSHostingView(
rootView: PopoverView(
coordinator: coordinator,
clock: MinuteClock(
now: Date(timeIntervalSince1970: 1_000),
updates: Empty<Date, Never>().eraseToAnyPublisher()
),
settingsWindowController: settings
)
)
Expand Down
Loading