Files
omnivore/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
2023-03-22 09:24:12 -07:00

221 lines
6.9 KiB
Swift

#if os(iOS)
import CoreData
import Models
import Services
import SwiftUI
import Views
@MainActor final class RecommendToViewModel: ObservableObject {
@Published var isLoading = false
@Published var networkError = false
@Published var recommendationGroups = [InternalRecommendationGroup]()
@Published var selectedGroups = [InternalRecommendationGroup]()
@Published var isRunning = false
@Published var showError = false
@Published var showNoteView = false
@Published var note: String = ""
@Published var withHighlights: Bool = true
let pageID: String
let highlightCount: Int
init(pageID: String, highlightCount: Int) {
self.pageID = pageID
self.highlightCount = highlightCount
}
func loadGroups(dataService: DataService) async {
isLoading = true
do {
dataService.viewContext.performAndWait {
let fetchRequest: NSFetchRequest<Models.RecommendationGroup> = RecommendationGroup.fetchRequest()
let sort = NSSortDescriptor(key: #keyPath(RecommendationGroup.name), ascending: true)
fetchRequest.predicate = NSPredicate(format: "canPost == %@", NSNumber(value: true))
fetchRequest.sortDescriptors = [sort]
// If this fails we will fallback to making the API call
let groups = try? dataService.viewContext.fetch(fetchRequest).compactMap { object in
InternalRecommendationGroup.make(from: object)
}
if let groups = groups {
self.recommendationGroups = groups
}
}
recommendationGroups = try await dataService.recommendationGroups()
.filter(\.canPost)
.sorted(by: { $0.name < $1.name })
} catch {
print("ERROR fetching recommendationGroups: ", error)
networkError = true
}
isLoading = false
}
func recommend(dataService: DataService) async -> Bool {
isRunning = true
defer { isRunning = false }
do {
try await dataService.recommendPage(pageID: pageID,
groupIDs: selectedGroups.map(\.id),
note: note.isEmpty ? nil : note,
withHighlights: withHighlights)
} catch {
showError = true
return false
}
isRunning = false
return true
}
}
struct RecommendToView: View {
var dataService: DataService
@StateObject var viewModel: RecommendToViewModel
@Environment(\.dismiss) private var dismiss
var nextButton: some View {
Button(action: {
self.viewModel.showNoteView = true
}, label: {
Text(LocalText.genericNext)
.bold()
})
.disabled(viewModel.selectedGroups.isEmpty)
}
var sendButton: some View {
if viewModel.isRunning {
return AnyView(ProgressView())
} else {
return AnyView(Button(action: {
Task {
if await viewModel.recommend(dataService: dataService) {
Snackbar.show(message: "Recommendation sent")
dismiss()
}
}
}, label: {
Text(LocalText.genericSend)
.bold()
})
.disabled(viewModel.selectedGroups.isEmpty)
)
}
}
var noteView: some View {
VStack {
HStack {
Text(LocalText.recommendationToPrefix)
.font(.appCaption)
.foregroundColor(.appGrayText)
Text(InternalRecommendationGroup.readable(list: viewModel.selectedGroups))
.font(.appCaption)
.foregroundColor(.appGrayTextContrast)
Spacer()
}
TextEditor(text: $viewModel.note)
.lineSpacing(6)
.accentColor(.appGraySolid)
.foregroundColor(.appGrayTextContrast)
.font(.appBody)
.padding(12)
.frame(height: 200)
.background(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color.appGrayBorder, lineWidth: 1)
.background(RoundedRectangle(cornerRadius: 8).fill(Color.systemBackground))
)
.overlay(
Text(LocalText.recommendationAddNote)
.allowsHitTesting(false)
.opacity(viewModel.note.isEmpty ? 0.4 : 0.0)
.font(.appBody)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(.top, 24)
.padding(.leading, 16)
)
if viewModel.highlightCount > 0 {
Toggle(isOn: $viewModel.withHighlights, label: {
HStack(alignment: .firstTextBaseline) {
Text("Include your \(viewModel.highlightCount) highlight\(viewModel.highlightCount > 1 ? "s" : "")")
}
})
}
Spacer()
}
.padding(16)
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
.navigationBarItems(trailing: sendButton)
}
var body: some View {
VStack {
NavigationLink(destination: noteView,
isActive: $viewModel.showNoteView) {
EmptyView()
}
List {
if !viewModel.isLoading, viewModel.recommendationGroups.count < 1 {
Text("""
\(LocalText.clubsNoneJoined)
[Learn more about clubs](https://blog.omnivore.app/p/dca38ba4-8a74-42cc-90ca-d5ffa5d075cc)
""")
.accentColor(.blue)
} else {
Section("Select clubs to recommend to") {
ForEach(viewModel.recommendationGroups) { group in
HStack {
Text(group.name)
Spacer()
if viewModel.selectedGroups.contains(where: { $0.id == group.id }) {
Image(systemName: "checkmark")
}
}
.contentShape(Rectangle())
.onTapGesture {
let idx = viewModel.selectedGroups.firstIndex(where: { $0.id == group.id })
if let idx = idx {
viewModel.selectedGroups.remove(at: idx)
} else {
viewModel.selectedGroups.append(group)
}
}
}
}
}
}
.listStyle(.grouped)
Spacer()
}
.alert(isPresented: $viewModel.showError) {
Alert(
title: Text(LocalText.recommendationError),
dismissButton: .cancel(Text(LocalText.genericOk)) {
viewModel.showError = false
}
)
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(.stack)
.navigationBarItems(leading: Button(action: {
dismiss()
}, label: { Text(LocalText.cancelGeneric) }),
trailing: nextButton)
.task {
await viewModel.loadGroups(dataService: dataService)
}
}
}
#endif