Files

150 lines
4.3 KiB
Swift

import Foundation
import Combine
// MARK: - ViewModel
@MainActor
final class ShareViewModel: ObservableObject {
// MARK: Published state
@Published var shelves: [ShelfSummary] = []
@Published var books: [BookSummary] = []
@Published var chapters: [ChapterSummary] = []
@Published var selectedShelf: ShelfSummary?
@Published var selectedBook: BookSummary?
@Published var selectedChapter: ChapterSummary?
@Published var pageTitle: String = ""
@Published var isLoading: Bool = false
@Published var errorMessage: String?
@Published var isSaved: Bool = false
// MARK: Read-only
let sharedText: String
let isConfigured: Bool
let serverURL: String
// MARK: Private
private let apiService: ShareAPIServiceProtocol
private let defaults: UserDefaults?
private let lastShelfIDKey = "shareExtension.lastShelfID"
private let lastBookIDKey = "shareExtension.lastBookID"
// MARK: Computed
var isSaveDisabled: Bool {
pageTitle.trimmingCharacters(in: .whitespaces).isEmpty
|| selectedBook == nil
|| isLoading
}
// MARK: - Init
init(
sharedText: String,
apiService: ShareAPIServiceProtocol,
serverURL: String = "",
isConfigured: Bool = true,
defaults: UserDefaults? = nil
) {
self.sharedText = sharedText
self.isConfigured = isConfigured
self.serverURL = serverURL
self.apiService = apiService
self.defaults = defaults
// Auto-populate title from the first non-empty line of the shared text.
let firstLine = sharedText
.components(separatedBy: .newlines)
.first { !$0.trimmingCharacters(in: .whitespaces).isEmpty } ?? ""
self.pageTitle = String(firstLine.prefix(100))
}
// MARK: - Actions
/// Loads all shelves and restores the last used shelf/book selection.
func loadShelves() async {
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
shelves = try await apiService.fetchShelves()
// Restore last selected shelf
let lastShelfID = defaults?.integer(forKey: lastShelfIDKey) ?? 0
if lastShelfID != 0, let match = shelves.first(where: { $0.id == lastShelfID }) {
await selectShelf(match)
}
} catch {
errorMessage = error.localizedDescription
}
}
func selectShelf(_ shelf: ShelfSummary) async {
selectedShelf = shelf
selectedBook = nil
selectedChapter = nil
books = []
chapters = []
defaults?.set(shelf.id, forKey: lastShelfIDKey)
isLoading = true
defer { isLoading = false }
do {
books = try await apiService.fetchBooks(shelfId: shelf.id)
// Restore last selected book
let lastBookID = defaults?.integer(forKey: lastBookIDKey) ?? 0
if lastBookID != 0, let match = books.first(where: { $0.id == lastBookID }) {
await selectBook(match)
}
} catch {
errorMessage = error.localizedDescription
}
}
func selectBook(_ book: BookSummary) async {
selectedBook = book
selectedChapter = nil
chapters = []
defaults?.set(book.id, forKey: lastBookIDKey)
isLoading = true
defer { isLoading = false }
do {
chapters = try await apiService.fetchChapters(bookId: book.id)
} catch {
errorMessage = error.localizedDescription
}
}
func savePage() async {
guard let book = selectedBook,
!pageTitle.trimmingCharacters(in: .whitespaces).isEmpty else { return }
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
_ = try await apiService.createPage(
bookId: book.id,
chapterId: selectedChapter?.id,
title: pageTitle.trimmingCharacters(in: .whitespaces),
markdown: sharedText
)
isSaved = true
} catch {
errorMessage = error.localizedDescription
}
}
}