diff --git a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5090a25ab..0667008b4 100644 --- a/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apple/Omnivore.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -153,6 +153,15 @@ "version" : "2.1.0" } }, + { + "identity" : "pspdfkit-sp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PSPDFKit/PSPDFKit-SP", + "state" : { + "branch" : "master", + "revision" : "e9757beadad1b30de84073d3c33c1cd0f7a94b80" + } + }, { "identity" : "sovran-swift", "kind" : "remoteSourceControl", diff --git a/apple/OmnivoreKit/Package.swift b/apple/OmnivoreKit/Package.swift index e72036ffe..f5ef78249 100644 --- a/apple/OmnivoreKit/Package.swift +++ b/apple/OmnivoreKit/Package.swift @@ -55,9 +55,9 @@ let package = Package( var appPackageDependencies: [Target.Dependency] { var deps: [Target.Dependency] = ["Views", "Services", "Models", "Utils"] - #if canImport(UIKit) - deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP")) - #endif +// #if canImport(UIKit) + deps.append(.product(name: "PSPDFKit", package: "PSPDFKit-SP")) +// #endif return deps } @@ -69,8 +69,8 @@ var dependencies: [Package.Dependency] { .package(url: "git@github.com:segmentio/analytics-swift.git", .upToNextMajor(from: "1.0.0")), .package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2") ] - #if canImport(UIKit) - deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master")) - #endif +// #if canImport(UIKit) + deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", branch: "master")) +// #endif return deps } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift deleted file mode 100644 index 9ecd7fc0f..000000000 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ /dev/null @@ -1,198 +0,0 @@ -import Foundation -import Models -import Services -import Utils -import Views - -class ExtensionSaveService { - let queue: OperationQueue - - init() { - self.queue = OperationQueue() - } - - #if os(iOS) - private func queueSaveOperation( - _ pageScrape: PageScrapePayload, - shareExtensionViewModel: ShareExtensionChildViewModel - ) { - ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in - guard !expiring else { - self.queue.cancelAllOperations() - self.queue.waitUntilAllOperationsAreFinished() - return - } - - let operation = SaveOperation(pageScrapePayload: pageScrape, shareExtensionViewModel: shareExtensionViewModel) - - self.queue.addOperation(operation) - self.queue.waitUntilAllOperationsAreFinished() - } - } - #endif - - public func save(_ extensionContext: NSExtensionContext, shareExtensionViewModel: ShareExtensionChildViewModel) { - PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in - guard let self = self else { return } - - switch result { - case let .success(payload): - DispatchQueue.main.async { - shareExtensionViewModel.status = .saved - - let url = URLComponents(string: payload.url) - let hostname = URL(string: payload.url)?.host ?? "" - - switch payload.contentType { - case let .html(html: _, title: title, iconURL: iconURL): - shareExtensionViewModel.title = title - shareExtensionViewModel.iconURL = iconURL - shareExtensionViewModel.url = hostname - case .none: - shareExtensionViewModel.url = hostname - shareExtensionViewModel.title = payload.url - if var url = url { - url.path = "/favicon.ico" - shareExtensionViewModel.iconURL = url.url?.absoluteString - } - case let .pdf(localUrl: localUrl): - shareExtensionViewModel.url = hostname - shareExtensionViewModel.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString) - Task { - let localThumbnail = try await PDFUtils.createThumbnailFor(inputUrl: localUrl) - DispatchQueue.main.async { - shareExtensionViewModel.iconURL = localThumbnail?.absoluteString - } - } - } - } - #if os(iOS) - // TODO: need alternative call for macos - self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel) - #endif - case .failure: - DispatchQueue.main.async { - shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content")) - } - } - } - } - - class SaveOperation: Operation, URLSessionDelegate { - let services: Services - let pageScrapePayload: PageScrapePayload - let shareExtensionViewModel: ShareExtensionChildViewModel - - var queue: OperationQueue? - var uploadTask: URLSessionTask? - - // swiftlint:disable:next nesting - enum State: Int { - case created - case started - case finished - } - - init(pageScrapePayload: PageScrapePayload, shareExtensionViewModel: ShareExtensionChildViewModel) { - self.pageScrapePayload = pageScrapePayload - self.shareExtensionViewModel = shareExtensionViewModel - - self.state = .created - self.services = Services() - } - - open var state: State = .created { - willSet { - willChangeValue(forKey: "isReady") - willChangeValue(forKey: "isExecuting") - willChangeValue(forKey: "isFinished") - willChangeValue(forKey: "isCancelled") - } - didSet { - didChangeValue(forKey: "isCancelled") - didChangeValue(forKey: "isFinished") - didChangeValue(forKey: "isExecuting") - didChangeValue(forKey: "isReady") - } - } - - override var isAsynchronous: Bool { - true - } - - override var isReady: Bool { - true - } - - override var isExecuting: Bool { - self.state == .started - } - - override var isFinished: Bool { - self.state == .finished - } - - override func start() { - guard !isCancelled else { return } - state = .started - queue = OperationQueue() - - Task { - await persist(services: self.services, pageScrapePayload: self.pageScrapePayload) - } - } - - override func cancel() { - super.cancel() - } - - private func updateStatus(_ requestId: String?, newStatus: ShareExtensionStatus) { - DispatchQueue.main.async { - self.shareExtensionViewModel.status = newStatus - if let requestId = requestId { - self.shareExtensionViewModel.requestId = requestId - } - } - } - - private func persist(services: Services, pageScrapePayload: PageScrapePayload) async { - var requestId = shareExtensionViewModel.requestId - - do { - try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) - } catch { - updateStatus(nil, newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content"))) - return - } - - do { - updateStatus(requestId, newStatus: .saved) - - switch pageScrapePayload.contentType { - case .none: - requestId = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url) - case let .pdf(localUrl): - try await services.dataService.createPageFromPdf( - id: requestId, - localPdfURL: localUrl, - url: pageScrapePayload.url - ) - case let .html(html, title, _): - requestId = try await services.dataService.createPage( - id: requestId, - originalHtml: html, - title: title, - url: pageScrapePayload.url - ) - } - - } catch { - updateStatus(nil, newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error"))) - return - } - - updateStatus(requestId, newStatus: .synced) - state = .finished - } - } -} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionSaveOperation.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionSaveOperation.swift new file mode 100644 index 000000000..8af1f4715 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionSaveOperation.swift @@ -0,0 +1,76 @@ +import Foundation +import Models +import Services +import Utils +import Views + +final class ShareExtensionSaveOperation: Operation, URLSessionDelegate { + let pageScrapePayload: PageScrapePayload + let shareExtensionViewModel: ShareExtensionViewModel + + var queue: OperationQueue? + var uploadTask: URLSessionTask? + + enum State: Int { + case created + case started + case finished + } + + init(pageScrapePayload: PageScrapePayload, shareExtensionViewModel: ShareExtensionViewModel) { + self.pageScrapePayload = pageScrapePayload + self.shareExtensionViewModel = shareExtensionViewModel + + self.state = .created + } + + public var state: State = .created { + willSet { + willChangeValue(forKey: "isReady") + willChangeValue(forKey: "isExecuting") + willChangeValue(forKey: "isFinished") + willChangeValue(forKey: "isCancelled") + } + didSet { + didChangeValue(forKey: "isCancelled") + didChangeValue(forKey: "isFinished") + didChangeValue(forKey: "isExecuting") + didChangeValue(forKey: "isReady") + } + } + + override var isAsynchronous: Bool { + true + } + + override var isReady: Bool { + true + } + + override var isExecuting: Bool { + self.state == .started + } + + override var isFinished: Bool { + self.state == .finished + } + + override func start() { + guard !isCancelled else { return } + state = .started + queue = OperationQueue() + + Task { + let pageCreated = await shareExtensionViewModel.createPage( + pageScrapePayload: pageScrapePayload + ) + if pageCreated { + state = .finished + } + } + } + + override func cancel() { + super.cancel() + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 1d262edb7..ec54f0503 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -1,9 +1,5 @@ -import Foundation -import Models -import Services import SwiftUI import Utils -import Views public extension PlatformViewController { static func makeShareExtensionController(extensionContext: NSExtensionContext?) -> PlatformViewController { @@ -18,52 +14,3 @@ public extension PlatformViewController { return hostingController } } - -public class ShareExtensionViewModel: ObservableObject { - @Published var title: String? - @Published var debugText: String? - - let saveService = ExtensionSaveService() - - func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) { - #if os(iOS) - if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { - let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") - application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) - } - #endif - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - } - - func savePage(extensionContext: NSExtensionContext?, shareExtensionViewModel: ShareExtensionChildViewModel) { - if let extensionContext = extensionContext { - saveService.save(extensionContext, shareExtensionViewModel: shareExtensionViewModel) - } else { - DispatchQueue.main.async { - shareExtensionViewModel.status = .failed(error: .unknown(description: "Internal Error")) - } - } - } -} - -struct ShareExtensionView: View { - let extensionContext: NSExtensionContext? - @StateObject private var viewModel = ShareExtensionViewModel() - @StateObject private var childViewModel = ShareExtensionChildViewModel() - - var body: some View { - ShareExtensionChildView( - viewModel: childViewModel, - onAppearAction: { - viewModel.savePage( - extensionContext: extensionContext, - shareExtensionViewModel: childViewModel - ) - }, - readNowButtonAction: { viewModel.handleReadNowAction(requestId: $0, extensionContext: extensionContext) }, - dismissButtonTappedAction: { _, _ in - extensionContext?.completeRequest(returningItems: [], completionHandler: nil) - } - ) - } -} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift new file mode 100644 index 000000000..06f6b7f6a --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -0,0 +1,194 @@ +import Models +import SwiftUI +import Utils +import Views + +public class ShareExtensionViewModel: ObservableObject { + @Published public var status: ShareExtensionStatus = .processing + @Published public var title: String? + @Published public var url: String? + @Published public var iconURL: String? + @Published public var linkedItem: LinkedItem? + @Published public var requestId = UUID().uuidString.lowercased() + @Published var debugText: String? + + let services = Services() + let queue = OperationQueue() + + func handleReadNowAction(extensionContext: NSExtensionContext?) { + #if os(iOS) + if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { + let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") + application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) + } + #endif + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + func savePage(extensionContext: NSExtensionContext?) { + if let extensionContext = extensionContext { + save(extensionContext) + } else { + DispatchQueue.main.async { + self.status = .failed(error: .unknown(description: "Internal Error")) + } + } + } + + #if os(iOS) + func queueSaveOperation(_ payload: PageScrapePayload) { + ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in + guard !expiring else { + self.queue.cancelAllOperations() + self.queue.waitUntilAllOperationsAreFinished() + return + } + + let operation = ShareExtensionSaveOperation(pageScrapePayload: payload, shareExtensionViewModel: self) + self.queue.addOperation(operation) + self.queue.waitUntilAllOperationsAreFinished() + } + } + #endif + + public func save(_ extensionContext: NSExtensionContext) { + PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in + guard let self = self else { return } + + switch result { + case let .success(payload): + DispatchQueue.main.async { + self.status = .saved + + let url = URLComponents(string: payload.url) + let hostname = URL(string: payload.url)?.host ?? "" + + switch payload.contentType { + case let .html(html: _, title: title, iconURL: iconURL): + self.title = title + self.iconURL = iconURL + self.url = hostname + case .none: + self.url = hostname + self.title = payload.url + if var url = url { + url.path = "/favicon.ico" + self.iconURL = url.url?.absoluteString + } + case let .pdf(localUrl: localUrl): + self.url = hostname + self.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString) + Task { + let localThumbnail = try await PDFUtils.createThumbnailFor(inputUrl: localUrl) + DispatchQueue.main.async { + self.iconURL = localThumbnail?.absoluteString + } + } + } + } + + #if os(iOS) + self.queueSaveOperation(payload) + #else + Task { + await createPage(services: self.services, pageScrapePayload: payload) + } + #endif + case .failure: + DispatchQueue.main.async { + self.status = .failed(error: .unknown(description: "Could not retrieve content")) + } + } + } + } + + func createPage(pageScrapePayload: PageScrapePayload) async -> Bool { + var newRequestID: String? + + do { + try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) + } catch { + updateStatusOnMain( + requestId: nil, + newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content")) + ) + return false + } + + do { + updateStatusOnMain(requestId: requestId, newStatus: .saved) + + switch pageScrapePayload.contentType { + case .none: + newRequestID = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url) + case let .pdf(localUrl): + try await services.dataService.createPageFromPdf( + id: requestId, + localPdfURL: localUrl, + url: pageScrapePayload.url + ) + case let .html(html, title, _): + newRequestID = try await services.dataService.createPage( + id: requestId, + originalHtml: html, + title: title, + url: pageScrapePayload.url + ) + } + } catch { + updateStatusOnMain( + requestId: nil, + newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error")) + ) + return false + } + + updateStatusOnMain(requestId: newRequestID, newStatus: .synced) + return true + } + + func updateStatusOnMain(requestId: String?, newStatus: ShareExtensionStatus) { + DispatchQueue.main.async { + self.status = newStatus + if let requestId = requestId { + self.requestId = requestId + } + } + } +} + +public enum ShareExtensionStatus { + case processing + case saved + case synced + case failed(error: SaveArticleError) + case syncFailed(error: SaveArticleError) + + var displayMessage: String { + switch self { + case .processing: + return LocalText.saveArticleProcessingState + case .saved: + return LocalText.saveArticleSavedState + case .synced: + return "Synced" + case let .failed(error: error): + return "Save failed \(error.displayMessage)" + case let .syncFailed(error: error): + return "Sync failed \(error.displayMessage)" + } + } +} + +private extension SaveArticleError { + var displayMessage: String { + switch self { + case .unauthorized: + return LocalText.extensionAppUnauthorized + case .network: + return LocalText.networkError + case .badData, .unknown: + return LocalText.genericError + } + } +} diff --git a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift similarity index 60% rename from apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift rename to apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index fb8d32fb7..3089ac009 100644 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -1,126 +1,16 @@ import Models +import Services import SwiftUI import Utils +import Views -public class ShareExtensionChildViewModel: ObservableObject { - @Published public var status: ShareExtensionStatus = .processing - @Published public var title: String? - @Published public var url: String? - @Published public var iconURL: String? - @Published public var requestId: String - - public init() { - self.requestId = UUID().uuidString.lowercased() - } -} - -public enum ShareExtensionStatus { - case processing - case saved - case synced - case failed(error: SaveArticleError) - case syncFailed(error: SaveArticleError) - - var displayMessage: String { - switch self { - case .processing: - return LocalText.saveArticleProcessingState - case .saved: - return LocalText.saveArticleSavedState - case .synced: - return "Synced" - case let .failed(error: error): - return "Save failed \(error.displayMessage)" - case let .syncFailed(error: error): - return "Sync failed \(error.displayMessage)" - } - } -} - -private extension SaveArticleError { - var displayMessage: String { - switch self { - case .unauthorized: - return LocalText.extensionAppUnauthorized - case .network: - return LocalText.networkError - case .badData, .unknown: - return LocalText.genericError - } - } -} - -struct IconButtonView: View { - let title: String - let systemIconName: String - let action: () -> Void - - var body: some View { - Button(action: action) { - VStack(alignment: .center, spacing: 8) { - Image(systemName: systemIconName) - .font(.appTitle) - .foregroundColor(.appYellow48) - Text(title) - .font(.appBody) - .foregroundColor(.appGrayText) - } - .frame( - maxWidth: .infinity, - maxHeight: .infinity - ) - .background(Color.appButtonBackground) - .cornerRadius(8) - } - .frame(height: 100) - } -} - -struct CheckmarkButtonView: View { - let titleText: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button( - action: action, - label: { - HStack { - Text(titleText) - Spacer() - if isSelected { - Image(systemName: "checkmark") - .foregroundColor(.appYellow48) - } - } - .padding(.vertical, 8) - } - ) - .buttonStyle(RectButtonStyle()) - } -} - -public struct ShareExtensionChildView: View { - let viewModel: ShareExtensionChildViewModel - let onAppearAction: () -> Void - let readNowButtonAction: (String) -> Void - let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void +public struct ShareExtensionView: View { + let extensionContext: NSExtensionContext? + @StateObject private var viewModel = ShareExtensionViewModel() @State var reminderTime: ReminderTime? @State var hideUntilReminded = false - public init( - viewModel: ShareExtensionChildViewModel, - onAppearAction: @escaping () -> Void, - readNowButtonAction: @escaping (String) -> Void, - dismissButtonTappedAction: @escaping (ReminderTime?, Bool) -> Void - ) { - self.viewModel = viewModel - self.onAppearAction = onAppearAction - self.readNowButtonAction = readNowButtonAction - self.dismissButtonTappedAction = dismissButtonTappedAction - } - private func handleReminderTimeSelection(_ selectedTime: ReminderTime) { if selectedTime == reminderTime { reminderTime = nil @@ -263,16 +153,20 @@ public struct ShareExtensionChildView: View { Spacer() + if let item = viewModel.linkedItem { + ApplyLabelsView(mode: .item(item), onSave: nil) + } + HStack { Button( - action: { readNowButtonAction(self.viewModel.requestId) }, + action: { viewModel.handleReadNowAction(extensionContext: extensionContext) }, label: { Text("Read Now").frame(maxWidth: .infinity) } ) .buttonStyle(RoundedRectButtonStyle()) Button( action: { - dismissButtonTappedAction(reminderTime, hideUntilReminded) + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) }, label: { Text("Dismiss") @@ -290,7 +184,8 @@ public struct ShareExtensionChildView: View { alignment: .topLeading ) .onAppear { - onAppearAction() + viewModel.savePage(extensionContext: extensionContext) } + .environmentObject(viewModel.services.dataService) } } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionViewComponents.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionViewComponents.swift new file mode 100644 index 000000000..fa8f67673 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionViewComponents.swift @@ -0,0 +1,53 @@ +import SwiftUI +import Views + +// TODO: maybe move this into Views package? +struct IconButtonView: View { + let title: String + let systemIconName: String + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(alignment: .center, spacing: 8) { + Image(systemName: systemIconName) + .font(.appTitle) + .foregroundColor(.appYellow48) + Text(title) + .font(.appBody) + .foregroundColor(.appGrayText) + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity + ) + .background(Color.appButtonBackground) + .cornerRadius(8) + } + .frame(height: 100) + } +} + +struct CheckmarkButtonView: View { + let titleText: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button( + action: action, + label: { + HStack { + Text(titleText) + Spacer() + if isSelected { + Image(systemName: "checkmark") + .foregroundColor(.appYellow48) + } + } + .padding(.vertical, 8) + } + ) + .buttonStyle(RectButtonStyle()) + } +} diff --git a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift new file mode 100644 index 000000000..4cc36fa9b --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift @@ -0,0 +1,120 @@ +import SwiftUI +import Utils +import Views + +#if os(macOS) + public struct MacMenuCommands: Commands { + @AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize = Int( + NSFont.userFont(ofSize: 16)?.pointSize ?? 16 + ) + @AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150 + @AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100 + + @Binding var preferredFont: String + @Binding var prefersHighContrastText: Bool + + public var fontSizeButtons: some View { + Group { + Button( + action: { + storedFontSize = max(storedFontSize - 2, 10) + NSNotification.readerSettingsChanged() + }, + label: { Text("Decrease Font Size") + } + ) + .keyboardShortcut("-") + + Button( + action: { + storedFontSize = min(storedFontSize + 2, 28) + NSNotification.readerSettingsChanged() + }, + label: { Text("Increase Font Size") } + ) + .keyboardShortcut("+") + } + } + + public var marginSizeButtons: some View { + Group { + Button( + action: { + storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) + NSNotification.readerSettingsChanged() + }, + label: { Text("Decrease Margin") + } + ) + .keyboardShortcut("[") + + Button( + action: { + storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) + NSNotification.readerSettingsChanged() + }, + label: { Text("Increase Margin") + } + ) + .keyboardShortcut("]") + } + } + + public var lineSpacingButtons: some View { + Group { + Button( + action: { + storedLineSpacing = max(storedLineSpacing - 25, 100) + NSNotification.readerSettingsChanged() + }, + label: { Text("Decrease Line Spacing") } + ) + .keyboardShortcut("k") + + Button( + action: { + storedLineSpacing = min(storedLineSpacing + 25, 300) + NSNotification.readerSettingsChanged() + }, + label: { Text("Increase Line Spacing") } + ) + .keyboardShortcut("l") + } + } + + public init( + preferredFont: Binding, + prefersHighContrastText: Binding + ) { + self._preferredFont = preferredFont + self._prefersHighContrastText = prefersHighContrastText + } + + public var body: some Commands { + CommandMenu("Reader Display") { + fontSizeButtons + + Divider() + + marginSizeButtons + + Divider() + + lineSpacingButtons + + Divider() + + Picker(selection: $preferredFont, label: Text("Font Family")) { + ForEach(WebFont.allCases, id: \.self) { font in + Text(font.displayValue).tag(font.rawValue) + } + } + + Toggle( + isOn: $prefersHighContrastText, + label: { Text("High Contrast Text") } + ) + } + } + } +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index abd162beb..bb53c910c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -22,7 +22,6 @@ import Views ZStack { if let linkRequest = viewModel.linkRequest { NavigationLink( - // TODO: add alt for macOS destination: WebReaderLoadingContainer(requestID: linkRequest.serverID), tag: linkRequest, selection: $viewModel.linkRequest diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index ff10e5e13..adced8706 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -18,93 +18,104 @@ import Views } var body: some View { - List { - Section { - ForEach(viewModel.items) { item in - FeedCardNavigationLink( - item: item, - viewModel: viewModel - ) - .contextMenu { - Button( - action: { viewModel.itemUnderTitleEdit = item }, - label: { Label("Edit Title/Description", systemImage: "textbox") } + ZStack { + if let linkRequest = viewModel.linkRequest { + NavigationLink( + destination: WebReaderLoadingContainer(requestID: linkRequest.serverID), + tag: linkRequest, + selection: $viewModel.linkRequest + ) { + EmptyView() + } + } + List { + Section { + ForEach(viewModel.items) { item in + FeedCardNavigationLink( + item: item, + viewModel: viewModel ) - Button( - action: { viewModel.itemUnderLabelEdit = item }, - label: { Label("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" + .contextMenu { + Button( + action: { viewModel.itemUnderTitleEdit = item }, + label: { Label("Edit Title/Description", systemImage: "textbox") } ) - }) - Button( - action: { - itemToRemove = item - confirmationShown = true - }, - label: { Label("Delete", systemImage: "trash") } - ) - if FeatureFlag.enableSnooze { - Button { - viewModel.itemToSnoozeID = item.id - viewModel.snoozePresented = true - } label: { - Label { Text("Snooze") } icon: { Image.moon } + Button( + action: { viewModel.itemUnderLabelEdit = item }, + label: { Label("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("Delete", systemImage: "trash") } + ) + if FeatureFlag.enableSnooze { + Button { + viewModel.itemToSnoozeID = item.id + viewModel.snoozePresented = true + } label: { + Label { Text("Snooze") } icon: { Image.moon } + } } } } } - } - if viewModel.isLoading { - LoadingSection() + if viewModel.isLoading { + LoadingSection() + } } - } - .listStyle(PlainListStyle()) - .navigationTitle("Home") - .searchable( - text: $viewModel.searchTerm, - placement: .toolbar - ) { - if viewModel.searchTerm.isEmpty { - Text("Inbox").searchCompletion("in:inbox ") - Text("All").searchCompletion("in:all ") - Text("Archived").searchCompletion("in:archive ") - Text("Files").searchCompletion("type:file ") + .listStyle(PlainListStyle()) + .navigationTitle("Home") + .searchable( + text: $viewModel.searchTerm, + placement: .toolbar + ) { + if viewModel.searchTerm.isEmpty { + Text("Inbox").searchCompletion("in:inbox ") + Text("All").searchCompletion("in:all ") + Text("Archived").searchCompletion("in:archive ") + Text("Files").searchCompletion("type:file ") + } } - } - .onChange(of: viewModel.searchTerm) { _ in - // Maybe we should debounce this, but - // it feels like it works ok without - 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() + .onChange(of: viewModel.searchTerm) { _ in + // Maybe we should debounce this, but + // it feels like it works ok without + 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() + } } } } @@ -131,6 +142,15 @@ import Views loadItems(isRefresh: true) } } + .handlesExternalEvents(preferring: Set(["shareExtensionRequestID"]), allowing: Set(["*"])) + .onOpenURL { url in + viewModel.linkRequest = nil + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + if let linkRequestID = DeepLink.make(from: url)?.linkRequestID { + viewModel.linkRequest = LinkRequest(id: UUID(), serverID: linkRequestID) + } + } + } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index 265196a09..52ddc2ef7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -11,11 +11,7 @@ struct WebReader: PlatformViewRepresentable { let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void let navBarVisibilityRatioUpdater: (Double) -> Void - @Binding var updateFontFamilyActionID: UUID? - @Binding var updateFontActionID: UUID? - @Binding var updateTextContrastActionID: UUID? - @Binding var updateMaxWidthActionID: UUID? - @Binding var updateLineHeightActionID: UUID? + @Binding var readerSettingsChangedTransactionID: UUID? @Binding var annotationSaveTransactionID: UUID? @Binding var showNavBarActionID: UUID? @Binding var shareActionID: UUID? @@ -79,35 +75,18 @@ struct WebReader: PlatformViewRepresentable { return webView } - // swiftlint:disable:next cyclomatic_complexity private func updatePlatformView(_ webView: WKWebView, context: Context) { if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID { context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID (webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation)) } - if updateFontFamilyActionID != context.coordinator.previousUpdateFontFamilyActionID { - context.coordinator.previousUpdateFontFamilyActionID = updateFontFamilyActionID + if readerSettingsChangedTransactionID != context.coordinator.previousReaderSettingsChangedUUID { + context.coordinator.previousReaderSettingsChangedUUID = readerSettingsChangedTransactionID (webView as? OmnivoreWebView)?.updateFontFamily() - } - - if updateFontActionID != context.coordinator.previousUpdateFontActionID { - context.coordinator.previousUpdateFontActionID = updateFontActionID (webView as? OmnivoreWebView)?.updateFontSize() - } - - if updateTextContrastActionID != context.coordinator.previousUpdateTextContrastActionID { - context.coordinator.previousUpdateTextContrastActionID = updateTextContrastActionID (webView as? OmnivoreWebView)?.updateTextContrast() - } - - if updateMaxWidthActionID != context.coordinator.previousUpdateMaxWidthActionID { - context.coordinator.previousUpdateMaxWidthActionID = updateMaxWidthActionID (webView as? OmnivoreWebView)?.updateMaxWidthPercentage() - } - - if updateLineHeightActionID != context.coordinator.previousUpdateLineHeightActionID { - context.coordinator.previousUpdateLineHeightActionID = updateLineHeightActionID (webView as? OmnivoreWebView)?.updateLineHeight() } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 81726b541..e12ba8b07 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -15,11 +15,7 @@ struct WebReaderContainerView: View { @State private var navBarVisibilityRatio = 1.0 @State private var showDeleteConfirmation = false @State private var progressViewOpacity = 0.0 - @State var updateFontFamilyActionID: UUID? - @State var updateFontActionID: UUID? - @State var updateTextContrastActionID: UUID? - @State var updateMaxWidthActionID: UUID? - @State var updateLineHeightActionID: UUID? + @State var readerSettingsChangedTransactionID: UUID? @State var annotationSaveTransactionID: UUID? @State var showNavBarActionID: UUID? @State var shareActionID: UUID? @@ -159,16 +155,14 @@ struct WebReaderContainerView: View { #endif } - var webPreferencesPopoverView: some View { - WebPreferencesPopoverView( - updateFontFamilyAction: { updateFontFamilyActionID = UUID() }, - updateFontAction: { updateFontActionID = UUID() }, - updateTextContrastAction: { updateTextContrastActionID = UUID() }, - updateMaxWidthAction: { updateMaxWidthActionID = UUID() }, - updateLineHeightAction: { updateLineHeightActionID = UUID() }, - dismissAction: { showPreferencesPopover = false } - ) - } + #if os(iOS) + var webPreferencesPopoverView: some View { + WebPreferencesPopoverView( + updateReaderPreferences: { readerSettingsChangedTransactionID = UUID() }, + dismissAction: { showPreferencesPopover = false } + ) + } + #endif var body: some View { ZStack { @@ -187,11 +181,7 @@ struct WebReaderContainerView: View { navBarVisibilityRatioUpdater: { navBarVisibilityRatio = $0 }, - updateFontFamilyActionID: $updateFontFamilyActionID, - updateFontActionID: $updateFontActionID, - updateTextContrastActionID: $updateTextContrastActionID, - updateMaxWidthActionID: $updateMaxWidthActionID, - updateLineHeightActionID: $updateLineHeightActionID, + readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID, annotationSaveTransactionID: $annotationSaveTransactionID, showNavBarActionID: $showNavBarActionID, shareActionID: $shareActionID, @@ -234,19 +224,20 @@ struct WebReaderContainerView: View { await viewModel.loadContent(dataService: dataService, itemID: item.unwrappedID) } } - VStack(spacing: 0) { - navBar - Spacer() - } + #if os(iOS) + VStack(spacing: 0) { + navBar + Spacer() + } + #endif } #if os(iOS) .formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) { webPreferencesPopoverView } #else - .sheet(isPresented: $showPreferencesPopover) { - webPreferencesPopoverView - .frame(minWidth: 400, minHeight: 400) + .onReceive(NSNotification.readerSettingsChangedPublisher) { _ in + readerSettingsChangedTransactionID = UUID() } #endif .onDisappear { diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift index 25cfcbd2d..acaaafd18 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift @@ -15,11 +15,7 @@ final class WebReaderCoordinator: NSObject { var linkHandler: (URL) -> Void = { _ in } var needsReload = false var lastSavedAnnotationID: UUID? - var previousUpdateFontFamilyActionID: UUID? - var previousUpdateFontActionID: UUID? - var previousUpdateTextContrastActionID: UUID? - var previousUpdateMaxWidthActionID: UUID? - var previousUpdateLineHeightActionID: UUID? + var previousReaderSettingsChangedUUID: UUID? var previousShowNavBarActionID: UUID? var previousShareActionID: UUID? var updateNavBarVisibilityRatio: (Double) -> Void = { _ in } diff --git a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift index 7ba4f8307..6905cd969 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WelcomeView.swift @@ -52,6 +52,9 @@ struct WelcomeView: View { } ) .foregroundColor(.appGrayTextContrast) + #if os(macOS) + .buttonStyle(PlainButtonStyle()) + #endif } } diff --git a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift index 12af0214f..c0cc1ff94 100644 --- a/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift +++ b/apple/OmnivoreKit/Sources/Models/PageScrapePayload.swift @@ -28,6 +28,11 @@ public struct PageScrapePayload { self.contentType = .pdf(localUrl: localUrl) } + init(localUrl: URL) { + self.url = localUrl.absoluteString + self.contentType = .pdf(localUrl: localUrl) + } + init(url: String, title: String?, html: String, iconURL: String? = nil) { self.url = url self.contentType = .html(html: html, title: title, iconURL: iconURL) @@ -238,15 +243,38 @@ private extension PageScrapePayload { if let url = item as? NSURL { return makeFromURL(url as URL) } + if let data = item as? NSData { + return makeFromData(data) + } return nil } static func sharedContainerURL() -> URL { - FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: "group.app.omnivoreapp" + #if os(iOS) + let appGroupID = "group.app.omnivoreapp" + #else + let appGroupID = "QJF2XZ86HB.app.omnivore.app" + #endif + return FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: appGroupID )! } + static func makeFromData(_ data: NSData) -> PageScrapePayload? { + // Copy PDFs into a temporary file where they are staged for processing. + var fileURL = sharedContainerURL() + let filePath = UUID().uuidString.lowercased() + ".pdf" + fileURL.appendPathComponent(filePath) + + do { + try data.write(to: fileURL) + return PageScrapePayload(localUrl: fileURL) + } catch { + print("error copying file locally", error) + return nil + } + } + static func makeFromURL(_ url: URL) -> PageScrapePayload? { if url.isFileURL { let type = try? url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier diff --git a/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift b/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift index e359a9411..e4daea999 100644 --- a/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift +++ b/apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift @@ -6,6 +6,10 @@ public enum PublicValet { public static var storedAppEnvironment: AppEnvironment? { ValetKey.appEnvironmentString.value().flatMap { AppEnvironment(rawValue: $0) } } + + public static var authToken: String? { + ValetKey.authToken.value() + } } enum ValetKey: String { diff --git a/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift b/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift index 0616e8375..fbcf2663c 100644 --- a/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift +++ b/apple/OmnivoreKit/Sources/Services/NSNotification+Operation.swift @@ -1,10 +1,3 @@ -// -// NSNotification+Operation.swift -// -// -// Created by Jackson Harper on 1/31/22. -// - import Foundation import Models @@ -12,6 +5,7 @@ public extension NSNotification { static let PushJSONArticle = Notification.Name("PushJSONArticle") static let OperationSuccess = Notification.Name("OperationSuccess") static let OperationFailure = Notification.Name("OperationFailure") + static let ReaderSettingsChanged = Notification.Name("ReaderSettingsChanged") static var pushFeedItemPublisher: NotificationCenter.Publisher { NotificationCenter.default.publisher(for: PushJSONArticle) @@ -25,6 +19,10 @@ public extension NSNotification { NotificationCenter.default.publisher(for: OperationFailure) } + static var readerSettingsChangedPublisher: NotificationCenter.Publisher { + NotificationCenter.default.publisher(for: ReaderSettingsChanged) + } + internal var operationMessage: String? { if let message = userInfo?["message"] as? String { return message @@ -47,4 +45,8 @@ public extension NSNotification { static func operationFailed(message: String) { NotificationCenter.default.post(name: NSNotification.OperationFailure, object: nil, userInfo: ["message": message]) } + + static func readerSettingsChanged() { + NotificationCenter.default.post(name: NSNotification.ReaderSettingsChanged, object: nil) + } } diff --git a/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift b/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift index 2490bdc9a..b25e732aa 100644 --- a/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift +++ b/apple/OmnivoreKit/Sources/Views/AsyncLoadingImage.swift @@ -3,23 +3,23 @@ import Models import SwiftUI import Utils -enum AsyncImageStatus { +public enum AsyncImageStatus { case loading case loaded(image: Image) case error } -struct AsyncLoadingImage: View { +public struct AsyncLoadingImage: View { let viewBuilder: (AsyncImageStatus) -> Content let url: URL @StateObject private var imageLoader = ImageLoader() - init(url: URL, @ViewBuilder viewBuilder: @escaping (AsyncImageStatus) -> Content) { + public init(url: URL, @ViewBuilder viewBuilder: @escaping (AsyncImageStatus) -> Content) { self.url = url self.viewBuilder = viewBuilder } - var body: some View { + public var body: some View { viewBuilder(imageLoader.status) .task { await imageLoader.load(fromUrl: url) } } diff --git a/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift b/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift index 6b6e974c6..443aee68c 100644 --- a/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift +++ b/apple/OmnivoreKit/Sources/Views/Buttons/ButtonStyles.swift @@ -42,16 +42,16 @@ public struct RoundedRectButtonStyle: ButtonStyle { } } -struct RectButtonStyle: ButtonStyle { +public struct RectButtonStyle: ButtonStyle { let backgroundColor: Color let textColor: Color - init(color: Color = .appButtonBackground, textColor: Color = .appGrayText) { + public init(color: Color = .appButtonBackground, textColor: Color = .appGrayText) { self.backgroundColor = color self.textColor = textColor } - func makeBody(configuration: Configuration) -> some View { + public func makeBody(configuration: Configuration) -> some View { configuration.label .font(.appBody) .foregroundColor(textColor) diff --git a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift index 164c3fcfe..e6877fe23 100644 --- a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift +++ b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift @@ -12,7 +12,7 @@ public enum WebFont: String, CaseIterable { case sourceserifpro = "Source Serif Pro" case openDyslexic = "OpenDyslexic" - var displayValue: String { + public var displayValue: String { switch self { case .inter, .merriweather, .lora, .opensans, .roboto, .crimsontext, .sourceserifpro: return rawValue @@ -24,169 +24,193 @@ public enum WebFont: String, CaseIterable { } } -public struct WebPreferencesPopoverView: View { - let updateFontFamilyAction: () -> Void - let updateFontAction: () -> Void - let updateTextContrastAction: () -> Void - let updateMaxWidthAction: () -> Void - let updateLineHeightAction: () -> Void - let dismissAction: () -> Void +#if os(iOS) + public struct WebPreferencesPopoverView: View { + let updateReaderPreferences: () -> Void + let dismissAction: () -> Void - static let preferredWebFontSizeKey = UserDefaultKey.preferredWebFontSize.rawValue - #if os(macOS) - @AppStorage(preferredWebFontSizeKey) var storedFontSize = Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16) - #else - @AppStorage(preferredWebFontSizeKey) var storedFontSize: Int = UITraitCollection.current.preferredWebFontSize - #endif + @AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize: Int = + UITraitCollection.current.preferredWebFontSize + @AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150 + @AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100 + @AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue + @AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true - @AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150 - @AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100 - @AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue - @AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true - - public init( - updateFontFamilyAction: @escaping () -> Void, - updateFontAction: @escaping () -> Void, - updateTextContrastAction: @escaping () -> Void, - updateMaxWidthAction: @escaping () -> Void, - updateLineHeightAction: @escaping () -> Void, - dismissAction: @escaping () -> Void - ) { - self.updateFontFamilyAction = updateFontFamilyAction - self.updateFontAction = updateFontAction - self.updateTextContrastAction = updateTextContrastAction - self.updateMaxWidthAction = updateMaxWidthAction - self.updateLineHeightAction = updateLineHeightAction - self.dismissAction = dismissAction - } - - var fontList: some View { - List { - ForEach(WebFont.allCases, id: \.self) { font in - Button( - action: { - preferredFont = font.rawValue - updateFontFamilyAction() - }, - label: { - HStack { - Text(font.displayValue).foregroundColor(.appGrayTextContrast) - Spacer() - if font.rawValue == preferredFont { - Image(systemName: "checkmark").foregroundColor(.appGrayTextContrast) - } - } - } - ) - } + public init( + updateReaderPreferences: @escaping () -> Void, + dismissAction: @escaping () -> Void + ) { + self.updateReaderPreferences = updateReaderPreferences + self.dismissAction = dismissAction } - .listStyle(.plain) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .navigationTitle("Reader Font") - } - public var body: some View { - NavigationView { - ScrollView(showsIndicators: false) { - VStack(alignment: .center) { - VStack { - LabelledStepper( - labelText: "Font Size:", - onIncrement: { - storedFontSize = min(storedFontSize + 2, 28) - updateFontAction() - }, - onDecrement: { - storedFontSize = max(storedFontSize - 2, 10) - updateFontAction() - } - ) - - LabelledStepper( - labelText: "Margin:", - onIncrement: { - storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) - updateMaxWidthAction() - }, - onDecrement: { - storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) - updateMaxWidthAction() - } - ) - - LabelledStepper( - labelText: "Line Spacing:", - onIncrement: { - storedLineSpacing = min(storedLineSpacing + 25, 300) - updateLineHeightAction() - }, - onDecrement: { - storedLineSpacing = max(storedLineSpacing - 25, 100) - updateLineHeightAction() - } - ) - - Toggle("High Contrast Text:", isOn: $prefersHighContrastText) - .frame(height: 40) - .padding(.trailing, 6) - .onChange(of: prefersHighContrastText) { _ in - updateTextContrastAction() - } - - HStack { - NavigationLink(destination: fontList) { - Text("Change Reader Font") - } - Image(systemName: "chevron.right") - Spacer() - } - .frame(height: 40) - - Spacer() - } - } - } - .padding() - .navigationTitle("Reader Preferences") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .toolbar { - ToolbarItem(placement: .barTrailing) { + var fontList: some View { + List { + ForEach(WebFont.allCases, id: \.self) { font in Button( - action: dismissAction, - label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() } + action: { + preferredFont = font.rawValue + updateReaderPreferences() + }, + label: { + HStack { + Text(font.displayValue).foregroundColor(.appGrayTextContrast) + Spacer() + if font.rawValue == preferredFont { + Image(systemName: "checkmark").foregroundColor(.appGrayTextContrast) + } + } + } ) } } + .listStyle(.plain) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Reader Font") } - #if os(iOS) + + public var body: some View { + NavigationView { + ScrollView(showsIndicators: false) { + VStack(alignment: .center) { + VStack { + LabelledStepper( + labelText: "Font Size:", + onIncrement: { + storedFontSize = min(storedFontSize + 2, 28) + updateReaderPreferences() + }, + onDecrement: { + storedFontSize = max(storedFontSize - 2, 10) + updateReaderPreferences() + } + ) + + LabelledStepper( + labelText: "Margin:", + onIncrement: { + storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) + updateReaderPreferences() + }, + onDecrement: { + storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) + updateReaderPreferences() + } + ) + + LabelledStepper( + labelText: "Line Spacing:", + onIncrement: { + storedLineSpacing = min(storedLineSpacing + 25, 300) + updateReaderPreferences() + }, + onDecrement: { + storedLineSpacing = max(storedLineSpacing - 25, 100) + updateReaderPreferences() + } + ) + + Toggle("High Contrast Text:", isOn: $prefersHighContrastText) + .frame(height: 40) + .padding(.trailing, 6) + .onChange(of: prefersHighContrastText) { _ in + updateReaderPreferences() + } + + HStack { + NavigationLink(destination: fontList) { + Text("Change Reader Font") + } + Image(systemName: "chevron.right") + Spacer() + } + .frame(height: 40) + + Spacer() + } + } + } + .padding() + .navigationTitle("Reader Preferences") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .barTrailing) { + Button( + action: dismissAction, + label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() } + ) + } + } + } .navigationViewStyle(.stack) - #endif - .accentColor(.appGrayTextContrast) + .accentColor(.appGrayTextContrast) + } } -} -struct LabelledStepper: View { - let labelText: String - let onIncrement: () -> Void - let onDecrement: () -> Void + struct LabelledStepper: View { + let labelText: String + let onIncrement: () -> Void + let onDecrement: () -> Void - var body: some View { - HStack(alignment: .center, spacing: 0) { - Text(labelText) - Spacer() - HStack(spacing: 0) { + var body: some View { + HStack(alignment: .center, spacing: 0) { + Text(labelText) + Spacer() + HStack(spacing: 0) { + Button( + action: onDecrement, + label: { + Image(systemName: "minus") + .foregroundColor(.systemLabel) + .padding() + } + ) + .frame(width: 55, height: 40, alignment: .center) + Divider() + .frame(height: 30) + .background(Color.systemLabel) + Button( + action: onIncrement, + label: { + Image(systemName: "plus") + .foregroundColor(.systemLabel) + .padding() + } + ) + .frame(width: 55, height: 40, alignment: .center) + } + .background(Color.appButtonBackground) + .cornerRadius(8) + } + } + } + + public struct FontSizeAdjustmentPopoverView: View { + let increaseFontAction: () -> Void + let decreaseFontAction: () -> Void + + public init( + increaseFontAction: @escaping () -> Void, + decreaseFontAction: @escaping () -> Void + ) { + self.increaseFontAction = increaseFontAction + self.decreaseFontAction = decreaseFontAction + } + + @AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize: Int = + UITraitCollection.current.preferredWebFontSize + + public var body: some View { + HStack(alignment: .center, spacing: 0) { Button( - action: onDecrement, + action: { + storedFontSize = max(storedFontSize - 2, 10) + decreaseFontAction() + }, label: { Image(systemName: "minus") - #if os(iOS) .foregroundColor(.systemLabel) .padding() - #endif } ) .frame(width: 55, height: 40, alignment: .center) @@ -194,75 +218,18 @@ struct LabelledStepper: View { .frame(height: 30) .background(Color.systemLabel) Button( - action: onIncrement, + action: { + storedFontSize = min(storedFontSize + 2, 28) + increaseFontAction() + }, label: { Image(systemName: "plus") - #if os(iOS) .foregroundColor(.systemLabel) .padding() - #endif } ) .frame(width: 55, height: 40, alignment: .center) } - .background(Color.appButtonBackground) - .cornerRadius(8) } } -} - -public struct FontSizeAdjustmentPopoverView: View { - let increaseFontAction: () -> Void - let decreaseFontAction: () -> Void - - public init( - increaseFontAction: @escaping () -> Void, - decreaseFontAction: @escaping () -> Void - ) { - self.increaseFontAction = increaseFontAction - self.decreaseFontAction = decreaseFontAction - } - - static let preferredWebFontSizeKey = UserDefaultKey.preferredWebFontSize.rawValue - #if os(macOS) - @AppStorage(preferredWebFontSizeKey) var storedFontSize = Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16) - #else - @AppStorage(preferredWebFontSizeKey) var storedFontSize: Int = UITraitCollection.current.preferredWebFontSize - #endif - - public var body: some View { - HStack(alignment: .center, spacing: 0) { - Button( - action: { - storedFontSize = max(storedFontSize - 2, 10) - decreaseFontAction() - }, - label: { - Image(systemName: "minus") - #if os(iOS) - .foregroundColor(.systemLabel) - .padding() - #endif - } - ) - .frame(width: 55, height: 40, alignment: .center) - Divider() - .frame(height: 30) - .background(Color.systemLabel) - Button( - action: { - storedFontSize = min(storedFontSize + 2, 28) - increaseFontAction() - }, - label: { - Image(systemName: "plus") - #if os(iOS) - .foregroundColor(.systemLabel) - .padding() - #endif - } - ) - .frame(width: 55, height: 40, alignment: .center) - } - } -} +#endif diff --git a/apple/OmnivoreKit/Sources/Views/LocalText.swift b/apple/OmnivoreKit/Sources/Views/LocalText.swift index 861532879..9b9b3b17f 100644 --- a/apple/OmnivoreKit/Sources/Views/LocalText.swift +++ b/apple/OmnivoreKit/Sources/Views/LocalText.swift @@ -10,12 +10,12 @@ public enum LocalText { static let registrationViewSignInHeadline = localText(key: "registrationViewSignInHeadline") static let registrationViewSignUpHeadline = localText(key: "registrationViewSignUpHeadline") public static let registrationViewHeadline = localText(key: "registrationViewHeadline") - static let networkError = localText(key: "error.network") - static let genericError = localText(key: "error.generic") + public static let networkError = localText(key: "error.network") + public static let genericError = localText(key: "error.generic") static let invalidCredsLoginError = localText(key: "loginError.invalidCreds") - static let saveArticleSavedState = localText(key: "saveArticleSavedState") - static let saveArticleProcessingState = localText(key: "saveArticleProcessingState") - static let extensionAppUnauthorized = localText(key: "extensionAppUnauthorized") + public static let saveArticleSavedState = localText(key: "saveArticleSavedState") + public static let saveArticleProcessingState = localText(key: "saveArticleProcessingState") + public static let extensionAppUnauthorized = localText(key: "extensionAppUnauthorized") static let dismissButton = localText(key: "dismissButton") static let usernameValidationErrorInvalid = localText(key: "username.validation.error.invalidPattern") static let usernameValidationErrorTooShort = localText(key: "username.validation.error.tooshort") diff --git a/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index f39b6241a..d2c39fe61 100644 --- a/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x9F", - "green" : "0xEA", - "red" : "0xFF" + "blue" : "0xA8", + "green" : "0xEB", + "red" : "0xFB" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x9F", - "green" : "0xEA", - "red" : "0xFF" + "blue" : "0x4C", + "green" : "0x4D", + "red" : "0x4F" } }, "idiom" : "universal" diff --git a/apple/Sources/MainApp.swift b/apple/Sources/MainApp.swift index 909a6be02..43fbf54eb 100644 --- a/apple/Sources/MainApp.swift +++ b/apple/Sources/MainApp.swift @@ -1,19 +1,22 @@ // swiftlint:disable weak_delegate import App import SwiftUI +import Utils #if os(macOS) import AppKit + import Views #elseif os(iOS) import Intercom import UIKit - import Utils #endif @main struct MainApp: App { #if os(macOS) @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue + @AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true #elseif os(iOS) @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate #endif @@ -33,6 +36,18 @@ struct MainApp: App { WindowGroup { RootView(intercomProvider: nil) } + .commands { + MacMenuCommands( + preferredFont: $preferredFont, + prefersHighContrastText: $prefersHighContrastText + ) + } + .onChange(of: preferredFont) { _ in + NSNotification.readerSettingsChanged() + } + .onChange(of: prefersHighContrastText) { _ in + NSNotification.readerSettingsChanged() + } #endif } } diff --git a/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift b/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift index b3367f6e2..0ce9bb902 100644 --- a/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift +++ b/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift @@ -1,22 +1,13 @@ -// -// SafariWebExtensionHandler.swift -// Shared (Extension) -// -// Created by JacksonH on 10/8/21. -// - import App -import os.log import SafariServices +import Services let SFExtensionMessageKey = "message" class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { - let services = Services() - func beginRequest(with context: NSExtensionContext) { let response = NSExtensionItem() - let authToken = services.authenticator.authToken + let authToken = PublicValet.authToken response.userInfo = [SFExtensionMessageKey: ["authToken": authToken]] context.completeRequest(returningItems: [response], completionHandler: nil) }