pommedoro/Sources/Pommedoro/WorkLogView.swift

215 lines
9.3 KiB
Swift

import AppKit
// MARK: - Work Log View Builder
enum WorkLogViewBuilder {
static func build(in window: NSWindow, isPrimary: Bool, delegate: AppDelegate) {
let size = window.frame.size
let view = NSView(frame: NSRect(origin: .zero, size: size))
view.wantsLayer = true
let bgColor = Settings.shared.gradientColor.withAlphaComponent(0.92)
view.layer?.backgroundColor = bgColor.cgColor
let entries = WorkLogStore.shared.todayEntries()
let summary = WorkLogStore.shared.todaySummary()
// Title
let title = NSTextField(labelWithString: "Today's Work Log")
title.font = NSFont.systemFont(ofSize: 48, weight: .bold)
title.textColor = .white
title.alignment = .center
title.sizeToFit()
title.frame.origin = CGPoint(
x: (size.width - title.frame.width) / 2,
y: size.height - 120
)
view.addSubview(title)
// Summary stats
let hours = summary.totalSeconds / 3600
let mins = (summary.totalSeconds % 3600) / 60
let timeStr = hours > 0
? String(format: "%dh %dm", hours, mins)
: String(format: "%dm", mins)
let completedCount = entries.filter { $0.completed }.count
let summaryText = "\(summary.intervals) intervals | \(completedCount) completed | \(timeStr) focused"
let summaryLabel = NSTextField(labelWithString: summaryText)
summaryLabel.font = NSFont.systemFont(ofSize: 22, weight: .medium)
summaryLabel.textColor = NSColor.white.withAlphaComponent(0.85)
summaryLabel.alignment = .center
summaryLabel.sizeToFit()
summaryLabel.frame.origin = CGPoint(
x: (size.width - summaryLabel.frame.width) / 2,
y: size.height - 165
)
view.addSubview(summaryLabel)
if isPrimary {
// Close button
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 = delegate
closeBtn.action = #selector(AppDelegate.dismissWorkLogAndResume)
view.addSubview(closeBtn)
// Scrollable entry list
let scrollW: CGFloat = min(700, size.width - 100)
let scrollH: CGFloat = size.height - 300
let scrollX = (size.width - scrollW) / 2
let scrollY: CGFloat = 120
let scrollView = NSScrollView(frame: NSRect(x: scrollX, y: scrollY, width: scrollW, height: scrollH))
scrollView.hasVerticalScroller = true
scrollView.drawsBackground = false
scrollView.autohidesScrollers = true
let contentView = buildEntryList(entries: entries, width: scrollW)
scrollView.documentView = contentView
view.addSubview(scrollView)
// Bottom buttons: Back to Work | Quit
let btnW: CGFloat = 180
let btnH: CGFloat = 44
let gap: CGFloat = 24
let totalW = btnW * 3 + gap * 2
let startX = (size.width - totalW) / 2
let btnY: CGFloat = 50
let copyBtn = NSButton(frame: NSRect(x: startX, y: btnY, width: btnW, height: btnH))
copyBtn.title = "Copy"
copyBtn.bezelStyle = .rounded
copyBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
copyBtn.target = delegate
copyBtn.action = #selector(AppDelegate.copyWorkLog)
view.addSubview(copyBtn)
let resumeBtn = NSButton(frame: NSRect(x: startX + btnW + gap, y: btnY, width: btnW, height: btnH))
resumeBtn.title = "Back to Work"
resumeBtn.bezelStyle = .rounded
resumeBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
resumeBtn.target = delegate
resumeBtn.action = #selector(AppDelegate.dismissWorkLogAndResume)
view.addSubview(resumeBtn)
let quitBtn = NSButton(frame: NSRect(x: startX + (btnW + gap) * 2, y: btnY, width: btnW, height: btnH))
quitBtn.title = "Quit Pommedoro"
quitBtn.bezelStyle = .rounded
quitBtn.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
quitBtn.target = delegate
quitBtn.action = #selector(AppDelegate.closePommedoro)
view.addSubview(quitBtn)
}
window.contentView = view
}
// MARK: - Entry List
private static func buildEntryList(entries: [WorkLogEntry], width: CGFloat) -> NSView {
let rowHeight: CGFloat = 86
let padding: CGFloat = 12
let totalHeight = max(CGFloat(entries.count) * (rowHeight + padding) + padding, 100)
let container = NSView(frame: NSRect(x: 0, y: 0, width: width, height: totalHeight))
if entries.isEmpty {
let empty = NSTextField(labelWithString: "No intervals logged yet today.\nStart a work cycle and your log will appear here.")
empty.font = NSFont.systemFont(ofSize: 18, weight: .medium)
empty.textColor = NSColor.white.withAlphaComponent(0.7)
empty.alignment = .center
empty.maximumNumberOfLines = 0
empty.preferredMaxLayoutWidth = width - 40
empty.sizeToFit()
empty.frame.origin = CGPoint(
x: (width - empty.frame.width) / 2,
y: (totalHeight - empty.frame.height) / 2
)
container.addSubview(empty)
return container
}
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
for (index, entry) in entries.enumerated() {
let y = totalHeight - CGFloat(index + 1) * (rowHeight + padding)
let row = NSView(frame: NSRect(x: 0, y: y, width: width, height: rowHeight))
row.wantsLayer = true
row.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.12).cgColor
row.layer?.cornerRadius = 8
// Time and duration
let timeStr = formatter.string(from: entry.date)
let durMins = entry.durationSeconds / 60
let status = entry.completed ? "completed" : "partial"
let header = "\(timeStr)\(durMins)m (\(status))"
let headerLabel = NSTextField(labelWithString: header)
headerLabel.font = NSFont.monospacedSystemFont(ofSize: 13, weight: .semibold)
headerLabel.textColor = NSColor.white.withAlphaComponent(0.9)
headerLabel.frame = NSRect(x: 16, y: rowHeight - 24, width: width - 32, height: 18)
row.addSubview(headerLabel)
// Intent (always show line; use "" when nil or empty)
let rawIntent = entry.intent?.trimmingCharacters(in: .whitespacesAndNewlines)
let intentText = (rawIntent?.isEmpty != false) ? "" : (rawIntent ?? "")
let intentLabel = NSTextField(labelWithString: "Intent: \(intentText)")
intentLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular)
intentLabel.textColor = NSColor.white.withAlphaComponent(0.75)
intentLabel.frame = NSRect(x: 16, y: rowHeight - 44, width: width - 32, height: 16)
intentLabel.lineBreakMode = .byTruncatingTail
if let intent = rawIntent, !intent.isEmpty { intentLabel.toolTip = intent }
row.addSubview(intentLabel)
// Reflection
let reflText = entry.reflection ?? ""
let reflLabel = NSTextField(labelWithString: "Reflection: \(reflText)")
reflLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular)
reflLabel.textColor = NSColor.white.withAlphaComponent(0.75)
reflLabel.frame = NSRect(x: 16, y: rowHeight - 62, width: width - 32, height: 16)
reflLabel.lineBreakMode = .byTruncatingTail
row.addSubview(reflLabel)
// Suggestion feedback
let suggText = formatSuggestionFeedback(entry)
let suggLabel = NSTextField(labelWithString: suggText)
suggLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular)
suggLabel.textColor = NSColor.white.withAlphaComponent(0.65)
suggLabel.frame = NSRect(x: 16, y: rowHeight - 80, width: width - 32, height: 16)
suggLabel.lineBreakMode = .byTruncatingTail
row.addSubview(suggLabel)
container.addSubview(row)
}
return container
}
private static func formatSuggestionFeedback(_ entry: WorkLogEntry) -> String {
guard let suggestion = entry.suggestion, !suggestion.isEmpty else {
return "Wellness: —"
}
let fbStr: String
switch entry.suggestionFeedback {
case .didIt:
let wellStr: String
switch entry.wellnessReflection {
case .great: wellStr = " — felt great"
case .same: wellStr = " — about the same"
case .notReally: wellStr = " — not really"
case .none: wellStr = ""
}
fbStr = "did it\(wellStr)"
case .skipped:
fbStr = "skipped"
case .dismissed:
fbStr = "removed from pool"
case .none:
fbStr = ""
}
return "Wellness: \(suggestion) (\(fbStr))"
}
}