krz/hutch

an ios client for sourcehut

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

v2.5.0: Hutch/Views/Pastes/PasteDetailViewModel.swift · raw

  1import Foundation
  2
  3@Observable
  4@MainActor
  5final class PasteDetailViewModel {
  6    private(set) var paste: Paste?
  7    private(set) var isLoading = false
  8    private(set) var isUpdatingVisibility = false
  9    private(set) var isDeleting = false
 10    private(set) var loadingFileHashes: Set<String> = []
 11    var error: String?
 12
 13    var selectedFileHash: String?
 14    private(set) var fileContents: [String: String] = [:]
 15
 16    private let pasteID: String
 17    private let service: PasteService
 18
 19    init(pasteID: String, initialPaste: Paste? = nil, service: PasteService) {
 20        self.pasteID = pasteID
 21        self.paste = initialPaste
 22        self.service = service
 23        self.selectedFileHash = initialPaste?.files.first?.hash
 24    }
 25
 26    var selectedFile: PasteFile? {
 27        let hash = selectedFileHash ?? paste?.files.first?.hash
 28        return paste?.files.first(where: { $0.hash == hash })
 29    }
 30
 31    var selectedFileContents: String? {
 32        guard let selectedFile else { return nil }
 33        return fileContents[selectedFile.hash]
 34    }
 35
 36    func loadPaste() async {
 37        guard !isLoading else { return }
 38        isLoading = true
 39        error = nil
 40        defer { isLoading = false }
 41
 42        do {
 43            let loaded = try await service.loadPaste(id: pasteID)
 44            paste = loaded
 45            if selectedFileHash == nil {
 46                selectedFileHash = loaded?.files.first?.hash
 47            }
 48            await loadSelectedFileContentsIfNeeded()
 49        } catch {
 50            self.error = error.userFacingMessage
 51        }
 52    }
 53
 54    func selectFile(hash: String) {
 55        selectedFileHash = hash
 56        Task {
 57            await loadSelectedFileContentsIfNeeded()
 58        }
 59    }
 60
 61    func updateVisibility(_ visibility: Visibility) async -> Paste? {
 62        guard !isUpdatingVisibility else { return nil }
 63        guard let paste else { return nil }
 64        guard paste.visibility != visibility else { return paste }
 65
 66        isUpdatingVisibility = true
 67        error = nil
 68        defer { isUpdatingVisibility = false }
 69
 70        do {
 71            let updatedPaste = try await service.updateVisibility(id: paste.id, visibility: visibility)
 72            if let updatedPaste {
 73                self.paste = updatedPaste
 74                if selectedFileHash == nil {
 75                    selectedFileHash = updatedPaste.files.first?.hash
 76                }
 77            }
 78            return updatedPaste
 79        } catch {
 80            self.error = error.userFacingMessage
 81            return nil
 82        }
 83    }
 84
 85    func deletePaste() async -> Bool {
 86        guard !isDeleting else { return false }
 87        isDeleting = true
 88        error = nil
 89        defer { isDeleting = false }
 90
 91        do {
 92            _ = try await service.deletePaste(id: pasteID)
 93            return true
 94        } catch {
 95            self.error = error.userFacingMessage
 96            return false
 97        }
 98    }
 99
100    func loadSelectedFileContentsIfNeeded() async {
101        guard let file = selectedFile, fileContents[file.hash] == nil else { return }
102        guard let url = file.contents else { return }
103        guard !loadingFileHashes.contains(file.hash) else { return }
104
105        loadingFileHashes.insert(file.hash)
106        defer { loadingFileHashes.remove(file.hash) }
107
108        do {
109            fileContents[file.hash] = try await service.loadContents(from: url)
110        } catch {
111            self.error = error.userFacingMessage
112        }
113    }
114}