411 lines
18 KiB
Swift
411 lines
18 KiB
Swift
import AppKit
|
|
|
|
extension AppDelegate {
|
|
|
|
func showFullOverlay() {
|
|
countdownTimer?.invalidate()
|
|
currentSuggestion = SuggestionManager.shared.next()
|
|
statusItem.button?.title = "🍅 Time's Up!"
|
|
|
|
for window in countdownWindows { window.orderOut(nil) }
|
|
|
|
breakCountdownLabels.removeAll()
|
|
breakRemainingSeconds = 300
|
|
startBreakTimer()
|
|
|
|
for (i, window) in overlayWindows.enumerated() {
|
|
window.ignoresMouseEvents = false
|
|
buildSuggestionScreen(window: window, isPrimary: i == 0)
|
|
NSAnimationContext.runAnimationGroup { ctx in
|
|
ctx.duration = 0.3
|
|
window.animator().alphaValue = 1.0
|
|
}
|
|
}
|
|
}
|
|
|
|
func startBreakTimer() {
|
|
breakTimer?.invalidate()
|
|
breakTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
|
|
guard let self = self else { timer.invalidate(); return }
|
|
self.breakRemainingSeconds -= 1
|
|
self.updateBreakCountdownLabels()
|
|
if self.breakRemainingSeconds <= 0 {
|
|
timer.invalidate()
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateBreakCountdownLabels() {
|
|
let minutes = breakRemainingSeconds / 60
|
|
let seconds = breakRemainingSeconds % 60
|
|
let text = String(format: "%02d:%02d", minutes, seconds)
|
|
for label in breakCountdownLabels {
|
|
label.stringValue = text
|
|
}
|
|
}
|
|
|
|
private var breakScreenColor: NSColor {
|
|
Settings.shared.gradientColor.withAlphaComponent(0.85)
|
|
}
|
|
|
|
/// Replaces overlay with suggestion UI. Called when break countdown reaches 0 (same pulse schedule as work).
|
|
func showSuggestionScreen() {
|
|
for window in countdownWindows { window.orderOut(nil) }
|
|
for (i, window) in overlayWindows.enumerated() {
|
|
window.ignoresMouseEvents = false
|
|
buildSuggestionScreen(window: window, isPrimary: i == 0)
|
|
NSAnimationContext.runAnimationGroup { ctx in
|
|
ctx.duration = 0.3
|
|
window.animator().alphaValue = 1.0
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildSuggestionScreen(window: NSWindow, isPrimary: Bool) {
|
|
let size = window.frame.size
|
|
let view = NSView(frame: NSRect(origin: .zero, size: size))
|
|
view.wantsLayer = true
|
|
view.layer?.backgroundColor = breakScreenColor.cgColor
|
|
|
|
let countdownLabel = NSTextField(labelWithString: "05:00")
|
|
countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 72, weight: .bold)
|
|
countdownLabel.textColor = .white
|
|
countdownLabel.alignment = .center
|
|
countdownLabel.sizeToFit()
|
|
countdownLabel.frame.origin = CGPoint(
|
|
x: (size.width - countdownLabel.frame.width) / 2,
|
|
y: (size.height / 2) + 80
|
|
)
|
|
view.addSubview(countdownLabel)
|
|
breakCountdownLabels.append(countdownLabel)
|
|
|
|
let suggestion = NSTextField(labelWithString: currentSuggestion)
|
|
suggestion.font = NSFont.systemFont(ofSize: 48, weight: .medium)
|
|
suggestion.textColor = NSColor.white.withAlphaComponent(0.9)
|
|
suggestion.alignment = .center
|
|
suggestion.sizeToFit()
|
|
suggestion.frame.origin = CGPoint(
|
|
x: (size.width - suggestion.frame.width) / 2,
|
|
y: (size.height / 2) - 10
|
|
)
|
|
view.addSubview(suggestion)
|
|
|
|
if isPrimary {
|
|
let closeBtn = NSButton(frame: NSRect(x: size.width - 140, y: size.height - 60, width: 120, height: 36))
|
|
closeBtn.title = "Close"
|
|
closeBtn.bezelStyle = .rounded
|
|
closeBtn.font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
closeBtn.target = self
|
|
closeBtn.action = #selector(showWorkLog)
|
|
view.addSubview(closeBtn)
|
|
|
|
let continueWorkingBtn = NSButton(frame: NSRect(x: (size.width - 200) / 2, y: size.height / 2 - 50, width: 200, height: 50))
|
|
continueWorkingBtn.title = "Continue Working"
|
|
continueWorkingBtn.bezelStyle = .rounded
|
|
continueWorkingBtn.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
|
|
continueWorkingBtn.target = self
|
|
continueWorkingBtn.action = #selector(resumePommedoro)
|
|
view.addSubview(continueWorkingBtn)
|
|
|
|
let btnY = size.height / 2 - 110
|
|
let btnW: CGFloat = 160
|
|
let btnH: CGFloat = 44
|
|
let gap: CGFloat = 16
|
|
let totalW = btnW * 3 + gap * 2
|
|
let startX = (size.width - totalW) / 2
|
|
|
|
let successBtn = NSButton(frame: NSRect(x: startX, y: btnY, width: btnW, height: btnH))
|
|
successBtn.title = "Success!"
|
|
successBtn.bezelStyle = .rounded
|
|
successBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
|
|
successBtn.target = self
|
|
successBtn.action = #selector(didSuccess)
|
|
view.addSubview(successBtn)
|
|
|
|
let nextTimeBtn = NSButton(frame: NSRect(x: startX + btnW + gap, y: btnY, width: btnW, height: btnH))
|
|
nextTimeBtn.title = "Next Time"
|
|
nextTimeBtn.bezelStyle = .rounded
|
|
nextTimeBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
|
|
nextTimeBtn.target = self
|
|
nextTimeBtn.action = #selector(skipSuggestion)
|
|
view.addSubview(nextTimeBtn)
|
|
|
|
let dontSuggestBtn = NSButton(frame: NSRect(x: startX + (btnW + gap) * 2, y: btnY, width: btnW, height: btnH))
|
|
dontSuggestBtn.title = "Don't Suggest"
|
|
dontSuggestBtn.bezelStyle = .rounded
|
|
dontSuggestBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
|
|
dontSuggestBtn.target = self
|
|
dontSuggestBtn.action = #selector(dismissSuggestion)
|
|
view.addSubview(dontSuggestBtn)
|
|
}
|
|
|
|
window.contentView = view
|
|
}
|
|
|
|
@objc func skipSuggestion() {
|
|
currentSuggestionFeedback = .skipped
|
|
resumePommedoro()
|
|
}
|
|
|
|
@objc func didSuccess() {
|
|
currentSuggestionFeedback = .didIt
|
|
breakCountdownLabels.removeAll()
|
|
let bgColor = breakScreenColor
|
|
|
|
for (i, window) in overlayWindows.enumerated() {
|
|
let size = window.frame.size
|
|
let view = NSView(frame: NSRect(origin: .zero, size: size))
|
|
view.wantsLayer = true
|
|
view.layer?.backgroundColor = bgColor.cgColor
|
|
|
|
let countdownLabel = NSTextField(labelWithString: String(format: "%02d:%02d", breakRemainingSeconds / 60, breakRemainingSeconds % 60))
|
|
countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold)
|
|
countdownLabel.textColor = NSColor.white.withAlphaComponent(0.7)
|
|
countdownLabel.alignment = .center
|
|
countdownLabel.sizeToFit()
|
|
countdownLabel.frame.origin = CGPoint(
|
|
x: (size.width - countdownLabel.frame.width) / 2,
|
|
y: size.height - 80
|
|
)
|
|
view.addSubview(countdownLabel)
|
|
breakCountdownLabels.append(countdownLabel)
|
|
|
|
let congrats = NSTextField(labelWithString: "Nice work!")
|
|
congrats.font = NSFont.systemFont(ofSize: 72, weight: .bold)
|
|
congrats.textColor = .white
|
|
congrats.alignment = .center
|
|
congrats.sizeToFit()
|
|
congrats.frame.origin = CGPoint(
|
|
x: (size.width - congrats.frame.width) / 2,
|
|
y: (size.height / 2) + 80
|
|
)
|
|
view.addSubview(congrats)
|
|
|
|
let prompt = NSTextField(labelWithString: "How did that impact your readiness for the day?")
|
|
prompt.font = NSFont.systemFont(ofSize: 36, weight: .medium)
|
|
prompt.textColor = NSColor.white.withAlphaComponent(0.9)
|
|
prompt.alignment = .center
|
|
prompt.sizeToFit()
|
|
prompt.frame.origin = CGPoint(
|
|
x: (size.width - prompt.frame.width) / 2,
|
|
y: (size.height / 2) + 10
|
|
)
|
|
view.addSubview(prompt)
|
|
|
|
if i == 0 {
|
|
let closeBtn = NSButton(frame: NSRect(x: size.width - 140, y: size.height - 60, width: 120, height: 36))
|
|
closeBtn.title = "Close"
|
|
closeBtn.bezelStyle = .rounded
|
|
closeBtn.font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
closeBtn.target = self
|
|
closeBtn.action = #selector(showWorkLog)
|
|
view.addSubview(closeBtn)
|
|
|
|
let reflY = size.height / 2 - 60
|
|
let btnW: CGFloat = 180
|
|
let btnH: CGFloat = 44
|
|
let gap: CGFloat = 16
|
|
let totalW = btnW * 3 + gap * 2
|
|
let startX = (size.width - totalW) / 2
|
|
|
|
let greatBtn = NSButton(frame: NSRect(x: startX, y: reflY, width: btnW, height: btnH))
|
|
greatBtn.title = "Feeling Great"
|
|
greatBtn.bezelStyle = .rounded
|
|
greatBtn.font = NSFont.systemFont(ofSize: 15, weight: .medium)
|
|
greatBtn.target = self
|
|
greatBtn.action = #selector(reflectionGreat)
|
|
view.addSubview(greatBtn)
|
|
|
|
let okBtn = NSButton(frame: NSRect(x: startX + btnW + gap, y: reflY, width: btnW, height: btnH))
|
|
okBtn.title = "About the Same"
|
|
okBtn.bezelStyle = .rounded
|
|
okBtn.font = NSFont.systemFont(ofSize: 15, weight: .medium)
|
|
okBtn.target = self
|
|
okBtn.action = #selector(reflectionSame)
|
|
view.addSubview(okBtn)
|
|
|
|
let notBtn = NSButton(frame: NSRect(x: startX + (btnW + gap) * 2, y: reflY, width: btnW, height: btnH))
|
|
notBtn.title = "Not Really"
|
|
notBtn.bezelStyle = .rounded
|
|
notBtn.font = NSFont.systemFont(ofSize: 15, weight: .medium)
|
|
notBtn.target = self
|
|
notBtn.action = #selector(reflectionNotReally)
|
|
view.addSubview(notBtn)
|
|
}
|
|
|
|
window.contentView = view
|
|
}
|
|
}
|
|
|
|
@objc func reflectionGreat() { currentWellnessReflection = .great; reflectionDone() }
|
|
@objc func reflectionSame() { currentWellnessReflection = .same; reflectionDone() }
|
|
@objc func reflectionNotReally() { currentWellnessReflection = .notReally; reflectionDone() }
|
|
|
|
@objc func reflectionDone() {
|
|
breakCountdownLabels.removeAll()
|
|
reflectionField = nil
|
|
intentField = nil
|
|
let bgColor = breakScreenColor
|
|
|
|
for (i, window) in overlayWindows.enumerated() {
|
|
let size = window.frame.size
|
|
let view = NSView(frame: NSRect(origin: .zero, size: size))
|
|
view.wantsLayer = true
|
|
view.layer?.backgroundColor = bgColor.cgColor
|
|
|
|
let countdownLabel = NSTextField(labelWithString: String(format: "%02d:%02d", breakRemainingSeconds / 60, breakRemainingSeconds % 60))
|
|
countdownLabel.font = NSFont.monospacedSystemFont(ofSize: 36, weight: .bold)
|
|
countdownLabel.textColor = NSColor.white.withAlphaComponent(0.7)
|
|
countdownLabel.alignment = .center
|
|
countdownLabel.sizeToFit()
|
|
countdownLabel.frame.origin = CGPoint(
|
|
x: (size.width - countdownLabel.frame.width) / 2,
|
|
y: size.height - 80
|
|
)
|
|
view.addSubview(countdownLabel)
|
|
breakCountdownLabels.append(countdownLabel)
|
|
|
|
let msg = NSTextField(labelWithString: "Ready when you are")
|
|
msg.font = NSFont.systemFont(ofSize: 56, weight: .bold)
|
|
msg.textColor = .white
|
|
msg.alignment = .center
|
|
msg.sizeToFit()
|
|
msg.frame.origin = CGPoint(
|
|
x: (size.width - msg.frame.width) / 2,
|
|
y: (size.height / 2) + 120
|
|
)
|
|
view.addSubview(msg)
|
|
|
|
if i == 0 {
|
|
let closeBtn = NSButton(frame: NSRect(x: size.width - 140, y: size.height - 60, width: 120, height: 36))
|
|
closeBtn.title = "Close"
|
|
closeBtn.bezelStyle = .rounded
|
|
closeBtn.font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
closeBtn.target = self
|
|
closeBtn.action = #selector(showWorkLog)
|
|
view.addSubview(closeBtn)
|
|
|
|
// Reflection text field
|
|
let fieldW: CGFloat = 560
|
|
let fieldH: CGFloat = 48
|
|
|
|
let reflLabel = NSTextField(labelWithString: "What did you accomplish this interval?")
|
|
reflLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
|
|
reflLabel.textColor = NSColor.white.withAlphaComponent(0.9)
|
|
reflLabel.alignment = .center
|
|
reflLabel.sizeToFit()
|
|
reflLabel.frame.origin = CGPoint(
|
|
x: (size.width - reflLabel.frame.width) / 2,
|
|
y: (size.height / 2) + 76
|
|
)
|
|
view.addSubview(reflLabel)
|
|
|
|
let reflField = NSTextField(frame: NSRect(
|
|
x: (size.width - fieldW) / 2,
|
|
y: (size.height / 2) + 24,
|
|
width: fieldW,
|
|
height: fieldH
|
|
))
|
|
reflField.placeholderString = "Optional — jot down what you worked on"
|
|
reflField.font = NSFont.systemFont(ofSize: 20)
|
|
reflField.alignment = .center
|
|
reflField.bezelStyle = .roundedBezel
|
|
reflField.isBordered = true
|
|
reflField.drawsBackground = true
|
|
reflField.backgroundColor = NSColor.textBackgroundColor
|
|
reflField.textColor = NSColor.labelColor
|
|
reflField.wantsLayer = true
|
|
reflField.layer?.cornerRadius = 10
|
|
reflField.focusRingType = .default
|
|
reflField.isEditable = true
|
|
reflField.isSelectable = true
|
|
view.addSubview(reflField)
|
|
self.reflectionField = reflField
|
|
|
|
// Intent text field
|
|
let intentLabel = NSTextField(labelWithString: "What do you intend to work on next?")
|
|
intentLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
|
|
intentLabel.textColor = NSColor.white.withAlphaComponent(0.9)
|
|
intentLabel.alignment = .center
|
|
intentLabel.sizeToFit()
|
|
intentLabel.frame.origin = CGPoint(
|
|
x: (size.width - intentLabel.frame.width) / 2,
|
|
y: (size.height / 2) - 10
|
|
)
|
|
view.addSubview(intentLabel)
|
|
|
|
let intField = NSTextField(frame: NSRect(
|
|
x: (size.width - fieldW) / 2,
|
|
y: (size.height / 2) - 62,
|
|
width: fieldW,
|
|
height: fieldH
|
|
))
|
|
intField.placeholderString = "Optional — set your intention for the next interval"
|
|
intField.font = NSFont.systemFont(ofSize: 20)
|
|
intField.alignment = .center
|
|
intField.bezelStyle = .roundedBezel
|
|
intField.isBordered = true
|
|
intField.drawsBackground = true
|
|
intField.backgroundColor = NSColor.textBackgroundColor
|
|
intField.textColor = NSColor.labelColor
|
|
intField.wantsLayer = true
|
|
intField.layer?.cornerRadius = 10
|
|
intField.focusRingType = .default
|
|
intField.isEditable = true
|
|
intField.isSelectable = true
|
|
view.addSubview(intField)
|
|
self.intentField = intField
|
|
|
|
// Pre-fill with the current or most recent intent from today
|
|
if let intent = self.currentIntent, !intent.isEmpty {
|
|
intField.stringValue = intent
|
|
} else if let lastIntent = WorkLogStore.shared.lastTodayIntent() {
|
|
intField.stringValue = lastIntent
|
|
}
|
|
|
|
// Buttons row: Pause | Finish Day | Continue
|
|
let btnW: CGFloat = 160
|
|
let btnH: CGFloat = 50
|
|
let gap: CGFloat = 20
|
|
let btnCount: CGFloat = 3
|
|
let totalW = btnW * btnCount + gap * (btnCount - 1)
|
|
let startX = (size.width - totalW) / 2
|
|
let btnY = size.height / 2 - 140
|
|
|
|
let pauseBtn = NSButton(frame: NSRect(x: startX, y: btnY, width: btnW, height: btnH))
|
|
pauseBtn.title = "Pause"
|
|
pauseBtn.bezelStyle = .rounded
|
|
pauseBtn.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
|
|
pauseBtn.target = self
|
|
pauseBtn.action = #selector(pauseFromOverlay)
|
|
view.addSubview(pauseBtn)
|
|
|
|
let finishBtn = NSButton(frame: NSRect(x: startX + btnW + gap, y: btnY, width: btnW, height: btnH))
|
|
finishBtn.title = "Finish Day"
|
|
finishBtn.bezelStyle = .rounded
|
|
finishBtn.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
|
|
finishBtn.target = self
|
|
finishBtn.action = #selector(showWorkLog)
|
|
view.addSubview(finishBtn)
|
|
|
|
let continueBtn = NSButton(frame: NSRect(x: startX + (btnW + gap) * 2, y: btnY, width: btnW, height: btnH))
|
|
continueBtn.title = "Continue"
|
|
continueBtn.bezelStyle = .rounded
|
|
continueBtn.font = NSFont.systemFont(ofSize: 18, weight: .semibold)
|
|
continueBtn.target = self
|
|
continueBtn.action = #selector(resumePommedoro)
|
|
view.addSubview(continueBtn)
|
|
}
|
|
|
|
window.contentView = view
|
|
|
|
if i == 0 {
|
|
window.makeKeyAndOrderFront(nil)
|
|
if let field = self.reflectionField {
|
|
window.makeFirstResponder(field)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|