krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.3.1: Hutch/Views/Pastes/PasteListViewModel.swift · raw
1import Foundation
2
3@Observable
4@MainActor
5final class PasteListViewModel {
6 private(set) var pastes: [Paste] = [] {
7 didSet { updateFilteredPastes() }
8 }
9 private(set) var isLoading = false
10 private(set) var isLoadingMore = false
11 private(set) var isRefreshing = false
12 private(set) var isCreatingPaste = false
13 var error: String?
14 var searchText = "" {
15 didSet { updateFilteredPastes() }
16 }
17 private(set) var filteredPastes: [Paste] = []
18
19 private var cursor: String?
20 private var hasMore = true
21 private let service: PasteService
22
23 init(service: PasteService) {
24 self.service = service
25 }
26
27 private func updateFilteredPastes() {
28 let q = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
29 let updated: [Paste]
30 if q.isEmpty {
31 updated = pastes
32 } else {
33 updated = pastes.filter {
34 $0.files.contains {
35 ($0.filename?.lowercased().contains(q) == true) ||
36 $0.hash.lowercased().hasPrefix(q)
37 }
38 }
39 }
40 if updated != filteredPastes {
41 filteredPastes = updated
42 }
43 }
44
45 func loadPastes() async {
46 if pastes.isEmpty, let cached = await service.loadCachedPastes() {
47 pastes = cached.results
48 cursor = cached.cursor
49 hasMore = cached.cursor != nil
50 }
51
52 if pastes.isEmpty {
53 isLoading = true
54 } else {
55 isRefreshing = true
56 }
57 error = nil
58 cursor = nil
59 hasMore = true
60
61 do {
62 let page = try await service.listPastes(cursor: nil, useCache: true)
63 pastes = page.results
64 cursor = page.cursor
65 hasMore = page.cursor != nil
66 } catch {
67 if pastes.isEmpty {
68 self.error = error.userFacingMessage
69 }
70 }
71
72 isLoading = false
73 isRefreshing = false
74 }
75
76 func loadMoreIfNeeded(currentItem: Paste) async {
77 guard let last = pastes.last,
78 last.id == currentItem.id,
79 hasMore,
80 !isLoadingMore else {
81 return
82 }
83
84 isLoadingMore = true
85 defer { isLoadingMore = false }
86
87 do {
88 let page = try await service.listPastes(cursor: cursor, useCache: false)
89 pastes.append(contentsOf: page.results)
90 cursor = page.cursor
91 hasMore = page.cursor != nil
92 } catch {
93 self.error = error.userFacingMessage
94 }
95 }
96
97 func createPaste(files: [PasteUploadDraft], visibility: Visibility) async -> Paste? {
98 guard !isCreatingPaste else { return nil }
99
100 let normalizedFiles = files.filter {
101 !$0.contents.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
102 }
103 guard !normalizedFiles.isEmpty else {
104 error = "Add at least one file with text content."
105 return nil
106 }
107
108 isCreatingPaste = true
109 error = nil
110 defer { isCreatingPaste = false }
111
112 do {
113 let paste = try await service.createPaste(files: normalizedFiles, visibility: visibility)
114 upsertPaste(paste)
115 return paste
116 } catch {
117 self.error = error.userFacingMessage
118 return nil
119 }
120 }
121
122 func deletePaste(_ paste: Paste) async {
123 do {
124 _ = try await service.deletePaste(id: paste.id)
125 removePaste(id: paste.id)
126 } catch {
127 self.error = error.userFacingMessage
128 }
129 }
130
131 func cycleVisibility(for paste: Paste) async {
132 let next: Visibility
133 switch paste.visibility {
134 case .publicVisibility:
135 next = .unlisted
136 case .unlisted:
137 next = .privateVisibility
138 case .privateVisibility:
139 next = .publicVisibility
140 }
141
142 let original = pastes
143 if let index = pastes.firstIndex(where: { $0.id == paste.id }) {
144 pastes[index] = Paste(
145 id: paste.id,
146 created: paste.created,
147 visibility: next,
148 files: paste.files,
149 user: paste.user
150 )
151 }
152
153 do {
154 if let updated = try await service.updateVisibility(id: paste.id, visibility: next) {
155 upsertPaste(updated)
156 }
157 } catch {
158 pastes = original
159 self.error = error.userFacingMessage
160 }
161 }
162
163 func upsertPaste(_ paste: Paste) {
164 if let index = pastes.firstIndex(where: { $0.id == paste.id }) {
165 pastes[index] = paste
166 } else {
167 pastes.insert(paste, at: 0)
168 }
169 }
170
171 func removePaste(id: String) {
172 pastes.removeAll { $0.id == id }
173 }
174}