From e4b86ecf34a60e0e2d325093bcb801950bdf7540 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Thu, 6 Jul 2023 14:31:19 -0700 Subject: [PATCH] Improvements to the Continue Reading section, add long press actions --- .../LibraryFeatureCardNavigationLink.swift | 9 +- .../App/Views/Home/HomeFeedViewIOS.swift | 54 ++- .../App/Views/Home/HomeFeedViewModel.swift | 84 +++-- .../Views/WebReader/WebReaderContainer.swift | 315 +++++++++--------- .../Sources/Models/LinkedItemFilter.swift | 4 +- .../UpdateArticleLabelsPublisher.swift | 3 + .../Views/FeedItem/LibraryFeatureCard.swift | 6 +- .../Sources/Views/SwiftUIDelayedGesture.swift | 105 ++++++ 8 files changed, 380 insertions(+), 200 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/Views/SwiftUIDelayedGesture.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift index 7ed839850..4f7cb2537 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/Components/LibraryFeatureCardNavigationLink.swift @@ -15,9 +15,10 @@ struct LibraryFeatureCardNavigationLink: View { @EnvironmentObject var audioController: AudioController let item: LinkedItem - @ObservedObject var viewModel: HomeFeedViewModel + let onLongPress: (LinkedItem) -> Void + var body: some View { ZStack { Button { @@ -29,11 +30,11 @@ struct LibraryFeatureCardNavigationLink: View { } .opacity(0) .buttonStyle(PlainButtonStyle()) - .onAppear { - Task { await viewModel.itemAppeared(item: item, dataService: dataService) } - } LibraryFeatureCard(item: item, viewer: dataService.currentViewer) } + .delayedGesture(LongPressGesture().onEnded { _ in + onLongPress(item) + }) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index b88006d12..d1d637faf 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -28,6 +28,7 @@ struct AnimatingCellHeight: AnimatableModifier { @State var searchPresented = false @State var addLinkPresented = false @State var settingsPresented = false + @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController @@ -260,6 +261,8 @@ struct AnimatingCellHeight: AnimatableModifier { @State private var itemToRemove: LinkedItem? @State private var confirmationShown = false @State private var showHideFeatureAlert = false + @State var showFeatureActions = false + @State var selectedFeatureItem: LinkedItem? @ObservedObject var viewModel: HomeFeedViewModel @@ -367,17 +370,17 @@ struct AnimatingCellHeight: AnimatableModifier { HStack { Menu(content: { Button(action: { - viewModel.updateFeatureFilter(dataService: dataService, filter: .continueReading) + viewModel.updateFeatureFilter(context: dataService.viewContext, filter: .continueReading) }, label: { Text("Continue Reading") }) Button(action: { - viewModel.updateFeatureFilter(dataService: dataService, filter: .pinned) + viewModel.updateFeatureFilter(context: dataService.viewContext, filter: .pinned) }, label: { Text("Pinned") }) Button(action: { - viewModel.updateFeatureFilter(dataService: dataService, filter: .newsletters) + viewModel.updateFeatureFilter(context: dataService.viewContext, filter: .newsletters) }, label: { Text("Newsletters") }) @@ -405,9 +408,14 @@ struct AnimatingCellHeight: AnimatableModifier { ScrollView(.horizontal, showsIndicators: false) { if viewModel.featureItems.count > 0 { LazyHStack(alignment: .top, spacing: 15) { + Spacer(minLength: 1).frame(width: 1) ForEach(viewModel.featureItems) { item in - LibraryFeatureCardNavigationLink(item: item, viewModel: viewModel) + LibraryFeatureCardNavigationLink(item: item, viewModel: viewModel, onLongPress: { item in + self.selectedFeatureItem = item + self.showFeatureActions = true + }) } + Spacer(minLength: 1).frame(width: 1) } .padding(.top, 0) } else { @@ -419,7 +427,7 @@ struct AnimatingCellHeight: AnimatableModifier { .fixedSize(horizontal: false, vertical: true) } } - }.padding(.horizontal, 15) + } Color.thBorderColor.frame(maxWidth: .infinity, maxHeight: 0.5) } } @@ -449,7 +457,8 @@ struct AnimatingCellHeight: AnimatableModifier { featureCard .listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowSeparator(.hidden, edges: .all) - .modifier(AnimatingCellHeight(height: viewModel.featureItems.count > 0 ? 200 : 130)) + // .modifier(AnimatingCellHeight(height: viewModel.isLoading || viewModel.featureItems.count > 0 ? 190 : 130)) + .modifier(AnimatingCellHeight(height: 190)) } ForEach(viewModel.items) { item in @@ -497,17 +506,46 @@ struct AnimatingCellHeight: AnimatableModifier { } Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFeatureAlert = false } } + .confirmationDialog("", isPresented: $showFeatureActions) { + if let item = selectedFeatureItem { + if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .pinned { + Button("Unpin", action: { + viewModel.unpinItem(dataService: dataService, item: item) + }) + } + Button("Pin", action: { + viewModel.pinItem(dataService: dataService, item: item) + }) + Button("Archive", action: { + viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: true) + }) + Button("Delete", action: { + viewModel.removeLink(dataService: dataService, objectID: item.objectID) + }) + if FeaturedItemFilter(rawValue: viewModel.featureFilter) == .continueReading { + Button("Mark Read", action: { + viewModel.markRead(dataService: dataService, item: item) + }) + Button("Mark Unread", action: { + viewModel.markUnread(dataService: dataService, item: item) + }) + } + Button("Dismiss", role: .cancel, action: { + showFeatureActions = false + }) + } + } } func swipeActionButton(action: SwipeAction, item: LinkedItem) -> AnyView { switch action { case .pin: return AnyView(Button(action: { - viewModel.addLabel(dataService: dataService, item: item, label: "Pinned") + viewModel.pinItem(dataService: dataService, item: item) }, label: { VStack { Image(systemName: "pin.fill") - .rotationEffect(Angle(degrees: 90)) + .rotationEffect(Angle(degrees: 180)) Text("Pin") } }).tint(Color(hex: "#0A84FF"))) diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 9b94b5a63..20a88e5d2 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -55,31 +55,25 @@ import Views super.init() } - func setItems(_ items: [LinkedItem]) { + func setItems(_ context: NSManagedObjectContext, _ items: [LinkedItem]) { self.items = items + updateFeatureFilter(context: context, filter: FeaturedItemFilter(rawValue: featureFilter)) } - func updateFeatureFilter(dataService: DataService, filter: FeaturedItemFilter?) { + func updateFeatureFilter(context: NSManagedObjectContext, filter: FeaturedItemFilter?) { Task { if let filter = filter { - featureItems = await loadFeatureItems(dataService: dataService, predicate: filter.predicate, sort: filter.sortDescriptor) - print("updated feature items: ", featureItems.count) + featureFilter = filter.rawValue + + featureItems = await loadFeatureItems( + context: context, + predicate: filter.predicate, + sort: filter.sortDescriptor + ) } else { featureItems = [] } } - if let filter = filter { - // now try to update the continue reading items: - - featureItems = (items.filter { item in - filter.predicate.evaluate(with: item) - } as NSArray) - .sortedArray(using: [filter.sortDescriptor]) - .compactMap { $0 as? LinkedItem } - featureFilter = filter.rawValue - } else { - featureItems = [] - } } func handleReaderItemNotification(objectID: NSManagedObjectID, dataService: DataService) { @@ -204,9 +198,9 @@ import Views // Don't use FRC for searching. Use server results directly. if fetchedResultsController != nil { fetchedResultsController = nil - setItems([]) + setItems(dataService.viewContext, []) } - setItems(isRefresh ? newItems : items + newItems) + setItems(dataService.viewContext, isRefresh ? newItems : items + newItems) } isLoading = false @@ -239,7 +233,7 @@ import Views updateFetchController(dataService: dataService) } - updateFeatureFilter(dataService: dataService, filter: FeaturedItemFilter(rawValue: featureFilter)) + updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter)) isLoading = false showLoadingBar = false @@ -255,13 +249,13 @@ import Views showLoadingBar = false } - func loadFeatureItems(dataService: DataService, predicate: NSPredicate, sort: NSSortDescriptor) async -> [LinkedItem] { + func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [LinkedItem] { let fetchRequest: NSFetchRequest = LinkedItem.fetchRequest() fetchRequest.fetchLimit = 25 fetchRequest.predicate = predicate fetchRequest.sortDescriptors = [sort] - return (try? dataService.viewContext.fetch(fetchRequest)) ?? [] + return (try? context.fetch(fetchRequest)) ?? [] } private var fetchRequest: NSFetchRequest { @@ -315,7 +309,7 @@ import Views fetchedResultsController.delegate = self try? fetchedResultsController.performFetch() - setItems(fetchedResultsController.fetchedObjects ?? []) + setItems(dataService.viewContext, fetchedResultsController.fetchedObjects ?? []) } func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) { @@ -331,12 +325,52 @@ import Views func addLabel(dataService: DataService, item: LinkedItem, label: String) { Task { if let label = LinkedItemLabel.named(label, inContext: dataService.viewContext) { - dataService.updateItemLabels(itemID: item.unwrappedID, labelIDs: [label.unwrappedID]) + // Label already exists, so just add it and refresh everything + let existingLabels = item.labels?.allObjects.compactMap { ($0 as? LinkedItemLabel)?.unwrappedID } ?? [] + dataService.updateItemLabels(itemID: item.unwrappedID, labelIDs: existingLabels + [label.unwrappedID]) + + item.update(inContext: dataService.viewContext) + updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter)) + } else if let labelID = try? await dataService.createLabel(name: "Pinned", color: "#0A84FF", description: ""), + let label = LinkedItemLabel.named(label, inContext: dataService.viewContext) + { + let existingLabels = item.labels?.allObjects.compactMap { ($0 as? LinkedItemLabel)?.unwrappedID } ?? [] + dataService.updateItemLabels(itemID: item.unwrappedID, labelIDs: existingLabels + [label.unwrappedID]) + + item.update(inContext: dataService.viewContext) + updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter)) } - updateFeatureFilter(dataService: dataService, filter: FeaturedItemFilter(rawValue: featureFilter)) } } + func removeLabel(dataService: DataService, item: LinkedItem, named: String) { + let labelIds = item.labels?.filter { ($0 as? LinkedItemLabel)?.name != named }.compactMap { ($0 as? LinkedItemLabel)?.unwrappedID } ?? [] + dataService.updateItemLabels(itemID: item.unwrappedID, labelIDs: labelIds) + item.update(inContext: dataService.viewContext) + } + + func pinItem(dataService: DataService, item: LinkedItem) { + addLabel(dataService: dataService, item: item, label: "Pinned") + if featureFilter == FeaturedItemFilter.pinned.rawValue { + updateFeatureFilter(context: dataService.viewContext, filter: .pinned) + } + } + + func unpinItem(dataService: DataService, item: LinkedItem) { + removeLabel(dataService: dataService, item: item, named: "Pinned") + if featureFilter == FeaturedItemFilter.pinned.rawValue { + updateFeatureFilter(context: dataService.viewContext, filter: .pinned) + } + } + + func markRead(dataService: DataService, item: LinkedItem) { + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 100, anchorIndex: 0) + } + + func markUnread(dataService: DataService, item: LinkedItem) { + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0, anchorIndex: 0) + } + func snoozeUntil(dataService: DataService, linkId: String, until: Date, successMessage: String?) async { isLoading = true @@ -398,6 +432,6 @@ import Views extension HomeFeedViewModel: NSFetchedResultsControllerDelegate { func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - setItems(controller.fetchedObjects as? [LinkedItem] ?? []) + setItems(controller.managedObjectContext, controller.fetchedObjects as? [LinkedItem] ?? []) } } diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 7c76591e6..6583c669c 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -395,174 +395,171 @@ struct WebReaderContainerView: View { var body: some View { ZStack { - GeometryReader { geo in - let foo = print("READER", geo.size) - if let articleContent = viewModel.articleContent { - WebReader( - item: item, - articleContent: articleContent, - openLinkAction: { - #if os(macOS) - NSWorkspace.shared.open($0) - #elseif os(iOS) - if UIDevice.current.userInterfaceIdiom == .phone, $0.absoluteString != item.unwrappedPageURLString { - linkToOpen = $0 - displayLinkSheet = true - } else { - safariWebLink = SafariWebLink(id: UUID(), url: $0) - } - #endif - }, - tapHandler: tapHandler, - scrollPercentHandler: scrollPercentHandler, - webViewActionHandler: webViewActionHandler, - navBarVisibilityRatioUpdater: { - navBarVisibilityRatio = $0 - }, - readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID, - annotationSaveTransactionID: $annotationSaveTransactionID, - showNavBarActionID: $showNavBarActionID, - shareActionID: $shareActionID, - annotation: $annotation, - showBottomBar: $showBottomBar, - showHighlightAnnotationModal: $showHighlightAnnotationModal - ) - .background(ThemeManager.currentBgColor) - .onAppear { - if item.isUnread { - dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0) - } - Task { - await audioController.preload(itemIDs: [item.unwrappedID]) - } - } - .confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet) { - Button(action: { - if let linkToOpen = linkToOpen { - safariWebLink = SafariWebLink(id: UUID(), url: linkToOpen) - } - }, label: { Text(LocalText.genericOpen) }) - Button(action: { - #if os(iOS) - UIPasteboard.general.string = item.unwrappedPageURLString - #else - // Pasteboard.general.string = item.unwrappedPageURLString TODO: fix for mac - #endif - showInSnackbar("Link Copied") - }, label: { Text(LocalText.readerCopyLink) }) - Button(action: { - if let linkToOpen = linkToOpen { - viewModel.saveLink(dataService: dataService, url: linkToOpen) - } - }, label: { Text(LocalText.readerSave) }) - } - #if os(iOS) - .fullScreenCover(item: $safariWebLink) { - SafariView(url: $0.url) - } - #endif - .alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) { - Button(LocalText.genericOk, role: .cancel, action: { - errorAlertMessage = nil - showErrorAlertMessage = false - }) - } - #if os(iOS) - .formSheet(isPresented: $showRecommendSheet) { - let highlightCount = item.highlights.asArray(of: Highlight.self).filter(\.createdByMe).count - - NavigationView { - RecommendToView( - dataService: dataService, - viewModel: RecommendToViewModel(pageID: item.unwrappedID, - highlightCount: highlightCount) - ) - }.onDisappear { - showRecommendSheet = false - } - } - #endif - .sheet(isPresented: $showHighlightAnnotationModal) { - NavigationView { - HighlightAnnotationSheet( - annotation: $annotation, - onSave: { - annotationSaveTransactionID = UUID() - }, - onCancel: { - showHighlightAnnotationModal = false - }, - errorAlertMessage: $errorAlertMessage, - showErrorAlertMessage: $showErrorAlertMessage - ) - } - } - .sheet(isPresented: $showHighlightLabelsModal) { - if let highlight = Highlight.lookup(byID: self.annotation, inContext: self.dataService.viewContext) { - ApplyLabelsView(mode: .highlight(highlight), isSearchFocused: false) { selectedLabels in - viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID, - labelIDs: selectedLabels.map(\.unwrappedID), - dataService: dataService) - } - } - } - } else if let errorMessage = viewModel.errorMessage { - VStack { - Text(errorMessage).padding() - if viewModel.allowRetry, viewModel.hasOriginalUrl(item) { - Button("Open Original", action: { - openOriginalURL(urlString: item.pageURLString) - }).buttonStyle(RoundedRectButtonStyle()) - if let urlStr = item.pageURLString, let username = dataService.currentViewer?.username, let url = URL(string: urlStr) { - Button("Attempt to Save Again", action: { - viewModel.errorMessage = nil - viewModel.saveLinkAndFetch(dataService: dataService, username: username, url: url) - }).buttonStyle(RoundedRectButtonStyle()) - } - } - } - } else { - ProgressView() - .opacity(progressViewOpacity) - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) { - progressViewOpacity = 1 - } - } - .task { - if let username = dataService.currentViewer?.username { - await viewModel.loadContent( - dataService: dataService, - username: username, - itemID: item.unwrappedID - ) + if let articleContent = viewModel.articleContent { + WebReader( + item: item, + articleContent: articleContent, + openLinkAction: { + #if os(macOS) + NSWorkspace.shared.open($0) + #elseif os(iOS) + if UIDevice.current.userInterfaceIdiom == .phone, $0.absoluteString != item.unwrappedPageURLString { + linkToOpen = $0 + displayLinkSheet = true } else { - viewModel.errorMessage = "You are not logged in." + safariWebLink = SafariWebLink(id: UUID(), url: $0) } + #endif + }, + tapHandler: tapHandler, + scrollPercentHandler: scrollPercentHandler, + webViewActionHandler: webViewActionHandler, + navBarVisibilityRatioUpdater: { + navBarVisibilityRatio = $0 + }, + readerSettingsChangedTransactionID: $readerSettingsChangedTransactionID, + annotationSaveTransactionID: $annotationSaveTransactionID, + showNavBarActionID: $showNavBarActionID, + shareActionID: $shareActionID, + annotation: $annotation, + showBottomBar: $showBottomBar, + showHighlightAnnotationModal: $showHighlightAnnotationModal + ) + .background(ThemeManager.currentBgColor) + .onAppear { + if item.isUnread { + dataService.updateLinkReadingProgress(itemID: item.unwrappedID, readingProgress: 0.1, anchorIndex: 0) + } + Task { + await audioController.preload(itemIDs: [item.unwrappedID]) + } + } + .confirmationDialog(linkToOpen?.absoluteString ?? "", isPresented: $displayLinkSheet) { + Button(action: { + if let linkToOpen = linkToOpen { + safariWebLink = SafariWebLink(id: UUID(), url: linkToOpen) } + }, label: { Text(LocalText.genericOpen) }) + Button(action: { + #if os(iOS) + UIPasteboard.general.string = item.unwrappedPageURLString + #else + // Pasteboard.general.string = item.unwrappedPageURLString TODO: fix for mac + #endif + showInSnackbar("Link Copied") + }, label: { Text(LocalText.readerCopyLink) }) + Button(action: { + if let linkToOpen = linkToOpen { + viewModel.saveLink(dataService: dataService, url: linkToOpen) + } + }, label: { Text(LocalText.readerSave) }) } #if os(iOS) - VStack(spacing: 0) { - navBar - Spacer() - if showBottomBar { - bottomButtons - .frame(height: 48) - .background(Color.webControlButtonBackground) - .cornerRadius(6) - .padding(.bottom, 34) - .shadow(color: .gray.opacity(0.13), radius: 8, x: 0, y: 4) - .opacity(bottomBarOpacity) - .onAppear { - withAnimation(Animation.linear(duration: 0.25)) { self.bottomBarOpacity = 1 } - } - .onDisappear { - self.bottomBarOpacity = 0 - } + .fullScreenCover(item: $safariWebLink) { + SafariView(url: $0.url) + } + #endif + .alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) { + Button(LocalText.genericOk, role: .cancel, action: { + errorAlertMessage = nil + showErrorAlertMessage = false + }) + } + #if os(iOS) + .formSheet(isPresented: $showRecommendSheet) { + let highlightCount = item.highlights.asArray(of: Highlight.self).filter(\.createdByMe).count + + NavigationView { + RecommendToView( + dataService: dataService, + viewModel: RecommendToViewModel(pageID: item.unwrappedID, + highlightCount: highlightCount) + ) + }.onDisappear { + showRecommendSheet = false } } #endif + .sheet(isPresented: $showHighlightAnnotationModal) { + NavigationView { + HighlightAnnotationSheet( + annotation: $annotation, + onSave: { + annotationSaveTransactionID = UUID() + }, + onCancel: { + showHighlightAnnotationModal = false + }, + errorAlertMessage: $errorAlertMessage, + showErrorAlertMessage: $showErrorAlertMessage + ) + } + } + .sheet(isPresented: $showHighlightLabelsModal) { + if let highlight = Highlight.lookup(byID: self.annotation, inContext: self.dataService.viewContext) { + ApplyLabelsView(mode: .highlight(highlight), isSearchFocused: false) { selectedLabels in + viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID, + labelIDs: selectedLabels.map(\.unwrappedID), + dataService: dataService) + } + } + } + } else if let errorMessage = viewModel.errorMessage { + VStack { + Text(errorMessage).padding() + if viewModel.allowRetry, viewModel.hasOriginalUrl(item) { + Button("Open Original", action: { + openOriginalURL(urlString: item.pageURLString) + }).buttonStyle(RoundedRectButtonStyle()) + if let urlStr = item.pageURLString, let username = dataService.currentViewer?.username, let url = URL(string: urlStr) { + Button("Attempt to Save Again", action: { + viewModel.errorMessage = nil + viewModel.saveLinkAndFetch(dataService: dataService, username: username, url: url) + }).buttonStyle(RoundedRectButtonStyle()) + } + } + } + } else { + ProgressView() + .opacity(progressViewOpacity) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1000)) { + progressViewOpacity = 1 + } + } + .task { + if let username = dataService.currentViewer?.username { + await viewModel.loadContent( + dataService: dataService, + username: username, + itemID: item.unwrappedID + ) + } else { + viewModel.errorMessage = "You are not logged in." + } + } } + #if os(iOS) + VStack(spacing: 0) { + navBar + Spacer() + if showBottomBar { + bottomButtons + .frame(height: 48) + .background(Color.webControlButtonBackground) + .cornerRadius(6) + .padding(.bottom, 34) + .shadow(color: .gray.opacity(0.13), radius: 8, x: 0, y: 4) + .opacity(bottomBarOpacity) + .onAppear { + withAnimation(Animation.linear(duration: 0.25)) { self.bottomBarOpacity = 1 } + } + .onDisappear { + self.bottomBarOpacity = 0 + } + } + } + #endif } #if os(macOS) .onReceive(NSNotification.readerSettingsChangedPublisher) { _ in diff --git a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift index 20fba6e67..63319bb5f 100644 --- a/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift +++ b/apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift @@ -116,7 +116,7 @@ public extension FeaturedItemFilter { case .continueReading: return "Your recently read items will appear here." case .pinned: - return "Create a label named Pinned and add it to items you'd like to appear here" + return "Create a label named Pinned and add it to items you would like to appear here." case .recommended: return "Reads recommended in your Clubs will appear here." case .newsletters: @@ -172,6 +172,8 @@ public extension FeaturedItemFilter { switch self { case .continueReading: return NSSortDescriptor(key: #keyPath(LinkedItem.readAt), ascending: false) + case .pinned: + return NSSortDescriptor(key: #keyPath(LinkedItem.updatedAt), ascending: false) default: return savedAtSort } diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift index cab778898..5eeaa5f52 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Mutations/UpdateArticleLabelsPublisher.swift @@ -19,6 +19,9 @@ extension DataService { } } + linkedItem.update(inContext: self.backgroundContext) + try? self.backgroundContext.save() + // Send update to server self.syncLabelUpdates(itemID: itemID, labelIDs: labelIDs) } diff --git a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift index ac2b14e69..eec61f4e2 100644 --- a/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift +++ b/apple/OmnivoreKit/Sources/Views/FeedItem/LibraryFeatureCard.swift @@ -18,9 +18,9 @@ public struct LibraryFeatureCard: View { imageBox title Spacer() - } - .padding(0) - .frame(maxWidth: 150) + + }.padding(0) + .frame(maxWidth: 150) } var isFullyRead: Bool { diff --git a/apple/OmnivoreKit/Sources/Views/SwiftUIDelayedGesture.swift b/apple/OmnivoreKit/Sources/Views/SwiftUIDelayedGesture.swift new file mode 100644 index 000000000..14a238398 --- /dev/null +++ b/apple/OmnivoreKit/Sources/Views/SwiftUIDelayedGesture.swift @@ -0,0 +1,105 @@ +// +// FROM: https://github.com/ciaranrobrien/SwiftUIDelayedGesture/tree/main +// + +import SwiftUI + +public extension View { + /// Sequences a gesture with a long press and attaches the result to the view, + /// which results in the gesture only receiving events after the long press + /// succeeds. + /// + /// Use this view modifier *instead* of `.gesture` to delay a gesture: + /// + /// ScrollView { + /// FooView() + /// .delayedGesture(someGesture, delay: 0.2) + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - mask: A value that controls how adding this gesture to the view + /// affects other gestures recognized by the view and its subviews. + /// - delay: A value that controls the duration of the long press that + /// must elapse before the gesture can be recognized by the view. + /// - action: An action to perform if a tap gesture is recognized + /// before the long press can be recognized by the view. + func delayedGesture(_ gesture: T, + including mask: GestureMask = .all, + delay: TimeInterval = 0.25, + onTapGesture action: @escaping () -> Void = {}) -> some View + { + modifier(DelaysTouches(duration: delay, action: action)) + .gesture(gesture, including: mask) + } + + /// Attaches a long press gesture to the view, which results in gestures with a + /// lower precedence only receiving events after the long press succeeds. + /// + /// Use this view modifier *before* `.gesture` to delay a gesture: + /// + /// ScrollView { + /// FooView() + /// .delayedInput(delay: 0.2) + /// .gesture(someGesture) + /// } + /// + /// - Parameters: + /// - delay: A value that controls the duration of the long press that + /// must elapse before lower precedence gestures can be recognized by + /// the view. + /// - action: An action to perform if a tap gesture is recognized + /// before the long press can be recognized by the view. + func delayedInput(delay: TimeInterval = 0.25, + onTapGesture action: @escaping () -> Void = {}) -> some View + { + modifier(DelaysTouches(duration: delay, action: action)) + } +} + +private struct DelaysTouches: ViewModifier { + @State private var disabled = false + @State private var touchDownDate: Date? = nil + + var duration: TimeInterval + var action: () -> Void + + func body(content: Content) -> some View { + Button(action: action) { + content + } + .buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate)) + .disabled(disabled) + } +} + +private struct DelaysTouchesButtonStyle: ButtonStyle { + @Binding var disabled: Bool + var duration: TimeInterval + @Binding var touchDownDate: Date? + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .onChange(of: configuration.isPressed, perform: handleIsPressed) + } + + private func handleIsPressed(isPressed: Bool) { + if isPressed { + let date = Date() + touchDownDate = date + + DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) { + if date == touchDownDate { + disabled = true + + DispatchQueue.main.async { + disabled = false + } + } + } + } else { + touchDownDate = nil + disabled = false + } + } +}