Allow setting labels on highlights

This commit is contained in:
Jackson Harper
2022-11-28 21:32:42 +08:00
parent aef72f00f0
commit 395b3d7ac7
10 changed files with 186 additions and 10 deletions

View File

@ -11,6 +11,7 @@ struct HighlightsListCard: View {
@Binding var hasHighlightMutations: Bool
let onSaveAnnotation: (String) -> Void
let onDeleteHighlight: () -> Void
let onSetLabels: (String) -> Void
var contextMenuView: some View {
Group {
@ -30,6 +31,12 @@ struct HighlightsListCard: View {
},
label: { Label("Copy", systemImage: "doc.on.doc") }
)
Button(
action: {
onSetLabels(highlightParams.highlightID)
},
label: { Label("Labels", systemImage: "tag") }
)
Button(
action: onDeleteHighlight,
label: { Label("Delete", systemImage: "trash") }
@ -109,6 +116,20 @@ struct HighlightsListCard: View {
} else {
noteSection
}
if highlightParams.labels.count > 0 {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(highlightParams.labels, id: \.self) {
TextChip(feedItemLabel: $0)
}
Spacer()
}
}.introspectScrollView { scrollView in
scrollView.bounces = false
}
.padding(.top, 0)
}
}
.padding(.bottom, 8)
}

View File

@ -11,6 +11,7 @@ struct HighlightsListView: View {
let itemObjectID: NSManagedObjectID
@Binding var hasHighlightMutations: Bool
@State var setLabelsHighlight: Highlight?
var emptyView: some View {
Text("""
@ -61,10 +62,21 @@ struct HighlightsListView: View {
highlightID: highlightParams.highlightID,
dataService: dataService
)
},
onSetLabels: { highlightID in
setLabelsHighlight = Highlight.lookup(byID: highlightID, inContext: dataService.viewContext)
}
)
}
}
}.sheet(item: $setLabelsHighlight) { highlight in
ApplyLabelsView(mode: .highlight(highlight), onSave: { selectedLabels in
hasHighlightMutations = true
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
labels: selectedLabels,
dataService: dataService)
})
}
}

View File

@ -10,6 +10,7 @@ struct HighlightListItemParams: Identifiable {
let title: String
let annotation: String
let quote: String
let labels: [LinkedItemLabel]
}
@MainActor final class HighlightsListViewModel: ObservableObject {
@ -29,7 +30,8 @@ struct HighlightListItemParams: Identifiable {
highlightID: highlightID,
title: highlightItems[index].title,
annotation: annotation,
quote: highlightItems[index].quote
quote: highlightItems[index].quote,
labels: highlightItems[index].labels
)
}
}
@ -39,6 +41,20 @@ struct HighlightListItemParams: Identifiable {
highlightItems.removeAll { $0.highlightID == highlightID }
}
func setLabelsForHighlight(highlightID: String, labels: [LinkedItemLabel], dataService: DataService) {
dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: labels.map(\.unwrappedID))
if let index = highlightItems.firstIndex(where: { $0.highlightID == highlightID }) {
highlightItems[index] = HighlightListItemParams(
highlightID: highlightID,
title: highlightItems[index].title,
annotation: highlightItems[index].annotation,
quote: highlightItems[index].quote,
labels: labels
)
}
}
private func loadHighlights(item: LinkedItem) {
let unsortedHighlights = item.highlights.asArray(of: Highlight.self)
@ -51,7 +67,8 @@ struct HighlightListItemParams: Identifiable {
highlightID: $0.unwrappedID,
title: "Highlight",
annotation: $0.annotation ?? "",
quote: $0.quote ?? ""
quote: $0.quote ?? "",
labels: $0.labels.asArray(of: LinkedItemLabel.self)
)
}
}

View File

@ -6,11 +6,12 @@ import Views
struct ApplyLabelsView: View {
enum Mode {
case item(LinkedItem)
case highlight(Highlight)
case list([LinkedItemLabel])
var navTitle: String {
switch self {
case .item:
case .item, .highlight:
return "Assign Labels"
case .list:
return "Apply Label Filters"
@ -19,7 +20,7 @@ struct ApplyLabelsView: View {
var confirmButtonText: String {
switch self {
case .item:
case .item, .highlight:
return "Save"
case .list:
return "Apply"
@ -109,6 +110,8 @@ struct ApplyLabelsView: View {
switch mode {
case let .item(feedItem):
viewModel.saveItemLabelChanges(itemID: feedItem.unwrappedID, dataService: dataService)
case .highlight:
onSave?(viewModel.selectedLabels)
case .list:
onSave?(viewModel.selectedLabels)
}
@ -148,6 +151,8 @@ struct ApplyLabelsView: View {
switch mode {
case let .item(feedItem):
await viewModel.loadLabels(dataService: dataService, item: feedItem)
case let .highlight(highlight):
await viewModel.loadLabels(dataService: dataService, highlight: highlight)
case let .list(labels):
await viewModel.loadLabels(dataService: dataService, initiallySelectedLabels: labels)
}

View File

@ -36,6 +36,29 @@ import Views
isLoading = false
}
func loadLabels(
dataService: DataService,
highlight: Highlight
) async {
isLoading = true
if let labelIDs = try? await dataService.labels() {
dataService.viewContext.performAndWait {
self.labels = labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel }
}
let selLabels = highlight.labels ?? []
for label in labels {
if selLabels.contains(label) {
selectedLabels.append(label)
} else {
unselectedLabels.append(label)
}
}
}
isLoading = false
}
func loadLabelsFromStore(dataService: DataService) async {
let fetchRequest: NSFetchRequest<Models.LinkedItemLabel> = LinkedItemLabel.fetchRequest()
@ -95,6 +118,10 @@ import Views
dataService.updateItemLabels(itemID: itemID, labelIDs: selectedLabels.map(\.unwrappedID))
}
func saveHighlightLabelChanges(highlightID: String, dataService: DataService) {
dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: selectedLabels.map(\.unwrappedID))
}
func addLabelToItem(_ label: LinkedItemLabel) {
selectedLabels.insert(label, at: 0)
unselectedLabels.removeAll { $0.name == label.name }

View File

@ -11,6 +11,7 @@ struct WebReaderContainerView: View {
@State private var showPreferencesPopover = false
@State private var showLabelsModal = false
@State private var showHighlightLabelsModal = false
@State private var showTitleEdit = false
@State private var showHighlightsView = false
@State private var hasPerformedHighlightMutations = false
@ -271,6 +272,9 @@ struct WebReaderContainerView: View {
.sheet(isPresented: $showLabelsModal) {
ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false })
}
.sheet(isPresented: $showHighlightLabelsModal) {
ApplyLabelsView(mode: .item(item), onSave: { _ in showLabelsModal = false })
}
.sheet(isPresented: $showTitleEdit) {
LinkedItemMetadataEditView(item: item)
}

View File

@ -0,0 +1,72 @@
import CoreData
import Foundation
import Models
import SwiftGraphQL
public extension DataService {
func setLabelsForHighlight(highlightID: String, labelIDs: [String]) {
backgroundContext.perform { [weak self] in
guard let self = self else { return }
guard let highlight = Highlight.lookup(byID: highlightID, inContext: self.backgroundContext) else { return }
if let existingLabels = highlight.labels {
highlight.removeFromLabels(existingLabels)
}
for labelID in labelIDs {
if let labelObject = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) {
highlight.addToLabels(labelObject)
}
}
// Send update to server
self.syncHighlightLabelUpdates(highlightID: highlightID, labelIDs: labelIDs)
}
}
func syncHighlightLabelUpdates(highlightID: String, labelIDs: [String]) {
enum MutationResult {
case saved(labels: [InternalLinkedItemLabel])
case error(errorCode: Enums.SetLabelsErrorCode)
}
let selection = Selection<MutationResult, Unions.SetLabelsResult> {
try $0.on(
setLabelsError: .init { .error(errorCode: try $0.errorCodes().first ?? .badRequest) },
setLabelsSuccess: .init { .saved(labels: try $0.labels(selection: highlightLabelSelection.list)) }
)
}
let mutation = Selection.Mutation {
try $0.setLabelsForHighlight(
input: InputObjects.SetLabelsForHighlightInput(
highlightId: highlightID,
labelIds: labelIDs
),
selection: selection
)
}
let path = appEnvironment.graphqlPath
let headers = networker.defaultHeaders
let context = backgroundContext
send(mutation, to: path, headers: headers) { result in
let data = try? result.get()
let syncStatus: ServerSyncStatus = data == nil ? .needsUpdate : .isNSync
context.perform {
guard let highlight = Highlight.lookup(byID: highlightID, inContext: context) else { return }
highlight.serverSyncStatus = Int64(syncStatus.rawValue)
do {
try context.save()
logger.debug("Highlight labels updated succesfully")
} catch {
context.rollback()
logger.debug("Failed to update highlight labels: \(error.localizedDescription)")
}
}
}
}
}

View File

@ -2,7 +2,7 @@ import CoreData
import Foundation
import Models
struct InternalLinkedItemLabel: Encodable {
public struct InternalLinkedItemLabel: Encodable {
let id: String
let name: String
let color: String
@ -39,7 +39,7 @@ struct InternalLinkedItemLabel: Encodable {
return label
}
static func make(_ labels: NSSet?) -> [InternalLinkedItemLabel] {
public static func make(_ labels: NSSet?) -> [InternalLinkedItemLabel] {
labels?
.compactMap { label in
if let label = label as? LinkedItemLabel {

View File

@ -208,10 +208,12 @@ public final class OmnivoreWebView: WKWebView {
// on iOS16 we use menuBuilder to create these items
currentMenu = .defaultMenu
let annotate = UIMenuItem(title: "Annotate", action: #selector(annotateSelection))
let labels = UIMenuItem(title: "Labels", action: #selector(setLabels))
let remove = UIMenuItem(title: "Remove", action: #selector(removeSelection))
// let share = UIMenuItem(title: "Share", action: #selector(shareSelection))
UIMenuController.shared.menuItems = [remove, /* share, */ annotate]
UIMenuController.shared.menuItems = [remove, labels, annotate]
}
}
@ -235,6 +237,7 @@ public final class OmnivoreWebView: WKWebView {
case #selector(shareSelection): return true
case #selector(removeSelection): return true
case #selector(copy(_:)): return true
case #selector(setLabels(_:)): return true
case Selector(("_lookup:")): return true
case Selector(("_define:")): return true
case Selector(("_findSelected:")): return true
@ -292,15 +295,18 @@ public final class OmnivoreWebView: WKWebView {
hideMenu()
}
@objc public func setLabels(_: Any?) {}
override public func buildMenu(with builder: UIMenuBuilder) {
if #available(iOS 16.0, *) {
let annotate = UICommand(title: "Note", action: #selector(annotateSelection))
let highlight = UICommand(title: "Highlight", action: #selector(highlightSelection))
let remove = UICommand(title: "Remove", action: #selector(removeSelection))
let setLabels = UICommand(title: "Labels", action: #selector(setLabels))
let omnivore = UIMenu(title: "",
options: .displayInline,
children: currentMenu == .defaultMenu ? [highlight, annotate] : [annotate, remove])
children: currentMenu == .defaultMenu ? [highlight, annotate] : [annotate, setLabels, remove])
builder.insertSibling(omnivore, beforeMenu: .lookup)
}

View File

@ -78,8 +78,20 @@ public struct TextChip: View {
.font(.appCaptionBold)
.foregroundColor(textColor)
.lineLimit(1)
.background(Capsule().fill(backgroundColor))
.overlay(Capsule().stroke(borderColor, lineWidth: 1))
// .background(Capsule().fill(backgroundColor))
// .overlay(Capsule().stroke(borderColor, lineWidth: 1))
// .background(Rectangle()
// .stroke(borderColor, lineWidth: 1)
// .background(Rectangle().fill(backgroundColor))
// .cornerRadius(4)
// )
// .overlay(Rectangle().stroke(borderColor, lineWidth: 1).cornerRadius(4))
.background(backgroundColor)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(borderColor, lineWidth: 1)
)
.padding(1)
.overlay(alignment: .topTrailing) {
if checked {