krz/hutch

an ios client for sourcehut

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

v2.10.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}