Files
Pulse/Sources/PulseUI/Features/FileViewer/RichTextView/RichTextView.swift
2024-07-14 13:40:50 -04:00

157 lines
4.9 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean).
#if os(macOS) || os(iOS) || os(visionOS)
import SwiftUI
import CoreData
import Pulse
import Combine
struct RichTextView: View {
@ObservedObject var viewModel: RichTextViewModel
var isTextViewBarItemsHidden = false
@State private var shareItems: ShareItems?
@State private var isWebViewOpen = false
@Environment(\.textViewSearchContext) private var searchContext
func textViewBarItemsHidden(_ isHidden: Bool) -> RichTextView {
var copy = self
copy.isTextViewBarItemsHidden = isHidden
return copy
}
#if os(iOS) || os(visionOS)
var body: some View {
contents
.onAppear { viewModel.prepare(searchContext) }
.navigationBarItems(trailing: navigationBarTrailingItems)
.sheet(item: $shareItems, content: ShareView.init)
.sheet(isPresented: $isWebViewOpen) {
NavigationView {
WebView(data: viewModel.textStorage.string.data(using: .utf8) ?? Data(), contentType: "application/html")
.inlineNavigationTitle("Browser Preview")
.navigationBarItems(trailing: Button(action: {
isWebViewOpen = false
}) { Image(systemName: "xmark") })
}
}
}
@ViewBuilder
private var contents: some View {
if #available(iOS 15, *) {
ContentView(viewModel: viewModel)
.searchable(text: $viewModel.searchTerm)
.disableAutocorrection(true)
} else {
WrappedTextView(viewModel: viewModel)
.edgesIgnoringSafeArea(.bottom)
}
}
@available(iOS 15, visionOS 1.0, *)
private struct ContentView: View {
@ObservedObject var viewModel: RichTextViewModel
@Environment(\.isSearching) private var isSearching
var body: some View {
WrappedTextView(viewModel: viewModel)
.edgesIgnoringSafeArea([.bottom])
.overlay {
if isSearching || !viewModel.matches.isEmpty {
VStack {
Spacer()
HStack {
Spacer()
RichTextViewSearchToobar(viewModel: viewModel)
.padding()
}
}
}
}
}
}
@ViewBuilder
private var navigationBarTrailingItems: some View {
if !isTextViewBarItemsHidden {
Menu(content: {
AttributedStringShareMenu(shareItems: $shareItems) {
viewModel.textStorage
}
}, label: {
Image(systemName: "square.and.arrow.up")
})
// TODO: This should be injected/added outside of the text view
if viewModel.contentType?.isHTML ?? false {
Menu(content: {
Section {
if viewModel.contentType?.isHTML == true {
Button(action: { isWebViewOpen = true }) {
Label("Open in Browser", systemImage: "safari")
}
}
}
}, label: {
Image(systemName: "ellipsis.circle")
})
}
}
}
#else
var body: some View {
VStack(spacing: 0) {
WrappedTextView(viewModel: viewModel)
.id(ObjectIdentifier(viewModel))
if !viewModel.isToolbarHidden {
RichTextViewSearchToobar(viewModel: viewModel)
}
}
.onAppear { viewModel.prepare(searchContext) }
}
#endif
}
#if DEBUG
struct RichTextView_Previews: PreviewProvider {
static var previews: some View {
let textView = RichTextView(viewModel: makePreviewViewModel())
#if os(macOS)
textView
.background(Color(.textBackgroundColor))
.frame(height: 600)
.previewLayout(.sizeThatFits)
#else
NavigationView {
textView
.inlineNavigationTitle("Rich Text View")
}
#endif
}
}
private func makePreviewViewModel() -> RichTextViewModel {
let json = try! JSONSerialization.jsonObject(with: MockJSON.allPossibleValues)
let string = TextRenderer().render(json: json)
return RichTextViewModel(string: string, contentType: "application/json")
}
#endif
private struct TextViewSearchContextKey: EnvironmentKey {
static var defaultValue: RichTextViewModel.SearchContext?
}
extension EnvironmentValues {
var textViewSearchContext: RichTextViewModel.SearchContext? {
get { self[TextViewSearchContextKey.self] }
set { self[TextViewSearchContextKey.self] = newValue }
}
}
#endif