Merge pull request #2781 from omnivore-app/fix/mac-extension-update

Update safari extension, clean up the macos app
This commit is contained in:
Jackson Harper
2023-09-20 15:20:12 +08:00
committed by GitHub
27 changed files with 522 additions and 433 deletions

View File

@ -1400,7 +1400,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.30.0;
MARKETING_VERSION = 1.31.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
@ -1435,7 +1435,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 1.30.0;
MARKETING_VERSION = 1.31.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -1490,7 +1490,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.30.0;
MARKETING_VERSION = 1.31.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1831,7 +1831,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.30.0;
MARKETING_VERSION = 1.31.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omnivore.app;
PRODUCT_NAME = Omnivore;
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -47,7 +47,7 @@ let package = Package(
.target(
name: "Utils",
dependencies: [
.product(name: "PostHog", package: "posthog-ios")
.product(name: "PostHog", package: "posthog-ios", condition: .when(platforms: [.iOS]))
],
resources: [.process("Resources")]
),
@ -58,7 +58,7 @@ let package = Package(
var appPackageDependencies: [Target.Dependency] {
var deps: [Target.Dependency] = ["Views", "Services", "Models", "Utils"]
// Comment out following line for macOS build
deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP"))
deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP", condition: .when(platforms: [.iOS])))
return deps
}

View File

@ -12,18 +12,15 @@ struct MacFeedCardNavigationLink: View {
@ObservedObject var viewModel: HomeFeedViewModel
var body: some View {
NavigationLink(
destination: LinkItemDetailView(
ZStack {
LibraryItemCard(item: item, viewer: dataService.currentViewer)
NavigationLink(destination: LinkItemDetailView(
linkedItemObjectID: item.objectID,
isPDF: item.isPDF
),
tag: item.objectID,
selection: $viewModel.selectedLinkItem
) {
LibraryItemCard(item: item, viewer: dataService.currentViewer)
), label: {
EmptyView()
}).opacity(0)
}
.opacity(0)
.buttonStyle(PlainButtonStyle())
.onAppear {
Task { await viewModel.itemAppeared(item: item, dataService: dataService) }
}

View File

@ -3,7 +3,6 @@ import Introspect
import Models
import Services
import SwiftUI
import UIKit
import Views
@MainActor final class FilterSelectorViewModel: NSObject, ObservableObject {
@ -44,9 +43,11 @@ struct FilterSelectorView: View {
.listStyle(.plain)
#endif
}
.navigationBarTitle("Library")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: doneButton)
#if os(iOS)
.navigationBarTitle("Library")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: doneButton)
#endif
}
private var innerBody: some View {

View File

@ -15,6 +15,8 @@ import Views
@State private var confirmationShown = false
@State private var presentProfileSheet = false
@Namespace var mainNamespace
@ObservedObject var viewModel: HomeFeedViewModel
func loadItems(isRefresh: Bool) {
@ -26,6 +28,95 @@ import Views
}
}
func menuItems(_ item: LinkedItem) -> some View {
Group {
Button(
action: { viewModel.itemUnderTitleEdit = item },
label: { Label("Edit Info", systemImage: "info.circle") }
)
Button(
action: { viewModel.itemUnderLabelEdit = item },
label: { Label(item.labels?.count == 0 ? "Add Labels" : "Edit Labels", systemImage: "tag") }
)
Button(action: {
withAnimation(.linear(duration: 0.4)) {
viewModel.setLinkArchived(
dataService: dataService,
objectID: item.objectID,
archived: !item.isArchived
)
}
}, label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
})
Button(
action: {
itemToRemove = item
confirmationShown = true
},
label: { Label("Remove", systemImage: "trash") }
)
}
}
var refreshButton: some View {
Button(
action: {
loadItems(isRefresh: true)
},
label: { Label("Refresh Feed", systemImage: "arrow.clockwise") }
)
.disabled(viewModel.isLoading)
.opacity(viewModel.isLoading ? 0 : 1)
.overlay {
if viewModel.isLoading {
ProgressView()
.controlSize(.small)
}
}
}
var itemsList: some View {
VStack {
MacSearchBar(searchTerm: $viewModel.searchTerm)
.padding(.leading, 10)
.padding(.trailing, 10)
.prefersDefaultFocus(false, in: mainNamespace)
List {
ForEach(viewModel.items) { item in
MacFeedCardNavigationLink(
item: item,
viewModel: viewModel
)
.listRowInsets(EdgeInsets())
.contextMenu {
menuItems(item)
}
Divider().padding(5)
}
.prefersDefaultFocus(true, in: mainNamespace)
if viewModel.isLoading {
LoadingSection()
}
}
.padding(0)
.listStyle(InsetListStyle())
.navigationTitle("Library")
.onChange(of: viewModel.searchTerm) { _ in
loadItems(isRefresh: true)
}
.toolbar {
Spacer()
refreshButton
}
}
}
var body: some View {
ZStack {
if let linkRequest = viewModel.linkRequest {
@ -37,97 +128,7 @@ import Views
EmptyView()
}
}
List {
Spacer(minLength: 10)
ForEach(viewModel.items) { item in
MacFeedCardNavigationLink(
item: item,
viewModel: viewModel
)
.listRowInsets(EdgeInsets())
.contextMenu {
Button(
action: { viewModel.itemUnderTitleEdit = item },
label: { Label("Edit Info", systemImage: "info.circle") }
)
Button(
action: { viewModel.itemUnderLabelEdit = item },
label: { Label(item.labels?.count == 0 ? "Add Labels" : "Edit Labels", systemImage: "tag") }
)
Button(action: {
withAnimation(.linear(duration: 0.4)) {
viewModel.setLinkArchived(
dataService: dataService,
objectID: item.objectID,
archived: !item.isArchived
)
}
}, label: {
Label(
item.isArchived ? "Unarchive" : "Archive",
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
)
})
Button(
action: {
itemToRemove = item
confirmationShown = true
},
label: { Label("Remove", systemImage: "trash") }
)
if FeatureFlag.enableSnooze {
Button {
viewModel.itemToSnoozeID = item.id
viewModel.snoozePresented = true
} label: {
Label { Text(LocalText.genericSnooze) } icon: { Image.moon }
}
}
}
Divider().padding(5)
}
if viewModel.isLoading {
LoadingSection()
}
}
.listStyle(InsetListStyle())
.navigationTitle("Library")
.searchable(
text: $viewModel.searchTerm,
placement: .toolbar
) {
if viewModel.searchTerm.isEmpty {
Text(LocalText.inboxGeneric).searchCompletion("in:inbox ")
Text(LocalText.allGeneric).searchCompletion("in:all ")
Text(LocalText.archivedGeneric).searchCompletion("in:archive ")
Text(LocalText.filesGeneric).searchCompletion("type:file ")
}
}
.onChange(of: viewModel.searchTerm) { _ in
loadItems(isRefresh: true)
}
.onSubmit(of: .search) {
loadItems(isRefresh: true)
}
.toolbar {
ToolbarItem {
Button(
action: {
loadItems(isRefresh: true)
},
label: { Label("Refresh Feed", systemImage: "arrow.clockwise") }
)
.disabled(viewModel.isLoading)
.opacity(viewModel.isLoading ? 0 : 1)
.overlay {
if viewModel.isLoading {
ProgressView()
}
}
}
}
itemsList
}
.alert("Are you sure?", isPresented: $confirmationShown) {
Button("Remove Link", role: .destructive) {
@ -160,6 +161,7 @@ import Views
loadItems(isRefresh: true)
}
}
.focusScope(mainNamespace)
.handlesExternalEvents(preferring: Set(["shareExtensionRequestID"]), allowing: Set(["*"]))
.onOpenURL { url in
viewModel.linkRequest = nil

View File

@ -1,81 +1,93 @@
#if os(iOS)
import Introspect
import Models
import Services
import SwiftUI
import UIKit
import Views
@MainActor final class LibraryAddLinkViewModel: NSObject, ObservableObject {
@Published var isLoading = false
@Published var errorMessage: String = ""
@Published var showErrorMessage: Bool = false
import Introspect
import Models
import Services
import SwiftUI
import Views
func addLink(dataService: DataService, newLinkURL: String, dismiss: DismissAction) {
isLoading = true
Task {
if URL(string: newLinkURL) == nil {
error("Invalid link")
@MainActor final class LibraryAddLinkViewModel: NSObject, ObservableObject {
@Published var isLoading = false
@Published var errorMessage: String = ""
@Published var showErrorMessage: Bool = false
func addLink(dataService: DataService, newLinkURL: String, dismiss: DismissAction) {
isLoading = true
Task {
if URL(string: newLinkURL) == nil {
error("Invalid link")
} else {
let result = try? await dataService.saveURL(id: UUID().uuidString, url: newLinkURL)
if result == nil {
error("Error adding link")
} else {
let result = try? await dataService.saveURL(id: UUID().uuidString, url: newLinkURL)
if result == nil {
error("Error adding link")
} else {
dismiss()
}
dismiss()
}
isLoading = false
}
}
func error(_ msg: String) {
errorMessage = msg
showErrorMessage = true
isLoading = false
}
}
struct LibraryAddLinkView: View {
@StateObject var viewModel = LibraryAddLinkViewModel()
func error(_ msg: String) {
errorMessage = msg
showErrorMessage = true
isLoading = false
}
}
@State var newLinkURL: String = ""
@EnvironmentObject var dataService: DataService
@Environment(\.dismiss) private var dismiss
struct LibraryAddLinkView: View {
@StateObject var viewModel = LibraryAddLinkViewModel()
enum FocusField: Hashable {
case addLinkEditor
}
@State var newLinkURL: String = ""
@EnvironmentObject var dataService: DataService
@Environment(\.dismiss) private var dismiss
@FocusState private var focusedField: FocusField?
enum FocusField: Hashable {
case addLinkEditor
}
var body: some View {
innerBody
.navigationTitle("Add Link")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
focusedField = .addLinkEditor
}
}
@FocusState private var focusedField: FocusField?
var innerBody: some View {
Form {
TextField("Add Link", text: $newLinkURL)
.keyboardType(.URL)
.autocorrectionDisabled(true)
.textFieldStyle(StandardTextFieldStyle())
.focused($focusedField, equals: .addLinkEditor)
Button(action: {
if let url = UIPasteboard.general.url {
newLinkURL = url.absoluteString
} else {
viewModel.error("No URL on pasteboard")
}
}, label: {
Text("Get from pasteboard")
})
}
var body: some View {
innerBody
.navigationTitle("Add Link")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.onAppear {
focusedField = .addLinkEditor
}
}
var pasteboardString: String? {
#if os(iOS)
UIPasteboard.general.url?.absoluteString
#else
NSPasteboard.general.string(forType: NSPasteboard.PasteboardType.URL)
#endif
}
var innerBody: some View {
Form {
TextField("Add Link", text: $newLinkURL)
#if os(iOS)
.keyboardType(.URL)
#endif
.autocorrectionDisabled(true)
.textFieldStyle(StandardTextFieldStyle())
.focused($focusedField, equals: .addLinkEditor)
Button(action: {
if let url = pasteboardString {
newLinkURL = url
} else {
viewModel.error("No URL on pasteboard")
}
}, label: {
Text("Get from pasteboard")
})
}
.navigationTitle("Add Link")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
@ -85,28 +97,28 @@
viewModel.isLoading ? AnyView(ProgressView()) : AnyView(addButton)
}
}
.alert(viewModel.errorMessage,
isPresented: $viewModel.showErrorMessage) {
Button(LocalText.genericOk, role: .cancel) { viewModel.showErrorMessage = false }
}
}
var addButton: some View {
Button(
action: {
viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss)
},
label: { Text("Add").bold() }
)
.disabled(viewModel.isLoading)
}
var dismissButton: some View {
Button(
action: { dismiss() },
label: { Text(LocalText.genericClose) }
)
.disabled(viewModel.isLoading)
#endif
.alert(viewModel.errorMessage,
isPresented: $viewModel.showErrorMessage) {
Button(LocalText.genericOk, role: .cancel) { viewModel.showErrorMessage = false }
}
}
#endif
var addButton: some View {
Button(
action: {
viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss)
},
label: { Text("Add").bold() }
)
.disabled(viewModel.isLoading)
}
var dismissButton: some View {
Button(
action: { dismiss() },
label: { Text(LocalText.genericClose) }
)
.disabled(viewModel.isLoading)
}
}

View File

@ -43,14 +43,9 @@ struct LibraryTabView: View {
)
var body: some View {
if #available(iOS 16.0, *) {
NavigationView {
HomeView(viewModel: libraryViewModel)
.navigationBarHidden(false)
}
} else {
// Fallback on earlier versions
EmptyView()
NavigationView {
HomeView(viewModel: libraryViewModel)
.navigationBarTitleDisplayMode(.inline)
}
}
}

View File

@ -89,10 +89,12 @@ struct LinkItemDetailView: View {
pdfContainerView
} else if let item = viewModel.item {
WebReaderContainerView(item: item, pop: { dismiss() })
#if os(iOS)
.navigationBarHidden(true)
.lazyPop(pop: {
dismiss()
}, isEnabled: $isEnabled)
#endif
}
}
.task {

View File

@ -9,6 +9,8 @@ import Views
PrimaryContentCategory.profile
]
@State var searchTerm: String = ""
public var body: some View {
innerBody
}
@ -18,9 +20,10 @@ import Views
if UIDevice.isIPad {
return AnyView(splitView)
} else {
return AnyView(LibraryTabView())
// .navigationViewStyle(.stack)
// .navigationBarTitleDisplayMode(.inline)
return AnyView(
LibraryTabView()
.navigationViewStyle(.stack)
)
}
#else
return AnyView(splitView)
@ -33,7 +36,6 @@ import Views
PrimaryContentCategory.feed.destinationView
Text(LocalText.navigationSelectLink)
}
.accentColor(.appGrayTextContrast)
}
#endif
@ -90,6 +92,7 @@ import Views
.sheet(isPresented: $addLinkPresented) {
NavigationView {
LibraryAddLinkView()
.navigationBarTitleDisplayMode(.inline)
}
}
}

View File

@ -6,60 +6,64 @@
// Copyright © 2016 Warif Akhand Rishi. All rights reserved.
//
import UIKit
#if os(iOS)
class SlideAnimatedTransitioning: NSObject {}
import UIKit
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
return
class SlideAnimatedTransitioning: NSObject {}
extension SlideAnimatedTransitioning: UIViewControllerAnimatedTransitioning {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
return
}
let width = containerView.frame.width
var offsetLeft = fromVC.view?.frame
offsetLeft?.origin.x = width
var offscreenRight = fromVC.view?.frame
offscreenRight?.origin.x = -width / 3.33
toVC.view?.frame = offscreenRight!
fromVC.view?.layer.shadowRadius = 5.0
fromVC.view?.layer.shadowOpacity = 1.0
toVC.view?.layer.opacity = 0.9
transitionContext.containerView.addSubview(toVC.view)
transitionContext.containerView.addSubview(fromVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: {
toVC.view?.frame = (fromVC.view?.frame)!
fromVC.view?.frame = offsetLeft!
toVC.view?.layer.opacity = 1.0
fromVC.view?.layer.shadowOpacity = 0.1
}, completion: { _ in
if !transitionContext.transitionWasCancelled {
toVC.view?.layer.opacity = 1.0
toVC.view?.layer.shadowOpacity = 0
fromVC.view?.layer.opacity = 1.0
fromVC.view?.layer.shadowOpacity = 0
fromVC.view.removeFromSuperview()
}
// when cancelling or completing the animation, ios simulator seems to sometimes flash black backgrounds during the animation. on devices, this doesn't seem to happen though.
// containerView.backgroundColor = [UIColor whiteColor];
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
let width = containerView.frame.width
var offsetLeft = fromVC.view?.frame
offsetLeft?.origin.x = width
var offscreenRight = fromVC.view?.frame
offscreenRight?.origin.x = -width / 3.33
toVC.view?.frame = offscreenRight!
fromVC.view?.layer.shadowRadius = 5.0
fromVC.view?.layer.shadowOpacity = 1.0
toVC.view?.layer.opacity = 0.9
transitionContext.containerView.addSubview(toVC.view)
transitionContext.containerView.addSubview(fromVC.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: {
toVC.view?.frame = (fromVC.view?.frame)!
fromVC.view?.frame = offsetLeft!
toVC.view?.layer.opacity = 1.0
fromVC.view?.layer.shadowOpacity = 0.1
}, completion: { _ in
if !transitionContext.transitionWasCancelled {
toVC.view?.layer.opacity = 1.0
toVC.view?.layer.shadowOpacity = 0
fromVC.view?.layer.opacity = 1.0
fromVC.view?.layer.shadowOpacity = 0
fromVC.view.removeFromSuperview()
}
// when cancelling or completing the animation, ios simulator seems to sometimes flash black backgrounds during the animation. on devices, this doesn't seem to happen though.
// containerView.backgroundColor = [UIColor whiteColor];
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
0.3
}
}
func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
0.3
}
}
#endif

View File

@ -10,144 +10,146 @@
// Copyright © 2019 Joseph Hinkle. All rights reserved.
//
import SwiftUI
private func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (lll?, rrr?):
return lll < rrr
case (nil, _?):
return true
default:
return false
}
}
#if os(iOS)
private func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (lll?, rrr?):
return lll > rrr
default:
return rhs < lhs
}
}
class SwipeRightToPopViewController<Content>: UIHostingController<Content>, UINavigationControllerDelegate where Content: View {
fileprivate var pop: (() -> Void)?
fileprivate var lazyPopContent: LazyPop<Content>?
private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition?
private var panGestureRecognizer: UIPanGestureRecognizer!
private var parentNavigationControllerToUse: UINavigationController?
private var gestureAdded = false
override func viewDidLayoutSubviews() {
// You need to add gesture events after every subview layout to protect against weird edge cases
// One notable edgecase is if you are in a splitview in landscape. In this case, there will be
// no nav controller with 2 vcs, so our addGesture will fail. After rotating back to portrait,
// the splitview will combine into one view with the details pushed on top. So only then would
// would the addGesture find a parent nav controller with 2 view controllers. I don't know if
// there are other edge cases, but running addGesture on every viewDidLayoutSubviews seems safe.
addGesture()
import SwiftUI
private func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (lll?, rrr?):
return lll < rrr
case (nil, _?):
return true
default:
return false
}
}
public func addGesture() {
if !gestureAdded {
// attempt to find a parent navigationController
var currentVc: UIViewController = self
while true {
if currentVc.navigationController != nil,
currentVc.navigationController?.viewControllers.count > 1
{
parentNavigationControllerToUse = currentVc.navigationController
break
private func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (lll?, rrr?):
return lll > rrr
default:
return rhs < lhs
}
}
class SwipeRightToPopViewController<Content>: UIHostingController<Content>, UINavigationControllerDelegate where Content: View {
fileprivate var pop: (() -> Void)?
fileprivate var lazyPopContent: LazyPop<Content>?
private var percentDrivenInteractiveTransition: UIPercentDrivenInteractiveTransition?
private var panGestureRecognizer: UIPanGestureRecognizer!
private var parentNavigationControllerToUse: UINavigationController?
private var gestureAdded = false
override func viewDidLayoutSubviews() {
// You need to add gesture events after every subview layout to protect against weird edge cases
// One notable edgecase is if you are in a splitview in landscape. In this case, there will be
// no nav controller with 2 vcs, so our addGesture will fail. After rotating back to portrait,
// the splitview will combine into one view with the details pushed on top. So only then would
// would the addGesture find a parent nav controller with 2 view controllers. I don't know if
// there are other edge cases, but running addGesture on every viewDidLayoutSubviews seems safe.
addGesture()
}
public func addGesture() {
if !gestureAdded {
// attempt to find a parent navigationController
var currentVc: UIViewController = self
while true {
if currentVc.navigationController != nil,
currentVc.navigationController?.viewControllers.count > 1
{
parentNavigationControllerToUse = currentVc.navigationController
break
}
guard let parent = currentVc.parent else {
return
}
currentVc = parent
}
guard let parent = currentVc.parent else {
guard parentNavigationControllerToUse?.viewControllers.count > 1 else {
return
}
currentVc = parent
}
guard parentNavigationControllerToUse?.viewControllers.count > 1 else {
return
}
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SwipeRightToPopViewController.handlePanGesture(_:)))
view.addGestureRecognizer(panGestureRecognizer)
gestureAdded = true
panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(SwipeRightToPopViewController.handlePanGesture(_:)))
view.addGestureRecognizer(panGestureRecognizer)
gestureAdded = true
}
}
}
@objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
// if the parentNavigationControllerToUse has a width value, use that because it's more accurate. Otherwise use this view's width as a backup
let total = parentNavigationControllerToUse?.view.frame.width ?? view.frame.width
let percent = max(panGesture.translation(in: view).x, 0) / total
@objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
// if the parentNavigationControllerToUse has a width value, use that because it's more accurate. Otherwise use this view's width as a backup
let total = parentNavigationControllerToUse?.view.frame.width ?? view.frame.width
let percent = max(panGesture.translation(in: view).x, 0) / total
switch panGesture.state {
case .began:
if lazyPopContent?.isEnabled == true {
parentNavigationControllerToUse?.delegate = self
if let pop = self.pop {
pop()
switch panGesture.state {
case .began:
if lazyPopContent?.isEnabled == true {
parentNavigationControllerToUse?.delegate = self
if let pop = self.pop {
pop()
}
}
}
case .changed:
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
percentDrivenInteractiveTransition.update(percent)
}
case .changed:
if let percentDrivenInteractiveTransition = percentDrivenInteractiveTransition {
percentDrivenInteractiveTransition.update(percent)
}
case .ended:
let velocity = panGesture.velocity(in: view).x
case .ended:
let velocity = panGesture.velocity(in: view).x
// Continue if drag more than 50% of screen width or velocity is higher than 100
if percent > 0.5 || velocity > 100 {
percentDrivenInteractiveTransition?.finish()
} else {
// Continue if drag more than 50% of screen width or velocity is higher than 100
if percent > 0.5 || velocity > 100 {
percentDrivenInteractiveTransition?.finish()
} else {
percentDrivenInteractiveTransition?.cancel()
}
case .cancelled, .failed:
percentDrivenInteractiveTransition?.cancel()
default:
break
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func navigationController(_: UINavigationController,
animationControllerFor _: UINavigationController.Operation,
from _: UIViewController,
to _: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
if #available(iOS 17.0, *) {
return nil
} else if UIDevice.isIPad {
return nil
} else {
return SlideAnimatedTransitioning()
}
}
func navigationController(_: UINavigationController,
interactionControllerFor _: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?
{
parentNavigationControllerToUse?.delegate = nil
// navigationController.delegate = nil
if panGestureRecognizer.state == .began {
percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
percentDrivenInteractiveTransition?.completionCurve = .easeOut
} else {
percentDrivenInteractiveTransition = nil
}
case .cancelled, .failed:
percentDrivenInteractiveTransition?.cancel()
default:
break
return percentDrivenInteractiveTransition
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func navigationController(_: UINavigationController,
animationControllerFor _: UINavigationController.Operation,
from _: UIViewController,
to _: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
if #available(iOS 17.0, *) {
return nil
} else if UIDevice.isIPad {
return nil
} else {
return SlideAnimatedTransitioning()
}
}
func navigationController(_: UINavigationController,
interactionControllerFor _: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning?
{
parentNavigationControllerToUse?.delegate = nil
// navigationController.delegate = nil
if panGestureRecognizer.state == .began {
percentDrivenInteractiveTransition = UIPercentDrivenInteractiveTransition()
percentDrivenInteractiveTransition?.completionCurve = .easeOut
} else {
percentDrivenInteractiveTransition = nil
}
return percentDrivenInteractiveTransition
}
}
//
// Lazy Pop SwiftUI Component
//
@ -155,33 +157,35 @@ class SwipeRightToPopViewController<Content>: UIHostingController<Content>, UINa
// Copyright © 2019 Joseph Hinkle. All rights reserved.
//
private struct LazyPop<Content: View>: UIViewControllerRepresentable {
let rootView: Content
let pop: () -> Void
@Binding var isEnabled: Bool
private struct LazyPop<Content: View>: UIViewControllerRepresentable {
let rootView: Content
let pop: () -> Void
@Binding var isEnabled: Bool
init(_ rootView: Content, pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) {
self.rootView = rootView
self.pop = pop
self._isEnabled = isEnabled ?? Binding<Bool>(get: { true }, set: { _ in })
}
init(_ rootView: Content, pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) {
self.rootView = rootView
self.pop = pop
self._isEnabled = isEnabled ?? Binding<Bool>(get: { true }, set: { _ in })
}
func makeUIViewController(context _: Context) -> UIViewController {
let vc = SwipeRightToPopViewController(rootView: rootView)
vc.pop = pop
vc.lazyPopContent = self
return vc
}
func makeUIViewController(context _: Context) -> UIViewController {
let vc = SwipeRightToPopViewController(rootView: rootView)
vc.pop = pop
vc.lazyPopContent = self
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context _: Context) {
if let host = uiViewController as? UIHostingController<Content> {
host.rootView = rootView
func updateUIViewController(_ uiViewController: UIViewController, context _: Context) {
if let host = uiViewController as? UIHostingController<Content> {
host.rootView = rootView
}
}
}
}
public extension View {
func lazyPop(pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) -> some View {
LazyPop(self, pop: pop, isEnabled: isEnabled)
public extension View {
func lazyPop(pop: @escaping () -> Void, isEnabled: (Binding<Bool>)? = nil) -> some View {
LazyPop(self, pop: pop, isEnabled: isEnabled)
}
}
}
#endif

View File

@ -427,7 +427,9 @@ struct WebReaderContainerView: View {
showHighlightAnnotationModal: $showHighlightAnnotationModal
)
.background(ThemeManager.currentBgColor)
.statusBar(hidden: prefersHideStatusBarInReader)
#if os(iOS)
.statusBar(hidden: prefersHideStatusBarInReader)
#endif
.onAppear {
if item.isUnread {
dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0)

View File

@ -7,7 +7,6 @@ import Views
struct WelcomeView: View {
@EnvironmentObject var dataService: DataService
@EnvironmentObject var authenticator: Authenticator
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.openURL) var openURL
@StateObject private var viewModel = RegistrationViewModel()

View File

@ -1,24 +1,29 @@
import Foundation
import PostHog
#if os(iOS)
import PostHog
#endif
public enum EventTracker {
public static var posthog: PHGPostHog? = {
guard let writeKey = AppKeys.sharedInstance?.posthogClientKey else {
return nil
}
#if os(iOS)
public static var posthog: PHGPostHog? = {
guard let writeKey = AppKeys.sharedInstance?.posthogClientKey else {
return nil
}
guard let posthogInstanceAddress = AppKeys.sharedInstance?.posthogInstanceAddress else {
return nil
}
guard let posthogInstanceAddress = AppKeys.sharedInstance?.posthogInstanceAddress else {
return nil
}
let configuration = PHGPostHogConfiguration(apiKey: writeKey, host: posthogInstanceAddress)
let configuration = PHGPostHogConfiguration(apiKey: writeKey, host: posthogInstanceAddress)
configuration.recordScreenViews = false
configuration.captureApplicationLifecycleEvents = true
configuration.recordScreenViews = false
configuration.captureApplicationLifecycleEvents = true
PHGPostHog.setup(with: configuration)
return PHGPostHog.shared()
}()
PHGPostHog.setup(with: configuration)
return PHGPostHog.shared()
}()
#endif
public static func trackForDebugging(_ message: String) {
#if DEBUG
@ -27,14 +32,20 @@ public enum EventTracker {
}
public static func track(_ event: TrackableEvent) {
posthog?.capture(event.name, properties: event.properties)
#if os(iOS)
posthog?.capture(event.name, properties: event.properties)
#endif
}
public static func registerUser(userID: String) {
posthog?.identify(userID)
#if os(iOS)
posthog?.identify(userID)
#endif
}
public static func reset() {
posthog?.reset()
#if os(iOS)
posthog?.reset()
#endif
}
}

View File

@ -0,0 +1,57 @@
import SwiftUI
#if os(macOS)
public struct MacSearchBar: NSViewRepresentable {
@Binding var searchTerm: String
public init(
searchTerm: Binding<String>
) {
self._searchTerm = searchTerm
}
public func makeNSView(context _: Context) -> NSSearchField {
let searchField = NSSearchField(frame: .zero)
searchField.translatesAutoresizingMaskIntoConstraints = false
searchField.heightAnchor.constraint(greaterThanOrEqualToConstant: 28).isActive = true
searchField.resignFirstResponder()
return searchField
}
func changeSearchFieldItem(searchField: NSSearchField, sender: AnyObject) -> NSSearchField {
// Based on the Menu item selection in the search field the placeholder string is set
(searchField.cell as? NSSearchFieldCell)?.placeholderString = sender.title
return searchField
}
public func updateNSView(_ searchField: NSSearchField, context: Context) {
searchField.font = searchField.font?.withSize(14)
searchField.stringValue = searchTerm
searchField.delegate = context.coordinator
searchField.resignFirstResponder()
}
public func makeCoordinator() -> Coordinator {
let coordinator = Coordinator(searchTerm: $searchTerm)
return coordinator
}
public class Coordinator: NSObject, NSSearchFieldDelegate {
var searchTerm: Binding<String>
init(searchTerm: Binding<String>) {
self.searchTerm = searchTerm
super.init()
}
public func controlTextDidChange(_ notification: Notification) {
guard let searchField = notification.object as? NSSearchField else {
// log.error("Unexpected control in update notification", source: .ui)
return
}
searchTerm.wrappedValue = searchField.stringValue
}
}
}
#endif

View File

@ -8,10 +8,10 @@ import SwiftUI
// https://stackoverflow.com/questions/63526478/swiftui-userinterfacesizeclass-for-universal-macos-ios-views
#if os(macOS)
public enum UserInterfaceSizeClass {
case compact
case regular
}
// public enum UserInterfaceSizeClass {
// case compact
// case regular
// }
public struct HorizontalSizeClassEnvironmentKey: EnvironmentKey {
public static let defaultValue: UserInterfaceSizeClass = .regular

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 716 B

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "Omnivore",
"short_name": "Omnivore",
"version": "2.2.0",
"version": "2.4.2",
"description": "Save PDFs and Articles to your Omnivore library",
"author": "Omnivore Media, Inc",
"default_locale": "en",
@ -75,12 +75,12 @@
],
"browser_action": {
"default_icon": {
"16": "/images/toolbar/icon-16.png",
"19": "/images/toolbar/icon-19.png",
"24": "/images/toolbar/icon-24.png",
"32": "/images/toolbar/icon-32.png",
"38": "/images/toolbar/icon-38.png",
"48": "/images/toolbar/icon-48.png"
"16": "/images/extension/icon-16.png",
"19": "/images/extension/icon-19.png",
"24": "/images/extension/icon-24.png",
"32": "/images/extension/icon-32.png",
"38": "/images/extension/icon-38.png",
"48": "/images/extension/icon-48.png"
},
"default_title": "Omnivore Save Article"
},

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
"use strict";!function(){const e={};function t(t){const n=t.tagName.toLowerCase();if("iframe"===n){const n=e[t.src];if(!n)return;const o=document.createElement("div");o.className="omnivore-instagram-embed",o.innerHTML=n;const r=t.parentNode;if(!r)return;return void r.replaceChild(o,t)}if("img"===n||"image"===n){if(-1===window.getComputedStyle(t).getPropertyValue("filter").indexOf("blur("))return;return void t.remove()}const o=window.getComputedStyle(t),r=o.getPropertyValue("background-image");if(r&&"none"!==r)return;const i=o.getPropertyValue("filter");if(i&&-1!==i.indexOf("blur("))return void t.remove();if(t.src)return;if(t.innerHTML.length>24)return;const c=/url\("(.+?)"\)/gi,a=c.exec(r);c.lastIndex=0;const l=a&&a[1];if(!l)return;const s=document.createElement("img");s.src=l;const d=t.parentNode;d&&d.replaceChild(s,t)}function n(){const e=document.createElement("div");e.style.position="absolute",e.style.left="-2000px",e.style.zIndex="-2000",e.innerHTML=document.body.innerHTML,document.documentElement.appendChild(e),Array.from(e.getElementsByTagName("*")).forEach(t);try{if("undefined"!=typeof create_time&&create_time){const t=new Date(1e3*create_time),n=document.createElement("div");n.className="omnivore-published-date",n.innerHTML=t.toLocaleString(),e.appendChild(n)}}catch(e){console.log("Error while trying to add published date to WeChat post",e)}const n=`<html><head>${document.head.innerHTML}</head><body>${e.innerHTML}</body></html>`;return e.remove(),n}function o(){const e=document.querySelectorAll(".webext-omnivore-backdrop");for(let t=0;t<e.length;t++)e[t].style.setProperty("opacity","0","important");setTimeout((()=>{for(let t=0;t<e.length;t++)e[t].remove()}),500)}browserApi.runtime.onMessage.addListener((({action:t,payload:n},o,r)=>{if(t!==ACTIONS.AddIframeContent)return;const{url:i,content:c}=n;e[i]=c,r({})})),window.prepareContent=async function(){const e=await async function(){const e=".pdf"===window.location.pathname.slice(-4).toLowerCase(),t=-1!==["application/acrobat","application/pdf","application/x-pdf","applications/vnd.pdf","text/pdf","text/x-pdf"].indexOf(document.contentType);if(!e&&!t)return Promise.resolve(null);const n=document.querySelector("embed");return n&&"application/pdf"!==n.type?Promise.resolve(null):ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS&&n.src?Promise.resolve({type:"url",uploadContentObjUrl:n.src}):new Promise(((e,t)=>{const n=new XMLHttpRequest;n.open("GET","",!0),n.responseType="blob",n.onload=function(n){200===this.status?e({type:"pdf",uploadContentObjUrl:URL.createObjectURL(this.response)}):t(n)},n.send()}))}();if(e)return e;const t=window.location.href;try{if(handleBackendUrl(t))return{type:"url"}}catch{console.log("error checking url")}return await async function(e){const t=document.scrollingElement||document.body,n=t.scrollTop,r=t.scrollHeight;o();const i=function(){const e=document.createElement("div");return e.className="webext-omnivore-backdrop",e.style.cssText="all: initial !important;\n position: fixed !important;\n top: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n left: 0 !important;\n z-index: 99999 !important;\n background: #fff !important;\n opacity: 0.8 !important;\n transition: opacity 0.3s !important;\n -webkit-backdrop-filter: blur(4px) !important;\n backdrop-filter: blur(4px) !important;\n ",e}();for(document.body.appendChild(i);t.scrollTop<=r-500&&window.location.href===e;){const e=t.scrollTop;if(t.scrollTop+=500,await new Promise((e=>{setTimeout(e,10)})),t.scrollTop===e)break}t.scrollTop=n,await new Promise((e=>{setTimeout(e,10)}))}(t),o(),{type:"html",content:n()}}}();
"use strict";!function(){const e={};function t(t){const n=t.tagName.toLowerCase();if("iframe"===n){const n=e[t.src];if(!n)return;const o=document.createElement("div");o.className="omnivore-instagram-embed",o.innerHTML=n;const r=t.parentNode;if(!r)return;return void r.replaceChild(o,t)}if("img"===n||"image"===n){if(-1===window.getComputedStyle(t).getPropertyValue("filter").indexOf("blur("))return;return void t.remove()}const o=window.getComputedStyle(t),r=o.getPropertyValue("background-image");if(r&&"none"!==r)return;const i=o.getPropertyValue("filter");if(i&&-1!==i.indexOf("blur("))return void t.remove();if(t.src)return;if(t.innerHTML.length>24)return;const c=/url\("(.+?)"\)/gi,a=c.exec(r);c.lastIndex=0;const l=a&&a[1];if(!l)return;const s=document.createElement("img");s.src=l;const p=t.parentNode;p&&p.replaceChild(s,t)}function n(){const e=document.createElement("div");e.style.position="absolute",e.style.left="-2000px",e.style.zIndex="-2000",e.innerHTML=document.body.innerHTML,document.documentElement.appendChild(e),Array.from(e.getElementsByTagName("*")).forEach(t);const n=`<html><head>${document.head.innerHTML}</head><body>${e.innerHTML}</body></html>`;return e.remove(),n}function o(){const e=document.querySelectorAll(".webext-omnivore-backdrop");for(let t=0;t<e.length;t++)e[t].style.setProperty("opacity","0","important");setTimeout((()=>{for(let t=0;t<e.length;t++)e[t].remove()}),500)}browserApi.runtime.onMessage.addListener((({action:t,payload:n},o,r)=>{if(t!==ACTIONS.AddIframeContent)return;const{url:i,content:c}=n;e[i]=c,r({})})),window.prepareContent=async function(){const e=await async function(){const e=".pdf"===window.location.pathname.slice(-4).toLowerCase(),t=-1!==["application/acrobat","application/pdf","application/x-pdf","applications/vnd.pdf","text/pdf","text/x-pdf"].indexOf(document.contentType);if(!e&&!t)return Promise.resolve(null);const n=document.querySelector("embed");return n&&"application/pdf"!==n.type?Promise.resolve(null):ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS&&n.src?Promise.resolve({type:"url",uploadContentObjUrl:n.src}):new Promise(((e,t)=>{const n=new XMLHttpRequest;n.open("GET","",!0),n.responseType="blob",n.onload=function(n){200===this.status?e({type:"pdf",uploadContentObjUrl:URL.createObjectURL(this.response)}):t(n)},n.send()}))}();if(e)return e;const t=window.location.href;try{if(handleBackendUrl(t))return{type:"url"}}catch{console.log("error checking url")}return await async function(e){const t=document.scrollingElement||document.body,n=t.scrollTop,r=t.scrollHeight;o();const i=function(){const e=document.createElement("div");return e.className="webext-omnivore-backdrop",e.style.cssText="all: initial !important;\n position: fixed !important;\n top: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n left: 0 !important;\n z-index: 99999 !important;\n background: #fff !important;\n opacity: 0.8 !important;\n transition: opacity 0.3s !important;\n -webkit-backdrop-filter: blur(4px) !important;\n backdrop-filter: blur(4px) !important;\n ",e}();for(document.body.appendChild(i);t.scrollTop<=r-500&&window.location.href===e;){const e=t.scrollTop;if(t.scrollTop+=500,await new Promise((e=>{setTimeout(e,10)})),t.scrollTop===e)break}t.scrollTop=n,await new Promise((e=>{setTimeout(e,10)}))}(t),o(),{type:"html",content:n()}}}();

File diff suppressed because one or more lines are too long