Skip to content

Commit 832ee5d

Browse files
committed
Add .tasks tab to NotchViewModel and create UI TasksPane
1 parent e8c724c commit 832ee5d

3 files changed

Lines changed: 115 additions & 3 deletions

File tree

Sources/VoidBar/Model/NotchViewModel.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Combine
44
@MainActor
55
final class NotchViewModel: ObservableObject {
66
enum Tab: String, CaseIterable, Identifiable {
7-
case media, shelf, clipboard, snippets, calendar, timer, translate, notes, teleprompter, monitor, weather
7+
case media, shelf, clipboard, snippets, calendar, timer, translate, notes, teleprompter, monitor, weather, tasks
88
var id: String { rawValue }
99

1010
var symbol: String {
@@ -20,6 +20,7 @@ final class NotchViewModel: ObservableObject {
2020
case .teleprompter: return "text.line.first.and.arrowtriangle.forward"
2121
case .monitor: return "cpu"
2222
case .weather: return "cloud.sun"
23+
case .tasks: return "checkmark.circle"
2324
}
2425
}
2526

@@ -36,6 +37,7 @@ final class NotchViewModel: ObservableObject {
3637
case .teleprompter: return localized("Teleprompter")
3738
case .monitor: return localized("Monitor")
3839
case .weather: return localized("Weather")
40+
case .tasks: return localized("Tasks")
3941
}
4042
}
4143

@@ -48,7 +50,7 @@ final class NotchViewModel: ObservableObject {
4850
/// body has — so growth continues in a second column on the right,
4951
/// which the scratch notes open.
5052
static let leftRail: [Tab] = [.media, .shelf, .clipboard, .snippets, .calendar, .timer, .translate]
51-
static let rightRail: [Tab] = [.notes, .teleprompter, .monitor, .weather]
53+
static let rightRail: [Tab] = [.notes, .tasks, .teleprompter, .monitor, .weather]
5254
}
5355

5456
@Published var isOpen = false {
@@ -99,6 +101,7 @@ final class NotchViewModel: ObservableObject {
99101
let teleprompter: TeleprompterStore
100102
let monitor: SystemMonitorStore
101103
let weather: WeatherStore
104+
let tickTick: TickTickStore
102105

103106
private var cancellables = Set<AnyCancellable>()
104107

@@ -115,6 +118,7 @@ final class NotchViewModel: ObservableObject {
115118
self.teleprompter = TeleprompterStore()
116119
self.monitor = SystemMonitorStore()
117120
self.weather = WeatherStore()
121+
self.tickTick = TickTickStore()
118122

119123
// The panel header reads through to the stores — counters, the source
120124
// name, the equalizer. Nested ObservableObjects do not propagate on
@@ -154,7 +158,8 @@ final class NotchViewModel: ObservableObject {
154158
shelf.objectWillChange,
155159
clipboard.objectWillChange,
156160
calendar.objectWillChange,
157-
monitor.objectWillChange
161+
monitor.objectWillChange,
162+
tickTick.objectWillChange
158163
] {
159164
child
160165
.sink { [weak self] _ in

Sources/VoidBar/UI/NotchContentView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ struct NotchContentView: View {
114114
EmptyView()
115115
case .weather:
116116
EmptyView()
117+
case .tasks:
118+
if !vm.tickTick.tasks.isEmpty {
119+
counter(vm.tickTick.tasks.filter { !$0.isCompleted }.count)
120+
} else {
121+
EmptyView()
122+
}
117123
}
118124
}
119125

@@ -183,6 +189,8 @@ struct NotchContentView: View {
183189
MonitorPane(monitor: vm.monitor)
184190
case .weather:
185191
WeatherPane(weatherStore: vm.weather)
192+
case .tasks:
193+
TasksPane(store: vm.tickTick)
186194
}
187195
}
188196
}

Sources/VoidBar/UI/TasksPane.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import SwiftUI
2+
3+
struct TasksPane: View {
4+
@ObservedObject var store: TickTickStore
5+
6+
var body: some View {
7+
VStack(spacing: 0) {
8+
header
9+
10+
if store.isLoading && store.tasks.isEmpty {
11+
Spacer()
12+
ProgressView()
13+
Spacer()
14+
} else if let error = store.errorMessage {
15+
Spacer()
16+
Text(error)
17+
.font(.system(size: 13, weight: .medium))
18+
.foregroundColor(Theme.tertiary)
19+
.multilineTextAlignment(.center)
20+
.padding()
21+
Spacer()
22+
} else if store.tasks.isEmpty {
23+
Spacer()
24+
VStack(spacing: 8) {
25+
Image(systemName: "checkmark.circle.fill")
26+
.font(.system(size: 24))
27+
.foregroundColor(Theme.tertiary)
28+
Text("All done for today!")
29+
.font(.system(size: 13, weight: .medium))
30+
.foregroundColor(Theme.tertiary)
31+
}
32+
Spacer()
33+
} else {
34+
ScrollView {
35+
VStack(alignment: .leading, spacing: 10) {
36+
ForEach(store.tasks) { task in
37+
taskRow(task)
38+
}
39+
}
40+
.padding(14)
41+
}
42+
}
43+
}
44+
}
45+
46+
private var header: some View {
47+
HStack {
48+
Text("TickTick")
49+
.font(.system(size: 11, weight: .semibold))
50+
.foregroundColor(Theme.secondary)
51+
Spacer()
52+
Button {
53+
Task {
54+
await store.fetchTasks()
55+
}
56+
} label: {
57+
Image(systemName: "arrow.clockwise")
58+
.font(.system(size: 10, weight: .semibold))
59+
.foregroundColor(Theme.tertiary)
60+
}
61+
.buttonStyle(.plain)
62+
}
63+
.padding(.horizontal, 14)
64+
.padding(.top, 14)
65+
.padding(.bottom, 6)
66+
}
67+
68+
private func taskRow(_ task: TickTickTask) -> some View {
69+
HStack(alignment: .top, spacing: 10) {
70+
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
71+
.font(.system(size: 14))
72+
.foregroundColor(task.isCompleted ? Theme.secondary : Theme.tertiary)
73+
.padding(.top, 2)
74+
75+
VStack(alignment: .leading, spacing: 4) {
76+
Text(task.title)
77+
.font(.system(size: 13, weight: .medium))
78+
.foregroundColor(task.isCompleted ? Theme.tertiary : .white)
79+
.strikethrough(task.isCompleted)
80+
81+
if let date = task.dueDate {
82+
Text(date, style: .date)
83+
.font(.system(size: 11))
84+
.foregroundColor(priorityColor(task.priority))
85+
}
86+
}
87+
Spacer()
88+
}
89+
}
90+
91+
private func priorityColor(_ priority: Int) -> Color {
92+
switch priority {
93+
case 5: return .red
94+
case 3: return .orange
95+
case 1: return .blue
96+
default: return Theme.tertiary
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)