Skip to content

Commit 5ea1ef3

Browse files
committed
ship v0.6.3
1 parent 0945640 commit 5ea1ef3

25 files changed

Lines changed: 1665 additions & 645 deletions

Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
<key>CFBundlePackageType</key>
1616
<string>APPL</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>0.6.2</string>
18+
<string>0.6.3</string>
1919
<key>CFBundleVersion</key>
20-
<string>23</string>
20+
<string>24</string>
2121
<key>LSMinimumSystemVersion</key>
2222
<string>13.0</string>
2323
<key>LSUIElement</key>

Resources/break-mist-hill.png

1.7 MB
Loading

Resources/break-rain-field.png

2.01 MB
Loading

Resources/break-sunlit-meadow.png

2.7 MB
Loading

Resources/break-window-rain.png

1.9 MB
Loading

Resources/navi-panda.png

-960 KB
Binary file not shown.

Sources/Kaji/AppDelegate.swift

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
2929
private lazy var workSession = WorkSessionController(prefs: prefs)
3030
private let systemMonitor = SystemMonitor()
3131
private let dailyGoals = DailyGoalStore()
32+
private let popoverNavigation = PopoverNavigation()
3233
private var breakWindows: [NSWindow] = []
34+
private var breakSceneSeed: UInt64?
3335
private var breakWatchdogTimer: Timer?
3436
private var petStateTimer: Timer?
3537
private var cancellables = Set<AnyCancellable>()
@@ -86,6 +88,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
8688
self?.rebuildPopoverContentIfShown()
8789
}
8890
.store(in: &cancellables)
91+
dailyGoals.objectWillChange
92+
.receive(on: RunLoop.main)
93+
.sink { [weak self] _ in
94+
DispatchQueue.main.async {
95+
self?.updateStatusItem()
96+
}
97+
}
98+
.store(in: &cancellables)
8999
NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.didActivateApplicationNotification)
90100
.receive(on: RunLoop.main)
91101
.sink { [weak self] _ in self?.publishPetState() }
@@ -193,6 +203,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
193203
// Re-check on reactivation; the checker's own once/6h throttle keeps this
194204
// from hitting the network on every menubar interaction.
195205
updateChecker.checkIfDue()
206+
dailyGoals.refreshPeriodBoundaries()
196207
}
197208

198209
// MARK: - Status item
@@ -204,7 +215,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
204215
let view = StatusItemView(providers: visibleProviders,
205216
showRemaining: prefs.showRemaining,
206217
updateAvailable: updateChecker.available != nil,
207-
workSlotLabel: workStatusSlotLabel)
218+
workSlotLabel: workStatusSlotLabel,
219+
goalsSlotLabel: goalsStatusSlotLabel,
220+
onQuotaClick: { [weak self] in self?.showPopover(.quota) },
221+
onWorkClick: { [weak self] in self?.showPopover(.work) },
222+
onGoalsClick: { [weak self] in self?.showPopover(.goalsToday) })
208223
hostingView = KajiHostingView(rootView: view)
209224
hostingView.configureKajiHost()
210225
hostingView.translatesAutoresizingMaskIntoConstraints = false
@@ -215,17 +230,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
215230
hostingView.topAnchor.constraint(equalTo: button.topAnchor),
216231
hostingView.bottomAnchor.constraint(equalTo: button.bottomAnchor),
217232
])
218-
219-
button.target = self
220-
button.action = #selector(statusButtonClicked(_:))
221-
button.sendAction(on: [.leftMouseUp, .rightMouseUp])
222233
}
223234

224235
private func updateStatusItem() {
225236
hostingView?.rootView = StatusItemView(providers: visibleProviders,
226237
showRemaining: prefs.showRemaining,
227238
updateAvailable: updateChecker.available != nil,
228-
workSlotLabel: workStatusSlotLabel)
239+
workSlotLabel: workStatusSlotLabel,
240+
goalsSlotLabel: goalsStatusSlotLabel,
241+
onQuotaClick: { [weak self] in self?.showPopover(.quota) },
242+
onWorkClick: { [weak self] in self?.showPopover(.work) },
243+
onGoalsClick: { [weak self] in self?.showPopover(.goalsToday) })
229244
statusItem.length = statusItemLength
230245
}
231246

@@ -246,20 +261,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
246261
)
247262
}
248263

264+
private var goalsStatusSlotLabel: String? {
265+
MenuBarSlotLogic.goalsLabel(
266+
enabled: prefs.isModuleEnabled(.goals),
267+
goals: dailyGoals.goals
268+
)
269+
}
270+
249271
private var statusItemLength: CGFloat {
250272
let count = max(1, min(4, visibleProviders.count))
251273
var length = CGFloat(count) * 26 + 6
252274
// Compact monospaced `MM:SS` to the right of the rings (~40pt).
253275
if workStatusSlotLabel != nil {
254276
length += 40
255277
}
278+
if let goalsStatusSlotLabel {
279+
length += 8 + CGFloat(goalsStatusSlotLabel.count) * 7
280+
}
256281
return length
257282
}
258283

259-
@objc private func statusButtonClicked(_ sender: NSStatusBarButton) {
260-
togglePopover(sender)
261-
}
262-
263284
// MARK: - Popover
264285

265286
private func setupPopover() {
@@ -269,11 +290,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
269290
popover = pop
270291
}
271292

272-
private func togglePopover(_ sender: NSStatusBarButton) {
293+
private func showPopover(_ destination: MenuBarDestination) {
294+
guard let sender = statusItem.button else { return }
295+
dailyGoals.refreshPeriodBoundaries()
273296
if popover.isShown {
274-
popover.performClose(sender)
297+
if destinationMatchesCurrentPanel(destination) {
298+
popover.performClose(sender)
299+
return
300+
}
301+
applyPopoverDestination(destination)
275302
return
276303
}
304+
305+
// AppKit's first fitting pass can leave the initial SwiftUI page offset.
306+
// Fit from another enabled page, then switch after placement so every
307+
// direct menu-bar destination follows the same path as normal paging.
308+
let stagingDestination = popoverStagingDestination(for: destination)
309+
applyPopoverDestination(stagingDestination)
310+
let requestedDestination = destination
311+
277312
// Rebuild content each open. Width is pinned to `prefs.panelSize`
278313
// so the popover follows S/M; height auto-fits since the popover
279314
// also shows the settings footer (which the HUD doesn't).
@@ -287,6 +322,54 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
287322
popover.contentViewController = controller
288323
popover.show(relativeTo: sender.bounds, of: sender, preferredEdge: .minY)
289324
popover.contentViewController?.view.window?.makeKey()
325+
326+
if requestedDestination != stagingDestination {
327+
DispatchQueue.main.async { [weak self] in
328+
self?.applyPopoverDestination(requestedDestination)
329+
}
330+
}
331+
}
332+
333+
private func popoverStagingDestination(for destination: MenuBarDestination) -> MenuBarDestination {
334+
switch destination {
335+
case .quota:
336+
if prefs.isModuleEnabled(.work) { return .work }
337+
if prefs.isModuleEnabled(.goals) { return .goalsToday }
338+
return .quota
339+
case .work, .goalsToday:
340+
return .quota
341+
}
342+
}
343+
344+
private func destinationMatchesCurrentPanel(_ destination: MenuBarDestination) -> Bool {
345+
switch destination {
346+
case .quota:
347+
return popoverNavigation.panel == .quota
348+
case .work:
349+
return popoverNavigation.panel == .work
350+
case .goalsToday:
351+
return popoverNavigation.panel == .goals
352+
}
353+
}
354+
355+
private func applyPopoverDestination(_ destination: MenuBarDestination) {
356+
switch destination {
357+
case .quota:
358+
popoverNavigation.panel = .quota
359+
case .work:
360+
guard prefs.isModuleEnabled(.work) else {
361+
popoverNavigation.panel = .quota
362+
break
363+
}
364+
popoverNavigation.panel = .work
365+
case .goalsToday:
366+
guard prefs.isModuleEnabled(.goals) else {
367+
popoverNavigation.panel = .quota
368+
break
369+
}
370+
popoverNavigation.panel = .goals
371+
popoverNavigation.goalHorizon = .today
372+
}
290373
}
291374

292375
private func makePopoverContentController(maxContentHeight: CGFloat? = nil) -> NSHostingController<AnyView> {
@@ -303,6 +386,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
303386
workSession: workSession,
304387
systemMonitor: systemMonitor,
305388
dailyGoals: dailyGoals,
389+
navigation: popoverNavigation,
306390
controls: controls,
307391
maxContentHeight: maxContentHeight ?? maxPopoverHeight(on: statusItem.button?.window?.screen),
308392
onContentSizeChange: { [weak self] size in
@@ -463,14 +547,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
463547
return
464548
}
465549
let mainScreen = NSScreen.main
550+
let sceneSeed = breakSceneSeed ?? UInt64.random(in: UInt64.min...UInt64.max)
551+
breakSceneSeed = sceneSeed
552+
let scene = BreakSceneModel.scene(sessionSeed: sceneSeed)
466553
breakWindows = NSScreen.screens.map { screen in
467554
let isPrimary = screen == mainScreen
468555
let view = BreakOverlayView(prefs: prefs,
469556
workSession: workSession,
470-
petCatalog: petCatalog,
471-
dailyGoals: dailyGoals,
557+
scene: scene,
472558
isPrimary: isPrimary,
473-
onStartBreak: { [weak self] in self?.workSession.startBreak() },
474559
onSkip: { [weak self] in self?.workSession.skipBreak() })
475560
.ignoresSafeArea()
476561
let hostingView = KajiHostingView(rootView: view)
@@ -509,6 +594,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
509594
private func closeBreakOverlay() {
510595
breakWindows.forEach { $0.close() }
511596
breakWindows.removeAll()
597+
breakSceneSeed = nil
512598
}
513599
}
514600

0 commit comments

Comments
 (0)