krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.0.4: 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 = 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 .public:
135            next = .unlisted
136        case .unlisted:
137            next = .private
138        case .private:
139            next = .public
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}