Merge pull request #688 from omnivore-app/fix/pdf-loading

Fix/pdf loading
This commit is contained in:
Satindar Dhillon 2022-05-23 11:53:17 -07:00 committed by GitHub
commit a1bccce9b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 357 additions and 290 deletions

View file

@ -10,9 +10,6 @@
0411792A26A22860004AE24F /* MacAppSmokeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0411792926A22860004AE24F /* MacAppSmokeTest.swift */; };
0418837E2742E99F003E0001 /* Intercom in Frameworks */ = {isa = PBXBuildFile; productRef = 0418837D2742E99F003E0001 /* Intercom */; };
041883802742FCF2003E0001 /* Utils in Frameworks */ = {isa = PBXBuildFile; productRef = 0418837F2742FCF2003E0001 /* Utils */; };
042184ED273AD426002357B0 /* PSPDFKit in Frameworks */ = {isa = PBXBuildFile; productRef = 042184EC273AD426002357B0 /* PSPDFKit */; };
042184EF273AD5F3002357B0 /* PDFViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042184EE273AD5F3002357B0 /* PDFViewer.swift */; };
042184F0273AD5F3002357B0 /* PDFViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042184EE273AD5F3002357B0 /* PDFViewer.swift */; };
042F48DC26DFD10E00BF98FC /* iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 042F48DB26DFD10E00BF98FC /* iOSLaunchTests.swift */; };
045B1681279147E7005047F7 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 045B1680279147E7005047F7 /* FirebaseMessaging */; };
0465B9BE26CDD35F005558CD /* MainApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81BE98F0CB588F5FC577A13 /* MainApp.swift */; };
@ -44,10 +41,6 @@
42321E882714E6B00056429F /* styles in Resources */ = {isa = PBXBuildFile; fileRef = 42321E832714E6B00056429F /* styles */; };
42321E892714E6B00056429F /* views in Resources */ = {isa = PBXBuildFile; fileRef = 42321E842714E6B00056429F /* views */; };
42321E8A2714E6B00056429F /* views in Resources */ = {isa = PBXBuildFile; fileRef = 42321E842714E6B00056429F /* views */; };
4255C6D82755A5350006422A /* NanoID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4255C6D72755A5350006422A /* NanoID.swift */; };
4255C6D92755A5350006422A /* NanoID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4255C6D72755A5350006422A /* NanoID.swift */; };
426408732744676C00A2AE46 /* PDFReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426408722744676B00A2AE46 /* PDFReaderView.swift */; };
426408742744676C00A2AE46 /* PDFReaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426408722744676B00A2AE46 /* PDFReaderView.swift */; };
42FF1B33271154A700B38C38 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */; };
42FF1B34271154A700B38C38 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */; };
42FF1B35271154A700B38C38 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 42FF1AED271154A600B38C38 /* _locales */; };
@ -184,7 +177,6 @@
0411792726A22860004AE24F /* UnitTests-MacApp.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UnitTests-MacApp.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
0411792926A22860004AE24F /* MacAppSmokeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacAppSmokeTest.swift; sourceTree = "<group>"; };
0411792B26A22860004AE24F /* MacUnitTests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = MacUnitTests.plist; sourceTree = "<group>"; };
042184EE273AD5F3002357B0 /* PDFViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFViewer.swift; sourceTree = "<group>"; };
042F48D926DFD10E00BF98FC /* UITests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UITests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
042F48DB26DFD10E00BF98FC /* iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLaunchTests.swift; sourceTree = "<group>"; };
042F48DD26DFD10E00BF98FC /* iOSUITests.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = iOSUITests.plist; sourceTree = "<group>"; };
@ -215,8 +207,6 @@
42321E822714E6B00056429F /* scripts */ = {isa = PBXFileReference; lastKnownFileType = folder; path = scripts; sourceTree = "<group>"; };
42321E832714E6B00056429F /* styles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = styles; sourceTree = "<group>"; };
42321E842714E6B00056429F /* views */ = {isa = PBXFileReference; lastKnownFileType = folder; path = views; sourceTree = "<group>"; };
4255C6D72755A5350006422A /* NanoID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NanoID.swift; sourceTree = "<group>"; };
426408722744676B00A2AE46 /* PDFReaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFReaderView.swift; sourceTree = "<group>"; };
42FF1AEB271154A600B38C38 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = "<group>"; };
42FF1AED271154A600B38C38 /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; path = _locales; sourceTree = "<group>"; };
42FF1AEE271154A600B38C38 /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = "<group>"; };
@ -315,7 +305,6 @@
0418837E2742E99F003E0001 /* Intercom in Frameworks */,
045B1681279147E7005047F7 /* FirebaseMessaging in Frameworks */,
041883802742FCF2003E0001 /* Utils in Frameworks */,
042184ED273AD426002357B0 /* PSPDFKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -478,9 +467,6 @@
children = (
0480E71B26D95096006CAE2F /* AppDelegate.swift */,
D81BE98F0CB588F5FC577A13 /* MainApp.swift */,
042184EE273AD5F3002357B0 /* PDFViewer.swift */,
426408722744676B00A2AE46 /* PDFReaderView.swift */,
4255C6D72755A5350006422A /* NanoID.swift */,
42FF1AEA271154A600B38C38 /* SafariExtension */,
B330B55BAF36E624637EE3BE /* ShareExtension */,
04920CC5279671EF003EC1B6 /* PushNotificationConfig.swift */,
@ -719,7 +705,6 @@
name = "Omnivore-iOS";
packageProductDependencies = (
8F13016D33DF1598D7157563 /* App */,
042184EC273AD426002357B0 /* PSPDFKit */,
0418837D2742E99F003E0001 /* Intercom */,
0418837F2742FCF2003E0001 /* Utils */,
045B1680279147E7005047F7 /* FirebaseMessaging */,
@ -796,7 +781,6 @@
);
mainGroup = 2EE5B57B588CF4ADBA39A106;
packageReferences = (
042184EB273AD426002357B0 /* XCRemoteSwiftPackageReference "PSPDFKit-SP" */,
0418837C2742E99F003E0001 /* XCRemoteSwiftPackageReference "intercom-ios" */,
048F592A2790EAF800E0B494 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
@ -1039,10 +1023,7 @@
buildActionMask = 2147483647;
files = (
0480E71D26D95096006CAE2F /* AppDelegate.swift in Sources */,
4255C6D92755A5350006422A /* NanoID.swift in Sources */,
0465B9BE26CDD35F005558CD /* MainApp.swift in Sources */,
426408742744676C00A2AE46 /* PDFReaderView.swift in Sources */,
042184F0273AD5F3002357B0 /* PDFViewer.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1075,11 +1056,8 @@
buildActionMask = 2147483647;
files = (
0480E71C26D95096006CAE2F /* AppDelegate.swift in Sources */,
4255C6D82755A5350006422A /* NanoID.swift in Sources */,
04920CC6279671EF003EC1B6 /* PushNotificationConfig.swift in Sources */,
CA7EE773095F267516D7AC98 /* MainApp.swift in Sources */,
426408732744676C00A2AE46 /* PDFReaderView.swift in Sources */,
042184EF273AD5F3002357B0 /* PDFViewer.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1463,7 +1441,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.7.0;
MARKETING_VERSION = 1.7.1;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1495,7 +1473,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.0;
MARKETING_VERSION = 1.7.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
@ -1534,7 +1512,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.0;
MARKETING_VERSION = 1.7.1;
MTL_FAST_MATH = YES;
OTHER_LDFLAGS = (
"-framework",
@ -1696,7 +1674,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.0;
MARKETING_VERSION = 1.7.1;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension";
PRODUCT_NAME = ShareExtension;
SDKROOT = iphoneos;
@ -1750,7 +1728,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.7.0;
MARKETING_VERSION = 1.7.1;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1778,7 +1756,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.7.0;
MARKETING_VERSION = 1.7.1;
PRODUCT_BUNDLE_IDENTIFIER = "app.omnivore.app.share-extension";
PRODUCT_NAME = ShareExtension;
SDKROOT = iphoneos;
@ -1964,14 +1942,6 @@
version = 11.1.2;
};
};
042184EB273AD426002357B0 /* XCRemoteSwiftPackageReference "PSPDFKit-SP" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/PSPDFKit/PSPDFKit-SP";
requirement = {
branch = master;
kind = branch;
};
};
048F592A2790EAF800E0B494 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
@ -1992,11 +1962,6 @@
isa = XCSwiftPackageProductDependency;
productName = Utils;
};
042184EC273AD426002357B0 /* PSPDFKit */ = {
isa = XCSwiftPackageProductDependency;
package = 042184EB273AD426002357B0 /* XCRemoteSwiftPackageReference "PSPDFKit-SP" */;
productName = PSPDFKit;
};
045B1680279147E7005047F7 /* FirebaseMessaging */ = {
isa = XCSwiftPackageProductDependency;
package = 048F592A2790EAF800E0B494 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;

View file

@ -141,7 +141,7 @@
"location" : "https://github.com/PSPDFKit/PSPDFKit-SP",
"state" : {
"branch" : "master",
"revision" : "344c895fea62eb42c6b56e6a060d5dff73cc5bc2"
"revision" : "0e18629c443e3f39ecfee0f600d9ef5551ecf488"
}
},
{

View file

@ -16,15 +16,9 @@ let package = Package(
.library(name: "Models", targets: ["Models"]),
.library(name: "Utils", targets: ["Utils"])
],
dependencies: [
.package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.4.0")),
.package(url: "https://github.com/Square/Valet", from: "4.1.2"),
.package(url: "https://github.com/maticzav/swift-graphql", from: "2.3.1"),
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
.package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0"))
],
dependencies: dependencies,
targets: [
.target(name: "App", dependencies: ["Views", "Services", "Models", "Utils"]),
.target(name: "App", dependencies: appPackageDependencies),
.testTarget(name: "AppTests", dependencies: ["App"]),
.target(
name: "Views",
@ -58,3 +52,25 @@ let package = Package(
.testTarget(name: "UtilsTests", dependencies: ["Utils"])
]
)
var appPackageDependencies: [Target.Dependency] {
var deps: [Target.Dependency] = ["Views", "Services", "Models", "Utils"]
// #if canImport(UIKit)
deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP"))
// #endif
return deps
}
var dependencies: [Package.Dependency] {
var deps: [Package.Dependency] = [
.package(url: "https://github.com/openid/AppAuth-iOS.git", .upToNextMajor(from: "1.4.0")),
.package(url: "https://github.com/Square/Valet", from: "4.1.2"),
.package(url: "https://github.com/maticzav/swift-graphql", from: "2.3.1"),
.package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"),
.package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0"))
]
// #if canImport(UIKit)
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master"))
// #endif
return deps
}

View file

@ -1,13 +1,18 @@
import Models
import SwiftUI
import Utils
import WebKit
#if os(iOS)
import Models
import PSPDFKit
import PSPDFKitUI
import SwiftUI
import Utils
import WebKit
struct PDFReaderViewController: UIViewControllerRepresentable {
static func registerKey() {
if let pspdfKitKey = AppKeys.sharedInstance?.pspdfKitKey {
SDK.setLicenseKey(pspdfKitKey)
}
}
let document: Document
@Environment(\.presentationMode) var presentationMode

View file

@ -1,127 +1,148 @@
import App
import Combine
import SwiftUI
import Utils
#if os(iOS)
import PDFKit
import PSPDFKit
import PSPDFKitUI
import Services
struct PDFViewer: View {
final class PDFStateObject: ObservableObject {
@Published var document: Document?
@Published var coordinator: PDFViewCoordinator?
@Published var controllerNeedsConfig = true
}
@EnvironmentObject var dataService: DataService
struct ShareLink: Identifiable {
let id: UUID
let url: URL
}
let pdfURL: URL
let document: Document
let viewModel: PDFViewerViewModel
let coordinator: PDFViewCoordinator
@StateObject var pdfStateObject = PDFStateObject()
@State var readerView: Bool = false
@State private var shareLink: ShareLink?
init(remoteURL: URL, viewModel: PDFViewerViewModel) {
self.pdfURL = viewModel.dataURL(remoteURL: remoteURL)
self.viewModel = viewModel
self.document = HighlightedDocument(url: pdfURL, viewModel: viewModel)
self.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel)
}
var body: some View {
PDFView(document: document)
.useParentNavigationBar(true)
.updateConfiguration { builder in
builder.textSelectionShouldSnapToWord = true
}
.updateControllerConfiguration { controller in
print("document is valid", document.isValid)
coordinator.setController(controller: controller)
// Disable the Document Editor
controller.navigationItem.setRightBarButtonItems(
[controller.thumbnailsButtonItem],
for: .thumbnails,
animated: false
)
let barButtonItems = [
UIBarButtonItem(
image: UIImage(systemName: "textformat"),
style: .plain,
target: controller.settingsButtonItem.target,
action: controller.settingsButtonItem.action
),
UIBarButtonItem(
image: UIImage(systemName: "book"),
style: .plain,
target: coordinator,
action: #selector(PDFViewCoordinator.toggleReaderView)
),
UIBarButtonItem(
image: UIImage(systemName: "magnifyingglass"),
style: .plain,
target: controller.searchButtonItem.target,
action: controller.searchButtonItem.action
)
]
document.areAnnotationsEnabled = true
coordinator.viewer = self
if viewModel.linkedItem.readingProgressAnchor > 0 {
let pageIndex = UInt(viewModel.linkedItem.readingProgressAnchor)
controller.setPageIndex(pageIndex, animated: false)
if let document = pdfStateObject.document, let coordinator = pdfStateObject.coordinator {
PDFView(document: document)
.useParentNavigationBar(true)
.updateConfiguration { builder in
builder.textSelectionShouldSnapToWord = true
}
.updateControllerConfiguration { controller in
// Store config state so we only run this update closure once
guard pdfStateObject.controllerNeedsConfig else { return }
print("document is valid", document.isValid)
coordinator.setController(controller: controller, dataService: dataService)
controller.navigationItem.setRightBarButtonItems(barButtonItems, for: .document, animated: false)
}
.onShouldShowMenuItemsForSelectedText(perform: { pageView, menuItems, selectedText in
let copy = menuItems.first(where: { $0.identifier == "Copy" })
let highlight = MenuItem(title: "Highlight", block: {
_ = self.coordinator.highlightSelection(pageView: pageView, selectedText: selectedText)
})
// Disable the Document Editor
controller.navigationItem.setRightBarButtonItems(
[controller.thumbnailsButtonItem],
for: .thumbnails,
animated: false
)
let barButtonItems = [
UIBarButtonItem(
image: UIImage(systemName: "textformat"),
style: .plain,
target: controller.settingsButtonItem.target,
action: controller.settingsButtonItem.action
),
UIBarButtonItem(
image: UIImage(systemName: "book"),
style: .plain,
target: coordinator,
action: #selector(PDFViewCoordinator.toggleReaderView)
),
UIBarButtonItem(
image: UIImage(systemName: "magnifyingglass"),
style: .plain,
target: controller.searchButtonItem.target,
action: controller.searchButtonItem.action
)
]
document.areAnnotationsEnabled = true
coordinator.viewer = self
if viewModel.pdfItem.readingProgressAnchor > 0 {
let pageIndex = UInt(viewModel.pdfItem.readingProgressAnchor)
controller.setPageIndex(pageIndex, animated: false)
}
controller.navigationItem.setRightBarButtonItems(barButtonItems, for: .document, animated: false)
pdfStateObject.controllerNeedsConfig = false
}
.onShouldShowMenuItemsForSelectedText(perform: { pageView, menuItems, selectedText in
let copy = menuItems.first(where: { $0.identifier == "Copy" })
let highlight = MenuItem(title: "Highlight", block: {
_ = coordinator.highlightSelection(
pageView: pageView,
selectedText: selectedText,
dataService: dataService
)
})
// let share = MenuItem(title: "Share", block: {
// let shortId = self.coordinator.highlightSelection(pageView: pageView, selectedText: selectedText)
// if let shareURL = viewModel.highlightShareURL(shortId: shortId) {
// shareLink = ShareLink(id: UUID(), url: shareURL)
// }
// })
return [copy, highlight /* , share */ ].compactMap { $0 }
})
.onShouldShowMenuItemsForSelectedAnnotations(perform: { _, menuItems, annotations in
var result = [MenuItem]()
if let copy = menuItems.first(where: { $0.identifier == "Copy" }) {
result.append(copy)
}
let remove = MenuItem(title: "Remove", block: {
self.coordinator.remove(annotations: annotations)
return [copy, highlight /* , share */ ].compactMap { $0 }
})
result.append(remove)
.onShouldShowMenuItemsForSelectedAnnotations(perform: { _, menuItems, annotations in
var result = [MenuItem]()
if let copy = menuItems.first(where: { $0.identifier == "Copy" }) {
result.append(copy)
}
let highlights = annotations?.compactMap { $0 as? HighlightAnnotation }
let shortId = highlights.flatMap { coordinator.shortHighlightIds($0).first }
if let shortId = shortId, FeatureFlag.enableShareButton {
let share = MenuItem(title: "Share", block: {
if let shareURL = viewModel.highlightShareURL(shortId: shortId) {
shareLink = ShareLink(id: UUID(), url: shareURL)
}
let remove = MenuItem(title: "Remove", block: {
coordinator.remove(dataService: dataService, annotations: annotations)
})
result.append(share)
}
result.append(remove)
return result
})
.fullScreenCover(isPresented: $readerView, content: {
PDFReaderViewController(document: document)
})
.accentColor(Color(red: 255 / 255.0, green: 234 / 255.0, blue: 159 / 255.0))
.sheet(item: $shareLink) {
ShareSheet(activityItems: [$0.url])
}
let highlights = annotations?.compactMap { $0 as? HighlightAnnotation }
let shortId = highlights.flatMap { coordinator.shortHighlightIds($0).first }
if let shortId = shortId, FeatureFlag.enableShareButton {
let share = MenuItem(title: "Share", block: {
if let shareURL = viewModel.highlightShareURL(dataService: dataService, shortId: shortId) {
shareLink = ShareLink(id: UUID(), url: shareURL)
}
})
result.append(share)
}
return result
})
.fullScreenCover(isPresented: $readerView, content: {
PDFReaderViewController(document: document)
})
.accentColor(Color(red: 255 / 255.0, green: 234 / 255.0, blue: 159 / 255.0))
.sheet(item: $shareLink) {
ShareSheet(activityItems: [$0.url])
}
} else {
ProgressView()
.task {
let document = HighlightedDocument(url: pdfURL, viewModel: viewModel)
pdfStateObject.document = document
pdfStateObject.coordinator = PDFViewCoordinator(document: document, viewModel: viewModel)
}
}
}
class PDFViewCoordinator: NSObject, PDFDocumentViewControllerDelegate, PDFViewControllerDelegate {
@ -137,7 +158,7 @@ import Utils
self.viewModel = viewModel
}
func setController(controller: PDFViewController) {
func setController(controller: PDFViewController, dataService: DataService) {
self.controller = controller
controller.pageIndexPublisher.sink { event in
@ -145,8 +166,12 @@ import Utils
let pageIndex = Int(event.pageIndex)
if let totalPageCount = controller.document?.pageCount {
let percent = min(100, max(0, ((Double(pageIndex) + 1.0) / Double(totalPageCount)) * 100.0))
if percent > self.viewModel.linkedItem.readingProgress {
self.viewModel.updateItemReadProgress(percent: percent, anchorIndex: pageIndex)
if percent > self.viewModel.pdfItem.readingProgress {
self.viewModel.updateItemReadProgress(
dataService: dataService,
percent: percent,
anchorIndex: pageIndex
)
}
}
}
@ -187,7 +212,7 @@ import Utils
return result
}
func highlightSelection(pageView: PDFPageView, selectedText: String) -> String {
func highlightSelection(pageView: PDFPageView, selectedText: String, dataService: DataService) -> String {
let highlightID = UUID().uuidString.lowercased()
let quote = quoteFromSelectedText(selectedText)
let shortId = NanoID.generate(alphabet: NanoID.Alphabet.urlSafe.rawValue, size: 8)
@ -199,7 +224,7 @@ import Utils
"id": highlightID,
"shortId": shortId,
"quote": quote,
"articleId": viewModel.linkedItem.unwrappedID
"articleId": viewModel.pdfItem.itemID
]
]
document.add(annotations: [highlight])
@ -208,7 +233,13 @@ import Utils
if let patchData = try? highlight.generateInstantJSON(), let patch = String(data: patchData, encoding: .utf8) {
if overlapping.isEmpty {
viewModel.createHighlight(shortId: shortId, highlightID: highlightID, quote: quote, patch: patch)
viewModel.createHighlight(
dataService: dataService,
shortId: shortId,
highlightID: highlightID,
quote: quote,
patch: patch
)
} else {
let overlappingRects = overlapping.map(\.rects).compactMap { $0 }.flatMap { $0 }
let rects = overlappingRects + (highlight.rects ?? [])
@ -223,6 +254,7 @@ import Utils
document.remove(annotations: overlapping + [highlight])
viewModel.mergeHighlight(
dataService: dataService,
shortId: shortId,
highlightID: highlightID,
quote: quote,
@ -238,10 +270,13 @@ import Utils
return shortId
}
public func remove(annotations: [Annotation]?) {
public func remove(dataService: DataService, annotations: [Annotation]?) {
if let annotations = annotations {
document.remove(annotations: annotations)
viewModel.removeHighlights(highlightIds: highlightIds(annotations.compactMap { $0 as? HighlightAnnotation }))
viewModel.removeHighlights(
dataService: dataService,
highlightIds: highlightIds(annotations.compactMap { $0 as? HighlightAnnotation })
)
}
}

View file

@ -8,15 +8,13 @@ public final class PDFViewerViewModel: ObservableObject {
@Published public var errorMessage: String?
@Published public var readerView: Bool = false
public var linkedItem: LinkedItem
public let pdfItem: PDFItem
private var storedURL: URL?
var subscriptions = Set<AnyCancellable>()
let services: Services
public init(services: Services, linkedItem: LinkedItem) {
self.services = services
self.linkedItem = linkedItem
public init(pdfItem: PDFItem) {
self.pdfItem = pdfItem
}
public func dataURL(remoteURL: URL) -> URL {
@ -24,9 +22,9 @@ public final class PDFViewerViewModel: ObservableObject {
return storedURL
}
guard let data = linkedItem.pdfData else { return remoteURL }
guard let data = pdfItem.documentData else { return remoteURL }
let subPath = linkedItem.unwrappedTitle.isEmpty ? UUID().uuidString : linkedItem.unwrappedTitle
let subPath = pdfItem.title.isEmpty ? UUID().uuidString : pdfItem.title
let path = FileManager.default
.urls(for: .cachesDirectory, in: .userDomainMask)[0]
@ -42,56 +40,63 @@ public final class PDFViewerViewModel: ObservableObject {
}
public func loadHighlightPatches(completion onComplete: @escaping ([String]) -> Void) {
onComplete(linkedItem.highlights.asArray(of: Highlight.self).map { $0.patch ?? "" })
onComplete(pdfItem.highlights.map { $0.patch ?? "" })
}
public func createHighlight(shortId: String, highlightID: String, quote: String, patch: String) {
_ = services.dataService.createHighlight(
public func createHighlight(
dataService: DataService,
shortId: String,
highlightID: String,
quote: String,
patch: String
) {
_ = dataService.createHighlight(
shortId: shortId,
highlightID: highlightID,
quote: quote,
patch: patch,
articleId: linkedItem.unwrappedID
articleId: pdfItem.itemID
)
}
public func mergeHighlight(
dataService: DataService,
shortId: String,
highlightID: String,
quote: String,
patch: String,
overlapHighlightIdList: [String]
) {
_ = services.dataService.mergeHighlights(
_ = dataService.mergeHighlights(
shortId: shortId,
highlightID: highlightID,
quote: quote,
patch: patch,
articleId: linkedItem.unwrappedID,
articleId: pdfItem.itemID,
overlapHighlightIdList: overlapHighlightIdList
)
}
public func removeHighlights(highlightIds: [String]) {
public func removeHighlights(dataService: DataService, highlightIds: [String]) {
highlightIds.forEach { highlightID in
services.dataService.deleteHighlight(highlightID: highlightID)
dataService.deleteHighlight(highlightID: highlightID)
}
}
public func updateItemReadProgress(percent: Double, anchorIndex: Int) {
services.dataService.updateLinkReadingProgress(
itemID: linkedItem.unwrappedID,
public func updateItemReadProgress(dataService: DataService, percent: Double, anchorIndex: Int) {
dataService.updateLinkReadingProgress(
itemID: pdfItem.itemID,
readingProgress: percent,
anchorIndex: anchorIndex
)
}
public func highlightShareURL(shortId: String) -> URL? {
let baseURL = services.dataService.appEnvironment.serverBaseURL
public func highlightShareURL(dataService: DataService, shortId: String) -> URL? {
let baseURL = dataService.appEnvironment.serverBaseURL
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)
if let username = services.dataService.currentViewer?.username {
components?.path = "/\(username)/\(linkedItem.unwrappedSlug)/highlights/\(shortId)"
if let username = dataService.currentViewer?.username {
components?.path = "/\(username)/\(pdfItem.slug)/highlights/\(shortId)"
} else {
return nil
}

View file

@ -20,61 +20,62 @@ public final class Services {
}
}
// Background fetching functions
extension Services {
public static func registerBackgroundFetch() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: fetchTaskID, using: nil) { task in
if let task = task as? BGAppRefreshTask {
EventTracker.trackForDebugging("executing app.omnivore.fetchLinkedItems bg task")
logger.debug("in background task register closure")
performBackgroundFetch(task: task)
#if os(iOS)
// Background fetching functions
extension Services {
public static func registerBackgroundFetch() {
BGTaskScheduler.shared.register(forTaskWithIdentifier: fetchTaskID, using: nil) { task in
if let task = task as? BGAppRefreshTask {
EventTracker.trackForDebugging("executing app.omnivore.fetchLinkedItems bg task")
logger.debug("in background task register closure")
performBackgroundFetch(task: task)
}
}
}
}
static func scheduleBackgroundFetch() {
BGTaskScheduler.shared.cancelAllTaskRequests()
let taskRequest = BGAppRefreshTaskRequest(identifier: fetchTaskID)
taskRequest.earliestBeginDate = Date(timeIntervalSinceNow: secondsToWaitBeforeNextBackgroundRefresh)
static func scheduleBackgroundFetch() {
BGTaskScheduler.shared.cancelAllTaskRequests()
let taskRequest = BGAppRefreshTaskRequest(identifier: fetchTaskID)
taskRequest.earliestBeginDate = Date(timeIntervalSinceNow: secondsToWaitBeforeNextBackgroundRefresh)
do {
try BGTaskScheduler.shared.submit(taskRequest)
logger.debug("\(fetchTaskID) task scheduled")
} catch {
logger.debug("task scheduling failed: \(fetchTaskID)")
}
}
static func performBackgroundFetch(task: BGAppRefreshTask) {
Services.logger.debug("starting background fetch")
scheduleBackgroundFetch()
let services = Services()
task.expirationHandler = {
EventTracker.trackForDebugging("background fetch expiration handler called")
logger.debug("handling background fetch expiration")
}
guard services.authenticator.hasValidAuthToken else {
EventTracker.trackForDebugging("background fetch failed: user does not have a valid auth token")
Services.logger.debug("background fetch failed: user does not habe a valid auth token")
task.setTaskCompleted(success: false)
return
}
Task {
do {
try await services.dataService.fetchLinkedItemsBackgroundTask()
logger.debug("fetch complete")
EventTracker.trackForDebugging("background fetch task completed successfully")
task.setTaskCompleted(success: true)
try BGTaskScheduler.shared.submit(taskRequest)
logger.debug("\(fetchTaskID) task scheduled")
} catch {
logger.debug("fetch failed")
EventTracker.trackForDebugging("background fetch task failed")
logger.debug("task scheduling failed: \(fetchTaskID)")
}
}
static func performBackgroundFetch(task: BGAppRefreshTask) {
Services.logger.debug("starting background fetch")
scheduleBackgroundFetch()
let services = Services()
task.expirationHandler = {
EventTracker.trackForDebugging("background fetch expiration handler called")
logger.debug("handling background fetch expiration")
}
guard services.authenticator.hasValidAuthToken else {
EventTracker.trackForDebugging("background fetch failed: user does not have a valid auth token")
Services.logger.debug("background fetch failed: user does not habe a valid auth token")
task.setTaskCompleted(success: false)
return
}
Task {
do {
try await services.dataService.fetchLinkedItemsBackgroundTask()
logger.debug("fetch complete")
EventTracker.trackForDebugging("background fetch task completed successfully")
task.setTaskCompleted(success: true)
} catch {
logger.debug("fetch failed")
EventTracker.trackForDebugging("background fetch task failed")
task.setTaskCompleted(success: false)
}
}
}
}
}
#endif
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.omnivore.fetchLinkedItems"]

View file

@ -6,12 +6,17 @@ import Views
struct FeedCardNavigationLink: View {
@EnvironmentObject var dataService: DataService
@ObservedObject var item: LinkedItem
let item: LinkedItem
@ObservedObject var viewModel: HomeFeedViewModel
var body: some View {
let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item))
let destination = LinkItemDetailView(
viewModel: LinkItemDetailViewModel(
linkedItemObjectID: item.objectID,
dataService: dataService
)
)
#if os(iOS)
let modifiedDestination = destination
.navigationTitle("")
@ -22,7 +27,7 @@ struct FeedCardNavigationLink: View {
return ZStack {
NavigationLink(
destination: modifiedDestination,
tag: item,
tag: item.objectID,
selection: $viewModel.selectedLinkItem
) {
EmptyView()
@ -50,7 +55,12 @@ struct GridCardNavigationLink: View {
@ObservedObject var viewModel: HomeFeedViewModel
var body: some View {
let destination = LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item))
let destination = LinkItemDetailView(
viewModel: LinkItemDetailViewModel(
linkedItemObjectID: item.objectID,
dataService: dataService
)
)
#if os(iOS)
let modifiedDestination = destination
.navigationTitle("")
@ -61,7 +71,7 @@ struct GridCardNavigationLink: View {
return ZStack {
NavigationLink(
destination: modifiedDestination,
tag: item,
tag: item.objectID,
selection: $viewModel.selectedLinkItem
) {
EmptyView()
@ -71,7 +81,7 @@ struct GridCardNavigationLink: View {
scale = 0.95
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) {
scale = 1.0
viewModel.selectedLinkItem = item
viewModel.selectedLinkItem = item.objectID
}
}
})

View file

@ -5,9 +5,9 @@ import UserNotifications
import Utils
import Views
private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
#if os(iOS)
private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
struct HomeFeedContainerView: View {
@EnvironmentObject var dataService: DataService
@AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = false
@ -101,7 +101,7 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
guard let objectID = dataService.persist(jsonArticle: jsonArticle) else { return }
guard let linkedItem = dataService.viewContext.object(with: objectID) as? LinkedItem else { return }
viewModel.pushFeedItem(item: linkedItem)
viewModel.selectedLinkItem = linkedItem
viewModel.selectedLinkItem = linkedItem.objectID
}
.onOpenURL { url in
withoutAnimation {

View file

@ -19,7 +19,7 @@ import Views
@Published var negatedLabels = [LinkedItemLabel]()
@Published var snoozePresented = false
@Published var itemToSnoozeID: String?
@Published var selectedLinkItem: LinkedItem?
@Published var selectedLinkItem: NSManagedObjectID?
@Published var linkRequest: LinkRequest?
@Published var showLoadingBar = false

View file

@ -1,38 +1,46 @@
import Combine
import CoreData
import Models
import Services
import SwiftUI
import Utils
import Views
enum PDFProvider {
static var pdfViewerProvider: ((URL, LinkedItem) -> AnyView)?
}
@MainActor final class LinkItemDetailViewModel: ObservableObject {
@Published var item: LinkedItem
let pdfItem: PDFItem?
let item: LinkedItem?
@Published var webAppWrapperViewModel: WebAppWrapperViewModel?
var subscriptions = Set<AnyCancellable>()
init(item: LinkedItem) {
self.item = item
init(linkedItemObjectID: NSManagedObjectID, dataService: DataService) {
if let linkedItem = dataService.viewContext.object(with: linkedItemObjectID) as? LinkedItem {
self.pdfItem = PDFItem.make(item: linkedItem)
self.item = linkedItem
} else {
self.pdfItem = nil
self.item = nil
}
}
func handleArchiveAction(dataService: DataService) {
dataService.archiveLink(objectID: item.objectID, archived: !item.isArchived)
Snackbar.show(message: !item.isArchived ? "Link archived" : "Link moved to Inbox")
guard let objectID = item?.objectID ?? pdfItem?.objectID else { return }
dataService.archiveLink(objectID: objectID, archived: !isItemArchived)
Snackbar.show(message: !isItemArchived ? "Link archived" : "Link moved to Inbox")
}
func handleDeleteAction(dataService: DataService) {
guard let objectID = item?.objectID ?? pdfItem?.objectID else { return }
Snackbar.show(message: "Link removed")
dataService.removeLink(objectID: item.objectID)
dataService.removeLink(objectID: objectID)
}
func updateItemReadStatus(dataService: DataService) {
guard let itemID = item?.unwrappedID ?? pdfItem?.itemID else { return }
dataService.updateLinkReadingProgress(
itemID: item.unwrappedID,
readingProgress: item.isRead ? 0 : 100,
itemID: itemID,
readingProgress: isItemRead ? 0 : 100,
anchorIndex: 0
)
}
@ -64,21 +72,34 @@ enum PDFProvider {
}
func trackReadEvent() {
guard let itemID = item?.unwrappedID ?? pdfItem?.itemID else { return }
guard let slug = item?.unwrappedSlug ?? pdfItem?.slug else { return }
guard let originalArticleURL = item?.unwrappedPageURLString ?? pdfItem?.originalArticleURL else { return }
EventTracker.track(
.linkRead(
linkID: item.unwrappedID,
slug: item.unwrappedSlug,
originalArticleURL: item.unwrappedPageURLString
linkID: itemID,
slug: slug,
originalArticleURL: originalArticleURL
)
)
}
var isItemRead: Bool {
item?.isRead ?? pdfItem?.isRead ?? false
}
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)/\(item.unwrappedSlug)",
urlPath: "/app/\(username)/\(slug)",
queryParams: ["isAppEmbedView": "true", "highlightBarDisabled": isMacApp ? "false" : "true"]
)
@ -111,7 +132,7 @@ struct LinkItemDetailView: View {
viewModel.updateItemReadStatus(dataService: dataService)
},
label: {
Image(systemName: viewModel.item.isRead ? "line.horizontal.3.decrease.circle" : "checkmark.circle")
Image(systemName: viewModel.isItemRead ? "line.horizontal.3.decrease.circle" : "checkmark.circle")
}
)
}
@ -139,15 +160,15 @@ struct LinkItemDetailView: View {
var body: some View {
#if os(iOS)
if viewModel.item.isPDF {
if viewModel.pdfItem != nil {
fixedNavBarReader
.navigationBarHidden(hideNavBar)
.task {
hideNavBar = true
viewModel.trackReadEvent()
}
} else {
WebReaderContainerView(item: viewModel.item)
} else if let item = viewModel.item {
WebReaderContainerView(item: item)
.navigationBarHidden(hideNavBar)
.task {
hideNavBar = true
@ -189,8 +210,8 @@ struct LinkItemDetailView: View {
action: { viewModel.handleArchiveAction(dataService: dataService) },
label: {
Label(
viewModel.item.isArchived ? "Unarchive" : "Archive",
systemImage: viewModel.item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
viewModel.isItemArchived ? "Unarchive" : "Archive",
systemImage: viewModel.isItemArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
}
)
@ -277,9 +298,9 @@ struct LinkItemDetailView: View {
#endif
@ViewBuilder private var fixedNavBarReader: some View {
if let pdfURL = viewModel.item.pdfURL {
if let pdfItem = viewModel.pdfItem, let pdfURL = pdfItem.pdfURL {
#if os(iOS)
PDFProvider.pdfViewerProvider?(pdfURL, viewModel.item)
PDFViewer(remoteURL: pdfURL, viewModel: PDFViewerViewModel(pdfItem: pdfItem))
.navigationBarTitleDisplayMode(.inline)
#elseif os(macOS)
PDFWrapperView(pdfURL: pdfURL)

View file

@ -6,20 +6,18 @@ import Views
public struct RootView: View {
@Environment(\.scenePhase) var scenePhase
let pdfViewerProvider: ((URL, PDFViewerViewModel) -> AnyView)?
@StateObject private var viewModel = RootViewModel()
public init(
pdfViewerProvider: ((URL, PDFViewerViewModel) -> AnyView)?,
intercomProvider: IntercomProvider?
) {
self.pdfViewerProvider = pdfViewerProvider
public init(intercomProvider: IntercomProvider?) {
if let intercomProvider = intercomProvider {
DataService.showIntercomMessenger = intercomProvider.showIntercomMessenger
DataService.registerIntercomUser = intercomProvider.registerIntercomUser
Authenticator.unregisterIntercomUser = intercomProvider.unregisterIntercomUser
}
#if os(iOS)
PDFReaderViewController.registerKey()
#endif
}
public var body: some View {
@ -27,14 +25,11 @@ public struct RootView: View {
.environmentObject(viewModel.services.authenticator)
.environmentObject(viewModel.services.dataService)
.environment(\.managedObjectContext, viewModel.services.dataService.viewContext)
.onAppear {
if let pdfViewerProvider = pdfViewerProvider {
viewModel.configurePDFProvider(pdfViewerProvider: pdfViewerProvider)
}
}
.onChange(of: scenePhase) { phase in
if phase == .background {
Services.scheduleBackgroundFetch()
#if os(iOS)
Services.scheduleBackgroundFetch()
#endif
}
}
}

View file

@ -32,15 +32,6 @@ public final class RootViewModel: ObservableObject {
#endif
}
func configurePDFProvider(pdfViewerProvider: @escaping (URL, PDFViewerViewModel) -> AnyView) {
guard PDFProvider.pdfViewerProvider == nil else { return }
PDFProvider.pdfViewerProvider = { [weak self] url, linkedItem in
guard let self = self else { return AnyView(Text("")) }
return pdfViewerProvider(url, PDFViewerViewModel(services: self.services, linkedItem: linkedItem))
}
}
func webAppWrapperViewModel(webLinkPath: String) -> WebAppWrapperViewModel {
let baseURL = services.dataService.appEnvironment.webAppBaseURL

View file

@ -0,0 +1,36 @@
import CoreData
import Foundation
public struct PDFItem {
public let objectID: NSManagedObjectID
public let itemID: String
public let pdfURL: URL?
public let documentData: Data?
public let title: String
public let slug: String
public let readingProgress: Double
public let readingProgressAnchor: Int
public let isArchived: Bool
public let isRead: Bool
public let originalArticleURL: String
public let highlights: [Highlight]
public static func make(item: LinkedItem) -> PDFItem? {
guard item.isPDF else { return nil }
return PDFItem(
objectID: item.objectID,
itemID: item.unwrappedID,
pdfURL: URL(string: item.unwrappedPageURLString),
documentData: item.pdfData,
title: item.unwrappedID,
slug: item.unwrappedSlug,
readingProgress: item.readingProgress,
readingProgressAnchor: Int(item.readingProgressAnchor),
isArchived: item.isArchived,
isRead: item.isRead,
originalArticleURL: item.unwrappedPageURLString,
highlights: item.highlights.asArray(of: Highlight.self)
)
}
}

View file

@ -15,7 +15,7 @@ public struct SearchBar: View {
TextField("Search", text: $searchTerm)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.background(Color.systemGray6)
.cornerRadius(8)
.focused($isFocused)
.overlay(

View file

@ -6,7 +6,6 @@ import SwiftUI
import AppKit
#elseif os(iOS)
import Intercom
import PSPDFKit
import UIKit
import Utils
#endif
@ -17,20 +16,12 @@ struct MainApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#elseif os(iOS)
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
// Activate PSPDFKit for app.omnivore.app
if let pspdfKitKey = AppKeys.sharedInstance?.pspdfKitKey {
SDK.setLicenseKey(pspdfKitKey)
}
}
#endif
var body: some Scene {
#if os(iOS)
WindowGroup {
RootView(
pdfViewerProvider: pdfViewerProvider,
intercomProvider: AppKeys.sharedInstance?.intercom != nil ? IntercomProvider(
registerIntercomUser: { Intercom.registerUser(withUserId: $0) },
unregisterIntercomUser: Intercom.logout,
@ -40,12 +31,8 @@ struct MainApp: App {
}
#elseif os(macOS)
WindowGroup {
RootView(pdfViewerProvider: nil, intercomProvider: nil)
RootView(intercomProvider: nil)
}
#endif
}
private func pdfViewerProvider(url: URL, viewModel: PDFViewerViewModel) -> AnyView {
AnyView(PDFViewer(remoteURL: url, viewModel: viewModel))
}
}