diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift index 9db48b14a..dbf0084ca 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/FeedCardNavigationLink.swift @@ -7,9 +7,6 @@ struct FeedCardNavigationLink: View { @EnvironmentObject var dataService: DataService let item: FeedItem - let searchQuery: String - - @Binding var selectedLinkItem: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -18,14 +15,14 @@ struct FeedCardNavigationLink: View { NavigationLink( destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)), tag: item, - selection: $selectedLinkItem + selection: $viewModel.selectedLinkItem ) { EmptyView() } .opacity(0) .buttonStyle(PlainButtonStyle()) .onAppear { - viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService) + viewModel.itemAppeared(item: item, dataService: dataService) } FeedCard(item: item) } @@ -38,10 +35,8 @@ struct GridCardNavigationLink: View { @State private var scale = 1.0 let item: FeedItem - let searchQuery: String let actionHandler: (GridCardAction) -> Void - @Binding var selectedLinkItem: FeedItem? @Binding var isContextMenuOpen: Bool @ObservedObject var viewModel: HomeFeedViewModel @@ -51,7 +46,7 @@ struct GridCardNavigationLink: View { NavigationLink( destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item, homeFeedViewModel: viewModel)), tag: item, - selection: $selectedLinkItem + selection: $viewModel.selectedLinkItem ) { EmptyView() } @@ -60,12 +55,12 @@ struct GridCardNavigationLink: View { scale = 0.95 DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { scale = 1.0 - selectedLinkItem = item + viewModel.selectedLinkItem = item } } }) .onAppear { - viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService) + viewModel.itemAppeared(item: item, dataService: dataService) } } .aspectRatio(1.8, contentMode: .fill) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index a45f7daad..7c5e60d82 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -10,10 +10,6 @@ import Views struct HomeFeedContainerView: View { @EnvironmentObject var dataService: DataService @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = UIDevice.isIPhone - @State private var searchQuery = "" - @State private var snoozePresented = false - @State private var itemToSnooze: FeedItem? - @State private var selectedLinkItem: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel var body: some View { @@ -21,33 +17,29 @@ import Views if #available(iOS 15.0, *) { HomeFeedView( prefersListLayout: $prefersListLayout, - searchQuery: $searchQuery, - selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, - itemToSnooze: $itemToSnooze, viewModel: viewModel ) .refreshable { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .searchable( - text: $searchQuery, + text: $viewModel.searchQuery, placement: .sidebar ) { - if searchQuery.isEmpty { + if viewModel.searchQuery.isEmpty { Text("Inbox").searchCompletion("in:inbox ") Text("All").searchCompletion("in:all ") Text("Archived").searchCompletion("in:archive ") Text("Files").searchCompletion("type:file ") } } - .onChange(of: searchQuery) { _ in + .onChange(of: viewModel.searchQuery) { _ in // Maybe we should debounce this, but // it feels like it works ok without - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .onSubmit(of: .search) { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .sheet(item: $viewModel.itemUnderLabelEdit) { item in ApplyLabelsView(item: item) { labels in @@ -57,10 +49,6 @@ import Views } else { HomeFeedView( prefersListLayout: $prefersListLayout, - searchQuery: $searchQuery, - selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, - itemToSnooze: $itemToSnooze, viewModel: viewModel ) .sheet(item: $viewModel.itemUnderLabelEdit) { item in @@ -74,7 +62,7 @@ import Views Button(action: {}, label: { ProgressView() }) } else { Button( - action: { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) }, + action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) } @@ -85,18 +73,18 @@ import Views .navigationTitle("Home") .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in // Don't refresh the list if the user is currently reading an article - if selectedLinkItem == nil { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + if viewModel.selectedLinkItem == nil { + viewModel.loadItems(dataService: dataService, isRefresh: true) } } .onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in if let feedItem = notification.userInfo?["feedItem"] as? FeedItem { viewModel.pushFeedItem(item: feedItem) - self.selectedLinkItem = feedItem + viewModel.selectedLinkItem = feedItem } } - .formSheet(isPresented: $snoozePresented) { - SnoozeView(snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze) { + .formSheet(isPresented: $viewModel.snoozePresented) { + SnoozeView(snoozePresented: $viewModel.snoozePresented, itemToSnooze: $viewModel.itemToSnooze) { viewModel.snoozeUntil( dataService: dataService, linkId: $0.feedItemId, @@ -107,10 +95,10 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } - .onChange(of: selectedLinkItem) { _ in + .onChange(of: viewModel.selectedLinkItem) { _ in viewModel.commitProgressUpdates() } } @@ -120,63 +108,46 @@ import Views @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool - @Binding var searchQuery: String - @Binding var selectedLinkItem: FeedItem? - @Binding var snoozePresented: Bool - @Binding var itemToSnooze: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel var body: some View { if prefersListLayout { - HomeFeedListView( - prefersListLayout: $prefersListLayout, - searchQuery: $searchQuery, - selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, - itemToSnooze: $itemToSnooze, - viewModel: viewModel - ) + HomeFeedListView(prefersListLayout: $prefersListLayout, viewModel: viewModel) } else { - HomeFeedGridView( - searchQuery: $searchQuery, - selectedLinkItem: $selectedLinkItem, - snoozePresented: $snoozePresented, - itemToSnooze: $itemToSnooze, - viewModel: viewModel - ) - .toolbar { - ToolbarItem { - if #available(iOS 15.0, *) { - Button("", action: {}) - .disabled(true) - .overlay { - if viewModel.isLoading { - ProgressView() + HomeFeedGridView(viewModel: viewModel) + .toolbar { + ToolbarItem { + if #available(iOS 15.0, *) { + Button("", action: {}) + .disabled(true) + .overlay { + if viewModel.isLoading { + ProgressView() + } } - } - } else { - if viewModel.isLoading { - Button(action: {}, label: { ProgressView() }) } else { + if viewModel.isLoading { + Button(action: {}, label: { ProgressView() }) + } else { + Button( + action: { viewModel.loadItems(dataService: dataService, isRefresh: true) }, + label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } + ) + } + } + } + ToolbarItem { + if UIDevice.isIPad { Button( - action: { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) }, - label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } + action: { prefersListLayout.toggle() }, + label: { + Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") + } ) } } } - ToolbarItem { - if UIDevice.isIPad { - Button( - action: { prefersListLayout.toggle() }, - label: { - Label("Toggle Feed Layout", systemImage: prefersListLayout ? "square.grid.2x2" : "list.bullet") - } - ) - } - } - } } } } @@ -184,10 +155,6 @@ import Views struct HomeFeedListView: View { @EnvironmentObject var dataService: DataService @Binding var prefersListLayout: Bool - @Binding var searchQuery: String - @Binding var selectedLinkItem: FeedItem? - @Binding var snoozePresented: Bool - @Binding var itemToSnooze: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -200,8 +167,6 @@ import Views ForEach(viewModel.items) { item in let link = FeedCardNavigationLink( item: item, - searchQuery: searchQuery, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) .contextMenu { @@ -224,8 +189,8 @@ import Views ) if FeatureFlag.enableSnooze { Button { - itemToSnooze = item - snoozePresented = true + viewModel.itemToSnooze = item + viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } } @@ -277,8 +242,8 @@ import Views .swipeActions(edge: .leading, allowsFullSwipe: true) { if FeatureFlag.enableSnooze { Button { - itemToSnooze = item - snoozePresented = true + viewModel.itemToSnooze = item + viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } }.tint(.appYellow48) @@ -315,10 +280,6 @@ import Views struct HomeFeedGridView: View { @EnvironmentObject var dataService: DataService - @Binding var searchQuery: String - @Binding var selectedLinkItem: FeedItem? - @Binding var snoozePresented: Bool - @Binding var itemToSnooze: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false @@ -344,9 +305,7 @@ import Views ForEach(viewModel.items) { item in let link = GridCardNavigationLink( item: item, - searchQuery: searchQuery, actionHandler: { contextMenuActionHandler(item: item, action: $0) }, - selectedLinkItem: $selectedLinkItem, isContextMenuOpen: $isContextMenuOpen, viewModel: viewModel ) @@ -380,7 +339,7 @@ import Views .onPreferenceChange(ScrollViewOffsetPreferenceKey.self) { offset in DispatchQueue.main.async { if !viewModel.isLoading, offset > 240 { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift index bd4849c14..91cad42e8 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewMac.swift @@ -9,12 +9,8 @@ import Views #if os(macOS) struct HomeFeedView: View { @EnvironmentObject var dataService: DataService - @State var searchQuery = "" - @State private var selectedLinkItem: FeedItem? @State private var itemToRemove: FeedItem? @State private var confirmationShown = false - @State private var snoozePresented = false - @State private var itemToSnooze: FeedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -33,8 +29,6 @@ import Views ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, - searchQuery: searchQuery, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) .contextMenu { @@ -55,8 +49,8 @@ import Views ) if FeatureFlag.enableSnooze { Button { - itemToSnooze = item - snoozePresented = true + viewModel.itemToSnooze = item + viewModel.snoozePresented = true } label: { Label { Text("Snooze") } icon: { Image.moon } } @@ -83,29 +77,29 @@ import Views .listStyle(PlainListStyle()) .navigationTitle("Home") .searchable( - text: $searchQuery, + text: $viewModel.searchQuery, placement: .toolbar ) { - if searchQuery.isEmpty { + if viewModel.searchQuery.isEmpty { Text("Inbox").searchCompletion("in:inbox ") Text("All").searchCompletion("in:all ") Text("Archived").searchCompletion("in:archive ") Text("Files").searchCompletion("type:file ") } } - .onChange(of: searchQuery) { _ in + .onChange(of: viewModel.searchQuery) { _ in // Maybe we should debounce this, but // it feels like it works ok without - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .onSubmit(of: .search) { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } .toolbar { ToolbarItem { Button( action: { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) @@ -120,7 +114,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } @@ -131,8 +125,6 @@ import Views ForEach(viewModel.items) { item in FeedCardNavigationLink( item: item, - searchQuery: searchQuery, - selectedLinkItem: $selectedLinkItem, viewModel: viewModel ) } @@ -148,7 +140,7 @@ import Views ToolbarItem { Button( action: { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) }, label: { Label("Refresh Feed", systemImage: "arrow.clockwise") } ) @@ -156,7 +148,7 @@ import Views } .onAppear { if viewModel.items.isEmpty { - viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) + viewModel.loadItems(dataService: dataService, isRefresh: true) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index f5e304094..7a9af7d98 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -15,6 +15,11 @@ final class HomeFeedViewModel: ObservableObject { @Published var isLoading = false @Published var showPushNotificationPrimer = false @Published var itemUnderLabelEdit: FeedItem? + @Published var searchQuery = "" + @Published var snoozePresented = false + @Published var itemToSnooze: FeedItem? + @Published var selectedLinkItem: FeedItem? + var cursor: String? var sendProgressUpdates = false @@ -27,14 +32,14 @@ final class HomeFeedViewModel: ObservableObject { init() {} - func itemAppeared(item: FeedItem, searchQuery: String, dataService: DataService) { + func itemAppeared(item: FeedItem, dataService: DataService) { if isLoading { return } let itemIndex = items.firstIndex(where: { $0.id == item.id }) let thresholdIndex = items.index(items.endIndex, offsetBy: -5) // Check if user has scrolled to the last five items in the list if let itemIndex = itemIndex, itemIndex > thresholdIndex, items.count < thresholdIndex + 10 { - loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: false) + loadItems(dataService: dataService, isRefresh: false) } } @@ -42,7 +47,7 @@ final class HomeFeedViewModel: ObservableObject { items.insert(item, at: 0) } - func loadItems(dataService: DataService, searchQuery: String?, isRefresh: Bool) { + func loadItems(dataService: DataService, isRefresh: Bool) { // Clear offline highlights since we'll be populating new FeedItems with the correct highlights set dataService.clearHighlights() @@ -63,7 +68,7 @@ final class HomeFeedViewModel: ObservableObject { dataService.libraryItemsPublisher( limit: 10, sortDescending: true, - searchQuery: searchQuery, + searchQuery: searchQuery.isEmpty ? nil : searchQuery, cursor: isRefresh ? nil : cursor ) .sink( diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift index 95ff997dd..1c1348c94 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/ApplyLabelsView.swift @@ -64,26 +64,28 @@ struct ApplyLabelsView: View { } } .navigationTitle("Assign Labels") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button( - action: { presentationMode.wrappedValue.dismiss() }, - label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } - ) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { presentationMode.wrappedValue.dismiss() }, + label: { Text("Cancel").foregroundColor(.appGrayTextContrast) } + ) + } + ToolbarItem(placement: .navigationBarTrailing) { + Button( + action: { + viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in + commitLabelChanges(labels) + presentationMode.wrappedValue.dismiss() + } + }, + label: { Text("Save").foregroundColor(.appGrayTextContrast) } + ) + } } - ToolbarItem(placement: .navigationBarTrailing) { - Button( - action: { - viewModel.saveItemLabelChanges(itemID: item.id, dataService: dataService) { labels in - commitLabelChanges(labels) - presentationMode.wrappedValue.dismiss() - } - }, - label: { Text("Save").foregroundColor(.appGrayTextContrast) } - ) - } - } + #endif .sheet(isPresented: $viewModel.showCreateEmailModal) { CreateLabelView(viewModel: viewModel) } @@ -93,17 +95,20 @@ struct ApplyLabelsView: View { NavigationView { if viewModel.isLoading { EmptyView() - } else { - if #available(iOS 15.0, *) { + #if os(iOS) + if #available(iOS 15.0, *) { + innerBody + .searchable( + text: $labelSearchFilter, + placement: .navigationBarDrawer(displayMode: .always) + ) + } else { + innerBody + } + #else innerBody - .searchable( - text: $labelSearchFilter, - placement: .navigationBarDrawer(displayMode: .always) - ) - } else { - innerBody - } + #endif } } .onAppear { diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift index e89c6144f..627305438 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Labels/LabelsView.swift @@ -96,8 +96,10 @@ struct CreateLabelView: View { NavigationView { VStack(spacing: 16) { TextField("Label Name", text: $newLabelName) + #if os(iOS) .keyboardType(.alphabet) - .textFieldStyle(StandardTextFieldStyle()) + #endif + .textFieldStyle(StandardTextFieldStyle()) ColorPicker( newLabelColor == .clear ? "Select Color" : newLabelColor.description, selection: $newLabelColor @@ -130,7 +132,9 @@ struct CreateLabelView: View { } } .navigationTitle("Create New Label") - .navigationBarTitleDisplayMode(.inline) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift index 26ee03a8d..a4544fafb 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReader.swift @@ -4,107 +4,109 @@ import Utils import Views import WebKit -struct WebReader: UIViewRepresentable { - let articleContent: ArticleContent - let item: FeedItem - let openLinkAction: (URL) -> Void - let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void - let navBarVisibilityRatioUpdater: (Double) -> Void +#if os(iOS) + struct WebReader: UIViewRepresentable { + let articleContent: ArticleContent + let item: FeedItem + let openLinkAction: (URL) -> Void + let webViewActionHandler: (WKScriptMessage, WKScriptMessageReplyHandler?) -> Void + let navBarVisibilityRatioUpdater: (Double) -> Void - @Binding var increaseFontActionID: UUID? - @Binding var decreaseFontActionID: UUID? - @Binding var annotationSaveTransactionID: UUID? - @Binding var annotation: String + @Binding var increaseFontActionID: UUID? + @Binding var decreaseFontActionID: UUID? + @Binding var annotationSaveTransactionID: UUID? + @Binding var annotation: String - func makeCoordinator() -> WebReaderCoordinator { - WebReaderCoordinator() - } - - func fontSize() -> Int { - let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue) - return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize - } - - func makeUIView(context: Context) -> WKWebView { - let webView = WebViewManager.shared() - let contentController = WKUserContentController() - - webView.navigationDelegate = context.coordinator - webView.isOpaque = false - webView.backgroundColor = .clear - webView.configuration.userContentController = contentController - webView.scrollView.delegate = context.coordinator - webView.scrollView.contentInset.top = readerViewNavBarHeight - webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight - - webView.configuration.userContentController.removeAllScriptMessageHandlers() - - for action in WebViewAction.allCases { - webView.configuration.userContentController.add(context.coordinator, name: action.rawValue) + func makeCoordinator() -> WebReaderCoordinator { + WebReaderCoordinator() } - webView.configuration.userContentController.add(webView, name: "viewerAction") - - webView.configuration.userContentController.addScriptMessageHandler( - context.coordinator, contentWorld: .page, name: "articleAction" - ) - - context.coordinator.linkHandler = openLinkAction - context.coordinator.webViewActionHandler = webViewActionHandler - context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater - loadContent(webView: webView) - - return webView - } - - func updateUIView(_ webView: WKWebView, context: Context) { - if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID { - context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID - (webView as? WebView)?.saveAnnotation(annotation: annotation) + func fontSize() -> Int { + let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue) + return storedSize <= 1 ? UITraitCollection.current.preferredWebFontSize : storedSize } - if increaseFontActionID != context.coordinator.previousIncreaseFontActionID { - context.coordinator.previousIncreaseFontActionID = increaseFontActionID - (webView as? WebView)?.increaseFontSize() - } + func makeUIView(context: Context) -> WKWebView { + let webView = WebViewManager.shared() + let contentController = WKUserContentController() - if decreaseFontActionID != context.coordinator.previousDecreaseFontActionID { - context.coordinator.previousDecreaseFontActionID = decreaseFontActionID - (webView as? WebView)?.decreaseFontSize() - } + webView.navigationDelegate = context.coordinator + webView.isOpaque = false + webView.backgroundColor = .clear + webView.configuration.userContentController = contentController + webView.scrollView.delegate = context.coordinator + webView.scrollView.contentInset.top = readerViewNavBarHeight + webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight - // If the webview had been terminated `needsReload` will have been set to true - if context.coordinator.needsReload { + webView.configuration.userContentController.removeAllScriptMessageHandlers() + + for action in WebViewAction.allCases { + webView.configuration.userContentController.add(context.coordinator, name: action.rawValue) + } + + webView.configuration.userContentController.add(webView, name: "viewerAction") + + webView.configuration.userContentController.addScriptMessageHandler( + context.coordinator, contentWorld: .page, name: "articleAction" + ) + + context.coordinator.linkHandler = openLinkAction + context.coordinator.webViewActionHandler = webViewActionHandler + context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater loadContent(webView: webView) - context.coordinator.needsReload = false - return + + return webView } - if webView.isLoading { return } + func updateUIView(_ webView: WKWebView, context: Context) { + if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID { + context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID + (webView as? WebView)?.saveAnnotation(annotation: annotation) + } - // If the root element is not detected then `WKWebView` may have unloaded the content - // so we need to load it again. - webView.evaluateJavaScript("document.getElementById('root') ? true : false") { hasRootElement, _ in - guard let hasRootElement = hasRootElement as? Bool else { return } + if increaseFontActionID != context.coordinator.previousIncreaseFontActionID { + context.coordinator.previousIncreaseFontActionID = increaseFontActionID + (webView as? WebView)?.increaseFontSize() + } - if !hasRootElement { - DispatchQueue.main.async { - loadContent(webView: webView) + if decreaseFontActionID != context.coordinator.previousDecreaseFontActionID { + context.coordinator.previousDecreaseFontActionID = decreaseFontActionID + (webView as? WebView)?.decreaseFontSize() + } + + // If the webview had been terminated `needsReload` will have been set to true + if context.coordinator.needsReload { + loadContent(webView: webView) + context.coordinator.needsReload = false + return + } + + if webView.isLoading { return } + + // If the root element is not detected then `WKWebView` may have unloaded the content + // so we need to load it again. + webView.evaluateJavaScript("document.getElementById('root') ? true : false") { hasRootElement, _ in + guard let hasRootElement = hasRootElement as? Bool else { return } + + if !hasRootElement { + DispatchQueue.main.async { + loadContent(webView: webView) + } } } } - } - func loadContent(webView: WKWebView) { - webView.loadHTMLString( - WebReaderContent( - articleContent: articleContent, - item: item, - isDark: UITraitCollection.current.userInterfaceStyle == .dark, - fontSize: fontSize() + func loadContent(webView: WKWebView) { + webView.loadHTMLString( + WebReaderContent( + articleContent: articleContent, + item: item, + isDark: UITraitCollection.current.userInterfaceStyle == .dark, + fontSize: fontSize() + ) + .styledContent, + baseURL: ViewsPackage.bundleURL ) - .styledContent, - baseURL: ViewsPackage.bundleURL - ) + } } -} +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 51bc8fca4..4759c527b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -5,278 +5,280 @@ import SwiftUI import Views import WebKit -struct WebReaderContainerView: View { - let item: FeedItem - let homeFeedViewModel: HomeFeedViewModel +#if os(iOS) + struct WebReaderContainerView: View { + let item: FeedItem + let homeFeedViewModel: HomeFeedViewModel - @State private var showFontSizePopover = false - @State var showHighlightAnnotationModal = false - @State var safariWebLink: SafariWebLink? - @State private var navBarVisibilityRatio = 1.0 - @State private var showDeleteConfirmation = false - @State private var showOverlay = true - @State var increaseFontActionID: UUID? - @State var decreaseFontActionID: UUID? - @State var annotationSaveTransactionID: UUID? - @State var annotation = String() + @State private var showFontSizePopover = false + @State var showHighlightAnnotationModal = false + @State var safariWebLink: SafariWebLink? + @State private var navBarVisibilityRatio = 1.0 + @State private var showDeleteConfirmation = false + @State private var showOverlay = true + @State var increaseFontActionID: UUID? + @State var decreaseFontActionID: UUID? + @State var annotationSaveTransactionID: UUID? + @State var annotation = String() - @EnvironmentObject var dataService: DataService - @Environment(\.presentationMode) var presentationMode: Binding - @StateObject var viewModel = WebReaderViewModel() + @EnvironmentObject var dataService: DataService + @Environment(\.presentationMode) var presentationMode: Binding + @StateObject var viewModel = WebReaderViewModel() - var fontAdjustmentPopoverView: some View { - FontSizeAdjustmentPopoverView( - increaseFontAction: { increaseFontActionID = UUID() }, - decreaseFontAction: { decreaseFontActionID = UUID() } - ) - } + var fontAdjustmentPopoverView: some View { + FontSizeAdjustmentPopoverView( + increaseFontAction: { increaseFontActionID = UUID() }, + decreaseFontAction: { decreaseFontActionID = UUID() } + ) + } - func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) { - if message.name == WebViewAction.readingProgressUpdate.rawValue { - let messageBody = message.body as? [String: Double] + func webViewActionHandler(message: WKScriptMessage, replyHandler: WKScriptMessageReplyHandler?) { + if message.name == WebViewAction.readingProgressUpdate.rawValue { + let messageBody = message.body as? [String: Double] - if let messageBody = messageBody, let progress = messageBody["progress"] { + if let messageBody = messageBody, let progress = messageBody["progress"] { + homeFeedViewModel.uncommittedReadingProgressUpdates[item.id] = Double(progress) + } + } + + if let replyHandler = replyHandler { + viewModel.webViewActionWithReplyHandler( + message: message, + replyHandler: replyHandler, + dataService: dataService + ) + return + } + + if message.name == WebViewAction.highlightAction.rawValue { + handleHighlightAction(message: message) + } + + if message.name == WebViewAction.readingProgressUpdate.rawValue { + guard let messageBody = message.body as? [String: Double] else { return } + guard let progress = messageBody["progress"] else { return } homeFeedViewModel.uncommittedReadingProgressUpdates[item.id] = Double(progress) } } - if let replyHandler = replyHandler { - viewModel.webViewActionWithReplyHandler( - message: message, - replyHandler: replyHandler, - dataService: dataService - ) - return - } + private func handleHighlightAction(message: WKScriptMessage) { + guard let messageBody = message.body as? [String: String] else { return } + guard let actionID = messageBody["actionID"] else { return } - if message.name == WebViewAction.highlightAction.rawValue { - handleHighlightAction(message: message) - } - - if message.name == WebViewAction.readingProgressUpdate.rawValue { - guard let messageBody = message.body as? [String: Double] else { return } - guard let progress = messageBody["progress"] else { return } - homeFeedViewModel.uncommittedReadingProgressUpdates[item.id] = Double(progress) - } - } - - private func handleHighlightAction(message: WKScriptMessage) { - guard let messageBody = message.body as? [String: String] else { return } - guard let actionID = messageBody["actionID"] else { return } - - switch actionID { - case "annotate": - annotation = messageBody["annotation"] ?? "" - showHighlightAnnotationModal = true - default: - break - } - } - - var navBariOS14: some View { - HStack(alignment: .center) { - Button( - action: { self.presentationMode.wrappedValue.dismiss() }, - label: { - Image(systemName: "chevron.backward") - .font(.appTitleTwo) - .foregroundColor(.appGrayTextContrast) - .padding(.horizontal) - } - ) - .scaleEffect(navBarVisibilityRatio) - Spacer() - Button( - action: { showFontSizePopover.toggle() }, - label: { - Image(systemName: "textformat.size") - .font(.appTitleTwo) - } - ) - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - } - .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) - .opacity(navBarVisibilityRatio) - .background(Color.systemBackground) - .onTapGesture { - showFontSizePopover = false - } - } - - @available(macOS 12.0, *) - @available(iOS 15.0, *) - var navBar: some View { - HStack(alignment: .center) { - Button( - action: { self.presentationMode.wrappedValue.dismiss() }, - label: { - Image(systemName: "chevron.backward") - .font(.appTitleTwo) - .foregroundColor(.appGrayTextContrast) - .padding(.horizontal) - } - ) - .scaleEffect(navBarVisibilityRatio) - Spacer() - Button( - action: { showFontSizePopover.toggle() }, - label: { - Image(systemName: "textformat.size") - .font(.appTitleTwo) - } - ) - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - Menu( - content: { - Group { - Button( - action: { - homeFeedViewModel.setLinkArchived( - dataService: dataService, - linkId: item.id, - archived: !item.isArchived - ) - }, - label: { - Label( - item.isArchived ? "Unarchive" : "Archive", - systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" - ) - } - ) - Button( - action: { showDeleteConfirmation = true }, - label: { Label("Delete", systemImage: "trash") } - ) - } - }, - label: { - Image.profile - .padding(.horizontal) - .scaleEffect(navBarVisibilityRatio) - } - ) - } - .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) - .opacity(navBarVisibilityRatio) - .background(Color.systemBackground) - .onTapGesture { - showFontSizePopover = false - } - .alert("Are you sure?", isPresented: $showDeleteConfirmation) { - Button("Remove Link", role: .destructive) { - homeFeedViewModel.removeLink(dataService: dataService, linkId: item.id) + switch actionID { + case "annotate": + annotation = messageBody["annotation"] ?? "" + showHighlightAnnotationModal = true + default: + break } - Button("Cancel", role: .cancel, action: {}) } - } - var body: some View { - ZStack { - if let articleContent = viewModel.articleContent { - WebReader( - articleContent: articleContent, - item: item, - openLinkAction: { - #if os(macOS) - NSWorkspace.shared.open($0) - #elseif os(iOS) - safariWebLink = SafariWebLink(id: UUID(), url: $0) - #endif - }, - webViewActionHandler: webViewActionHandler, - navBarVisibilityRatioUpdater: { - if $0 < 1 { - showFontSizePopover = false - } - navBarVisibilityRatio = $0 - }, - increaseFontActionID: $increaseFontActionID, - decreaseFontActionID: $decreaseFontActionID, - annotationSaveTransactionID: $annotationSaveTransactionID, - annotation: $annotation + var navBariOS14: some View { + HStack(alignment: .center) { + Button( + action: { self.presentationMode.wrappedValue.dismiss() }, + label: { + Image(systemName: "chevron.backward") + .font(.appTitleTwo) + .foregroundColor(.appGrayTextContrast) + .padding(.horizontal) + } ) - .overlay( - Group { - if showOverlay { - Color.systemBackground - .transition(.opacity) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - withAnimation(.linear(duration: 0.2)) { - showOverlay = false + .scaleEffect(navBarVisibilityRatio) + Spacer() + Button( + action: { showFontSizePopover.toggle() }, + label: { + Image(systemName: "textformat.size") + .font(.appTitleTwo) + } + ) + .padding(.horizontal) + .scaleEffect(navBarVisibilityRatio) + } + .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) + .opacity(navBarVisibilityRatio) + .background(Color.systemBackground) + .onTapGesture { + showFontSizePopover = false + } + } + + @available(macOS 12.0, *) + @available(iOS 15.0, *) + var navBar: some View { + HStack(alignment: .center) { + Button( + action: { self.presentationMode.wrappedValue.dismiss() }, + label: { + Image(systemName: "chevron.backward") + .font(.appTitleTwo) + .foregroundColor(.appGrayTextContrast) + .padding(.horizontal) + } + ) + .scaleEffect(navBarVisibilityRatio) + Spacer() + Button( + action: { showFontSizePopover.toggle() }, + label: { + Image(systemName: "textformat.size") + .font(.appTitleTwo) + } + ) + .padding(.horizontal) + .scaleEffect(navBarVisibilityRatio) + Menu( + content: { + Group { + Button( + action: { + homeFeedViewModel.setLinkArchived( + dataService: dataService, + linkId: item.id, + archived: !item.isArchived + ) + }, + label: { + Label( + item.isArchived ? "Unarchive" : "Archive", + systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox" + ) + } + ) + Button( + action: { showDeleteConfirmation = true }, + label: { Label("Delete", systemImage: "trash") } + ) + } + }, + label: { + Image.profile + .padding(.horizontal) + .scaleEffect(navBarVisibilityRatio) + } + ) + } + .frame(height: readerViewNavBarHeight * navBarVisibilityRatio) + .opacity(navBarVisibilityRatio) + .background(Color.systemBackground) + .onTapGesture { + showFontSizePopover = false + } + .alert("Are you sure?", isPresented: $showDeleteConfirmation) { + Button("Remove Link", role: .destructive) { + homeFeedViewModel.removeLink(dataService: dataService, linkId: item.id) + } + Button("Cancel", role: .cancel, action: {}) + } + } + + var body: some View { + ZStack { + if let articleContent = viewModel.articleContent { + WebReader( + articleContent: articleContent, + item: item, + openLinkAction: { + #if os(macOS) + NSWorkspace.shared.open($0) + #elseif os(iOS) + safariWebLink = SafariWebLink(id: UUID(), url: $0) + #endif + }, + webViewActionHandler: webViewActionHandler, + navBarVisibilityRatioUpdater: { + if $0 < 1 { + showFontSizePopover = false + } + navBarVisibilityRatio = $0 + }, + increaseFontActionID: $increaseFontActionID, + decreaseFontActionID: $decreaseFontActionID, + annotationSaveTransactionID: $annotationSaveTransactionID, + annotation: $annotation + ) + .overlay( + Group { + if showOverlay { + Color.systemBackground + .transition(.opacity) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { + withAnimation(.linear(duration: 0.2)) { + showOverlay = false + } } } - } - } - } - ) - .sheet(item: $safariWebLink) { - SafariView(url: $0.url) - } - .sheet(isPresented: $showHighlightAnnotationModal) { - HighlightAnnotationSheet( - annotation: $annotation, - onSave: { - annotationSaveTransactionID = UUID() - showHighlightAnnotationModal = false - }, - onCancel: { - showHighlightAnnotationModal = false + } } ) - } - } else { - Color.clear - .contentShape(Rectangle()) - .onAppear { - if !viewModel.isLoading { - viewModel.loadContent(dataService: dataService, slug: item.slug) - } + .sheet(item: $safariWebLink) { + SafariView(url: $0.url) } - } - if showFontSizePopover { - VStack { + .sheet(isPresented: $showHighlightAnnotationModal) { + HighlightAnnotationSheet( + annotation: $annotation, + onSave: { + annotationSaveTransactionID = UUID() + showHighlightAnnotationModal = false + }, + onCancel: { + showHighlightAnnotationModal = false + } + ) + } + } else { Color.clear .contentShape(Rectangle()) - .frame(height: LinkItemDetailView.navBarHeight) - HStack { + .onAppear { + if !viewModel.isLoading { + viewModel.loadContent(dataService: dataService, slug: item.slug) + } + } + } + if showFontSizePopover { + VStack { + Color.clear + .contentShape(Rectangle()) + .frame(height: LinkItemDetailView.navBarHeight) + HStack { + Spacer() + fontAdjustmentPopoverView + .background(Color.appButtonBackground) + .cornerRadius(8) + .padding(.trailing, 44) + } Spacer() - fontAdjustmentPopoverView - .background(Color.appButtonBackground) - .cornerRadius(8) - .padding(.trailing, 44) } - Spacer() + .background( + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + showFontSizePopover = false + } + ) } - .background( - Color.clear - .contentShape(Rectangle()) - .onTapGesture { - showFontSizePopover = false - } - ) - } - if #available(iOS 15.0, *) { - VStack(spacing: 0) { - navBar - Spacer() + if #available(iOS 15.0, *) { + VStack(spacing: 0) { + navBar + Spacer() + } + .navigationBarHidden(true) + } else { + VStack(spacing: 0) { + navBariOS14 + Spacer() + } + .navigationBarHidden(true) } - .navigationBarHidden(true) - } else { - VStack(spacing: 0) { - navBariOS14 - Spacer() - } - .navigationBarHidden(true) - } - }.onDisappear { - // Clear the shared webview content when exiting - WebViewManager.shared().loadHTMLString("", baseURL: nil) + }.onDisappear { + // Clear the shared webview content when exiting + WebViewManager.shared().loadHTMLString("", baseURL: nil) + } + .navigationBarHidden(true) } - .navigationBarHidden(true) } -} +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift index c75bca135..54f7edb5b 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderCoordinator.swift @@ -2,7 +2,9 @@ import Combine import Models import Services import SwiftUI -import UIKit +#if os(iOS) + import UIKit +#endif import Utils import Views import WebKit diff --git a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift index 2c20306f6..a676666c5 100644 --- a/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift +++ b/apple/OmnivoreKit/Sources/Utils/ColorUtils.swift @@ -45,9 +45,17 @@ public extension Color { } private func toHex() -> String? { - guard let components = UIColor(self).cgColor.components, components.count >= 3 else { - return nil - } + #if os(iOS) + guard let components = UIColor(self).cgColor.components, components.count >= 3 else { + return nil + } + #endif + + #if os(macOS) + guard let components = NSColor(self).cgColor.components, components.count >= 3 else { + return nil + } + #endif let red = Float(components[0]) let green = Float(components[1]) let blue = Float(components[2]) @@ -61,9 +69,17 @@ public extension Color { } var isDark: Bool { - guard let components = UIColor(self).cgColor.components, components.count >= 3 else { - return false - } + #if os(iOS) + guard let components = UIColor(self).cgColor.components, components.count >= 3 else { + return false + } + #endif + + #if os(macOS) + guard let components = NSColor(self).cgColor.components, components.count >= 3 else { + return false + } + #endif let lum = 0.2126 * Float(components[0]) + 0.7152 * Float(components[1]) + 0.0722 * Float(components[2]) return lum < 0.50 diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift index 07e458365..a78344f6e 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebAppView.swift @@ -10,7 +10,9 @@ enum WebViewConfigurationManager { static func create() -> WKWebViewConfiguration { let config = WKWebViewConfiguration() config.processPool = processPool - config.allowsInlineMediaPlayback = true + #if os(iOS) + config.allowsInlineMediaPlayback = true + #endif config.mediaTypesRequiringUserActionForPlayback = .audio return config } diff --git a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift index 9744d2f9a..5a6277f00 100644 --- a/apple/OmnivoreKit/Sources/Views/Article/WebView.swift +++ b/apple/OmnivoreKit/Sources/Views/Article/WebView.swift @@ -55,7 +55,7 @@ public final class WebView: WKWebView { } #elseif os(macOS) - override func viewDidChangeEffectiveAppearance() { + override public func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) { case .some(.darkAqua):