krz/hutch

an ios client for sourcehut

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

v3.4.0: 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    /// Identity and display fields for a project.
 75    struct Metadata: Sendable, Hashable {
 76        let id: String
 77        let name: String
 78        let description: String?
 79        let website: String?
 80        let visibility: Visibility
 81        let tags: [String]
 82        let updated: Date
 83    }
 84
 85    struct Resources: Sendable, Hashable {
 86        let mailingLists: [MailingList]
 87        let sources: [SourceRepo]
 88        let trackers: [Tracker]
 89        let isFullyLoaded: Bool
 90    }
 91
 92    init(metadata: Metadata, resources: Resources) {
 93        id = metadata.id
 94        name = metadata.name
 95        description = metadata.description
 96        website = metadata.website
 97        visibility = metadata.visibility
 98        tags = metadata.tags
 99        updated = metadata.updated
100        mailingLists = resources.mailingLists
101        sources = resources.sources
102        trackers = resources.trackers
103        isFullyLoaded = resources.isFullyLoaded
104    }
105
106    var resourceSummary: String? {
107        let parts = [
108            Self.resourceCountText(count: sources.count, singular: "repo"),
109            Self.resourceCountText(count: trackers.count, singular: "tracker"),
110            Self.resourceCountText(count: mailingLists.count, singular: "list")
111        ].compactMap { $0 }
112
113        if !parts.isEmpty {
114            return parts.joined(separator: "")
115        }
116
117        if let website, !website.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
118            return "Website linked"
119        }
120
121        return nil
122    }
123
124    var displayName: String {
125        Self.normalizedText(name) ?? "Untitled Project"
126    }
127
128    var displayDescription: String? {
129        Self.normalizedText(description)
130    }
131
132    var displayTags: [String] {
133        var seen = Set<String>()
134        return tags.compactMap { tag in
135            guard let normalized = Self.normalizedText(tag) else { return nil }
136            let key = normalized.lowercased()
137            guard seen.insert(key).inserted else { return nil }
138            return normalized
139        }
140    }
141
142    var websiteURL: URL? {
143        guard let website = Self.normalizedText(website) else { return nil }
144        return URL(string: website)
145    }
146
147    var hasLinkedResources: Bool {
148        !sources.isEmpty || !trackers.isEmpty || !mailingLists.isEmpty
149    }
150
151    var metadataLine: String {
152        var parts = [visibility.displayName, updated.relativeDescription]
153        if let summary = resourceSummary {
154            parts.append(summary)
155        }
156        return parts.joined(separator: "")
157    }
158
159    private static func resourceCountText(count: Int, singular: String) -> String? {
160        guard count > 0 else { return nil }
161        let label = count == 1 ? singular : "\(singular)s"
162        return "\(count) \(label)"
163    }
164
165    fileprivate static func normalizedText(_ value: String?) -> String? {
166        guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
167              !trimmed.isEmpty else {
168            return nil
169        }
170        return trimmed
171    }
172}
173
174extension Project.MailingList {
175    var displayName: String {
176        Project.normalizedText(name) ?? "Untitled Mailing List"
177    }
178
179    var displayDescription: String? {
180        Project.normalizedText(description)
181    }
182
183    var ownerDisplayName: String {
184        Project.normalizedText(owner.canonicalName) ?? "~unknown"
185    }
186}
187
188extension Project.SourceRepo {
189    var displayName: String {
190        Project.normalizedText(name) ?? "Untitled Repository"
191    }
192
193    var displayDescription: String? {
194        Project.normalizedText(description)
195    }
196
197    var ownerDisplayName: String {
198        Project.normalizedText(owner.canonicalName) ?? "~unknown"
199    }
200
201    var webURL: URL? {
202        SRHTWebURL.projectSource(self)
203    }
204}
205
206extension Project.Tracker {
207    var displayName: String {
208        Project.normalizedText(name) ?? "Untitled Tracker"
209    }
210
211    var displayDescription: String? {
212        Project.normalizedText(description)
213    }
214
215    var ownerDisplayName: String {
216        Project.normalizedText(owner.canonicalName) ?? "~unknown"
217    }
218
219    var webURL: URL? {
220        SRHTWebURL.tracker(ownerUsername: ownerUsername, trackerName: name)
221    }
222}
223
224extension String {
225    var srhtUsername: String {
226        hasPrefix("~") ? String(dropFirst()) : self
227    }
228}
229
230extension Visibility {
231    var displayName: String {
232        switch self {
233        case .publicVisibility:
234            "Public"
235        case .unlisted:
236            "Unlisted"
237        case .privateVisibility:
238            "Private"
239        }
240    }
241}