Improvements to the Continue Reading section, add long press actions
This commit is contained in:
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")))
|
||||
|
||||
@ -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<Models.LinkedItem> = 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<Models.LinkedItem> {
|
||||
@ -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<NSFetchRequestResult>) {
|
||||
setItems(controller.fetchedObjects as? [LinkedItem] ?? [])
|
||||
setItems(controller.managedObjectContext, controller.fetchedObjects as? [LinkedItem] ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -18,9 +18,9 @@ public struct LibraryFeatureCard: View {
|
||||
imageBox
|
||||
title
|
||||
Spacer()
|
||||
}
|
||||
.padding(0)
|
||||
.frame(maxWidth: 150)
|
||||
|
||||
}.padding(0)
|
||||
.frame(maxWidth: 150)
|
||||
}
|
||||
|
||||
var isFullyRead: Bool {
|
||||
|
||||
105
apple/OmnivoreKit/Sources/Views/SwiftUIDelayedGesture.swift
Normal file
105
apple/OmnivoreKit/Sources/Views/SwiftUIDelayedGesture.swift
Normal file
@ -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<T: Gesture>(_ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user