mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #269 from omnivore-app/feature/apple-local-webview
Apple local webview
This commit is contained in:
commit
a2a0b1f3de
23 changed files with 673 additions and 22 deletions
4
Makefile
4
Makefile
|
|
@ -6,3 +6,7 @@ apple_graphql_gen:
|
|||
|
||||
apple_extension_gen:
|
||||
$(MAKE) -C apple extension_gen
|
||||
|
||||
apple_webview_gen:
|
||||
yarn workspace @omnivore/appreader build
|
||||
cp packages/appreader/build/bundle.js apple/OmnivoreKit/Sources/Views/Resources/bundle.js
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ struct LinkItemDetailView: View {
|
|||
#if os(iOS)
|
||||
if viewModel.item.isPDF {
|
||||
fixedNavBarReader
|
||||
} else if FeatureFlag.useLocalWebView {
|
||||
WebReaderContainerView(item: viewModel.item)
|
||||
} else {
|
||||
hidingNavBarReader
|
||||
}
|
||||
|
|
|
|||
335
apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift
Normal file
335
apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Utils
|
||||
import Views
|
||||
import WebKit
|
||||
|
||||
struct SafariWebLink: Identifiable {
|
||||
let id: UUID
|
||||
let url: URL
|
||||
}
|
||||
|
||||
final class WebReaderViewModel: ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var htmlContent: String?
|
||||
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
func loadContent(dataService: DataService, slug: String) {
|
||||
isLoading = true
|
||||
|
||||
guard let viewer = dataService.currentViewer else { return }
|
||||
|
||||
dataService.articleContentPublisher(username: viewer.username, slug: slug).sink(
|
||||
receiveCompletion: { [weak self] completion in
|
||||
guard case .failure = completion else { return }
|
||||
self?.isLoading = false
|
||||
},
|
||||
receiveValue: { [weak self] htmlContent in
|
||||
self?.htmlContent = htmlContent
|
||||
}
|
||||
)
|
||||
.store(in: &subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
struct WebReaderContainerView: View {
|
||||
let item: FeedItem
|
||||
|
||||
@State private var showFontSizePopover = false
|
||||
@State var showHighlightAnnotationModal = false
|
||||
@State var safariWebLink: SafariWebLink?
|
||||
@State private var navBarVisibilityRatio = 1.0
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var showOverlay = true
|
||||
@State var increaseFontActionID: UUID?
|
||||
@State var decreaseFontActionID: UUID?
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var authenticator: Authenticator
|
||||
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||
@StateObject var viewModel = WebReaderViewModel()
|
||||
|
||||
var fontAdjustmentPopoverView: some View {
|
||||
FontSizeAdjustmentPopoverView(
|
||||
increaseFontAction: { increaseFontActionID = UUID() },
|
||||
decreaseFontAction: { decreaseFontActionID = UUID() }
|
||||
)
|
||||
}
|
||||
|
||||
var navBariOS14: some View {
|
||||
HStack(alignment: .center) {
|
||||
Button(
|
||||
action: { self.presentationMode.wrappedValue.dismiss() },
|
||||
label: {
|
||||
Image(systemName: "chevron.backward")
|
||||
.font(.appTitleTwo)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
)
|
||||
.scaleEffect(navBarVisibilityRatio)
|
||||
Spacer()
|
||||
Button(
|
||||
action: { showFontSizePopover.toggle() },
|
||||
label: {
|
||||
Image(systemName: "textformat.size")
|
||||
.font(.appTitleTwo)
|
||||
}
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.scaleEffect(navBarVisibilityRatio)
|
||||
}
|
||||
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
|
||||
.opacity(navBarVisibilityRatio)
|
||||
.background(Color.systemBackground)
|
||||
.onTapGesture {
|
||||
showFontSizePopover = false
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 12.0, *)
|
||||
@available(iOS 15.0, *)
|
||||
var navBar: some View {
|
||||
HStack(alignment: .center) {
|
||||
Button(
|
||||
action: { self.presentationMode.wrappedValue.dismiss() },
|
||||
label: {
|
||||
Image(systemName: "chevron.backward")
|
||||
.font(.appTitleTwo)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
)
|
||||
.scaleEffect(navBarVisibilityRatio)
|
||||
Spacer()
|
||||
Button(
|
||||
action: { showFontSizePopover.toggle() },
|
||||
label: {
|
||||
Image(systemName: "textformat.size")
|
||||
.font(.appTitleTwo)
|
||||
}
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.scaleEffect(navBarVisibilityRatio)
|
||||
Menu(
|
||||
content: {
|
||||
Group {
|
||||
Button(
|
||||
action: {}, // ,viewModel.handleArchiveAction(dataService: dataService) },
|
||||
label: {
|
||||
Label(
|
||||
item.isArchived ? "Unarchive" : "Archive",
|
||||
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
|
||||
)
|
||||
}
|
||||
)
|
||||
Button(
|
||||
action: { showDeleteConfirmation = true },
|
||||
label: { Label("Delete", systemImage: "trash") }
|
||||
)
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Image.profile
|
||||
.padding(.horizontal)
|
||||
.scaleEffect(navBarVisibilityRatio)
|
||||
}
|
||||
)
|
||||
}
|
||||
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
|
||||
.opacity(navBarVisibilityRatio)
|
||||
.background(Color.systemBackground)
|
||||
.onTapGesture {
|
||||
showFontSizePopover = false
|
||||
}
|
||||
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
|
||||
Button("Remove Link", role: .destructive) {
|
||||
// viewModel.handleDeleteAction(dataService: dataService)
|
||||
}
|
||||
Button("Cancel", role: .cancel, action: {})
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if let htmlContent = viewModel.htmlContent {
|
||||
WebReader(
|
||||
htmlContent: htmlContent,
|
||||
item: item,
|
||||
openLinkAction: { url in print(url) },
|
||||
webViewActionHandler: { _ in },
|
||||
navBarVisibilityRatioUpdater: {
|
||||
if $0 < 1 {
|
||||
showFontSizePopover = false
|
||||
}
|
||||
navBarVisibilityRatio = $0
|
||||
},
|
||||
authToken: authenticator.authToken ?? "",
|
||||
appEnv: dataService.appEnvironment,
|
||||
increaseFontActionID: $increaseFontActionID,
|
||||
decreaseFontActionID: $decreaseFontActionID,
|
||||
annotationSaveTransactionID: nil
|
||||
)
|
||||
.overlay(
|
||||
Group {
|
||||
if showOverlay {
|
||||
Color.systemBackground
|
||||
.transition(.opacity)
|
||||
.onAppear {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
||||
withAnimation(.linear(duration: 0.2)) {
|
||||
showOverlay = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.onAppear {
|
||||
if !viewModel.isLoading {
|
||||
viewModel.loadContent(dataService: dataService, slug: item.slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
if #available(iOS 15.0, *) {
|
||||
VStack(spacing: 0) {
|
||||
navBar
|
||||
Spacer()
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
navBariOS14
|
||||
Spacer()
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
|
||||
}.onDisappear {
|
||||
// Clear the shared webview content when exiting
|
||||
WebViewManager.shared().loadHTMLString("<html></html>", baseURL: nil)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement things WebAppWrapperView does
|
||||
struct WebReader: UIViewRepresentable {
|
||||
let htmlContent: String
|
||||
let item: FeedItem
|
||||
let openLinkAction: (URL) -> Void
|
||||
let webViewActionHandler: (WKScriptMessage) -> Void
|
||||
let navBarVisibilityRatioUpdater: (Double) -> Void
|
||||
let authToken: String
|
||||
let appEnv: AppEnvironment
|
||||
|
||||
@Binding var increaseFontActionID: UUID?
|
||||
@Binding var decreaseFontActionID: UUID?
|
||||
|
||||
@State var annotationSaveTransactionID: UUID?
|
||||
@State private var annotation = String()
|
||||
|
||||
func makeCoordinator() -> WebReaderCoordinator {
|
||||
WebReaderCoordinator()
|
||||
}
|
||||
|
||||
func fontSize() -> Int {
|
||||
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue)
|
||||
return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WebViewManager.create()
|
||||
let contentController = WKUserContentController()
|
||||
|
||||
webView.loadHTMLString(
|
||||
WebReaderContent(
|
||||
htmlContent: htmlContent,
|
||||
item: item,
|
||||
authToken: authToken,
|
||||
isDark: UITraitCollection.current.userInterfaceStyle == .dark,
|
||||
fontSize: fontSize(),
|
||||
appEnv: appEnv
|
||||
)
|
||||
.styledContent,
|
||||
baseURL: ViewsPackage.bundleURL
|
||||
)
|
||||
|
||||
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: dataService.appEnvironment.webAppBaseURL,
|
||||
// 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 annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
|
||||
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
|
||||
(webView as? WebView)?.saveAnnotation(annotation: annotation)
|
||||
}
|
||||
|
||||
if increaseFontActionID != context.coordinator.previousIncreaseFontActionID {
|
||||
context.coordinator.previousIncreaseFontActionID = increaseFontActionID
|
||||
(webView as? WebView)?.increaseFontSize()
|
||||
}
|
||||
|
||||
if decreaseFontActionID != context.coordinator.previousDecreaseFontActionID {
|
||||
context.coordinator.previousDecreaseFontActionID = decreaseFontActionID
|
||||
(webView as? WebView)?.decreaseFontSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import Foundation
|
||||
import Models
|
||||
import Utils
|
||||
|
||||
struct WebReaderContent {
|
||||
let textFontSize: Int
|
||||
let content: String
|
||||
let item: FeedItem
|
||||
let themeKey: String
|
||||
let authToken: String
|
||||
let appEnv: AppEnvironment
|
||||
|
||||
init(
|
||||
htmlContent: String,
|
||||
item: FeedItem,
|
||||
authToken: String,
|
||||
isDark: Bool,
|
||||
fontSize: Int,
|
||||
appEnv: AppEnvironment
|
||||
) {
|
||||
self.textFontSize = fontSize
|
||||
self.content = htmlContent
|
||||
self.item = item
|
||||
self.themeKey = isDark ? "Gray" : "LightGray"
|
||||
self.authToken = authToken
|
||||
self.appEnv = appEnv
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
var styledContent: String {
|
||||
"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no' />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<script type="text/javascript">
|
||||
window.omnivoreEnv = {
|
||||
"NEXT_PUBLIC_APP_ENV": "\(appEnv.rawValue)",
|
||||
"NEXT_PUBLIC_LOCAL_BASE_URL": "\(appEnv.webAppBaseURL.absoluteString)",
|
||||
"NEXT_PUBLIC_LOCAL_SERVER_BASE_URL": "\(appEnv.serverBaseURL.absoluteString)",
|
||||
"NEXT_PUBLIC_LOCAL_HIGHLIGHTS_BASE_URL": "\(appEnv.highlightsServerBaseURL.absoluteString)"
|
||||
}
|
||||
|
||||
window.omnivoreArticle = {
|
||||
id: "test",
|
||||
linkId: "test",
|
||||
slug: "test-slug",
|
||||
createdAt: new Date().toISOString(),
|
||||
savedAt: new Date().toISOString(),
|
||||
url: "https://example.com",
|
||||
title: `\(item.title)`,
|
||||
content: `\(content)`,
|
||||
originalArticleUrl: "https://example.com",
|
||||
contentReader: "WEB",
|
||||
readingProgressPercent: \(item.readingProgress),
|
||||
readingProgressAnchorIndex: \(item.readingProgressAnchor),
|
||||
highlights: [],
|
||||
}
|
||||
|
||||
window.fontSize = \(textFontSize)
|
||||
window.localStorage.setItem("authToken", "\(authToken)")
|
||||
window.localStorage.setItem("theme", "\(themeKey)")
|
||||
</script>
|
||||
</div>
|
||||
<script src="bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import Combine
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import Utils
|
||||
import Views
|
||||
import WebKit
|
||||
|
||||
final class WebReaderCoordinator: NSObject {
|
||||
var webViewActionHandler: (WKScriptMessage) -> Void = { _ in }
|
||||
var linkHandler: (URL) -> Void = { _ in }
|
||||
var needsReload = true
|
||||
var lastSavedAnnotationID: UUID?
|
||||
var previousIncreaseFontActionID: UUID?
|
||||
var previousDecreaseFontActionID: 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 WebReaderCoordinator: WKScriptMessageHandler {
|
||||
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
webViewActionHandler(message)
|
||||
}
|
||||
}
|
||||
|
||||
extension WebReaderCoordinator: 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 WebReaderCoordinator: 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)
|
||||
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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,11 @@ private let prodBaseURL = "https://api-prod.omnivore.app"
|
|||
|
||||
private let devWebURL = "https://web-dev.omnivore.app"
|
||||
private let demoWebURL = "https://demo.omnivore.app"
|
||||
private let prodWebURL = "https://web-prod.omnivore.app"
|
||||
private let prodWebURL = "https://omnivore.app"
|
||||
|
||||
private let devHighlightsServerURL = "https://highlights-dev.omnivore.app"
|
||||
private let demoHighlightsServerURL = "https://highlights-demo.omnivore.app"
|
||||
private let prodHighlightsServerURL = "https://highlights.omnivore.app"
|
||||
|
||||
public extension AppEnvironment {
|
||||
var graphqlPath: String {
|
||||
|
|
@ -59,4 +63,17 @@ public extension AppEnvironment {
|
|||
return URL(string: "http://localhost:3000")!
|
||||
}
|
||||
}
|
||||
|
||||
var highlightsServerBaseURL: URL {
|
||||
switch self {
|
||||
case .dev:
|
||||
return URL(string: devHighlightsServerURL)!
|
||||
case .demo:
|
||||
return URL(string: demoHighlightsServerURL)!
|
||||
case .prod:
|
||||
return URL(string: prodHighlightsServerURL)!
|
||||
case .test, .local:
|
||||
return URL(string: "http://localhost:8080")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func articleContentPublisher(username: String, slug: String) -> AnyPublisher<String, ServerError> {
|
||||
enum QueryResult {
|
||||
case success(result: String)
|
||||
case error(error: String)
|
||||
}
|
||||
|
||||
let articleSelection = Selection.Article {
|
||||
try $0.content()
|
||||
}
|
||||
|
||||
let selection = Selection<QueryResult, Unions.ArticleResult> {
|
||||
try $0.on(
|
||||
articleSuccess: .init {
|
||||
QueryResult.success(result: try $0.article(selection: articleSelection))
|
||||
},
|
||||
articleError: .init {
|
||||
QueryResult.error(error: try $0.errorCodes().description)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let query = Selection.Query {
|
||||
try $0.article(username: username, slug: slug, selection: selection)
|
||||
}
|
||||
|
||||
let path = appEnvironment.graphqlPath
|
||||
let headers = networker.defaultHeaders
|
||||
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
send(query, to: path, headers: headers) { result in
|
||||
switch result {
|
||||
case let .success(payload):
|
||||
switch payload.data {
|
||||
case let .success(result: result):
|
||||
promise(.success(result))
|
||||
case .error:
|
||||
promise(.failure(.unknown))
|
||||
}
|
||||
case .failure:
|
||||
promise(.failure(.unknown))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,3 +12,10 @@ public extension Bundle {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience for locating package resources externally
|
||||
public enum UtilsPackage {
|
||||
public static var bundleURL: URL {
|
||||
Bundle.module.bundleURL
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ public enum FeatureFlag {
|
|||
public static let enableShareButton = false
|
||||
public static let enableSnooze = false
|
||||
public static let showFeedItemTags = false
|
||||
public static let useLocalWebView = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ enum WebViewConfigurationManager {
|
|||
}
|
||||
}
|
||||
|
||||
enum WebViewManager {
|
||||
public enum WebViewManager {
|
||||
public static let sharedView = create()
|
||||
public static func shared() -> WebView {
|
||||
sharedView
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import WebKit
|
|||
|
||||
/// Describes actions that can be sent from the WebView back to native views.
|
||||
/// The names on the javascript side must match for an action to be handled.
|
||||
enum WebViewAction: String, CaseIterable {
|
||||
public enum WebViewAction: String, CaseIterable {
|
||||
case highlightAction
|
||||
case readingProgressUpdate
|
||||
}
|
||||
|
||||
final class WebView: WKWebView {
|
||||
public final class WebView: WKWebView {
|
||||
#if os(iOS)
|
||||
private var panGestureRecognizer: UIPanGestureRecognizer?
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
|
|
@ -26,11 +26,11 @@ final class WebView: WKWebView {
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func increaseFontSize() {
|
||||
public func increaseFontSize() {
|
||||
dispatchEvent("increaseFontSize")
|
||||
}
|
||||
|
||||
func decreaseFontSize() {
|
||||
public func decreaseFontSize() {
|
||||
dispatchEvent("decreaseFontSize")
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ final class WebView: WKWebView {
|
|||
}
|
||||
|
||||
#if os(iOS)
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
override public func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else { return }
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ final class WebView: WKWebView {
|
|||
setDefaultMenu()
|
||||
}
|
||||
|
||||
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
public func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard let messageBody = message.body as? [String: Any] else { return }
|
||||
guard let actionID = messageBody["actionID"] as? String else { return }
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ final class WebView: WKWebView {
|
|||
UIMenuController.shared.menuItems = [remove, /* share, */ annotate]
|
||||
}
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
override public var canBecomeFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -123,11 +123,12 @@ final class WebView: WKWebView {
|
|||
setDefaultMenu()
|
||||
}
|
||||
|
||||
func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool {
|
||||
// swiftlint:disable:next line_length
|
||||
public func gestureRecognizer(_: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool {
|
||||
override public func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool {
|
||||
switch action {
|
||||
case #selector(annotateSelection): return true
|
||||
case #selector(highlightSelection): return true
|
||||
|
|
@ -163,7 +164,7 @@ final class WebView: WKWebView {
|
|||
hideMenu()
|
||||
}
|
||||
|
||||
@objc override func copy(_ sender: Any?) {
|
||||
@objc override public func copy(_ sender: Any?) {
|
||||
super.copy(sender)
|
||||
dispatchEvent("copyHighlight")
|
||||
hideMenu()
|
||||
|
|
@ -209,7 +210,7 @@ final class WebView: WKWebView {
|
|||
UIMenuController.shared.showMenu(from: self, rect: rect)
|
||||
}
|
||||
|
||||
func saveAnnotation(annotation: String) {
|
||||
public func saveAnnotation(annotation: String) {
|
||||
// swiftlint:disable:next line_length
|
||||
let dispatch = "var event = new Event('saveAnnotation');event.annotation = '\(annotation)';document.dispatchEvent(event);"
|
||||
evaluateJavaScript(dispatch) { obj, err in
|
||||
|
|
|
|||
8
apple/OmnivoreKit/Sources/Views/BundleFinder.swift
Normal file
8
apple/OmnivoreKit/Sources/Views/BundleFinder.swift
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import Foundation
|
||||
|
||||
// Convenience for locating package resources externally
|
||||
public enum ViewsPackage {
|
||||
public static var bundleURL: URL {
|
||||
Bundle.module.bundleURL
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,16 @@ import SwiftUI
|
|||
@discardableResult
|
||||
public func registerFonts() -> Bool {
|
||||
[
|
||||
registerFont(bundle: .module, fontName: "Inter-Black", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-ExtraBold", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Bold", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-SemiBold", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Medium", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Regular", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Light", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-ExtraLight", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Thin", fontExtension: "ttf")
|
||||
registerFont(bundle: .module, fontName: "Inter-Black-900", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-ExtraBold-800", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Bold-700", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-SemiBold-600", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Medium-500", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Regular-400", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Light-300", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-ExtraLight-200", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "Inter-Thin", fontExtension: "ttf"),
|
||||
registerFont(bundle: .module, fontName: "SFMonoRegular", fontExtension: "otf")
|
||||
]
|
||||
.allSatisfy { $0 }
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
2
apple/OmnivoreKit/Sources/Views/Resources/bundle.js
Normal file
2
apple/OmnivoreKit/Sources/Views/Resources/bundle.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue