280 lines
8.5 KiB
Swift
280 lines
8.5 KiB
Swift
import AVFoundation
|
|
import Models
|
|
import Services
|
|
import SwiftUI
|
|
import Utils
|
|
import Views
|
|
import WebKit
|
|
|
|
struct WebReaderContainerView: View {
|
|
let item: LinkedItem
|
|
|
|
@State private var showPreferencesPopover = false
|
|
@State private var showLabelsModal = false
|
|
@State private var showTitleEdit = false
|
|
@State var showHighlightAnnotationModal = false
|
|
@State var safariWebLink: SafariWebLink?
|
|
@State private var navBarVisibilityRatio = 1.0
|
|
@State private var showDeleteConfirmation = false
|
|
@State private var progressViewOpacity = 0.0
|
|
@State var readerSettingsChangedTransactionID: UUID?
|
|
@State var annotationSaveTransactionID: UUID?
|
|
@State var showNavBarActionID: UUID?
|
|
@State var shareActionID: UUID?
|
|
@State var annotation = String()
|
|
|
|
@EnvironmentObject var dataService: DataService
|
|
@EnvironmentObject var audioSession: AudioSession
|
|
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
|
@StateObject var viewModel = WebReaderViewModel()
|
|
|
|
func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) {
|
|
if let replyHandler = replyHandler {
|
|
viewModel.webViewActionWithReplyHandler(
|
|
message: message,
|
|
replyHandler: replyHandler,
|
|
dataService: dataService
|
|
)
|
|
return
|
|
}
|
|
|
|
if message.name == WebViewAction.highlightAction.rawValue {
|
|
handleHighlightAction(message: message)
|
|
}
|
|
}
|
|
|
|
private func handleHighlightAction(message: WKScriptMessage) {
|
|
guard let messageBody = message.body as? [String: String] else { return }
|
|
guard let actionID = messageBody["actionID"] else { return }
|
|
|
|
switch actionID {
|
|
case "annotate":
|
|
annotation = messageBody["annotation"] ?? ""
|
|
showHighlightAnnotationModal = true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
var navBar: some View {
|
|
HStack(alignment: .center) {
|
|
#if os(iOS)
|
|
Button(
|
|
action: { self.presentationMode.wrappedValue.dismiss() },
|
|
label: {
|
|
Image(systemName: "chevron.backward")
|
|
.font(.appTitleTwo)
|
|
.foregroundColor(.appGrayTextContrast)
|
|
.padding(.horizontal)
|
|
}
|
|
)
|
|
.scaleEffect(navBarVisibilityRatio)
|
|
Spacer()
|
|
#endif
|
|
if FeatureFlag.enableTextToSpeechButton {
|
|
Button(
|
|
action: {
|
|
switch audioSession.state {
|
|
case .playing:
|
|
if audioSession.item == self.item {
|
|
audioSession.pause()
|
|
return
|
|
}
|
|
fallthrough
|
|
case .paused:
|
|
if audioSession.item == self.item {
|
|
audioSession.unpause()
|
|
return
|
|
}
|
|
fallthrough
|
|
default:
|
|
audioSession.play(item: self.item)
|
|
}
|
|
},
|
|
label: {
|
|
Image(systemName: audioSession.isPlayingItem(item: item) ? "pause.circle" : "play.circle")
|
|
.font(.appTitleTwo)
|
|
}
|
|
)
|
|
.padding(.horizontal)
|
|
.scaleEffect(navBarVisibilityRatio)
|
|
}
|
|
Button(
|
|
action: { showPreferencesPopover.toggle() },
|
|
label: {
|
|
Image(systemName: "textformat.size")
|
|
.font(.appTitleTwo)
|
|
}
|
|
)
|
|
.padding(.horizontal)
|
|
.scaleEffect(navBarVisibilityRatio)
|
|
#if os(macOS)
|
|
Spacer()
|
|
#endif
|
|
Menu(
|
|
content: {
|
|
Group {
|
|
Button(
|
|
action: { showTitleEdit = true },
|
|
label: { Label("Edit Title/Description", systemImage: "textbox") }
|
|
)
|
|
Button(
|
|
action: { showLabelsModal = true },
|
|
label: { Label("Edit Labels", systemImage: "tag") }
|
|
)
|
|
Button(
|
|
action: {
|
|
dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived)
|
|
#if os(iOS)
|
|
presentationMode.wrappedValue.dismiss()
|
|
#endif
|
|
Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox")
|
|
},
|
|
label: {
|
|
Label(
|
|
item.isArchived ? "Unarchive" : "Archive",
|
|
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
|
|
)
|
|
}
|
|
)
|
|
Button(
|
|
action: { shareActionID = UUID() },
|
|
label: { Label("Share Original", systemImage: "square.and.arrow.up") }
|
|
)
|
|
Button(
|
|
action: { showDeleteConfirmation = true },
|
|
label: { Label("Delete", systemImage: "trash") }
|
|
)
|
|
}
|
|
},
|
|
label: {
|
|
#if os(iOS)
|
|
Image.profile
|
|
.padding(.horizontal)
|
|
.scaleEffect(navBarVisibilityRatio)
|
|
#else
|
|
Text("Options")
|
|
#endif
|
|
}
|
|
)
|
|
#if os(macOS)
|
|
.frame(maxWidth: 100)
|
|
.padding(.trailing, 16)
|
|
#endif
|
|
}
|
|
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
|
|
.opacity(navBarVisibilityRatio)
|
|
.background(Color.systemBackground)
|
|
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
|
|
Button("Remove Link", role: .destructive) {
|
|
Snackbar.show(message: "Link removed")
|
|
dataService.removeLink(objectID: item.objectID)
|
|
#if os(iOS)
|
|
presentationMode.wrappedValue.dismiss()
|
|
#endif
|
|
}
|
|
Button("Cancel", role: .cancel, action: {})
|
|
}
|
|
.sheet(isPresented: $showLabelsModal) {
|
|
ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false })
|
|
}
|
|
.sheet(isPresented: $showTitleEdit) {
|
|
LinkedItemTitleEditView(item: item)
|
|
}
|
|
#if os(macOS)
|
|
.buttonStyle(PlainButtonStyle())
|
|
#endif
|
|
}
|
|
|
|
#if os(iOS)
|
|
var webPreferencesPopoverView: some View {
|
|
WebPreferencesPopoverView(
|
|
updateReaderPreferences: { readerSettingsChangedTransactionID = UUID() },
|
|
dismissAction: { showPreferencesPopover = false }
|
|
)
|
|
}
|
|
#endif
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
if let articleContent = viewModel.articleContent {
|
|
WebReader(
|
|
item: item,
|
|
articleContent: articleContent,
|
|
openLinkAction: {
|
|
#if os(macOS)
|
|
NSWorkspace.shared.open($0)
|
|
#elseif os(iOS)
|
|
safariWebLink = SafariWebLink(id: UUID(), url: $0)
|
|
#endif
|
|
},
|
|
webViewActionHandler: webViewActionHandler,
|
|
navBarVisibilityRatioUpdater: {
|
|
navBarVisibilityRatio = $0
|
|
},
|
|
readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID,
|
|
annotationSaveTransactionID: $annotationSaveTransactionID,
|
|
showNavBarActionID: $showNavBarActionID,
|
|
shareActionID: $shareActionID,
|
|
annotation: $annotation
|
|
)
|
|
.onTapGesture {
|
|
withAnimation {
|
|
navBarVisibilityRatio = 1
|
|
showNavBarActionID = UUID()
|
|
}
|
|
}
|
|
#if os(iOS)
|
|
.fullScreenCover(item: $safariWebLink) {
|
|
SafariView(url: $0.url)
|
|
}
|
|
#endif
|
|
.sheet(isPresented: $showHighlightAnnotationModal) {
|
|
HighlightAnnotationSheet(
|
|
annotation: $annotation,
|
|
onSave: {
|
|
annotationSaveTransactionID = UUID()
|
|
showHighlightAnnotationModal = false
|
|
},
|
|
onCancel: {
|
|
showHighlightAnnotationModal = false
|
|
}
|
|
)
|
|
}
|
|
} else if let errorMessage = viewModel.errorMessage {
|
|
Text(errorMessage).padding()
|
|
} else {
|
|
ProgressView()
|
|
.opacity(progressViewOpacity)
|
|
.onAppear {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) {
|
|
progressViewOpacity = 1
|
|
}
|
|
}
|
|
.task {
|
|
await viewModel.loadContent(dataService: dataService, itemID: item.unwrappedID)
|
|
}
|
|
}
|
|
#if os(iOS)
|
|
VStack(spacing: 0) {
|
|
navBar
|
|
Spacer()
|
|
}
|
|
#endif
|
|
}
|
|
#if os(iOS)
|
|
.formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) {
|
|
webPreferencesPopoverView
|
|
}
|
|
#else
|
|
.onReceive(NSNotification.readerSettingsChangedPublisher) { _ in
|
|
readerSettingsChangedTransactionID = UUID()
|
|
}
|
|
#endif
|
|
.onDisappear {
|
|
// Clear the shared webview content when exiting
|
|
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
|
|
}
|
|
}
|
|
}
|