From 0b3993215ff2f048e510e65ee81318753e4f3716 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 30 Jun 2022 14:17:49 -0700 Subject: [PATCH 01/21] update share extension save service to work outside of a operation queue --- .../Share/ExtensionSaveService.swift | 102 +++++++++++------- .../Share/ShareExtensionScene.swift | 1 + 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index 9ecd7fc0f..c078f49c2 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -5,6 +5,10 @@ import Utils import Views class ExtensionSaveService { + #if os(macOS) + let services = Services() + #endif + let queue: OperationQueue init() { @@ -66,9 +70,13 @@ class ExtensionSaveService { } } } + #if os(iOS) - // TODO: need alternative call for macos self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel) + #else + Task { + await shareExtensionViewModel.createPage(services: self.services, pageScrapePayload: payload) + } #endif case .failure: DispatchQueue.main.async { @@ -138,61 +146,73 @@ class ExtensionSaveService { queue = OperationQueue() Task { - await persist(services: self.services, pageScrapePayload: self.pageScrapePayload) + let pageCreated = await shareExtensionViewModel.createPage( + services: services, + pageScrapePayload: pageScrapePayload + ) + if pageCreated { + state = .finished + } } } 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 - } - } +extension ShareExtensionChildViewModel { + func createPage(services: Services, pageScrapePayload: PageScrapePayload) async -> Bool { + 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 } - private func persist(services: Services, pageScrapePayload: PageScrapePayload) async { - var requestId = shareExtensionViewModel.requestId + do { + updateStatusOnMain(requestId: requestId, newStatus: .saved) - do { - try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) - } catch { - updateStatus(nil, newStatus: .failed(error: SaveArticleError.unknown(description: "Unable to access content"))) - return + 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 + ) } - do { - updateStatus(requestId, newStatus: .saved) + } catch { + updateStatusOnMain( + requestId: nil, + newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error")) + ) + return false + } - 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 - ) - } + updateStatusOnMain(requestId: requestId, newStatus: .synced) + return true + } - } catch { - updateStatus(nil, newStatus: .syncFailed(error: SaveArticleError.unknown(description: "Unknown Error"))) - return + public func updateStatusOnMain(requestId: String?, newStatus: ShareExtensionStatus) { + DispatchQueue.main.async { + self.status = newStatus + if let requestId = requestId { + self.requestId = requestId } - - updateStatus(requestId, newStatus: .synced) - state = .finished } } } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 1d262edb7..34ace4698 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -26,6 +26,7 @@ public class ShareExtensionViewModel: ObservableObject { let saveService = ExtensionSaveService() func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) { + // TODO: write macos version #if os(iOS) if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") From 62a7b41ccc1aef2ff673b5e4cd44b5a265a21ec2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 30 Jun 2022 14:29:40 -0700 Subject: [PATCH 02/21] set requestID on main thread only --- .../Share/ExtensionSaveService.swift | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index c078f49c2..8590ab4c3 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -4,7 +4,7 @@ import Services import Utils import Views -class ExtensionSaveService { +final class ExtensionSaveService { #if os(macOS) let services = Services() #endif @@ -86,7 +86,7 @@ class ExtensionSaveService { } } - class SaveOperation: Operation, URLSessionDelegate { + final class SaveOperation: Operation, URLSessionDelegate { let services: Services let pageScrapePayload: PageScrapePayload let shareExtensionViewModel: ShareExtensionChildViewModel @@ -109,7 +109,7 @@ class ExtensionSaveService { self.services = Services() } - open var state: State = .created { + public var state: State = .created { willSet { willChangeValue(forKey: "isReady") willChangeValue(forKey: "isExecuting") @@ -164,6 +164,8 @@ class ExtensionSaveService { extension ShareExtensionChildViewModel { func createPage(services: Services, pageScrapePayload: PageScrapePayload) async -> Bool { + var newRequestID: String? + do { try await services.dataService.persistPageScrapePayload(pageScrapePayload, requestId: requestId) } catch { @@ -179,7 +181,7 @@ extension ShareExtensionChildViewModel { switch pageScrapePayload.contentType { case .none: - requestId = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url) + newRequestID = try await services.dataService.createPageFromUrl(id: requestId, url: pageScrapePayload.url) case let .pdf(localUrl): try await services.dataService.createPageFromPdf( id: requestId, @@ -187,14 +189,13 @@ extension ShareExtensionChildViewModel { url: pageScrapePayload.url ) case let .html(html, title, _): - requestId = try await services.dataService.createPage( + newRequestID = try await services.dataService.createPage( id: requestId, originalHtml: html, title: title, url: pageScrapePayload.url ) } - } catch { updateStatusOnMain( requestId: nil, @@ -203,11 +204,11 @@ extension ShareExtensionChildViewModel { return false } - updateStatusOnMain(requestId: requestId, newStatus: .synced) + updateStatusOnMain(requestId: newRequestID, newStatus: .synced) return true } - public func updateStatusOnMain(requestId: String?, newStatus: ShareExtensionStatus) { + func updateStatusOnMain(requestId: String?, newStatus: ShareExtensionStatus) { DispatchQueue.main.async { self.status = newStatus if let requestId = requestId { From b752fdc9d04d7a07031b563a2d92f4de6f1cb85d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 30 Jun 2022 16:02:19 -0700 Subject: [PATCH 03/21] open mac app when read now button is tapped from share extension --- .../App/AppExtensions/Share/ShareExtensionScene.swift | 9 +++++++-- .../Sources/App/Views/Home/HomeFeedViewMac.swift | 9 +++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 34ace4698..32fb64f83 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift @@ -26,12 +26,17 @@ public class ShareExtensionViewModel: ObservableObject { let saveService = ExtensionSaveService() func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) { - // TODO: write macos version #if os(iOS) + let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { - let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) } + #else + let deepLinkUrl = URL(string: "omnivore://shareExtensionRequestID/\(requestId)") + let workspace = NSWorkspace.value(forKeyPath: #keyPath(NSWorkspace.shared)) as? NSWorkspace + if let workspace = workspace, let deepLinkUrl = deepLinkUrl { + workspace.open(deepLinkUrl) + } #endif extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index ff10e5e13..f0a5831a2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -131,6 +131,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) + } + } + } } } From 73f9b96479df07e02d8fc46d001dc77d54d1565b Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 30 Jun 2022 16:11:15 -0700 Subject: [PATCH 04/21] load article in macos app when read now is tapped from share ext --- .../App/Views/Home/HomeFeedViewIOS.swift | 1 - .../App/Views/Home/HomeFeedViewMac.swift | 167 ++++++++++-------- 2 files changed, 89 insertions(+), 79 deletions(-) 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 f0a5831a2..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() + } } } } From 5db9b8ee7e4e3df970ea342b0c0d620b3b12fb49 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 30 Jun 2022 18:18:09 -0700 Subject: [PATCH 05/21] add a makeFromData function to PageScrapePayload --- .../Sources/Models/PageScrapePayload.swift | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) 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 From 24032eaae395700e71b28629e62f79fd140b0db2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 13:12:55 -0700 Subject: [PATCH 06/21] add mac commands menu file --- .../Sources/App/MacMenuCommands.swift | 90 +++++++++++++++++++ .../Sources/App/Views/WelcomeView.swift | 3 + .../Views/FontSizeAdjustmentPopoverView.swift | 2 +- apple/Sources/MainApp.swift | 3 + 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 apple/OmnivoreKit/Sources/App/MacMenuCommands.swift diff --git a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift new file mode 100644 index 000000000..2ad723125 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift @@ -0,0 +1,90 @@ +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 + @AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue + @AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true + + public var fontSizeButtons: some View { + Group { + Button( + action: { storedFontSize = min(storedFontSize + 2, 28) }, + label: { Text("Increase Reader Font Size") } + ) + + Button( + action: { storedFontSize = max(storedFontSize - 2, 10) }, + label: { Text("Decrease Reader Font Size") + } + ) + } + } + + public var marginSizeButtons: some View { + Group { + Button( + action: { storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) }, + label: { Text("Increase Reader Margin") + } + ) + + Button( + action: { storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) }, + label: { Text("Decrease Reader Margin") + } + ) + } + } + + public var lineSpacingButtons: some View { + Group { + Button( + action: { storedLineSpacing = min(storedLineSpacing + 25, 300) }, + label: { Text("Increase Reader Line Spacing") } + ) + + Button( + action: { storedLineSpacing = max(storedLineSpacing - 25, 100) }, + label: { Text("Decrease Reader Line Spacing") } + ) +// .keyboardShortcut("l") + } + } + + public init() {} + + public var body: some Commands { + CommandMenu("Reader Settings") { + 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/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/Views/FontSizeAdjustmentPopoverView.swift b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift index 164c3fcfe..bca934802 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 diff --git a/apple/Sources/MainApp.swift b/apple/Sources/MainApp.swift index 909a6be02..5635335b8 100644 --- a/apple/Sources/MainApp.swift +++ b/apple/Sources/MainApp.swift @@ -33,6 +33,9 @@ struct MainApp: App { WindowGroup { RootView(intercomProvider: nil) } + .commands { + MacMenuCommands() + } #endif } } From 1bfe7ff18af693fd7f1daa97f0650e730443064e Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 21:54:05 -0700 Subject: [PATCH 07/21] set WebPreferencesPopoverView for ios only --- .../Views/WebReader/WebReaderContainer.swift | 27 +- .../Views/FontSizeAdjustmentPopoverView.swift | 393 +++++++++--------- 2 files changed, 198 insertions(+), 222 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 81726b541..60b6e5820 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -159,16 +159,18 @@ 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( + updateFontFamilyAction: { updateFontFamilyActionID = UUID() }, + updateFontAction: { updateFontActionID = UUID() }, + updateTextContrastAction: { updateTextContrastActionID = UUID() }, + updateMaxWidthAction: { updateMaxWidthActionID = UUID() }, + updateLineHeightAction: { updateLineHeightActionID = UUID() }, + dismissAction: { showPreferencesPopover = false } + ) + } + #endif var body: some View { ZStack { @@ -243,11 +245,6 @@ struct WebReaderContainerView: View { .formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) { webPreferencesPopoverView } - #else - .sheet(isPresented: $showPreferencesPopover) { - webPreferencesPopoverView - .frame(minWidth: 400, minHeight: 400) - } #endif .onDisappear { // Clear the shared webview content when exiting diff --git a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift index bca934802..654a82b98 100644 --- a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift +++ b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift @@ -24,169 +24,205 @@ 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 updateFontFamilyAction: () -> Void + let updateFontAction: () -> Void + let updateTextContrastAction: () -> Void + let updateMaxWidthAction: () -> Void + let updateLineHeightAction: () -> 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( + 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 } - .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 + updateFontFamilyAction() + }, + 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) + 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") + .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 +230,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 From fb361096fa2263c73c181617474e4f031912ed80 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 22:07:42 -0700 Subject: [PATCH 08/21] bundle all reader preferences cahnges into a single transaction --- .../App/Views/WebReader/WebReader.swift | 27 ++------------- .../Views/WebReader/WebReaderContainer.swift | 18 ++-------- .../WebReader/WebReaderCoordinator.swift | 6 +--- .../Views/FontSizeAdjustmentPopoverView.swift | 34 ++++++------------- 4 files changed, 18 insertions(+), 67 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index 265196a09..c80d23d53 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.previousReaderSettingsChangedTransactionID { + context.coordinator.previousReaderSettingsChangedTransactionID = 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 60b6e5820..084b30d8b 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? @@ -162,11 +158,7 @@ struct WebReaderContainerView: View { #if os(iOS) var webPreferencesPopoverView: some View { WebPreferencesPopoverView( - updateFontFamilyAction: { updateFontFamilyActionID = UUID() }, - updateFontAction: { updateFontActionID = UUID() }, - updateTextContrastAction: { updateTextContrastActionID = UUID() }, - updateMaxWidthAction: { updateMaxWidthActionID = UUID() }, - updateLineHeightAction: { updateLineHeightActionID = UUID() }, + updateReaderPreferences: { readerSettingsChangedTransactionID = UUID() }, dismissAction: { showPreferencesPopover = false } ) } @@ -189,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, diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift index 25cfcbd2d..dc539a946 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 previousReaderSettingsChangedTransactionID: UUID? var previousShowNavBarActionID: UUID? var previousShareActionID: UUID? var updateNavBarVisibilityRatio: (Double) -> Void = { _ in } diff --git a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift index 654a82b98..e6877fe23 100644 --- a/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift +++ b/apple/OmnivoreKit/Sources/Views/FontSizeAdjustmentPopoverView.swift @@ -26,11 +26,7 @@ public enum WebFont: String, CaseIterable { #if os(iOS) public struct WebPreferencesPopoverView: View { - let updateFontFamilyAction: () -> Void - let updateFontAction: () -> Void - let updateTextContrastAction: () -> Void - let updateMaxWidthAction: () -> Void - let updateLineHeightAction: () -> Void + let updateReaderPreferences: () -> Void let dismissAction: () -> Void @AppStorage(UserDefaultKey.preferredWebFontSize.rawValue) var storedFontSize: Int = @@ -41,18 +37,10 @@ public enum WebFont: String, CaseIterable { @AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true public init( - updateFontFamilyAction: @escaping () -> Void, - updateFontAction: @escaping () -> Void, - updateTextContrastAction: @escaping () -> Void, - updateMaxWidthAction: @escaping () -> Void, - updateLineHeightAction: @escaping () -> Void, + updateReaderPreferences: @escaping () -> Void, dismissAction: @escaping () -> Void ) { - self.updateFontFamilyAction = updateFontFamilyAction - self.updateFontAction = updateFontAction - self.updateTextContrastAction = updateTextContrastAction - self.updateMaxWidthAction = updateMaxWidthAction - self.updateLineHeightAction = updateLineHeightAction + self.updateReaderPreferences = updateReaderPreferences self.dismissAction = dismissAction } @@ -62,7 +50,7 @@ public enum WebFont: String, CaseIterable { Button( action: { preferredFont = font.rawValue - updateFontFamilyAction() + updateReaderPreferences() }, label: { HStack { @@ -90,11 +78,11 @@ public enum WebFont: String, CaseIterable { labelText: "Font Size:", onIncrement: { storedFontSize = min(storedFontSize + 2, 28) - updateFontAction() + updateReaderPreferences() }, onDecrement: { storedFontSize = max(storedFontSize - 2, 10) - updateFontAction() + updateReaderPreferences() } ) @@ -102,11 +90,11 @@ public enum WebFont: String, CaseIterable { labelText: "Margin:", onIncrement: { storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) - updateMaxWidthAction() + updateReaderPreferences() }, onDecrement: { storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) - updateMaxWidthAction() + updateReaderPreferences() } ) @@ -114,11 +102,11 @@ public enum WebFont: String, CaseIterable { labelText: "Line Spacing:", onIncrement: { storedLineSpacing = min(storedLineSpacing + 25, 300) - updateLineHeightAction() + updateReaderPreferences() }, onDecrement: { storedLineSpacing = max(storedLineSpacing - 25, 100) - updateLineHeightAction() + updateReaderPreferences() } ) @@ -126,7 +114,7 @@ public enum WebFont: String, CaseIterable { .frame(height: 40) .padding(.trailing, 6) .onChange(of: prefersHighContrastText) { _ in - updateTextContrastAction() + updateReaderPreferences() } HStack { From 6b5f8b744365b0a230cf93993152de7502a39ae5 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 22:30:29 -0700 Subject: [PATCH 09/21] use NSNotification to propagate reader setting changes --- .../Sources/App/MacMenuCommands.swift | 38 ++++++++++++++++--- .../Views/WebReader/WebReaderContainer.swift | 4 ++ .../Services/NSNotification+Operation.swift | 16 ++++---- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift index 2ad723125..b6201c4dc 100644 --- a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift +++ b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift @@ -15,12 +15,18 @@ import Views public var fontSizeButtons: some View { Group { Button( - action: { storedFontSize = min(storedFontSize + 2, 28) }, + action: { + storedFontSize = min(storedFontSize + 2, 28) + NSNotification.readerSettingsChanged() + }, label: { Text("Increase Reader Font Size") } ) Button( - action: { storedFontSize = max(storedFontSize - 2, 10) }, + action: { + storedFontSize = max(storedFontSize - 2, 10) + NSNotification.readerSettingsChanged() + }, label: { Text("Decrease Reader Font Size") } ) @@ -30,13 +36,19 @@ import Views public var marginSizeButtons: some View { Group { Button( - action: { storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) }, + action: { + storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40) + NSNotification.readerSettingsChanged() + }, label: { Text("Increase Reader Margin") } ) Button( - action: { storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) }, + action: { + storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) + NSNotification.readerSettingsChanged() + }, label: { Text("Decrease Reader Margin") } ) @@ -46,12 +58,18 @@ import Views public var lineSpacingButtons: some View { Group { Button( - action: { storedLineSpacing = min(storedLineSpacing + 25, 300) }, + action: { + storedLineSpacing = min(storedLineSpacing + 25, 300) + NSNotification.readerSettingsChanged() + }, label: { Text("Increase Reader Line Spacing") } ) Button( - action: { storedLineSpacing = max(storedLineSpacing - 25, 100) }, + action: { + storedLineSpacing = max(storedLineSpacing - 25, 100) + NSNotification.readerSettingsChanged() + }, label: { Text("Decrease Reader Line Spacing") } ) // .keyboardShortcut("l") @@ -78,12 +96,20 @@ import Views ForEach(WebFont.allCases, id: \.self) { font in Text(font.displayValue).tag(font.rawValue) } + // TODO: fix this since it doesn't work + .onChange(of: preferredFont) { _ in + NSNotification.readerSettingsChanged() + } } Toggle( isOn: $prefersHighContrastText, label: { Text("High Contrast Text") } ) + // TODO: fix this since it doesn't work + .onChange(of: prefersHighContrastText) { _ in + NSNotification.readerSettingsChanged() + } } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 084b30d8b..66284f89c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -233,6 +233,10 @@ struct WebReaderContainerView: View { .formSheet(isPresented: $showPreferencesPopover, useSmallDetent: false) { webPreferencesPopoverView } + #else + .onReceive(NSNotification.readerSettingsChangedPublisher) { _ in + readerSettingsChangedTransactionID = UUID() + } #endif .onDisappear { // Clear the shared webview content when exiting 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) + } } From f4706205b0a54199bff17c4ae1dfa0181c792ac4 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 22:38:12 -0700 Subject: [PATCH 10/21] observe font change and contrast change in main app for macos --- .../Sources/App/MacMenuCommands.swift | 21 +++++++++---------- apple/Sources/MainApp.swift | 16 ++++++++++++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift index b6201c4dc..e84877e05 100644 --- a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift +++ b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift @@ -9,8 +9,9 @@ import Views ) @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 + + @Binding var preferredFont: String + @Binding var prefersHighContrastText: Bool public var fontSizeButtons: some View { Group { @@ -76,7 +77,13 @@ import Views } } - public init() {} + public init( + preferredFont: Binding, + prefersHighContrastText: Binding + ) { + self._preferredFont = preferredFont + self._prefersHighContrastText = prefersHighContrastText + } public var body: some Commands { CommandMenu("Reader Settings") { @@ -96,20 +103,12 @@ import Views ForEach(WebFont.allCases, id: \.self) { font in Text(font.displayValue).tag(font.rawValue) } - // TODO: fix this since it doesn't work - .onChange(of: preferredFont) { _ in - NSNotification.readerSettingsChanged() - } } Toggle( isOn: $prefersHighContrastText, label: { Text("High Contrast Text") } ) - // TODO: fix this since it doesn't work - .onChange(of: prefersHighContrastText) { _ in - NSNotification.readerSettingsChanged() - } } } } diff --git a/apple/Sources/MainApp.swift b/apple/Sources/MainApp.swift index 5635335b8..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 @@ -34,7 +37,16 @@ struct MainApp: App { RootView(intercomProvider: nil) } .commands { - MacMenuCommands() + MacMenuCommands( + preferredFont: $preferredFont, + prefersHighContrastText: $prefersHighContrastText + ) + } + .onChange(of: preferredFont) { _ in + NSNotification.readerSettingsChanged() + } + .onChange(of: prefersHighContrastText) { _ in + NSNotification.readerSettingsChanged() } #endif } From 5a76bebaae2c054c9b1a371ce3d508865b3c44e3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 22:57:42 -0700 Subject: [PATCH 11/21] add keyboard shortcuts for reader settings --- apple/OmnivoreKit/Sources/App/MacMenuCommands.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift index e84877e05..a17f1f192 100644 --- a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift +++ b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift @@ -22,6 +22,7 @@ import Views }, label: { Text("Increase Reader Font Size") } ) + .keyboardShortcut("+") Button( action: { @@ -31,6 +32,7 @@ import Views label: { Text("Decrease Reader Font Size") } ) + .keyboardShortcut("-") } } @@ -44,6 +46,7 @@ import Views label: { Text("Increase Reader Margin") } ) + .keyboardShortcut("]") Button( action: { @@ -53,6 +56,7 @@ import Views label: { Text("Decrease Reader Margin") } ) + .keyboardShortcut("[") } } @@ -65,6 +69,7 @@ import Views }, label: { Text("Increase Reader Line Spacing") } ) + .keyboardShortcut("l") Button( action: { @@ -73,7 +78,7 @@ import Views }, label: { Text("Decrease Reader Line Spacing") } ) -// .keyboardShortcut("l") + .keyboardShortcut("k") } } From 5beb80745838f7bafc992df5cd21bbea19f98b55 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Sun, 3 Jul 2022 23:07:04 -0700 Subject: [PATCH 12/21] remove navBar from reader on mac app --- .../App/Views/WebReader/WebReaderContainer.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 66284f89c..e12ba8b07 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -224,10 +224,12 @@ 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) { From 02bb54b813e97b1dcec7c2a03ad1f99afd9e40d2 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Jul 2022 07:59:06 -0700 Subject: [PATCH 13/21] change order on increase/decrease in mac menu --- .../Sources/App/MacMenuCommands.swift | 64 +++++++++---------- .../App/Views/WebReader/WebReader.swift | 4 +- .../WebReader/WebReaderCoordinator.swift | 2 +- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift index a17f1f192..4cc36fa9b 100644 --- a/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift +++ b/apple/OmnivoreKit/Sources/App/MacMenuCommands.swift @@ -15,70 +15,70 @@ import Views public var fontSizeButtons: some View { Group { - Button( - action: { - storedFontSize = min(storedFontSize + 2, 28) - NSNotification.readerSettingsChanged() - }, - label: { Text("Increase Reader Font Size") } - ) - .keyboardShortcut("+") - Button( action: { storedFontSize = max(storedFontSize - 2, 10) NSNotification.readerSettingsChanged() }, - label: { Text("Decrease Reader Font Size") + 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 = max(storedMaxWidthPercentage - 10, 40) - NSNotification.readerSettingsChanged() - }, - label: { Text("Increase Reader Margin") - } - ) - .keyboardShortcut("]") - Button( action: { storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100) NSNotification.readerSettingsChanged() }, - label: { Text("Decrease Reader Margin") + 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 = min(storedLineSpacing + 25, 300) - NSNotification.readerSettingsChanged() - }, - label: { Text("Increase Reader Line Spacing") } - ) - .keyboardShortcut("l") - Button( action: { storedLineSpacing = max(storedLineSpacing - 25, 100) NSNotification.readerSettingsChanged() }, - label: { Text("Decrease Reader Line Spacing") } + label: { Text("Decrease Line Spacing") } ) .keyboardShortcut("k") + + Button( + action: { + storedLineSpacing = min(storedLineSpacing + 25, 300) + NSNotification.readerSettingsChanged() + }, + label: { Text("Increase Line Spacing") } + ) + .keyboardShortcut("l") } } @@ -91,7 +91,7 @@ import Views } public var body: some Commands { - CommandMenu("Reader Settings") { + CommandMenu("Reader Display") { fontSizeButtons Divider() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index c80d23d53..52ddc2ef7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -81,8 +81,8 @@ struct WebReader: PlatformViewRepresentable { (webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation)) } - if readerSettingsChangedTransactionID != context.coordinator.previousReaderSettingsChangedTransactionID { - context.coordinator.previousReaderSettingsChangedTransactionID = readerSettingsChangedTransactionID + if readerSettingsChangedTransactionID != context.coordinator.previousReaderSettingsChangedUUID { + context.coordinator.previousReaderSettingsChangedUUID = readerSettingsChangedTransactionID (webView as? OmnivoreWebView)?.updateFontFamily() (webView as? OmnivoreWebView)?.updateFontSize() (webView as? OmnivoreWebView)?.updateTextContrast() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift index dc539a946..acaaafd18 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift @@ -15,7 +15,7 @@ final class WebReaderCoordinator: NSObject { var linkHandler: (URL) -> Void = { _ in } var needsReload = false var lastSavedAnnotationID: UUID? - var previousReaderSettingsChangedTransactionID: UUID? + var previousReaderSettingsChangedUUID: UUID? var previousShowNavBarActionID: UUID? var previousShareActionID: UUID? var updateNavBarVisibilityRatio: (Double) -> Void = { _ in } From 448780678a7cca916af861c96416192d2894d29c Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Tue, 5 Jul 2022 09:15:10 -0700 Subject: [PATCH 14/21] update mac accent color for dark mode --- .../AccentColor.colorset/Contents.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index f39b6241a..e4f21c016 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" : "0x32", + "green" : "0x32", + "red" : "0x32" } }, "idiom" : "universal" From 6072638d0056a8a60901f36f481099cdfd7b0843 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Jul 2022 09:37:46 -0700 Subject: [PATCH 15/21] update mac accent color --- .../Assets.xcassets/AccentColor.colorset/Contents.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index e4f21c016..d2c39fe61 100644 --- a/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/apple/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x32", - "green" : "0x32", - "red" : "0x32" + "blue" : "0x4C", + "green" : "0x4D", + "red" : "0x4F" } }, "idiom" : "universal" From 4747b685ba3a8759c9b0f346a8210fe24bccedd3 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Jul 2022 13:12:18 -0700 Subject: [PATCH 16/21] resolve stash conflict --- .../xcshareddata/swiftpm/Package.resolved | 9 +++ apple/OmnivoreKit/Package.swift | 12 ++-- .../Share/ShareExtensionScene.swift | 59 ------------------- .../Share/ShareExtensionView.swift | 54 +++++++++++++++++ 4 files changed, 69 insertions(+), 65 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift 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/ShareExtensionScene.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionScene.swift index 32fb64f83..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,58 +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) - let deepLinkUrl = NSURL(string: "omnivore://shareExtensionRequestID/\(requestId)") - if let application = UIApplication.value(forKeyPath: #keyPath(UIApplication.shared)) as? UIApplication { - application.perform(NSSelectorFromString("openURL:"), with: deepLinkUrl) - } - #else - let deepLinkUrl = URL(string: "omnivore://shareExtensionRequestID/\(requestId)") - let workspace = NSWorkspace.value(forKeyPath: #keyPath(NSWorkspace.shared)) as? NSWorkspace - if let workspace = workspace, let deepLinkUrl = deepLinkUrl { - workspace.open(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/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift new file mode 100644 index 000000000..7d0f1fa29 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift @@ -0,0 +1,54 @@ +import Models +import Services +import SwiftUI +import Utils +import Views + +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) + } + ) + } +} From d6ccc5b31878c1b16d020112a16d1bfb8b4ecd4a Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Jul 2022 13:27:43 -0700 Subject: [PATCH 17/21] move share extension view from Views to App --- .../Share/ShareExtensionView.swift | 222 +++++++++++-- .../Share/ShareExtensionViewComponents.swift | 53 ++++ .../Share/ShareExtensionViewModel.swift | 79 +++++ .../Sources/Views/AsyncLoadingImage.swift | 8 +- .../Sources/Views/Buttons/ButtonStyles.swift | 6 +- .../OmnivoreKit/Sources/Views/LocalText.swift | 10 +- .../Sources/Views/ShareExtensionView.swift | 296 ------------------ 7 files changed, 339 insertions(+), 335 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewComponents.swift create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift index 7d0f1fa29..5d1306885 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift @@ -4,33 +4,6 @@ import SwiftUI import Utils import Views -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() @@ -52,3 +25,198 @@ struct ShareExtensionView: View { ) } } + +public struct ShareExtensionChildView: View { + let viewModel: ShareExtensionChildViewModel + let onAppearAction: () -> Void + let readNowButtonAction: (String) -> Void + let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void + + @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 + hideUntilReminded = false + } else { + reminderTime = selectedTime + hideUntilReminded = true + } + } + + private var titleText: String { + switch viewModel.status { + case .saved, .synced, .syncFailed(error: _): + return "Saved to Omnivore" + case .processing: + return "Saving to Omnivore" + case .failed(error: _): + return "Error saving to Omnivore" + } + } + + private var cloudIconName: String { + switch viewModel.status { + case .synced: + return "checkmark.icloud" + case .saved, .processing: + return "icloud" + case .failed(error: _), .syncFailed(error: _): + return "exclamationmark.icloud" + } + } + + private var cloudIconColor: Color { + switch viewModel.status { + case .saved: + return .appGrayText + case .processing: + return .clear + case .failed(error: _), .syncFailed(error: _): + return .red + case .synced: + return .blue + } + } + + private func localImage(from url: URL) -> Image? { + #if os(iOS) + if let data = try? Data(contentsOf: url), let img = UIImage(data: data) { + return Image(uiImage: img) + } + #else + if let data = try? Data(contentsOf: url), let img = NSImage(data: data) { + return Image(nsImage: img) + } + #endif + return nil + } + + public var previewCard: some View { + HStack { + if let iconURLStr = viewModel.iconURL, let iconURL = URL(string: iconURLStr) { + if !iconURL.isFileURL { + AsyncLoadingImage(url: iconURL) { imageStatus in + if case let AsyncImageStatus.loaded(image) = imageStatus { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + .clipped() + } else { + Color.appButtonBackground + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + } + } + } else { + if let localImage = localImage(from: iconURL) { + localImage + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + .clipped() + } else { + Color.appButtonBackground + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + } + } + } else { + Color.appButtonBackground + .aspectRatio(contentMode: .fill) + .frame(width: 61, height: 61) + } + + VStack(alignment: .leading) { + Text(viewModel.title ?? "") + .lineLimit(1) + .foregroundColor(.appGrayText) + .font(Font.system(size: 15, weight: .semibold)) + Text(viewModel.url ?? "") + .lineLimit(1) + .foregroundColor(.appGrayText) + .font(Font.system(size: 12, weight: .regular)) + } + Spacer() + VStack { + Spacer() + Image(systemName: cloudIconName) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 12, height: 12, alignment: .trailing) + .foregroundColor(cloudIconColor) + // .padding(.trailing, 6) + .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 8)) + } + } + .background(Color.appButtonBackground) + .frame(maxWidth: .infinity, maxHeight: 61) + .cornerRadius(8) + } + + public var body: some View { + VStack(alignment: .leading) { + Text(titleText) + .foregroundColor(.appGrayText) + .font(Font.system(size: 17, weight: .semibold)) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 23) + .padding(.bottom, 12) + + Rectangle() + .foregroundColor(.appGrayText) + .frame(maxWidth: .infinity, maxHeight: 1) + .opacity(0.06) + .padding(.top, 0) + .padding(.bottom, 18) + + previewCard + .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + + Spacer() + + HStack { + Button( + action: { readNowButtonAction(self.viewModel.requestId) }, + label: { Text("Read Now").frame(maxWidth: .infinity) } + ) + .buttonStyle(RoundedRectButtonStyle()) + + Button( + action: { + dismissButtonTappedAction(reminderTime, hideUntilReminded) + }, + label: { + Text("Dismiss") + .frame(maxWidth: .infinity) + } + ) + .buttonStyle(RoundedRectButtonStyle()) + } + .padding(.horizontal) + .padding(.bottom) + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .topLeading + ) + .onAppear { + onAppearAction() + } + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewComponents.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewComponents.swift new file mode 100644 index 000000000..fa8f67673 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/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/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift new file mode 100644 index 000000000..4da076d77 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -0,0 +1,79 @@ +import Models +import SwiftUI +import Utils +import Views + +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")) + } + } + } +} + +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 + } + } +} 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/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/OmnivoreKit/Sources/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift deleted file mode 100644 index fb8d32fb7..000000000 --- a/apple/OmnivoreKit/Sources/Views/ShareExtensionView.swift +++ /dev/null @@ -1,296 +0,0 @@ -import Models -import SwiftUI -import Utils - -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 - - @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 - hideUntilReminded = false - } else { - reminderTime = selectedTime - hideUntilReminded = true - } - } - - private var titleText: String { - switch viewModel.status { - case .saved, .synced, .syncFailed(error: _): - return "Saved to Omnivore" - case .processing: - return "Saving to Omnivore" - case .failed(error: _): - return "Error saving to Omnivore" - } - } - - private var cloudIconName: String { - switch viewModel.status { - case .synced: - return "checkmark.icloud" - case .saved, .processing: - return "icloud" - case .failed(error: _), .syncFailed(error: _): - return "exclamationmark.icloud" - } - } - - private var cloudIconColor: Color { - switch viewModel.status { - case .saved: - return .appGrayText - case .processing: - return .clear - case .failed(error: _), .syncFailed(error: _): - return .red - case .synced: - return .blue - } - } - - private func localImage(from url: URL) -> Image? { - #if os(iOS) - if let data = try? Data(contentsOf: url), let img = UIImage(data: data) { - return Image(uiImage: img) - } - #else - if let data = try? Data(contentsOf: url), let img = NSImage(data: data) { - return Image(nsImage: img) - } - #endif - return nil - } - - public var previewCard: some View { - HStack { - if let iconURLStr = viewModel.iconURL, let iconURL = URL(string: iconURLStr) { - if !iconURL.isFileURL { - AsyncLoadingImage(url: iconURL) { imageStatus in - if case let AsyncImageStatus.loaded(image) = imageStatus { - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 61, height: 61) - .clipped() - } else { - Color.appButtonBackground - .aspectRatio(contentMode: .fill) - .frame(width: 61, height: 61) - } - } - } else { - if let localImage = localImage(from: iconURL) { - localImage - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 61, height: 61) - .clipped() - } else { - Color.appButtonBackground - .aspectRatio(contentMode: .fill) - .frame(width: 61, height: 61) - } - } - } else { - Color.appButtonBackground - .aspectRatio(contentMode: .fill) - .frame(width: 61, height: 61) - } - - VStack(alignment: .leading) { - Text(viewModel.title ?? "") - .lineLimit(1) - .foregroundColor(.appGrayText) - .font(Font.system(size: 15, weight: .semibold)) - Text(viewModel.url ?? "") - .lineLimit(1) - .foregroundColor(.appGrayText) - .font(Font.system(size: 12, weight: .regular)) - } - Spacer() - VStack { - Spacer() - Image(systemName: cloudIconName) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 12, height: 12, alignment: .trailing) - .foregroundColor(cloudIconColor) - // .padding(.trailing, 6) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 8)) - } - } - .background(Color.appButtonBackground) - .frame(maxWidth: .infinity, maxHeight: 61) - .cornerRadius(8) - } - - public var body: some View { - VStack(alignment: .leading) { - Text(titleText) - .foregroundColor(.appGrayText) - .font(Font.system(size: 17, weight: .semibold)) - .frame(maxWidth: .infinity, alignment: .center) - .padding(.top, 23) - .padding(.bottom, 12) - - Rectangle() - .foregroundColor(.appGrayText) - .frame(maxWidth: .infinity, maxHeight: 1) - .opacity(0.06) - .padding(.top, 0) - .padding(.bottom, 18) - - previewCard - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) - - Spacer() - - HStack { - Button( - action: { readNowButtonAction(self.viewModel.requestId) }, - label: { Text("Read Now").frame(maxWidth: .infinity) } - ) - .buttonStyle(RoundedRectButtonStyle()) - - Button( - action: { - dismissButtonTappedAction(reminderTime, hideUntilReminded) - }, - label: { - Text("Dismiss") - .frame(maxWidth: .infinity) - } - ) - .buttonStyle(RoundedRectButtonStyle()) - } - .padding(.horizontal) - .padding(.bottom) - } - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .topLeading - ) - .onAppear { - onAppearAction() - } - } -} From d7370a579d59b95f37cfab5d1d4b0d3cc1600744 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Jul 2022 13:58:59 -0700 Subject: [PATCH 18/21] merge ExtensionSaveService into ExtensionViewModel --- .../Share/ExtensionSaveService.swift | 211 ++++++++---------- .../Share/ShareExtensionView.swift | 44 +--- .../Share/ShareExtensionViewModel.swift | 44 ++-- 3 files changed, 121 insertions(+), 178 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index 8590ab4c3..c793b93b2 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -4,165 +4,58 @@ import Services import Utils import Views -final class ExtensionSaveService { - #if os(macOS) - let services = Services() - #endif - - 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) { +extension ShareExtensionViewModel { + 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 { - shareExtensionViewModel.status = .saved + 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): - shareExtensionViewModel.title = title - shareExtensionViewModel.iconURL = iconURL - shareExtensionViewModel.url = hostname + self.title = title + self.iconURL = iconURL + self.url = hostname case .none: - shareExtensionViewModel.url = hostname - shareExtensionViewModel.title = payload.url + self.url = hostname + self.title = payload.url if var url = url { url.path = "/favicon.ico" - shareExtensionViewModel.iconURL = url.url?.absoluteString + self.iconURL = url.url?.absoluteString } case let .pdf(localUrl: localUrl): - shareExtensionViewModel.url = hostname - shareExtensionViewModel.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString) + self.url = hostname + self.title = PDFUtils.titleFromPdfFile(localUrl.absoluteString) Task { let localThumbnail = try await PDFUtils.createThumbnailFor(inputUrl: localUrl) DispatchQueue.main.async { - shareExtensionViewModel.iconURL = localThumbnail?.absoluteString + self.iconURL = localThumbnail?.absoluteString } } } } #if os(iOS) - self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel) + self.queueSaveOperation(payload) #else Task { - await shareExtensionViewModel.createPage(services: self.services, pageScrapePayload: payload) + await createPage(services: self.services, pageScrapePayload: payload) } #endif case .failure: DispatchQueue.main.async { - shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content")) + self.status = .failed(error: .unknown(description: "Could not retrieve content")) } } } } - final 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() - } - - 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( - services: services, - pageScrapePayload: pageScrapePayload - ) - if pageCreated { - state = .finished - } - } - } - - override func cancel() { - super.cancel() - } - } -} - -extension ShareExtensionChildViewModel { func createPage(services: Services, pageScrapePayload: PageScrapePayload) async -> Bool { var newRequestID: String? @@ -217,3 +110,77 @@ extension ShareExtensionChildViewModel { } } } + +final class SaveOperation: Operation, URLSessionDelegate { + let services: Services + 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 + self.services = Services() + } + + 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( + services: services, + pageScrapePayload: pageScrapePayload + ) + if pageCreated { + state = .finished + } + } + } + + override func cancel() { + super.cancel() + } +} diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift index 5d1306885..fc1d1d171 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift @@ -4,49 +4,13 @@ import SwiftUI import Utils import Views -struct ShareExtensionView: View { +public 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) - } - ) - } -} - -public struct ShareExtensionChildView: View { - let viewModel: ShareExtensionChildViewModel - let onAppearAction: () -> Void - let readNowButtonAction: (String) -> Void - let dismissButtonTappedAction: (ReminderTime?, Bool) -> Void @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 @@ -191,14 +155,14 @@ public struct ShareExtensionChildView: View { 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") @@ -216,7 +180,7 @@ public struct ShareExtensionChildView: View { alignment: .topLeading ) .onAppear { - onAppearAction() + viewModel.savePage(extensionContext: extensionContext) } } } diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 4da076d77..76c1e2ea9 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -4,12 +4,20 @@ import Utils import Views public class ShareExtensionViewModel: ObservableObject { - @Published var title: String? + @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 = UUID().uuidString.lowercased() @Published var debugText: String? - let saveService = ExtensionSaveService() + #if os(macOS) + let services = Services() + #endif - func handleReadNowAction(requestId: String, extensionContext: NSExtensionContext?) { + 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)") @@ -19,27 +27,31 @@ public class ShareExtensionViewModel: ObservableObject { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } - func savePage(extensionContext: NSExtensionContext?, shareExtensionViewModel: ShareExtensionChildViewModel) { + func savePage(extensionContext: NSExtensionContext?) { if let extensionContext = extensionContext { - saveService.save(extensionContext, shareExtensionViewModel: shareExtensionViewModel) + save(extensionContext) } else { DispatchQueue.main.async { - shareExtensionViewModel.status = .failed(error: .unknown(description: "Internal Error")) + self.status = .failed(error: .unknown(description: "Internal Error")) } } } -} -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 + #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 + } - public init() { - self.requestId = UUID().uuidString.lowercased() - } + let operation = SaveOperation(pageScrapePayload: payload, shareExtensionViewModel: self) + self.queue.addOperation(operation) + self.queue.waitUntilAllOperationsAreFinished() + } + } + #endif } public enum ShareExtensionStatus { From 16358bf2307fb6b7b99e4783c2c8a0a2b069254d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Jul 2022 14:12:41 -0700 Subject: [PATCH 19/21] remove extra init of Services in share extension --- .../App/AppExtensions/Share/ExtensionSaveService.swift | 5 +---- .../App/AppExtensions/Share/ShareExtensionViewModel.swift | 5 +---- apple/OmnivoreKit/Sources/Services/Keychain/ValetKey.swift | 4 ++++ .../Sources/SafariExtension/SafariWebExtensionHandler.swift | 5 ++--- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift index c793b93b2..c262936e3 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift @@ -56,7 +56,7 @@ extension ShareExtensionViewModel { } } - func createPage(services: Services, pageScrapePayload: PageScrapePayload) async -> Bool { + func createPage(pageScrapePayload: PageScrapePayload) async -> Bool { var newRequestID: String? do { @@ -112,7 +112,6 @@ extension ShareExtensionViewModel { } final class SaveOperation: Operation, URLSessionDelegate { - let services: Services let pageScrapePayload: PageScrapePayload let shareExtensionViewModel: ShareExtensionViewModel @@ -130,7 +129,6 @@ final class SaveOperation: Operation, URLSessionDelegate { self.shareExtensionViewModel = shareExtensionViewModel self.state = .created - self.services = Services() } public var state: State = .created { @@ -171,7 +169,6 @@ final class SaveOperation: Operation, URLSessionDelegate { Task { let pageCreated = await shareExtensionViewModel.createPage( - services: services, pageScrapePayload: pageScrapePayload ) if pageCreated { diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 76c1e2ea9..d9a2b24eb 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -11,10 +11,7 @@ public class ShareExtensionViewModel: ObservableObject { @Published public var requestId = UUID().uuidString.lowercased() @Published var debugText: String? - #if os(macOS) - let services = Services() - #endif - + let services = Services() let queue = OperationQueue() func handleReadNowAction(extensionContext: NSExtensionContext?) { 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/Sources/SafariExtension/SafariWebExtensionHandler.swift b/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift index b3367f6e2..307197fe9 100644 --- a/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift +++ b/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift @@ -8,15 +8,14 @@ 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) } From 9c2509951dc1d94420857f0377f7fff99b101f8d Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Wed, 6 Jul 2022 15:10:59 -0700 Subject: [PATCH 20/21] consolidate share extension view code --- .../Share/ExtensionSaveService.swift | 183 ------------------ .../Share/ShareExtensionSaveOperation.swift | 76 ++++++++ .../Share/ShareExtensionViewModel.swift | 107 +++++++++- .../{ => Views}/ShareExtensionView.swift | 0 .../ShareExtensionViewComponents.swift | 0 .../SafariWebExtensionHandler.swift | 8 - 6 files changed, 182 insertions(+), 192 deletions(-) delete mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift create mode 100644 apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionSaveOperation.swift rename apple/OmnivoreKit/Sources/App/AppExtensions/Share/{ => Views}/ShareExtensionView.swift (100%) rename apple/OmnivoreKit/Sources/App/AppExtensions/Share/{ => Views}/ShareExtensionViewComponents.swift (100%) 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 c262936e3..000000000 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ExtensionSaveService.swift +++ /dev/null @@ -1,183 +0,0 @@ -import Foundation -import Models -import Services -import Utils -import Views - -extension ShareExtensionViewModel { - 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 - } - } - } -} - -final class SaveOperation: 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/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/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index d9a2b24eb..4d68a0add 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -43,12 +43,117 @@ public class ShareExtensionViewModel: ObservableObject { return } - let operation = SaveOperation(pageScrapePayload: payload, shareExtensionViewModel: self) + 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 { diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift similarity index 100% rename from apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionView.swift rename to apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewComponents.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionViewComponents.swift similarity index 100% rename from apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewComponents.swift rename to apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionViewComponents.swift diff --git a/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift b/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift index 307197fe9..0ce9bb902 100644 --- a/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift +++ b/apple/Sources/SafariExtension/SafariWebExtensionHandler.swift @@ -1,12 +1,4 @@ -// -// SafariWebExtensionHandler.swift -// Shared (Extension) -// -// Created by JacksonH on 10/8/21. -// - import App -import os.log import SafariServices import Services From 4bc9119b594c018787873440d1b5d9471dd0ffe1 Mon Sep 17 00:00:00 2001 From: Satindar Dhillon Date: Thu, 7 Jul 2022 08:35:42 -0700 Subject: [PATCH 21/21] add ApplyLabelsView to share extension view --- .../App/AppExtensions/Share/ShareExtensionViewModel.swift | 1 + .../App/AppExtensions/Share/Views/ShareExtensionView.swift | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift index 4d68a0add..06f6b7f6a 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/ShareExtensionViewModel.swift @@ -8,6 +8,7 @@ public class ShareExtensionViewModel: ObservableObject { @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? diff --git a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift index fc1d1d171..3089ac009 100644 --- a/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift +++ b/apple/OmnivoreKit/Sources/App/AppExtensions/Share/Views/ShareExtensionView.swift @@ -153,6 +153,10 @@ public struct ShareExtensionView: View { Spacer() + if let item = viewModel.linkedItem { + ApplyLabelsView(mode: .item(item), onSave: nil) + } + HStack { Button( action: { viewModel.handleReadNowAction(extensionContext: extensionContext) }, @@ -182,5 +186,6 @@ public struct ShareExtensionView: View { .onAppear { viewModel.savePage(extensionContext: extensionContext) } + .environmentObject(viewModel.services.dataService) } }