From 86bcd9452d3107f68aca085e03e69b02667c0dbb Mon Sep 17 00:00:00 2001 From: Christian Cleberg Date: Sun, 12 Apr 2026 22:06:16 -0500 Subject: feat: polish read-only project views and home integration Refs: https://todo.sr.ht/~ccleberg/hutch/36 Refs: https://todo.sr.ht/~ccleberg/hutch/37 Refs: https://todo.sr.ht/~ccleberg/hutch/38 --- Hutch/Models/Project.swift | 106 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) (limited to 'Hutch/Models/Project.swift') diff --git a/Hutch/Models/Project.swift b/Hutch/Models/Project.swift index 54c0318..678d52e 100644 --- a/Hutch/Models/Project.swift +++ b/Hutch/Models/Project.swift @@ -115,11 +115,104 @@ struct Project: Identifiable, Hashable, Sendable { return nil } + var displayName: String { + Self.normalizedText(name) ?? "Untitled Project" + } + + var displayDescription: String? { + Self.normalizedText(description) + } + + var displayTags: [String] { + var seen = Set() + return tags.compactMap { tag in + guard let normalized = Self.normalizedText(tag) else { return nil } + let key = normalized.lowercased() + guard seen.insert(key).inserted else { return nil } + return normalized + } + } + + var websiteURL: URL? { + guard let website = Self.normalizedText(website) else { return nil } + return URL(string: website) + } + + var hasLinkedResources: Bool { + !sources.isEmpty || !trackers.isEmpty || !mailingLists.isEmpty + } + + var metadataLine: String { + var parts = [visibility.displayName, updated.relativeDescription] + if let summary = resourceSummary { + parts.append(summary) + } + return parts.joined(separator: " • ") + } + private static func resourceCountText(count: Int, singular: String) -> String? { guard count > 0 else { return nil } let label = count == 1 ? singular : "\(singular)s" return "\(count) \(label)" } + + fileprivate static func normalizedText(_ value: String?) -> String? { + guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), + !trimmed.isEmpty else { + return nil + } + return trimmed + } +} + +extension Project.MailingList { + var displayName: String { + Project.normalizedText(name) ?? "Untitled Mailing List" + } + + var displayDescription: String? { + Project.normalizedText(description) + } + + var ownerDisplayName: String { + Project.normalizedText(owner.canonicalName) ?? "~unknown" + } +} + +extension Project.SourceRepo { + var displayName: String { + Project.normalizedText(name) ?? "Untitled Repository" + } + + var displayDescription: String? { + Project.normalizedText(description) + } + + var ownerDisplayName: String { + Project.normalizedText(owner.canonicalName) ?? "~unknown" + } + + var webURL: URL? { + SRHTWebURL.projectSource(self) + } +} + +extension Project.Tracker { + var displayName: String { + Project.normalizedText(name) ?? "Untitled Tracker" + } + + var displayDescription: String? { + Project.normalizedText(description) + } + + var ownerDisplayName: String { + Project.normalizedText(owner.canonicalName) ?? "~unknown" + } + + var webURL: URL? { + SRHTWebURL.tracker(ownerUsername: ownerUsername, trackerName: name) + } } extension String { @@ -127,3 +220,16 @@ extension String { hasPrefix("~") ? String(dropFirst()) : self } } + +extension Visibility { + var displayName: String { + switch self { + case .public: + "Public" + case .unlisted: + "Unlisted" + case .private: + "Private" + } + } +} -- cgit v1.2.3