krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.10.0: Hutch/Views/Projects/ProjectFormSheet.swift · raw
1import SwiftUI
2
3/// Shared create/edit form for hub.sr.ht projects. The parent owns the save
4/// state and performs the mutation via `onSave`, dismissing on success.
5struct ProjectFormSheet: View {
6 let title: String
7 let confirmationTitle: String
8 let isSaving: Bool
9 let error: String?
10 /// `createProject` takes no website; only the edit flow shows the field.
11 let includeWebsite: Bool
12 let onSave: (ProjectFormValues) async -> Bool
13
14 @Environment(\.dismiss) private var dismiss
15 @State private var name: String
16 @State private var description: String
17 @State private var website: String
18 @State private var tags: String
19 @State private var visibility: Visibility
20
21 init(
22 title: String,
23 confirmationTitle: String,
24 isSaving: Bool,
25 error: String?,
26 includeWebsite: Bool,
27 initialName: String = "",
28 initialDescription: String = "",
29 initialWebsite: String = "",
30 initialTags: [String] = [],
31 initialVisibility: Visibility = .publicVisibility,
32 onSave: @escaping (ProjectFormValues) async -> Bool
33 ) {
34 self.title = title
35 self.confirmationTitle = confirmationTitle
36 self.isSaving = isSaving
37 self.error = error
38 self.includeWebsite = includeWebsite
39 self.onSave = onSave
40 _name = State(initialValue: initialName)
41 _description = State(initialValue: initialDescription)
42 _website = State(initialValue: initialWebsite)
43 _tags = State(initialValue: initialTags.joined(separator: ", "))
44 _visibility = State(initialValue: initialVisibility)
45 }
46
47 private var trimmedName: String {
48 name.trimmingCharacters(in: .whitespacesAndNewlines)
49 }
50
51 var body: some View {
52 NavigationStack {
53 Form {
54 Section("Project Details") {
55 TextField("Project name", text: $name)
56 .textInputAutocapitalization(.never)
57 .autocorrectionDisabled()
58 .themedRow()
59 TextField("Description (optional)", text: $description, axis: .vertical)
60 .lineLimit(2...4)
61 .themedRow()
62 if includeWebsite {
63 TextField("Website (optional)", text: $website)
64 .textInputAutocapitalization(.never)
65 .autocorrectionDisabled()
66 .keyboardType(.URL)
67 .themedRow()
68 }
69 Picker("Visibility", selection: $visibility) {
70 Text("Public").tag(Visibility.publicVisibility)
71 Text("Unlisted").tag(Visibility.unlisted)
72 Text("Private").tag(Visibility.privateVisibility)
73 }
74 .themedRow()
75 }
76
77 Section {
78 TextField("Tags (comma separated)", text: $tags)
79 .textInputAutocapitalization(.never)
80 .autocorrectionDisabled()
81 .themedRow()
82 } footer: {
83 Text("Separate tags with commas.")
84 }
85
86 if let error, !error.isEmpty {
87 Section {
88 Text(error)
89 .foregroundStyle(.red)
90 .themedRow()
91 }
92 }
93 }
94 .themedList()
95 .navigationTitle(title)
96 .navigationBarTitleDisplayMode(.inline)
97 .toolbar {
98 ToolbarItem(placement: .cancellationAction) {
99 Button("Cancel") { dismiss() }
100 }
101 ToolbarItem(placement: .confirmationAction) {
102 Button {
103 Task {
104 let values = ProjectFormValues(
105 name: trimmedName,
106 description: description.trimmingCharacters(in: .whitespacesAndNewlines),
107 website: website.trimmingCharacters(in: .whitespacesAndNewlines),
108 visibility: visibility,
109 tags: Self.parseTags(tags)
110 )
111 if await onSave(values) {
112 dismiss()
113 }
114 }
115 } label: {
116 if isSaving {
117 ProgressView().controlSize(.small)
118 } else {
119 Text(confirmationTitle)
120 }
121 }
122 .disabled(trimmedName.isEmpty || isSaving)
123 }
124 }
125 }
126 }
127
128 static func parseTags(_ raw: String) -> [String] {
129 var seen = Set<String>()
130 return raw
131 .split(whereSeparator: { $0 == "," || $0.isNewline })
132 .map { $0.trimmingCharacters(in: .whitespaces) }
133 .filter { !$0.isEmpty }
134 .filter { seen.insert($0.lowercased()).inserted }
135 }
136}
137
138struct ProjectFormValues: Sendable {
139 let name: String
140 let description: String
141 let website: String
142 let visibility: Visibility
143 let tags: [String]
144}