krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.0.4: Hutch/Models/Project.swift · raw

  1import Foundation
  2
  3struct Project: Identifiable, Hashable, Sendable {
  4    struct MailingList: Identifiable, Hashable, Sendable {
  5        let id: String
  6        let name: String
  7        let description: String?
  8        let visibility: Visibility
  9        let owner: Entity
 10
 11        var ownerUsername: String {
 12            owner.canonicalName.srhtUsername
 13        }
 14
 15        var inboxReference: InboxMailingListReference {
 16            InboxMailingListReference(
 17                id: 0,
 18                rid: id,
 19                name: name,
 20                owner: owner
 21            )
 22        }
 23    }
 24
 25    struct SourceRepo: Identifiable, Hashable, Sendable {
 26        enum RepoType: String, Decodable, Sendable {
 27            case git = "GIT"
 28            case hg = "HG"
 29
 30            var service: SRHTService {
 31                switch self {
 32                case .git: .git
 33                case .hg: .hg
 34                }
 35            }
 36        }
 37
 38        let id: String
 39        let name: String
 40        let description: String?
 41        let visibility: Visibility
 42        let owner: Entity
 43        let repoType: RepoType
 44
 45        var ownerUsername: String {
 46            owner.canonicalName.srhtUsername
 47        }
 48    }
 49
 50    struct Tracker: Identifiable, Hashable, Sendable {
 51        let id: String
 52        let name: String
 53        let description: String?
 54        let visibility: Visibility
 55        let owner: Entity
 56
 57        var ownerUsername: String {
 58            owner.canonicalName.srhtUsername
 59        }
 60    }
 61
 62    let id: String
 63    let name: String
 64    let description: String?
 65    let website: String?
 66    let visibility: Visibility
 67    let tags: [String]
 68    let updated: Date
 69    let mailingLists: [MailingList]
 70    let sources: [SourceRepo]
 71    let trackers: [Tracker]
 72    let isFullyLoaded: Bool
 73
 74    init(
 75        id: String,
 76        name: String,
 77        description: String?,
 78        website: String?,
 79        visibility: Visibility,
 80        tags: [String],
 81        updated: Date,
 82        mailingLists: [MailingList],
 83        sources: [SourceRepo],
 84        trackers: [Tracker],
 85        isFullyLoaded: Bool = true
 86    ) {
 87        self.id = id
 88        self.name = name
 89        self.description = description
 90        self.website = website
 91        self.visibility = visibility
 92        self.tags = tags
 93        self.updated = updated
 94        self.mailingLists = mailingLists
 95        self.sources = sources
 96        self.trackers = trackers
 97        self.isFullyLoaded = isFullyLoaded
 98    }
 99
100    var resourceSummary: String? {
101        let parts = [
102            Self.resourceCountText(count: sources.count, singular: "repo"),
103            Self.resourceCountText(count: trackers.count, singular: "tracker"),
104            Self.resourceCountText(count: mailingLists.count, singular: "list")
105        ].compactMap { $0 }
106
107        if !parts.isEmpty {
108            return parts.joined(separator: "")
109        }
110
111        if let website, !website.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
112            return "Website linked"
113        }
114
115        return nil
116    }
117
118    var displayName: String {
119        Self.normalizedText(name) ?? "Untitled Project"
120    }
121
122    var displayDescription: String? {
123        Self.normalizedText(description)
124    }
125
126    var displayTags: [String] {
127        var seen = Set<String>()
128        return tags.compactMap { tag in
129            guard let normalized = Self.normalizedText(tag) else { return nil }
130            let key = normalized.lowercased()
131            guard seen.insert(key).inserted else { return nil }
132            return normalized
133        }
134    }
135
136    var websiteURL: URL? {
137        guard let website = Self.normalizedText(website) else { return nil }
138        return URL(string: website)
139    }
140
141    var hasLinkedResources: Bool {
142        !sources.isEmpty || !trackers.isEmpty || !mailingLists.isEmpty
143    }
144
145    var metadataLine: String {
146        var parts = [visibility.displayName, updated.relativeDescription]
147        if let summary = resourceSummary {
148            parts.append(summary)
149        }
150        return parts.joined(separator: "")
151    }
152
153    private static func resourceCountText(count: Int, singular: String) -> String? {
154        guard count > 0 else { return nil }
155        let label = count == 1 ? singular : "\(singular)s"
156        return "\(count) \(label)"
157    }
158
159    fileprivate static func normalizedText(_ value: String?) -> String? {
160        guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
161              !trimmed.isEmpty else {
162            return nil
163        }
164        return trimmed
165    }
166}
167
168extension Project.MailingList {
169    var displayName: String {
170        Project.normalizedText(name) ?? "Untitled Mailing List"
171    }
172
173    var displayDescription: String? {
174        Project.normalizedText(description)
175    }
176
177    var ownerDisplayName: String {
178        Project.normalizedText(owner.canonicalName) ?? "~unknown"
179    }
180}
181
182extension Project.SourceRepo {
183    var displayName: String {
184        Project.normalizedText(name) ?? "Untitled Repository"
185    }
186
187    var displayDescription: String? {
188        Project.normalizedText(description)
189    }
190
191    var ownerDisplayName: String {
192        Project.normalizedText(owner.canonicalName) ?? "~unknown"
193    }
194
195    var webURL: URL? {
196        SRHTWebURL.projectSource(self)
197    }
198}
199
200extension Project.Tracker {
201    var displayName: String {
202        Project.normalizedText(name) ?? "Untitled Tracker"
203    }
204
205    var displayDescription: String? {
206        Project.normalizedText(description)
207    }
208
209    var ownerDisplayName: String {
210        Project.normalizedText(owner.canonicalName) ?? "~unknown"
211    }
212
213    var webURL: URL? {
214        SRHTWebURL.tracker(ownerUsername: ownerUsername, trackerName: name)
215    }
216}
217
218extension String {
219    var srhtUsername: String {
220        hasPrefix("~") ? String(dropFirst()) : self
221    }
222}
223
224extension Visibility {
225    var displayName: String {
226        switch self {
227        case .public:
228            "Public"
229        case .unlisted:
230            "Unlisted"
231        case .private:
232            "Private"
233        }
234    }
235}