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