gitbay/Discovery/ProfileViewModel.swift
91 lines · 3067 bytes
1import Foundation
2import Observation
3
4/// `profile show <name>` — the whole profile in one read: description,
5/// links, org membership (or an org's members), the repositories the
6/// account may see, and a year of activity.
7@Observable
8@MainActor
9final class ProfileViewModel {
10
11 nonisolated struct Profile: Decodable, Sendable, Hashable {
12 let name: String
13 let kind: String // user | org
14 let description: String?
15 let website: String?
16 let orgs: [Member]?
17 let members: [Member]?
18 let repos: [Repo]
19 let activity: [Day]?
20 let activityTotal: Int
21
22 enum CodingKeys: String, CodingKey {
23 case name, kind, description, website, orgs, members, repos, activity
24 case activityTotal = "activity_total"
25 }
26
27 var isOrg: Bool { kind == "org" }
28
29 nonisolated struct Member: Decodable, Sendable, Hashable, Identifiable {
30 let name: String
31 let role: String?
32 var id: String { name }
33 }
34
35 nonisolated struct Repo: Decodable, Sendable, Hashable, Identifiable {
36 let path: String
37 let visibility: String
38 let description: String?
39 let archived: Bool?
40 let topics: [String]?
41 let license: String?
42 let defaultBranch: String?
43 let updated: String?
44
45 enum CodingKeys: String, CodingKey {
46 case path, visibility, description, archived, topics, license, updated
47 case defaultBranch = "default_branch"
48 }
49
50 var id: String { path }
51 var isPrivate: Bool { visibility == "private" }
52 var isArchived: Bool { archived ?? false }
53 var name: String { String(path.split(separator: "/").last ?? "") }
54
55 /// "main · MIT · updated 2026-08-30", the line the web's repo
56 /// rows carry. Parts the server did not report are left out.
57 var meta: String {
58 var parts: [String] = []
59 if let defaultBranch, !defaultBranch.isEmpty { parts.append(defaultBranch) }
60 if let license, !license.isEmpty { parts.append(license) }
61 if let updated, !updated.isEmpty { parts.append("updated \(updated)") }
62 return parts.joined(separator: " · ")
63 }
64 }
65
66 /// One day's contributions. Days with none are omitted, so the
67 /// calendar is filled in by the view.
68 nonisolated struct Day: Decodable, Sendable, Hashable {
69 let date: String
70 let count: Int
71 }
72 }
73
74 private(set) var state: LoadState<Profile> = .loading
75
76 private let client: GitbayClient
77 let name: String
78
79 init(client: GitbayClient, name: String) {
80 self.client = client
81 self.name = name
82 }
83
84 func load() async {
85 do {
86 state = .loaded(try await client.read(["profile", "show", name], as: Profile.self))
87 } catch {
88 state = .from(error)
89 }
90 }
91}