Merge pull request #898 from omnivore-app/macos-web-reader

Mac App bundled reader
This commit is contained in:
Satindar Dhillon
2022-06-29 22:59:32 -07:00
committed by GitHub
18 changed files with 543 additions and 1004 deletions

View File

@ -22,6 +22,7 @@ import Views
ZStack {
if let linkRequest = viewModel.linkRequest {
NavigationLink(
// TODO: add alt for macOS
destination: WebReaderLoadingContainer(requestID: linkRequest.serverID),
tag: linkRequest,
selection: $viewModel.linkRequest

View File

@ -8,7 +8,6 @@ import Views
@MainActor final class LinkItemDetailViewModel: ObservableObject {
let pdfItem: PDFItem?
let item: LinkedItem?
@Published var webAppWrapperViewModel: WebAppWrapperViewModel?
init(linkedItemObjectID: NSManagedObjectID, dataService: DataService) {
if let linkedItem = dataService.viewContext.object(with: linkedItemObjectID) as? LinkedItem {
@ -42,32 +41,6 @@ import Views
)
}
func loadWebAppWrapper(dataService: DataService, rawAuthCookie: String?) async {
let viewer: Viewer? = await {
if let currentViewer = dataService.currentViewer {
return currentViewer
}
guard let viewerObjectID = try? await dataService.fetchViewer() else { return nil }
var result: Viewer?
await dataService.viewContext.perform {
result = dataService.viewContext.object(with: viewerObjectID) as? Viewer
}
return result
}()
if let viewer = viewer {
createWebAppWrapperViewModel(
username: viewer.unwrappedUsername,
dataService: dataService,
rawAuthCookie: rawAuthCookie
)
}
}
func trackReadEvent() {
guard let itemID = item?.unwrappedID ?? pdfItem?.itemID else { return }
guard let slug = item?.unwrappedSlug ?? pdfItem?.slug else { return }
@ -89,23 +62,6 @@ import Views
var isItemArchived: Bool {
item?.isArchived ?? pdfItem?.isArchived ?? false
}
private func createWebAppWrapperViewModel(username: String, dataService: DataService, rawAuthCookie: String?) {
guard let slug = item?.unwrappedSlug ?? pdfItem?.slug else { return }
let baseURL = dataService.appEnvironment.webAppBaseURL
let urlRequest = URLRequest.webRequest(
baseURL: dataService.appEnvironment.webAppBaseURL,
urlPath: "/app/\(username)/\(slug)",
queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"]
)
webAppWrapperViewModel = WebAppWrapperViewModel(
webViewURLRequest: urlRequest,
baseURL: baseURL,
rawAuthCookie: rawAuthCookie
)
}
}
struct LinkItemDetailView: View {
@ -144,39 +100,31 @@ struct LinkItemDetailView: View {
)
}
var fontAdjustmentPopoverView: some View {
FontSizeAdjustmentPopoverView(
increaseFontAction: { viewModel.webAppWrapperViewModel?.sendIncreaseFontSignal = true },
decreaseFontAction: { viewModel.webAppWrapperViewModel?.sendDecreaseFontSignal = true }
)
}
// We always want this hidden but setting it to false initially
// fixes a bug where SwiftUI searchable will always show the nav bar
// if the search field is active when pushing.
@State var hideNavBar = false
var body: some View {
#if os(iOS)
if viewModel.pdfItem != nil {
fixedNavBarReader
.navigationBarHidden(hideNavBar)
.task {
hideNavBar = true
viewModel.trackReadEvent()
}
} else if let item = viewModel.item {
WebReaderContainerView(item: item)
.navigationBarHidden(hideNavBar)
.task {
hideNavBar = true
viewModel.trackReadEvent()
}
}
#else
if viewModel.pdfItem != nil {
fixedNavBarReader
.task { viewModel.trackReadEvent() }
#endif
#if os(iOS)
.navigationBarHidden(hideNavBar)
#endif
.task {
hideNavBar = true
viewModel.trackReadEvent()
}
} else if let item = viewModel.item {
WebReaderContainerView(item: item)
#if os(iOS)
.navigationBarHidden(hideNavBar)
#endif
.task {
hideNavBar = true
viewModel.trackReadEvent()
}
}
}
var navBar: some View {
@ -249,61 +197,6 @@ struct LinkItemDetailView: View {
}
}
#if os(iOS)
@ViewBuilder private var hidingNavBarReader: some View {
if let webAppWrapperViewModel = viewModel.webAppWrapperViewModel {
ZStack {
WebAppWrapperView(
viewModel: webAppWrapperViewModel,
navBarVisibilityRatioUpdater: {
if $0 < 1 {
showFontSizePopover = false
}
navBarVisibilityRatio = $0
}
)
if showFontSizePopover {
VStack {
Color.clear
.contentShape(Rectangle())
.frame(height: LinkItemDetailView.navBarHeight)
HStack {
Spacer()
fontAdjustmentPopoverView
.background(Color.appButtonBackground)
.cornerRadius(8)
.padding(.trailing, 44)
}
Spacer()
}
.background(
Color.clear
.contentShape(Rectangle())
.onTapGesture {
showFontSizePopover = false
}
)
}
VStack(spacing: 0) {
navBar
Spacer()
}
}
} else {
VStack(spacing: 0) {
navBar
Spacer()
}
.task {
await viewModel.loadWebAppWrapper(
dataService: dataService,
rawAuthCookie: authenticator.omnivoreAuthCookieString
)
}
}
}
#endif
@ViewBuilder private var fixedNavBarReader: some View {
if let pdfItem = viewModel.pdfItem, let pdfURL = pdfItem.pdfURL {
#if os(iOS)
@ -312,39 +205,12 @@ struct LinkItemDetailView: View {
#elseif os(macOS)
PDFWrapperView(pdfURL: pdfURL)
#endif
} else if let webAppWrapperViewModel = viewModel.webAppWrapperViewModel {
WebAppWrapperView(viewModel: webAppWrapperViewModel)
.toolbar {
ToolbarItem(placement: .automatic) {
Button(
action: { showFontSizePopover = true },
label: {
Image(systemName: "textformat.size")
}
)
#if os(iOS)
.fittedPopover(isPresented: $showFontSizePopover) {
fontAdjustmentPopoverView
}
#else
.popover(isPresented: $showFontSizePopover) {
fontAdjustmentPopoverView
}
#endif
}
}
} else {
HStack(alignment: .center) {
Spacer()
Text("Loading...")
Spacer()
}
.task {
await viewModel.loadWebAppWrapper(
dataService: dataService,
rawAuthCookie: authenticator.omnivoreAuthCookieString
)
}
}
}
}

View File

@ -32,22 +32,6 @@ public final class RootViewModel: ObservableObject {
#endif
}
func webAppWrapperViewModel(webLinkPath: String) -> WebAppWrapperViewModel {
let baseURL = services.dataService.appEnvironment.webAppBaseURL
let urlRequest = URLRequest.webRequest(
baseURL: services.dataService.appEnvironment.webAppBaseURL,
urlPath: webLinkPath,
queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"]
)
return WebAppWrapperViewModel(
webViewURLRequest: urlRequest,
baseURL: baseURL,
rawAuthCookie: services.authenticator.omnivoreAuthCookieString
)
}
func triggerPushNotificationRequestIfNeeded() {
guard FeatureFlag.enablePushNotifications else { return }

View File

@ -4,167 +4,201 @@ import Utils
import Views
import WebKit
#if os(iOS)
struct WebReader: UIViewRepresentable {
let item: LinkedItem
let articleContent: ArticleContent
let openLinkAction: (URL) -> Void
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
struct WebReader: PlatformViewRepresentable {
let item: LinkedItem
let articleContent: ArticleContent
let openLinkAction: (URL) -> Void
let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
@Binding var updateFontFamilyActionID: UUID?
@Binding var updateFontActionID: UUID?
@Binding var updateTextContrastActionID: UUID?
@Binding var updateMaxWidthActionID: UUID?
@Binding var updateLineHeightActionID: UUID?
@Binding var annotationSaveTransactionID: UUID?
@Binding var showNavBarActionID: UUID?
@Binding var shareActionID: UUID?
@Binding var annotation: String
@Binding var updateFontFamilyActionID: UUID?
@Binding var updateFontActionID: UUID?
@Binding var updateTextContrastActionID: UUID?
@Binding var updateMaxWidthActionID: UUID?
@Binding var updateLineHeightActionID: UUID?
@Binding var annotationSaveTransactionID: UUID?
@Binding var showNavBarActionID: UUID?
@Binding var shareActionID: UUID?
@Binding var annotation: String
func makeCoordinator() -> WebReaderCoordinator {
WebReaderCoordinator()
}
func makeCoordinator() -> WebReaderCoordinator {
WebReaderCoordinator()
}
func fontSize() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue)
func fontSize() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue)
#if os(iOS)
return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize
}
#else
return storedSize <= 1 ? Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16) : storedSize
#endif
}
func lineHeight() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebLineSpacing.rawValue)
return storedSize <= 1 ? 150 : storedSize
}
func lineHeight() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebLineSpacing.rawValue)
return storedSize <= 1 ? 150 : storedSize
}
func maxWidthPercentage() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebMaxWidthPercentage.rawValue)
return storedSize <= 1 ? 100 : storedSize
}
func maxWidthPercentage() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebMaxWidthPercentage.rawValue)
return storedSize <= 1 ? 100 : storedSize
}
func makeUIView(context: Context) -> WKWebView {
let webView = WebViewManager.shared()
let contentController = WKUserContentController()
private func makePlatformView(context: Context) -> WKWebView {
let webView = WebViewManager.shared()
let contentController = WKUserContentController()
webView.navigationDelegate = context.coordinator
webView.navigationDelegate = context.coordinator
webView.configuration.userContentController = contentController
webView.configuration.userContentController.removeAllScriptMessageHandlers()
#if os(iOS)
webView.isOpaque = false
webView.backgroundColor = .clear
webView.configuration.userContentController = contentController
webView.scrollView.delegate = context.coordinator
webView.scrollView.contentInset.top = readerViewNavBarHeight
webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight
webView.configuration.userContentController.removeAllScriptMessageHandlers()
for action in WebViewAction.allCases {
webView.configuration.userContentController.add(context.coordinator, name: action.rawValue)
}
webView.configuration.userContentController.add(webView, name: "viewerAction")
#else
webView.setValue(false, forKey: "drawsBackground")
#endif
webView.configuration.userContentController.addScriptMessageHandler(
context.coordinator, contentWorld: .page, name: "articleAction"
)
for action in WebViewAction.allCases {
webView.configuration.userContentController.add(context.coordinator, name: action.rawValue)
}
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
webView.configuration.userContentController.addScriptMessageHandler(
context.coordinator, contentWorld: .page, name: "articleAction"
)
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
loadContent(webView: webView)
return webView
}
// swiftlint:disable:next cyclomatic_complexity
private func updatePlatformView(_ webView: WKWebView, context: Context) {
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
(webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
}
if updateFontFamilyActionID != context.coordinator.previousUpdateFontFamilyActionID {
context.coordinator.previousUpdateFontFamilyActionID = updateFontFamilyActionID
(webView as? OmnivoreWebView)?.updateFontFamily()
}
if updateFontActionID != context.coordinator.previousUpdateFontActionID {
context.coordinator.previousUpdateFontActionID = updateFontActionID
(webView as? OmnivoreWebView)?.updateFontSize()
}
if updateTextContrastActionID != context.coordinator.previousUpdateTextContrastActionID {
context.coordinator.previousUpdateTextContrastActionID = updateTextContrastActionID
(webView as? OmnivoreWebView)?.updateTextContrast()
}
if updateMaxWidthActionID != context.coordinator.previousUpdateMaxWidthActionID {
context.coordinator.previousUpdateMaxWidthActionID = updateMaxWidthActionID
(webView as? OmnivoreWebView)?.updateMaxWidthPercentage()
}
if updateLineHeightActionID != context.coordinator.previousUpdateLineHeightActionID {
context.coordinator.previousUpdateLineHeightActionID = updateLineHeightActionID
(webView as? OmnivoreWebView)?.updateLineHeight()
}
if showNavBarActionID != context.coordinator.previousShowNavBarActionID {
context.coordinator.previousShowNavBarActionID = showNavBarActionID
context.coordinator.showNavBar()
}
if shareActionID != context.coordinator.previousShareActionID {
context.coordinator.previousShareActionID = shareActionID
(webView as? OmnivoreWebView)?.shareOriginalItem()
}
// If the webview had been terminated `needsReload` will have been set to true
if context.coordinator.needsReload {
loadContent(webView: webView)
return webView
context.coordinator.needsReload = false
return
}
if webView.isLoading { return }
// If the root element is not detected then `WKWebView` may have unloaded the content
// so we need to load it again.
webView.evaluateJavaScript("document.getElementById('root') ? true : false") { hasRootElement, _ in
guard let hasRootElement = hasRootElement as? Bool else { return }
if !hasRootElement {
DispatchQueue.main.async {
loadContent(webView: webView)
}
}
}
}
private func loadContent(webView: WKWebView) {
let fontFamilyValue = UserDefaults.standard.string(forKey: UserDefaultKey.preferredWebFont.rawValue)
let prefersHighContrastText: Bool = {
let key = UserDefaultKey.prefersHighContrastWebFont.rawValue
if UserDefaults.standard.object(forKey: key) != nil {
return UserDefaults.standard.bool(forKey: key)
} else {
return true
}
}()
let fontFamily = fontFamilyValue.flatMap { WebFont(rawValue: $0) } ?? .system
let htmlString = WebReaderContent(
item: item,
articleContent: articleContent,
isDark: isDarkMode,
fontSize: fontSize(),
lineHeight: lineHeight(),
maxWidthPercentage: maxWidthPercentage(),
fontFamily: fontFamily,
prefersHighContrastText: prefersHighContrastText
)
.styledContent
webView.loadHTMLString(htmlString, baseURL: ViewsPackage.resourceURL)
}
var isDarkMode: Bool {
#if os(iOS)
UITraitCollection.current.userInterfaceStyle == .dark
#else
NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua
#endif
}
}
#if os(iOS)
extension WebReader {
func makeUIView(context: Context) -> WKWebView {
makePlatformView(context: context)
}
// swiftlint:disable:next cyclomatic_complexity
func updateUIView(_ webView: WKWebView, context: Context) {
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
(webView as? WebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
}
if updateFontFamilyActionID != context.coordinator.previousUpdateFontFamilyActionID {
context.coordinator.previousUpdateFontFamilyActionID = updateFontFamilyActionID
(webView as? WebView)?.updateFontFamily()
}
if updateFontActionID != context.coordinator.previousUpdateFontActionID {
context.coordinator.previousUpdateFontActionID = updateFontActionID
(webView as? WebView)?.updateFontSize()
}
if updateTextContrastActionID != context.coordinator.previousUpdateTextContrastActionID {
context.coordinator.previousUpdateTextContrastActionID = updateTextContrastActionID
(webView as? WebView)?.updateTextContrast()
}
if updateMaxWidthActionID != context.coordinator.previousUpdateMaxWidthActionID {
context.coordinator.previousUpdateMaxWidthActionID = updateMaxWidthActionID
(webView as? WebView)?.updateMaxWidthPercentage()
}
if updateLineHeightActionID != context.coordinator.previousUpdateLineHeightActionID {
context.coordinator.previousUpdateLineHeightActionID = updateLineHeightActionID
(webView as? WebView)?.updateLineHeight()
}
if showNavBarActionID != context.coordinator.previousShowNavBarActionID {
context.coordinator.previousShowNavBarActionID = showNavBarActionID
context.coordinator.showNavBar()
}
if shareActionID != context.coordinator.previousShareActionID {
context.coordinator.previousShareActionID = shareActionID
(webView as? WebView)?.shareOriginalItem()
}
// If the webview had been terminated `needsReload` will have been set to true
if context.coordinator.needsReload {
loadContent(webView: webView)
context.coordinator.needsReload = false
return
}
if webView.isLoading { return }
// If the root element is not detected then `WKWebView` may have unloaded the content
// so we need to load it again.
webView.evaluateJavaScript("document.getElementById('root') ? true : false") { hasRootElement, _ in
guard let hasRootElement = hasRootElement as? Bool else { return }
if !hasRootElement {
DispatchQueue.main.async {
loadContent(webView: webView)
}
}
}
updatePlatformView(webView, context: context)
}
}
#else
extension WebReader {
func makeNSView(context: Context) -> WKWebView {
makePlatformView(context: context)
}
func loadContent(webView: WKWebView) {
let fontFamilyValue = UserDefaults.standard.string(forKey: UserDefaultKey.preferredWebFont.rawValue)
let prefersHighContrastText: Bool = {
let key = UserDefaultKey.prefersHighContrastWebFont.rawValue
if UserDefaults.standard.object(forKey: key) != nil {
return UserDefaults.standard.bool(forKey: key)
} else {
return true
}
}()
let fontFamily = fontFamilyValue.flatMap { WebFont(rawValue: $0) } ?? .inter
webView.loadHTMLString(
WebReaderContent(
item: item,
articleContent: articleContent,
isDark: UITraitCollection.current.userInterfaceStyle == .dark,
fontSize: fontSize(),
lineHeight: lineHeight(),
maxWidthPercentage: maxWidthPercentage(),
fontFamily: fontFamily,
prefersHighContrastText: prefersHighContrastText
)
.styledContent,
baseURL: ViewsPackage.bundleURL
)
func updateNSView(_ webView: WKWebView, context: Context) {
updatePlatformView(webView, context: context)
}
}
#endif

View File

@ -4,62 +4,62 @@ import SwiftUI
import Views
import WebKit
#if os(iOS)
struct WebReaderContainerView: View {
let item: LinkedItem
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 updateFontFamilyActionID: UUID?
@State var updateFontActionID: UUID?
@State var updateTextContrastActionID: UUID?
@State var updateMaxWidthActionID: UUID?
@State var updateLineHeightActionID: UUID?
@State var annotationSaveTransactionID: UUID?
@State var showNavBarActionID: UUID?
@State var shareActionID: UUID?
@State var annotation = String()
@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 updateFontFamilyActionID: UUID?
@State var updateFontActionID: UUID?
@State var updateTextContrastActionID: UUID?
@State var updateMaxWidthActionID: UUID?
@State var updateLineHeightActionID: UUID?
@State var annotationSaveTransactionID: UUID?
@State var showNavBarActionID: UUID?
@State var shareActionID: UUID?
@State var annotation = String()
@EnvironmentObject var dataService: DataService
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@StateObject var viewModel = WebReaderViewModel()
@EnvironmentObject var dataService: DataService
@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)
}
func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) {
if let replyHandler = replyHandler {
viewModel.webViewActionWithReplyHandler(
message: message,
replyHandler: replyHandler,
dataService: dataService
)
return
}
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
}
if message.name == WebViewAction.highlightAction.rawValue {
handleHighlightAction(message: message)
}
}
var navBar: some View {
HStack(alignment: .center) {
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: {
@ -71,156 +71,187 @@ import WebKit
)
.scaleEffect(navBarVisibilityRatio)
Spacer()
Button(
action: { showPreferencesPopover.toggle() },
label: {
Image(systemName: "textformat.size")
.font(.appTitleTwo)
}
)
.padding(.horizontal)
.scaleEffect(navBarVisibilityRatio)
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)
#endif
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()
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: {
#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)
}
)
}
.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)
presentationMode.wrappedValue.dismiss()
#else
Text("Options")
#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)
.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
}
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
},
updateFontFamilyActionID: $updateFontFamilyActionID,
updateFontActionID: $updateFontActionID,
updateTextContrastActionID: $updateTextContrastActionID,
updateMaxWidthActionID: $updateMaxWidthActionID,
updateLineHeightActionID: $updateLineHeightActionID,
annotationSaveTransactionID: $annotationSaveTransactionID,
showNavBarActionID: $showNavBarActionID,
shareActionID: $shareActionID,
annotation: $annotation
)
.onTapGesture {
withAnimation {
navBarVisibilityRatio = 1
showNavBarActionID = UUID()
}
var webPreferencesPopoverView: some View {
WebPreferencesPopoverView(
updateFontFamilyAction: { updateFontFamilyActionID = UUID() },
updateFontAction: { updateFontActionID = UUID() },
updateTextContrastAction: { updateTextContrastActionID = UUID() },
updateMaxWidthAction: { updateMaxWidthActionID = UUID() },
updateLineHeightAction: { updateLineHeightActionID = UUID() },
dismissAction: { showPreferencesPopover = false }
)
}
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
},
updateFontFamilyActionID: $updateFontFamilyActionID,
updateFontActionID: $updateFontActionID,
updateTextContrastActionID: $updateTextContrastActionID,
updateMaxWidthActionID: $updateMaxWidthActionID,
updateLineHeightActionID: $updateLineHeightActionID,
annotationSaveTransactionID: $annotationSaveTransactionID,
showNavBarActionID: $showNavBarActionID,
shareActionID: $shareActionID,
annotation: $annotation
)
.onTapGesture {
withAnimation {
navBarVisibilityRatio = 1
showNavBarActionID = UUID()
}
}
#if os(iOS)
.fullScreenCover(item: $safariWebLink) {
SafariView(url: $0.url)
}
.sheet(isPresented: $showHighlightAnnotationModal) {
HighlightAnnotationSheet(
annotation: $annotation,
onSave: {
annotationSaveTransactionID = UUID()
showHighlightAnnotationModal = false
},
onCancel: {
showHighlightAnnotationModal = false
}
)
#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)
}
} 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)
}
}
VStack(spacing: 0) {
navBar
Spacer()
}
}
.formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) {
WebPreferencesPopoverView(
updateFontFamilyAction: { updateFontFamilyActionID = UUID() },
updateFontAction: { updateFontActionID = UUID() },
updateTextContrastAction: { updateTextContrastActionID = UUID() },
updateMaxWidthAction: { updateMaxWidthActionID = UUID() },
updateLineHeightAction: { updateLineHeightActionID = UUID() },
dismissAction: { showPreferencesPopover = false }
)
}
.onDisappear {
// Clear the shared webview content when exiting
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
VStack(spacing: 0) {
navBar
Spacer()
}
}
#if os(iOS)
.formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) {
webPreferencesPopoverView
}
#else
.sheet(isPresented: $showPreferencesPopover) {
webPreferencesPopoverView
.frame(minWidth: 400, minHeight: 400)
}
#endif
.onDisappear {
// Clear the shared webview content when exiting
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
}
}
#endif
}

View File

@ -51,6 +51,7 @@ struct WebReaderContent {
</head>
<body>
<div id="root" />
<div>HIIIIII</div>
<div id='_omnivore-htmlContent' style="display: none;">
\(articleContent.htmlContent)
</div>
@ -86,6 +87,7 @@ struct WebReaderContent {
window.lineHeight = \(lineHeight)
window.localStorage.setItem("theme", "\(themeKey)")
window.prefersHighContrastFont = \(prefersHighContrastText)
window.enableHighlightBar = \(isMacApp)
</script>
<script src="bundle.js"></script>
<script src="mathJaxConfiguration.js" id="MathJax-script"></script>

View File

@ -3,57 +3,64 @@ import Models
import Services
import SwiftUI
import Utils
import Views
#if os(iOS)
@MainActor final class WebReaderLoadingContainerViewModel: ObservableObject {
@Published var item: LinkedItem?
@Published var errorMessage: String?
@MainActor final class WebReaderLoadingContainerViewModel: ObservableObject {
@Published var item: LinkedItem?
@Published var errorMessage: String?
func loadItem(dataService: DataService, requestID: String) async {
guard let objectID = try? await dataService.loadItemContentUsingRequestID(requestID: requestID) else { return }
item = dataService.viewContext.object(with: objectID) as? LinkedItem
}
func trackReadEvent() {
guard let item = item else { return }
EventTracker.track(
.linkRead(
linkID: item.unwrappedID,
slug: item.unwrappedSlug,
originalArticleURL: item.unwrappedPageURLString
)
)
}
func loadItem(dataService: DataService, requestID: String) async {
guard let objectID = try? await dataService.loadItemContentUsingRequestID(requestID: requestID) else { return }
item = dataService.viewContext.object(with: objectID) as? LinkedItem
}
public struct WebReaderLoadingContainer: View {
let requestID: String
func trackReadEvent() {
guard let item = item else { return }
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = WebReaderLoadingContainerViewModel()
EventTracker.track(
.linkRead(
linkID: item.unwrappedID,
slug: item.unwrappedSlug,
originalArticleURL: item.unwrappedPageURLString
)
)
}
}
public var body: some View {
if let item = viewModel.item {
if let pdfItem = PDFItem.make(item: item) {
public struct WebReaderLoadingContainer: View {
let requestID: String
@EnvironmentObject var dataService: DataService
@StateObject var viewModel = WebReaderLoadingContainerViewModel()
public var body: some View {
if let item = viewModel.item {
if let pdfItem = PDFItem.make(item: item) {
#if os(iOS)
PDFViewer(viewModel: PDFViewerViewModel(pdfItem: pdfItem))
.navigationBarHidden(true)
.navigationViewStyle(.stack)
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }
} else {
WebReaderContainerView(item: item)
.navigationBarHidden(true)
.navigationViewStyle(.stack)
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }
}
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
#else
if let pdfURL = pdfItem.pdfURL {
PDFWrapperView(pdfURL: pdfURL)
}
#endif
} else {
ProgressView()
.task { await viewModel.loadItem(dataService: dataService, requestID: requestID) }
WebReaderContainerView(item: item)
#if os(iOS)
.navigationBarHidden(true)
.navigationViewStyle(.stack)
#endif
.accentColor(.appGrayTextContrast)
.task { viewModel.trackReadEvent() }
}
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
} else {
ProgressView()
.task { await viewModel.loadItem(dataService: dataService, requestID: requestID) }
}
}
#endif
}

View File

@ -3,6 +3,7 @@ import SwiftUI
#if os(iOS)
import UIKit
public typealias PlatformViewController = UIViewController
public typealias PlatformViewRepresentable = UIViewRepresentable
public typealias PlatformHostingController = UIHostingController
let osVersion = UIDevice.current.systemVersion
public let userAgent = "ios-\(osVersion)"
@ -10,6 +11,7 @@ import SwiftUI
import AppKit
public typealias PlatformViewController = NSViewController
public typealias PlatformHostingController = NSHostingController
public typealias PlatformViewRepresentable = NSViewRepresentable
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
public let userAgent = "macos-\(osVersion)"

View File

@ -8,7 +8,7 @@ public enum WebViewAction: String, CaseIterable {
case readingProgressUpdate
}
public final class WebView: WKWebView {
public final class OmnivoreWebView: WKWebView {
#if os(iOS)
private var panGestureRecognizer: UIPanGestureRecognizer?
private var tapGestureRecognizer: UITapGestureRecognizer?
@ -95,7 +95,7 @@ public final class WebView: WKWebView {
}
#if os(iOS)
extension WebView: UIGestureRecognizerDelegate, WKScriptMessageHandler {
extension OmnivoreWebView: UIGestureRecognizerDelegate, WKScriptMessageHandler {
func initNativeIOSMenus() {
isUserInteractionEnabled = true

View File

@ -0,0 +1,35 @@
import SafariServices
import SwiftUI
import WebKit
#if os(iOS)
public struct SafariView: UIViewControllerRepresentable {
let url: URL
public init(url: URL) {
self.url = url
}
public func makeUIViewController(
context _: UIViewControllerRepresentableContext<SafariView>
) -> SFSafariViewController {
SFSafariViewController(url: url)
}
// swiftlint:disable:next line_length
public func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
}
#elseif os(macOS)
public struct SafariView: View {
let url: URL
public init(url: URL) {
self.url = url
}
public var body: some View {
Color.clear
}
}
#endif

View File

@ -1,187 +0,0 @@
import Models
import SwiftUI
import Utils
import WebKit
public let readerViewNavBarHeight = 50.0
enum WebViewConfigurationManager {
private static let processPool = WKProcessPool()
static func create() -> WKWebViewConfiguration {
let config = WKWebViewConfiguration()
config.processPool = processPool
#if os(iOS)
config.allowsInlineMediaPlayback = true
#endif
config.mediaTypesRequiringUserActionForPlayback = .audio
return config
}
}
public enum WebViewManager {
public static let sharedView = create()
public static func shared() -> WebView {
sharedView
}
public static func create() -> WebView {
WebView(frame: CGRect.zero, configuration: WebViewConfigurationManager.create())
}
}
#if os(iOS)
struct WebAppView: UIViewRepresentable {
let request: URLRequest
let baseURL: URL
let rawAuthCookie: String?
let openLinkAction: (URL) -> Void
let webViewActionHandler: (WKScriptMessage) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
@Binding var annotation: String
@Binding var annotationSaveTransactionID: UUID?
@Binding var sendIncreaseFontSignal: Bool
@Binding var sendDecreaseFontSignal: Bool
func makeCoordinator() -> WebAppViewCoordinator {
WebAppViewCoordinator()
}
func fontSize() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: "preferredWebFontSize")
return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize
}
func makeUIView(context: Context) -> WKWebView {
let webView = WebViewManager.create()
let contentController = WKUserContentController()
webView.navigationDelegate = context.coordinator
webView.isOpaque = false
webView.backgroundColor = .clear
webView.configuration.userContentController = contentController
webView.scrollView.delegate = context.coordinator
webView.scrollView.contentInset.top = readerViewNavBarHeight
webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight
for action in WebViewAction.allCases {
webView.configuration.userContentController.add(context.coordinator, name: action.rawValue)
}
webView.configuration.userContentController.add(webView, name: "viewerAction")
webView.configureForOmnivoreAppEmbed(
config: WebViewConfig(
url: baseURL,
themeId: UITraitCollection.current.userInterfaceStyle == .dark ? "Gray" : "LightGray",
margin: 0,
fontSize: fontSize(),
fontFamily: "inter",
rawAuthCookie: rawAuthCookie
)
)
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
if context.coordinator.needsReload {
webView.load(request)
context.coordinator.needsReload = false
}
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
(webView as? WebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
}
if sendIncreaseFontSignal {
sendIncreaseFontSignal = false
(webView as? WebView)?.updateFontSize()
}
if sendDecreaseFontSignal {
sendDecreaseFontSignal = false
(webView as? WebView)?.updateFontSize()
}
}
}
#endif
#if os(macOS)
struct WebAppView: NSViewRepresentable {
let request: URLRequest
let baseURL: URL
let rawAuthCookie: String?
let openLinkAction: (URL) -> Void
let webViewActionHandler: (WKScriptMessage) -> Void
let navBarVisibilityRatioUpdater: (Double) -> Void
@Binding var annotation: String
@Binding var annotationSaveTransactionID: UUID?
@Binding var sendIncreaseFontSignal: Bool
@Binding var sendDecreaseFontSignal: Bool
func makeCoordinator() -> WebAppViewCoordinator {
WebAppViewCoordinator()
}
func fontSize() -> Int {
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue)
return storedSize <= 1 ? Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16) : storedSize
}
func makeNSView(context: Context) -> WKWebView {
let contentController = WKUserContentController()
let webView = WebView(frame: CGRect.zero)
webView.navigationDelegate = context.coordinator
webView.configuration.userContentController = contentController
webView.setValue(false, forKey: "drawsBackground")
for action in WebViewAction.allCases {
webView.configuration.userContentController.add(context.coordinator, name: action.rawValue)
}
webView.configureForOmnivoreAppEmbed(
config: WebViewConfig(
url: baseURL,
themeId: NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua ? "Gray" : "LightGray",
margin: 0,
fontSize: fontSize(),
fontFamily: "inter",
rawAuthCookie: rawAuthCookie
)
)
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
return webView
}
func updateNSView(_ webView: WKWebView, context: Context) {
if context.coordinator.needsReload {
webView.load(request)
context.coordinator.needsReload = false
}
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
(webView as? WebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
}
if sendIncreaseFontSignal {
sendIncreaseFontSignal = false
(webView as? WebView)?.updateFontSize()
}
if sendDecreaseFontSignal {
sendDecreaseFontSignal = false
(webView as? WebView)?.updateFontSize()
}
}
}
#endif

View File

@ -1,153 +0,0 @@
import SwiftUI
import WebKit
final class WebAppViewCoordinator: NSObject {
var webViewActionHandler: (WKScriptMessage) -> Void = { _ in }
var linkHandler: (URL) -> Void = { _ in }
var needsReload = true
var lastSavedAnnotationID: UUID?
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in }
private var yOffsetAtStartOfDrag: Double?
private var lastYOffset: Double = 0
private var hasDragged = false
private var isNavBarHidden = false
override init() {
super.init()
}
var navBarVisibilityRatio: Double = 1.0 {
didSet {
isNavBarHidden = navBarVisibilityRatio == 0
updateNavBarVisibilityRatio(navBarVisibilityRatio)
}
}
}
extension WebAppViewCoordinator: WKScriptMessageHandler {
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
webViewActionHandler(message)
}
}
extension WebAppViewCoordinator: WKNavigationDelegate {
// swiftlint:disable:next line_length
func webView(_: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated {
if let linkURL = navigationAction.request.url {
linkHandler(linkURL)
}
decisionHandler(.cancel)
} else {
decisionHandler(.allow)
}
}
func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
#if os(iOS)
webView.isOpaque = true
webView.backgroundColor = .systemBackground
#endif
}
}
#if os(iOS)
extension WebAppViewCoordinator: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
hasDragged = true
yOffsetAtStartOfDrag = scrollView.contentOffset.y + scrollView.contentInset.top
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard hasDragged else { return }
let yOffset = scrollView.contentOffset.y
if yOffset == 0 {
scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1
return
}
if yOffset < 0 {
navBarVisibilityRatio = 1
scrollView.contentInset.top = readerViewNavBarHeight
return
}
if yOffset < readerViewNavBarHeight {
let isScrollingUp = yOffsetAtStartOfDrag ?? 0 > yOffset
navBarVisibilityRatio = isScrollingUp || yOffset < 0 ? 1 : min(1, 1 - (yOffset / readerViewNavBarHeight))
scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight
return
}
guard let yOffsetAtStartOfDrag = yOffsetAtStartOfDrag else { return }
if yOffset > yOffsetAtStartOfDrag, !isNavBarHidden {
let translation = yOffset - yOffsetAtStartOfDrag
let ratio = translation < readerViewNavBarHeight ? 1 - (translation / readerViewNavBarHeight) : 0
navBarVisibilityRatio = min(ratio, 1)
scrollView.contentInset.top = navBarVisibilityRatio * readerViewNavBarHeight
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate, scrollView.contentOffset.y + scrollView.contentInset.top < (yOffsetAtStartOfDrag ?? 0) {
scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1
}
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
scrollView.contentInset.top = readerViewNavBarHeight
navBarVisibilityRatio = 1
return false
}
}
#endif
struct WebViewConfig {
let url: URL
let themeId: String
let margin: Int
let fontSize: Int
let fontFamily: String
let rawAuthCookie: String?
}
extension WKWebView {
func configureForOmnivoreAppEmbed(config: WebViewConfig) {
// Set cookies to pass article preferences to web view
injectCookie(cookieString: "theme=\(config.themeId); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "margin=\(config.margin); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "fontSize=\(config.fontSize); Max-Age=31536000;", url: config.url)
injectCookie(cookieString: "fontFamily=\(config.fontFamily); Max-Age=31536000;", url: config.url)
let authToken = extractAuthToken(rawAuthCookie: config.rawAuthCookie, url: config.url)
if let authToken = authToken {
injectCookie(cookieString: "authToken=\(authToken); Max-Age=31536000;", url: config.url)
} else {
injectCookie(cookieString: config.rawAuthCookie, url: config.url)
}
}
func injectCookie(cookieString: String?, url: URL) {
if let cookieString = cookieString {
for cookie in HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": cookieString], for: url) {
configuration.websiteDataStore.httpCookieStore.setCookie(cookie) {}
}
}
}
}
// Temp utility until we swap out the web build used in prod.
// Once we do that we can just pass in the authToken directly
// and remove the rawAuthCookie param.
// This util just makes it easier to feature flag the web build used
private func extractAuthToken(rawAuthCookie: String?, url: URL) -> String? {
guard let rawAuthCookie = rawAuthCookie else { return nil }
let cookies = HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": rawAuthCookie], for: url)
return cookies.first?.value
}

View File

@ -1,127 +0,0 @@
import SafariServices
import SwiftUI
import WebKit
public final class WebAppWrapperViewModel: ObservableObject {
public enum Action {
case shareHighlight(highlightID: String)
}
let webViewURLRequest: URLRequest
let baseURL: URL
let rawAuthCookie: String?
@Published public var sendIncreaseFontSignal: Bool = false
@Published public var sendDecreaseFontSignal: Bool = false
public init(webViewURLRequest: URLRequest, baseURL: URL, rawAuthCookie: String?) {
self.webViewURLRequest = webViewURLRequest
self.rawAuthCookie = rawAuthCookie
self.baseURL = baseURL
}
}
public struct WebAppWrapperView: View {
struct SafariWebLink: Identifiable {
let id: UUID
let url: URL
}
@ObservedObject private var viewModel: WebAppWrapperViewModel
@State var showHighlightAnnotationModal = false
@State private var annotation = String()
@State var annotationSaveTransactionID: UUID?
@State var safariWebLink: SafariWebLink?
let navBarVisibilityRatioUpdater: (Double) -> Void
public init(viewModel: WebAppWrapperViewModel, navBarVisibilityRatioUpdater: ((Double) -> Void)? = nil) {
self.viewModel = viewModel
self.navBarVisibilityRatioUpdater = navBarVisibilityRatioUpdater ?? { _ in }
}
public var body: some View {
VStack {
WebAppView(
request: viewModel.webViewURLRequest,
baseURL: viewModel.baseURL,
rawAuthCookie: viewModel.rawAuthCookie,
openLinkAction: {
#if os(macOS)
NSWorkspace.shared.open($0)
#elseif os(iOS)
safariWebLink = SafariWebLink(id: UUID(), url: $0)
#endif
},
webViewActionHandler: webViewActionHandler,
navBarVisibilityRatioUpdater: navBarVisibilityRatioUpdater,
annotation: $annotation,
annotationSaveTransactionID: $annotationSaveTransactionID,
sendIncreaseFontSignal: $viewModel.sendIncreaseFontSignal,
sendDecreaseFontSignal: $viewModel.sendDecreaseFontSignal
)
}
.sheet(item: $safariWebLink) {
SafariView(url: $0.url)
}
.sheet(isPresented: $showHighlightAnnotationModal) {
HighlightAnnotationSheet(
annotation: $annotation,
onSave: {
annotationSaveTransactionID = UUID()
showHighlightAnnotationModal = false
},
onCancel: {
showHighlightAnnotationModal = false
}
)
}
}
func webViewActionHandler(message: WKScriptMessage) {
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 }
if actionID == "annotate" {
annotation = messageBody["annotation"] ?? ""
showHighlightAnnotationModal = true
}
}
}
#if os(iOS)
public struct SafariView: UIViewControllerRepresentable {
let url: URL
public init(url: URL) {
self.url = url
}
public func makeUIViewController(
context _: UIViewControllerRepresentableContext<SafariView>
) -> SFSafariViewController {
SFSafariViewController(url: url)
}
// swiftlint:disable:next line_length
public func updateUIViewController(_: SFSafariViewController, context _: UIViewControllerRepresentableContext<SafariView>) {}
}
#elseif os(macOS)
public struct SafariView: View {
let url: URL
public init(url: URL) {
self.url = url
}
public var body: some View {
Color.clear
}
}
#endif

View File

@ -0,0 +1,30 @@
import Models
import SwiftUI
import Utils
import WebKit
public let readerViewNavBarHeight = 50.0
enum WebViewConfigurationManager {
private static let processPool = WKProcessPool()
static func create() -> WKWebViewConfiguration {
let config = WKWebViewConfiguration()
config.processPool = processPool
#if os(iOS)
config.allowsInlineMediaPlayback = true
#endif
config.mediaTypesRequiringUserActionForPlayback = .audio
return config
}
}
public enum WebViewManager {
public static let sharedView = create()
public static func shared() -> OmnivoreWebView {
sharedView
}
public static func create() -> OmnivoreWebView {
OmnivoreWebView(frame: CGRect.zero, configuration: WebViewConfigurationManager.create())
}
}

View File

@ -5,4 +5,8 @@ public enum ViewsPackage {
public static var bundleURL: URL {
Bundle.module.bundleURL
}
public static var resourceURL: URL {
Bundle.module.resourceURL ?? bundleURL
}
}

File diff suppressed because one or more lines are too long

View File

@ -47,7 +47,7 @@ import WebKit
}
public func makeNSView(context _: Context) -> WKWebView {
let webView = WebView(frame: CGRect.zero)
let webView = OmnivoreWebView(frame: CGRect.zero)
if let url = request.url {
// Dark mode is still rendering a white background on mac for some reason.
// Forcing light mode for now until we figure out a fix
@ -73,3 +73,13 @@ public final class BasicWebAppViewCoordinator: NSObject {
super.init()
}
}
extension WKWebView {
func injectCookie(cookieString: String?, url: URL) {
if let cookieString = cookieString {
for cookie in HTTPCookie.cookies(withResponseHeaderFields: ["Set-Cookie": cookieString], for: url) {
configuration.websiteDataStore.httpCookieStore.setCookie(cookie) {}
}
}
}
}

View File

@ -37,7 +37,7 @@ const App = () => {
article={window.omnivoreArticle}
labels={window.omnivoreArticle.labels}
isAppleAppEmbed={true}
highlightBarDisabled={true}
highlightBarDisabled={!window.enableHighlightBar}
highlightsBaseURL="https://example.com"
fontSize={window.fontSize ?? 18}
fontFamily={window.fontFamily ?? 'inter'}