Files
omnivore/apple/OmnivoreKit/Sources/App/Views/WebReader/RecommendToView.swift
Jackson Harper 03766734e5 Pull out popup view and replace with Transmission snackbars
PopupView and Transimission can interfere with each other and
cause an issue with the root screen becoming black and the app
getting stuck.
2024-02-06 11:43:43 +08: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", dismissAfter: 2000)
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