gitbay/Views/Issues/MilestoneListView.swift
74 lines · 2466 bytes
1import SwiftUI
2
3/// Milestones and their progress.
4struct MilestoneListView: View {
5
6 @State private var model: MilestoneListViewModel
7
8 init(client: GitbayClient, repo: String) {
9 _model = State(initialValue: MilestoneListViewModel(client: client, repoPath: repo))
10 }
11
12 var body: some View {
13 List {
14 Picker("State", selection: Bindable(model).filter) {
15 ForEach(MilestoneListViewModel.StateFilter.allCases) { filter in
16 Text(filter.rawValue.capitalized).tag(filter)
17 }
18 }
19 .pickerStyle(.segmented)
20 .listRowBackground(Color.clear)
21 .listRowInsets(EdgeInsets())
22
23 ForEach(model.state.value ?? []) { milestone in
24 MilestoneRow(milestone: milestone)
25 }
26 }
27 .overlay { LoadStateOverlay(state: model.state) }
28 .navigationTitle("Milestones")
29 .navigationBarTitleDisplayMode(.inline)
30 .task { await model.load() }
31 .refreshable { await model.load() }
32 }
33}
34
35private struct MilestoneRow: View {
36 let milestone: Milestone
37
38 private var total: Int { milestone.open + milestone.closed }
39 private var fraction: Double {
40 total == 0 ? 0 : Double(milestone.closed) / Double(total)
41 }
42
43 var body: some View {
44 VStack(alignment: .leading, spacing: 5) {
45 HStack(spacing: 6) {
46 Text(milestone.title)
47 .font(.gbSans(.subheadline).weight(.medium))
48 if milestone.state != "open" {
49 GBChip(milestone.state, .gbDone)
50 }
51 Spacer()
52 if let due = milestone.due, !due.isEmpty {
53 Text(due)
54 .font(.gbSans(.caption))
55 .foregroundStyle(.tertiary)
56 }
57 }
58 if let description = milestone.description, !description.isEmpty {
59 Text(description)
60 .font(.gbSans(.caption))
61 .foregroundStyle(.secondary)
62 .lineLimit(2)
63 }
64 if total > 0 {
65 ProgressView(value: fraction)
66 .tint(.gbAccent)
67 Text("\(milestone.closed) of \(total) closed")
68 .font(.gbSans(.caption2))
69 .foregroundStyle(.secondary)
70 }
71 }
72 .padding(.vertical, 2)
73 }
74}