summaryrefslogtreecommitdiff
path: root/Hutch/Models
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-12 22:06:16 -0500
committerChristian Cleberg <[email protected]>2026-04-12 22:06:16 -0500
commit86bcd9452d3107f68aca085e03e69b02667c0dbb (patch)
tree8b0adece5cb0f8ffb178eaab2cd12d025c9ebf0a /Hutch/Models
parent31cbca674957eea7ae2529012e20a9d95158e561 (diff)
downloadhutch-86bcd9452d3107f68aca085e03e69b02667c0dbb.tar.gz
hutch-86bcd9452d3107f68aca085e03e69b02667c0dbb.tar.bz2
hutch-86bcd9452d3107f68aca085e03e69b02667c0dbb.zip
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
Diffstat (limited to 'Hutch/Models')
-rw-r--r--Hutch/Models/Project.swift106
1 files changed, 106 insertions, 0 deletions
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<String>()
+ 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"
+ }
+ }
+}