Skip to content
Open
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
4 changes: 2 additions & 2 deletions Dayflow/Dayflow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_ASSET_PATHS = "\"Dayflow/Preview Content\"";
DEVELOPMENT_TEAM = L75WYD8X4Y;
DEVELOPMENT_TEAM = AD6J74H5UC;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down Expand Up @@ -446,7 +446,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 83;
DEVELOPMENT_ASSET_PATHS = "\"Dayflow/Preview Content\"";
DEVELOPMENT_TEAM = L75WYD8X4Y;
DEVELOPMENT_TEAM = AD6J74H5UC;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down
3 changes: 3 additions & 0 deletions Dayflow/Dayflow/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ final class AppState: ObservableObject, AppStateManaging { // <-- Add AppStateMa
}
}

@Published var productivityScore: Int = 0
@Published var hasProductivityData: Bool = false

private init() {
// Always start with false - AppDelegate will set the correct value
// didSet doesn't fire during initialization, so this won't save
Expand Down
51 changes: 44 additions & 7 deletions Dayflow/Dayflow/System/StatusBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import Combine

@MainActor
final class StatusBarController: NSObject {
private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
private let popover = NSPopover()
private var cancellable: AnyCancellable?
private var cancellables = Set<AnyCancellable>()

override init() {
super.init()
Expand All @@ -17,21 +17,58 @@ final class StatusBarController: NSObject {
// Set initial icon based on current recording state
let isRecording = AppState.shared.isRecording
button.image = NSImage(named: isRecording ? "MenuBarOnIcon" : "MenuBarOffIcon")
button.imagePosition = .imageOnly
button.imagePosition = .imageLeading
button.target = self
button.action = #selector(togglePopover(_:))
}

// Observe recording state changes and update icon
cancellable = AppState.shared.$isRecording
AppState.shared.$isRecording
.removeDuplicates()
.sink { [weak self] isRecording in
self?.updateIcon(isRecording: isRecording)
self?.updateDisplay(isRecording: isRecording)
}
.store(in: &cancellables)

// Observe productivity score changes
AppState.shared.$productivityScore
.combineLatest(AppState.shared.$hasProductivityData)
.sink { [weak self] score, hasData in
self?.updateDisplay(score: score, hasData: hasData)
}
.store(in: &cancellables)
}

private func updateIcon(isRecording: Bool) {
statusItem.button?.image = NSImage(named: isRecording ? "MenuBarOnIcon" : "MenuBarOffIcon")
private func updateDisplay(isRecording: Bool? = nil, score: Int? = nil, hasData: Bool? = nil) {
guard let button = statusItem.button else { return }

// Update icon if recording state changed
if let isRecording = isRecording {
button.image = NSImage(named: isRecording ? "MenuBarOnIcon" : "MenuBarOffIcon")
}

// Update productivity score if provided
let currentScore = score ?? AppState.shared.productivityScore
let currentHasData = hasData ?? AppState.shared.hasProductivityData

if currentHasData {
let color: NSColor
if currentScore >= 70 {
color = NSColor(red: 0.30, green: 0.69, blue: 0.31, alpha: 1.0) // Green
} else if currentScore >= 60 {
color = NSColor(red: 0.95, green: 0.52, blue: 0.29, alpha: 1.0) // Orange
} else {
color = NSColor(red: 0.96, green: 0.26, blue: 0.21, alpha: 1.0) // Red
}

let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .semibold),
.foregroundColor: color
]
button.attributedTitle = NSAttributedString(string: " \(currentScore)", attributes: attributes)
} else {
button.attributedTitle = NSAttributedString(string: "")
}
}

@objc private func togglePopover(_ sender: Any?) {
Expand Down
106 changes: 106 additions & 0 deletions Dayflow/Dayflow/Views/Components/DaySummaryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ struct DaySummaryView: View {
UserDefaults.standard.set(true, forKey: earlyAccessStorageKey)
}
}
.onChange(of: productivityScore) { _, newScore in
AppState.shared.productivityScore = newScore
}
.onChange(of: productivityScoreHasData) { _, hasData in
AppState.shared.hasProductivityData = hasData
}
.contentShape(Rectangle())
.onTapGesture {
if isEditingFocusCategories {
Expand Down Expand Up @@ -449,6 +455,14 @@ struct DaySummaryView: View {
.padding(.vertical, 12)
}

private var productivityScoreSection: some View {
ProductivityScoreCard(
score: productivityScore,
hasData: productivityScoreHasData,
isUsingReviewData: reviewSummary.hasData
)
}

private var reviewSection: some View {
TimelineReviewSummaryCard(
summary: reviewSummary,
Expand Down Expand Up @@ -593,6 +607,12 @@ struct DaySummaryView: View {
TotalFocusCard(value: formatDurationTitleCase(totalFocusTime))

LongestFocusCard(focusBlocks: focusBlocks)

ProductivityScoreCard(
score: productivityScore,
hasData: productivityScoreHasData,
isUsingReviewData: reviewSummary.hasData
)
}
.opacity(isFocusSelectionEmpty ? 0.45 : 1)
}
Expand Down Expand Up @@ -681,6 +701,29 @@ struct DaySummaryView: View {
return min(max(ratio, 0), 1)
}

private var productivityScore: Int {
// Prefer manual review data if available
if reviewSummary.hasData {
let score = (reviewSummary.productiveRatio * 100) + (reviewSummary.neutralRatio * 50)
return Int(score.rounded())
}

// Fall back to category-based calculation
let captured = totalCapturedTime
guard captured > 0 else { return 0 }

let focusRatio = totalFocusTime / captured
let distractedRatio = totalDistractedTime / captured
let neutralRatio = max(0, 1 - focusRatio - distractedRatio)

let score = (focusRatio * 100) + (neutralRatio * 50)
return Int(score.rounded())
}

private var productivityScoreHasData: Bool {
reviewSummary.hasData || totalCapturedTime > 0
}

// MARK: - Helpers

private func isFocusCategory(_ category: String) -> Bool {
Expand Down Expand Up @@ -1119,6 +1162,69 @@ private struct CheckmarkShape: Shape {
}
}

private struct ProductivityScoreCard: View {
let score: Int
let hasData: Bool
let isUsingReviewData: Bool

private var scoreColor: Color {
if score >= 70 {
return Color(hex: "4CAF50") // Green
} else if score >= 60 {
return Color(hex: "F3854B") // Orange
} else {
return Color(hex: "F44336") // Red
}
}

var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
Text("Productivity score")
.font(.custom("InstrumentSerif-Regular", size: 16))
.foregroundColor(Color(hex: "333333"))

Image(systemName: "info.circle")
.font(.system(size: 12))
.foregroundColor(Color(hex: "CFC7BE"))
.help(isUsingReviewData
? "Based on your manual reviews (focused/neutral/distracted). Focused = 100pts, Neutral = 50pts, Distracted = 0pts"
: "Based on your focus/distraction categories. Review timeline cards for more accurate scoring.")

Spacer()
}

if hasData {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("\(score)")
.font(.custom("InstrumentSerif-Regular", size: 44))
.foregroundColor(scoreColor)

Text("out of 100")
.font(.custom("Nunito", size: 11))
.foregroundColor(Color(hex: "707070"))
.offset(y: -2)
}
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Text("No activity captured yet")
.font(.custom("Nunito", size: 11))
.foregroundColor(Color(hex: "707070"))
.padding(.top, 4)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(hex: "F7F7F7"))
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.white, lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}

private struct TotalFocusCard: View {
let value: String

Expand Down