feat(macos): add native menu bar app with notch overlay
Introduces a SwiftUI companion app for the Fail Well PWA: menu bar status item, popover with task list / record / history views, command palette, and a Dynamic Island-style notch overlay that shows the active step and timer beside the hardware notch and expands on hover to reveal pause/next controls.
This commit is contained in:
8
macos/Sources/FailWell/Models/Step.swift
Normal file
8
macos/Sources/FailWell/Models/Step.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
struct Step: Codable, Identifiable, Hashable, Sendable {
|
||||
let id: String
|
||||
var title: String
|
||||
/// estimation in minutes
|
||||
var estimation: Int
|
||||
}
|
||||
54
macos/Sources/FailWell/Models/Task.swift
Normal file
54
macos/Sources/FailWell/Models/Task.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
|
||||
/// A planned task with a title, optional link, and an append-only history of step lists.
|
||||
/// Named `TaskPlan` to avoid collision with `_Concurrency.Task`.
|
||||
struct TaskPlan: Codable, Identifiable, Hashable, Sendable {
|
||||
let id: String
|
||||
var title: String
|
||||
var date: Date
|
||||
var link: String?
|
||||
var stepHistory: [[Step]]
|
||||
|
||||
init(id: String, title: String, date: Date = Date(), link: String? = nil, stepHistory: [[Step]] = []) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.date = date
|
||||
self.link = link
|
||||
self.stepHistory = stepHistory
|
||||
}
|
||||
|
||||
var steps: [Step] {
|
||||
stepHistory.last ?? []
|
||||
}
|
||||
|
||||
var initialPlan: [Step] {
|
||||
stepHistory.first ?? []
|
||||
}
|
||||
|
||||
var wasUpdated: Bool {
|
||||
stepHistory.count > 1
|
||||
}
|
||||
|
||||
var totalEstimation: Int {
|
||||
steps.reduce(0) { $0 + $1.estimation }
|
||||
}
|
||||
|
||||
mutating func newSteps(_ steps: [Step]) {
|
||||
stepHistory.append(steps)
|
||||
}
|
||||
|
||||
mutating func editSteps(_ steps: [Step]) {
|
||||
stepHistory.append(self.steps + steps)
|
||||
}
|
||||
|
||||
mutating func updateSteps(_ steps: [Step]) {
|
||||
stepHistory.append(steps)
|
||||
}
|
||||
|
||||
mutating func removeStep(at index: Int) {
|
||||
guard index >= 0, index < steps.count else { return }
|
||||
stepHistory.append(steps.enumerated().compactMap { i, step in
|
||||
i == index ? nil : step
|
||||
})
|
||||
}
|
||||
}
|
||||
41
macos/Sources/FailWell/Models/TaskRecord.swift
Normal file
41
macos/Sources/FailWell/Models/TaskRecord.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
struct TaskRecord: Codable, Hashable, Sendable {
|
||||
let taskId: String
|
||||
var start: Date
|
||||
var end: Date?
|
||||
var stepRecords: [String: TimeRange]
|
||||
var notes: String
|
||||
var breakTime: TimeRange?
|
||||
var currentStepId: String?
|
||||
|
||||
init(
|
||||
taskId: String,
|
||||
start: Date = Date(),
|
||||
end: Date? = nil,
|
||||
stepRecords: [String: TimeRange] = [:],
|
||||
notes: String = "",
|
||||
breakTime: TimeRange? = nil,
|
||||
currentStepId: String? = nil
|
||||
) {
|
||||
self.taskId = taskId
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.stepRecords = stepRecords
|
||||
self.notes = notes
|
||||
self.breakTime = breakTime
|
||||
self.currentStepId = currentStepId
|
||||
}
|
||||
|
||||
var isActive: Bool {
|
||||
end == nil && currentStepId != nil
|
||||
}
|
||||
|
||||
var isPaused: Bool {
|
||||
breakTime != nil && breakTime?.end == nil
|
||||
}
|
||||
|
||||
var hasStarted: Bool {
|
||||
!stepRecords.isEmpty
|
||||
}
|
||||
}
|
||||
11
macos/Sources/FailWell/Models/TimeRange.swift
Normal file
11
macos/Sources/FailWell/Models/TimeRange.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
struct TimeRange: Codable, Hashable, Sendable {
|
||||
var start: Date
|
||||
var end: Date?
|
||||
|
||||
var duration: TimeInterval? {
|
||||
guard let end else { return nil }
|
||||
return end.timeIntervalSince(start)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user