Merge pull request #1475 from omnivore-app/feat/highlight-labels
Allow setting labels from the highlights list view on iOS
This commit is contained in:
@ -76,6 +76,8 @@
|
||||
var menuButton: some View {
|
||||
Menu {
|
||||
Menu(String(format: "Playback Speed (%.1f×)", audioController.playbackRate)) {
|
||||
playbackRateButton(rate: 0.8, title: "0.8×", selected: audioController.playbackRate == 0.8)
|
||||
playbackRateButton(rate: 0.9, title: "0.9×", selected: audioController.playbackRate == 0.9)
|
||||
playbackRateButton(rate: 1.0, title: "1.0×", selected: audioController.playbackRate == 1.0)
|
||||
playbackRateButton(rate: 1.1, title: "1.1×", selected: audioController.playbackRate == 1.1)
|
||||
playbackRateButton(rate: 1.2, title: "1.2×", selected: audioController.playbackRate == 1.2)
|
||||
@ -344,6 +346,10 @@
|
||||
if let itemAudioProperties = self.audioController.itemAudioProperties {
|
||||
playerContent(itemAudioProperties)
|
||||
.tint(.appGrayTextContrast)
|
||||
.alert("There was an error playing back your audio.",
|
||||
isPresented: $audioController.playbackError) {
|
||||
Button("Dismiss", role: .none) {}
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
@ -11,6 +11,10 @@ struct HighlightsListCard: View {
|
||||
@Binding var hasHighlightMutations: Bool
|
||||
let onSaveAnnotation: (String) -> Void
|
||||
let onDeleteHighlight: () -> Void
|
||||
let onSetLabels: (String) -> Void
|
||||
|
||||
@State var errorAlertMessage: String?
|
||||
@State var showErrorAlertMessage = false
|
||||
|
||||
var contextMenuView: some View {
|
||||
Group {
|
||||
@ -30,6 +34,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") }
|
||||
@ -38,19 +48,19 @@ struct HighlightsListCard: View {
|
||||
}
|
||||
|
||||
var noteSection: some View {
|
||||
Group {
|
||||
HStack {
|
||||
Image(systemName: "note.text")
|
||||
HStack {
|
||||
let isEmpty = highlightParams.annotation.isEmpty
|
||||
Spacer(minLength: 6)
|
||||
|
||||
Text("Note")
|
||||
.font(.appSubheadline)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text(highlightParams.annotation)
|
||||
Text(isEmpty ? "Add Notes..." : highlightParams.annotation)
|
||||
.lineSpacing(6)
|
||||
.accentColor(.appGraySolid)
|
||||
.foregroundColor(isEmpty ? .appGrayText : .appGrayTextContrast)
|
||||
.font(.appSubheadline)
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.appButtonBackground)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.onTapGesture {
|
||||
annotation = highlightParams.annotation
|
||||
@ -58,20 +68,24 @@ struct HighlightsListCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
var addNoteSection: some View {
|
||||
HStack {
|
||||
Image(systemName: "note.text.badge.plus").foregroundColor(.appGrayTextContrast)
|
||||
|
||||
Text("ADD NOTE")
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(Color.appCtaYellow)
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.onTapGesture {
|
||||
annotation = highlightParams.annotation
|
||||
showAnnotationModal = true
|
||||
var labelsView: some View {
|
||||
if highlightParams.labels.count > 0 {
|
||||
return AnyView(ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(highlightParams.labels, id: \.self) {
|
||||
TextChip(feedItemLabel: $0)
|
||||
.padding(.leading, 0)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}.introspectScrollView { scrollView in
|
||||
scrollView.bounces = false
|
||||
}
|
||||
.padding(.top, 0)
|
||||
.padding(.leading, 0)
|
||||
.padding(.bottom, 0))
|
||||
} else {
|
||||
return AnyView(EmptyView())
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,17 +115,14 @@ struct HighlightsListCard: View {
|
||||
.padding(.top, 2)
|
||||
.padding(.trailing, 6)
|
||||
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(highlightParams.quote)
|
||||
|
||||
if highlightParams.annotation.isEmpty {
|
||||
addNoteSection
|
||||
} else {
|
||||
noteSection
|
||||
}
|
||||
labelsView
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
|
||||
noteSection
|
||||
}
|
||||
.sheet(isPresented: $showAnnotationModal) {
|
||||
HighlightAnnotationSheet(
|
||||
@ -123,7 +134,9 @@ struct HighlightsListCard: View {
|
||||
},
|
||||
onCancel: {
|
||||
showAnnotationModal = false
|
||||
}
|
||||
},
|
||||
errorAlertMessage: $errorAlertMessage,
|
||||
showErrorAlertMessage: $showErrorAlertMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,10 +11,11 @@ struct HighlightsListView: View {
|
||||
|
||||
let itemObjectID: NSManagedObjectID
|
||||
@Binding var hasHighlightMutations: Bool
|
||||
@State var setLabelsHighlight: Highlight?
|
||||
|
||||
var emptyView: some View {
|
||||
Text("""
|
||||
You have not added any highlights to this page.
|
||||
You have not added any highlights or notes to this page.
|
||||
""")
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(16)
|
||||
@ -22,7 +23,7 @@ struct HighlightsListView: View {
|
||||
|
||||
var innerBody: some View {
|
||||
(viewModel.highlightItems.count > 0 ? AnyView(listView) : AnyView(emptyView))
|
||||
.navigationTitle("Highlights & Notes")
|
||||
.navigationTitle("Notebook")
|
||||
.listStyle(PlainListStyle())
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@ -61,10 +62,22 @@ struct HighlightsListView: View {
|
||||
highlightID: highlightParams.highlightID,
|
||||
dataService: dataService
|
||||
)
|
||||
},
|
||||
onSetLabels: { highlightID in
|
||||
setLabelsHighlight = Highlight.lookup(byID: highlightID, inContext: dataService.viewContext)
|
||||
}
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
}.sheet(item: $setLabelsHighlight) { highlight in
|
||||
ApplyLabelsView(mode: .highlight(highlight), onSave: { selectedLabels in
|
||||
hasHighlightMutations = true
|
||||
|
||||
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
|
||||
labels: selectedLabels,
|
||||
dataService: dataService)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,15 +271,13 @@ import Views
|
||||
|
||||
func menuItems(for item: LinkedItem) -> some View {
|
||||
Group {
|
||||
if (item.highlights?.count ?? 0) > 0 {
|
||||
Button(
|
||||
action: { viewModel.itemForHighlightsView = item },
|
||||
label: { Label("View Highlights & Notes", systemImage: "highlighter") }
|
||||
)
|
||||
}
|
||||
Button(
|
||||
action: { viewModel.itemForHighlightsView = item },
|
||||
label: { Label("Notebook", systemImage: "highlighter") }
|
||||
)
|
||||
Button(
|
||||
action: { viewModel.itemUnderTitleEdit = item },
|
||||
label: { Label("Edit Metadata", systemImage: "textbox") }
|
||||
label: { Label("Edit Info", systemImage: "info.circle") }
|
||||
)
|
||||
Button(
|
||||
action: { viewModel.itemUnderLabelEdit = item },
|
||||
@ -346,6 +344,7 @@ import Views
|
||||
|
||||
List {
|
||||
filtersHeader
|
||||
.listRowSeparator(.hidden, edges: .top)
|
||||
|
||||
ForEach(viewModel.items) { item in
|
||||
FeedCardNavigationLink(
|
||||
|
||||
@ -46,7 +46,7 @@ import Views
|
||||
// TODO: add highlights view button
|
||||
Button(
|
||||
action: { viewModel.itemUnderTitleEdit = item },
|
||||
label: { Label("Edit Metadata", systemImage: "textbox") }
|
||||
label: { Label("Edit Info", systemImage: "info.circle") }
|
||||
)
|
||||
Button(
|
||||
action: { viewModel.itemUnderLabelEdit = item },
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -150,7 +150,7 @@ struct LinkItemDetailView: View {
|
||||
Group {
|
||||
Button(
|
||||
action: { showTitleEdit = true },
|
||||
label: { Label("Edit Metadata", systemImage: "textbox") }
|
||||
label: { Label("Edit Info", systemImage: "info.circle") }
|
||||
)
|
||||
Button(
|
||||
action: { viewModel.handleArchiveAction(dataService: dataService) },
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
}
|
||||
innerBody
|
||||
}
|
||||
}
|
||||
}.navigationTitle("Text to Speech")
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
|
||||
@ -87,9 +87,6 @@ struct WebReader: PlatformViewRepresentable {
|
||||
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
|
||||
do {
|
||||
try (webView as? OmnivoreWebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
|
||||
showHighlightAnnotationModal = false
|
||||
}
|
||||
} catch {
|
||||
showInSnackbar("Error saving note.")
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -26,6 +27,8 @@ struct WebReaderContainerView: View {
|
||||
@State var annotation = String()
|
||||
@State var showBottomBar = false
|
||||
@State private var bottomBarOpacity = 0.0
|
||||
@State private var errorAlertMessage: String?
|
||||
@State private var showErrorAlertMessage = false
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
@ -72,6 +75,14 @@ struct WebReaderContainerView: View {
|
||||
case "annotate":
|
||||
annotation = messageBody["annotation"] ?? ""
|
||||
showHighlightAnnotationModal = true
|
||||
case "noteCreated":
|
||||
showHighlightAnnotationModal = false
|
||||
case "highlightError":
|
||||
errorAlertMessage = messageBody["error"] ?? "An error occurred."
|
||||
showErrorAlertMessage = true
|
||||
case "setHighlightLabels":
|
||||
annotation = messageBody["highlightID"] ?? ""
|
||||
showHighlightLabelsModal = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -153,19 +164,27 @@ struct WebReaderContainerView: View {
|
||||
}.foregroundColor(.appGrayTextContrast)
|
||||
}
|
||||
|
||||
func audioMenuItem() -> some View {
|
||||
Button(
|
||||
action: {
|
||||
viewModel.downloadAudio(audioController: audioController, item: item)
|
||||
},
|
||||
label: {
|
||||
Label(viewModel.isDownloadingAudio ? "Downloading Audio" : "Download Audio", systemImage: "icloud.and.arrow.down")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func menuItems(for item: LinkedItem) -> some View {
|
||||
let hasLabels = item.labels?.count == 0
|
||||
let hasHighlights = (item.highlights?.count ?? 0) > 0
|
||||
return Group {
|
||||
if hasHighlights {
|
||||
Button(
|
||||
action: { showHighlightsView = true },
|
||||
label: { Label("View Highlights & Notes", systemImage: "highlighter") }
|
||||
)
|
||||
}
|
||||
Button(
|
||||
action: { showHighlightsView = true },
|
||||
label: { Label("Notebook", systemImage: "highlighter") }
|
||||
)
|
||||
Button(
|
||||
action: { showTitleEdit = true },
|
||||
label: { Label("Edit Metadata", systemImage: "textbox") }
|
||||
label: { Label("Edit Info", systemImage: "info.circle") }
|
||||
)
|
||||
Button(
|
||||
action: editLabels,
|
||||
@ -188,12 +207,8 @@ struct WebReaderContainerView: View {
|
||||
},
|
||||
label: { Label("Reset Read Location", systemImage: "arrow.counterclockwise.circle") }
|
||||
)
|
||||
Button(
|
||||
action: {
|
||||
viewModel.downloadAudio(audioController: audioController, item: item)
|
||||
},
|
||||
label: { Label("Download Audio", systemImage: "icloud.and.arrow.down") }
|
||||
)
|
||||
audioMenuItem()
|
||||
|
||||
if viewModel.hasOriginalUrl(item) {
|
||||
Button(
|
||||
action: share,
|
||||
@ -330,6 +345,12 @@ struct WebReaderContainerView: View {
|
||||
SafariView(url: $0.url)
|
||||
}
|
||||
#endif
|
||||
.alert(errorAlertMessage ?? "An error occurred", isPresented: $showErrorAlertMessage) {
|
||||
Button("Ok", role: .cancel, action: {
|
||||
errorAlertMessage = nil
|
||||
showErrorAlertMessage = false
|
||||
})
|
||||
}
|
||||
.sheet(isPresented: $showHighlightAnnotationModal) {
|
||||
HighlightAnnotationSheet(
|
||||
annotation: $annotation,
|
||||
@ -338,9 +359,20 @@ struct WebReaderContainerView: View {
|
||||
},
|
||||
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)) { selectedLabels in
|
||||
viewModel.setLabelsForHighlight(highlightID: highlight.unwrappedID,
|
||||
labelIDs: selectedLabels.map(\.unwrappedID),
|
||||
dataService: dataService)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let errorMessage = viewModel.errorMessage {
|
||||
Text(errorMessage).padding()
|
||||
} else {
|
||||
|
||||
@ -12,6 +12,12 @@ struct SafariWebLink: Identifiable {
|
||||
@MainActor final class WebReaderViewModel: ObservableObject {
|
||||
@Published var articleContent: ArticleContent?
|
||||
@Published var errorMessage: String?
|
||||
@Published var isDownloadingAudio: Bool = false
|
||||
@Published var audioDownloadTask: Task<Void, Error>?
|
||||
|
||||
deinit {
|
||||
print("deinit WebReaderViewModel")
|
||||
}
|
||||
|
||||
func hasOriginalUrl(_ item: LinkedItem) -> Bool {
|
||||
if let pageURLString = item.pageURLString, let host = URL(string: pageURLString)?.host {
|
||||
@ -25,9 +31,22 @@ struct SafariWebLink: Identifiable {
|
||||
|
||||
func downloadAudio(audioController: AudioController, item: LinkedItem) {
|
||||
Snackbar.show(message: "Downloading Offline Audio")
|
||||
Task {
|
||||
let downloaded = await audioController.downloadForOffline(itemID: item.unwrappedID)
|
||||
Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio")
|
||||
isDownloadingAudio = true
|
||||
|
||||
if let audioDownloadTask = audioDownloadTask {
|
||||
audioDownloadTask.cancel()
|
||||
}
|
||||
|
||||
let itemID = item.unwrappedID
|
||||
audioDownloadTask = Task.detached(priority: .background) {
|
||||
let canceled = Task.isCancelled
|
||||
let downloaded = await audioController.downloadForOffline(itemID: itemID)
|
||||
DispatchQueue.main.async {
|
||||
self.isDownloadingAudio = false
|
||||
if !canceled {
|
||||
Snackbar.show(message: downloaded ? "Audio file downloaded" : "Error downloading audio")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,4 +188,11 @@ struct SafariWebLink: Identifiable {
|
||||
replyHandler(nil, "Unknown actionID: \(actionID)")
|
||||
}
|
||||
}
|
||||
|
||||
func setLabelsForHighlight(highlightID: String,
|
||||
labelIDs: [String],
|
||||
dataService: DataService)
|
||||
{
|
||||
dataService.setLabelsForHighlight(highlightID: highlightID, labelIDs: labelIDs)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<attribute name="shortId" attributeType="String"/>
|
||||
<attribute name="suffix" optional="YES" attributeType="String"/>
|
||||
<attribute name="updatedAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="labels" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItemLabel" inverseName="highlights" inverseEntity="LinkedItemLabel"/>
|
||||
<relationship name="linkedItem" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LinkedItem" inverseName="highlights" inverseEntity="LinkedItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
@ -67,6 +68,7 @@
|
||||
<attribute name="labelDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="serverSyncStatus" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="highlights" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Highlight" inverseName="labels" inverseEntity="Highlight"/>
|
||||
<relationship name="linkedItems" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItem" inverseName="labels" inverseEntity="LinkedItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
|
||||
@ -120,7 +120,7 @@
|
||||
}
|
||||
|
||||
public func stopWithError() {
|
||||
stop()
|
||||
pause()
|
||||
playbackError = true
|
||||
}
|
||||
|
||||
@ -667,7 +667,7 @@
|
||||
@objc func fireTimer() {
|
||||
if let player = player {
|
||||
if player.error != nil || player.currentItem?.error != nil {
|
||||
stop()
|
||||
stopWithError()
|
||||
}
|
||||
|
||||
if let durations = durations {
|
||||
|
||||
@ -30,8 +30,13 @@ public enum VoiceCategory: String, CaseIterable {
|
||||
case enSG = "English (Singapore)"
|
||||
case enUK = "English (UK)"
|
||||
case deDE = "German (Germany)"
|
||||
case hiIN = "Hindi (India)"
|
||||
case esES = "Spanish (Spain)"
|
||||
case jaJP = "Japanese (Japan)"
|
||||
case taIN = "Tamil (India)"
|
||||
case taLK = "Tamil (Sri Lanka)"
|
||||
case taMY = "Tamil (Malaysia)"
|
||||
case taSG = "Tamil (Singapore)"
|
||||
case zhCN = "Chinese (China Mainland)"
|
||||
}
|
||||
|
||||
@ -62,8 +67,10 @@ public enum Voices {
|
||||
English,
|
||||
VoiceLanguage(key: "zh", name: "Chinese", defaultVoice: "zh-CN-XiaochenNeural", categories: [.zhCN]),
|
||||
VoiceLanguage(key: "de", name: "German", defaultVoice: "de-CH-JanNeural", categories: [.deDE]),
|
||||
VoiceLanguage(key: "hi", name: "Hindi", defaultVoice: "hi-IN-MadhurNeural", categories: [.hiIN]),
|
||||
VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]),
|
||||
VoiceLanguage(key: "es", name: "Spanish", defaultVoice: "es-ES-AlvaroNeural", categories: [.esES])
|
||||
VoiceLanguage(key: "es", name: "Spanish", defaultVoice: "es-ES-AlvaroNeural", categories: [.esES]),
|
||||
VoiceLanguage(key: "ta", name: "Tamil", defaultVoice: "ta-IN-PallaviNeural", categories: [.taIN, .taLK, .taMY, .taSG])
|
||||
]
|
||||
|
||||
// swiftlint:disable all
|
||||
@ -83,7 +90,12 @@ public enum Voices {
|
||||
VoicePair(firstKey: "de-CH-LeniNeural", secondKey: "de-DE-KatjaNeural", firstName: "Leni", secondName: "Katja", language: "de-DE", category: .deDE),
|
||||
VoicePair(firstKey: "de-DE-AmalaNeural", secondKey: "de-DE-BerndNeural", firstName: "Amala", secondName: "Bernd", language: "de-DE", category: .deDE),
|
||||
VoicePair(firstKey: "de-DE-ChristophNeural", secondKey: "de-DE-LouisaNeural", firstName: "Christoph", secondName: "Louisa", language: "de-DE", category: .deDE),
|
||||
VoicePair(firstKey: "ja-JP-NanamiNeural", secondKey: "ja-JP-KeitaNeural", firstName: "Nanami", secondName: "Keita", language: "ja-JP", category: .jaJP)
|
||||
VoicePair(firstKey: "ja-JP-NanamiNeural", secondKey: "ja-JP-KeitaNeural", firstName: "Nanami", secondName: "Keita", language: "ja-JP", category: .jaJP),
|
||||
VoicePair(firstKey: "hi-IN-MadhurNeural", secondKey: "hi-IN-SwaraNeural", firstName: "Madhur", secondName: "Swara", language: "hi-IN", category: .hiIN),
|
||||
VoicePair(firstKey: "ta-IN-PallaviNeural", secondKey: "ta-IN-ValluvarNeural", firstName: "Pallavi", secondName: "Valluvar", language: "ta-IN", category: .taIN),
|
||||
VoicePair(firstKey: "ta-LK-KumarNeural", secondKey: "ta-LK-SaranyaNeural", firstName: "Kumar", secondName: "Saranya", language: "ta-LK", category: .taLK),
|
||||
VoicePair(firstKey: "ta-MY-KaniNeural", secondKey: "ta-MY-SuryaNeural", firstName: "Kani", secondName: "Surya", language: "ta-MY", category: .taMY),
|
||||
VoicePair(firstKey: "ta-SG-AnbuNeural", secondKey: "ta-SG-VenbaNeural", firstName: "Anbu", secondName: "Venba", language: "ta-SG", category: .taSG)
|
||||
]
|
||||
|
||||
public static let UltraPairs = [
|
||||
|
||||
@ -6315,6 +6315,7 @@ extension Objects {
|
||||
let highlightPositionAnchorIndex: [String: Int]
|
||||
let highlightPositionPercent: [String: Double]
|
||||
let id: [String: String]
|
||||
let labels: [String: [Objects.Label]]
|
||||
let patch: [String: String]
|
||||
let prefix: [String: String]
|
||||
let quote: [String: String]
|
||||
@ -6368,6 +6369,10 @@ extension Objects.Highlight: Decodable {
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "labels":
|
||||
if let value = try container.decode([Objects.Label]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "patch":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
@ -6424,6 +6429,7 @@ extension Objects.Highlight: Decodable {
|
||||
highlightPositionAnchorIndex = map["highlightPositionAnchorIndex"]
|
||||
highlightPositionPercent = map["highlightPositionPercent"]
|
||||
id = map["id"]
|
||||
labels = map["labels"]
|
||||
patch = map["patch"]
|
||||
prefix = map["prefix"]
|
||||
quote = map["quote"]
|
||||
@ -6537,6 +6543,22 @@ extension Fields where TypeLock == Objects.Highlight {
|
||||
}
|
||||
}
|
||||
|
||||
func labels<Type>(selection: Selection<Type, [Objects.Label]?>) throws -> Type {
|
||||
let field = GraphQLField.composite(
|
||||
name: "labels",
|
||||
arguments: [],
|
||||
selection: selection.selection
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return try selection.decode(data: data.labels[field.alias!])
|
||||
case .mocking:
|
||||
return selection.mock()
|
||||
}
|
||||
}
|
||||
|
||||
func patch() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "patch",
|
||||
|
||||
@ -21,7 +21,8 @@ extension DataService {
|
||||
annotation: annotation,
|
||||
createdAt: nil,
|
||||
updatedAt: nil,
|
||||
createdByMe: true
|
||||
createdByMe: true,
|
||||
labels: []
|
||||
)
|
||||
|
||||
internalHighlight.persist(context: backgroundContext, associatedItemID: articleId)
|
||||
|
||||
@ -23,7 +23,8 @@ extension DataService {
|
||||
annotation: nil,
|
||||
createdAt: nil,
|
||||
updatedAt: nil,
|
||||
createdByMe: true
|
||||
createdByMe: true,
|
||||
labels: []
|
||||
)
|
||||
|
||||
internalHighlight.persist(
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,16 @@
|
||||
import Models
|
||||
import SwiftGraphQL
|
||||
|
||||
let highlightLabelSelection = Selection.Label {
|
||||
InternalLinkedItemLabel(
|
||||
id: try $0.id(),
|
||||
name: try $0.name(),
|
||||
color: try $0.color(),
|
||||
createdAt: try $0.createdAt()?.value,
|
||||
labelDescription: try $0.description()
|
||||
)
|
||||
}
|
||||
|
||||
let highlightSelection = Selection.Highlight {
|
||||
InternalHighlight(
|
||||
id: try $0.id(),
|
||||
@ -12,6 +22,7 @@ let highlightSelection = Selection.Highlight {
|
||||
annotation: try $0.annotation(),
|
||||
createdAt: try $0.createdAt().value,
|
||||
updatedAt: try $0.updatedAt().value,
|
||||
createdByMe: try $0.createdByMe()
|
||||
createdByMe: try $0.createdByMe(),
|
||||
labels: try $0.labels(selection: highlightLabelSelection.list.nullable) ?? []
|
||||
)
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ struct InternalHighlight: Encodable {
|
||||
let createdAt: Date?
|
||||
let updatedAt: Date?
|
||||
let createdByMe: Bool
|
||||
var labels: [InternalLinkedItemLabel]
|
||||
|
||||
func asManagedObject(context: NSManagedObjectContext) -> Highlight {
|
||||
let fetchRequest: NSFetchRequest<Models.Highlight> = Highlight.fetchRequest()
|
||||
@ -33,6 +34,15 @@ struct InternalHighlight: Encodable {
|
||||
highlight.createdAt = createdAt
|
||||
highlight.updatedAt = updatedAt
|
||||
highlight.createdByMe = createdByMe
|
||||
|
||||
if let existingLabels = highlight.labels {
|
||||
highlight.removeFromLabels(existingLabels)
|
||||
}
|
||||
|
||||
for label in labels {
|
||||
highlight.addToLabels(label.asManagedObject(inContext: context))
|
||||
}
|
||||
|
||||
return highlight
|
||||
}
|
||||
|
||||
@ -47,7 +57,8 @@ struct InternalHighlight: Encodable {
|
||||
annotation: highlight.annotation,
|
||||
createdAt: highlight.createdAt,
|
||||
updatedAt: highlight.updatedAt,
|
||||
createdByMe: highlight.createdByMe
|
||||
createdByMe: highlight.createdByMe,
|
||||
labels: InternalLinkedItemLabel.make(highlight.labels)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import CoreData
|
||||
import Foundation
|
||||
import Models
|
||||
|
||||
struct InternalLinkedItemLabel {
|
||||
public struct InternalLinkedItemLabel: Encodable {
|
||||
let id: String
|
||||
let name: String
|
||||
let color: String
|
||||
@ -38,6 +38,22 @@ struct InternalLinkedItemLabel {
|
||||
label.labelDescription = labelDescription
|
||||
return label
|
||||
}
|
||||
|
||||
public static func make(_ labels: NSSet?) -> [InternalLinkedItemLabel] {
|
||||
labels?
|
||||
.compactMap { label in
|
||||
if let label = label as? LinkedItemLabel {
|
||||
return InternalLinkedItemLabel(
|
||||
id: label.id ?? "",
|
||||
name: label.name ?? "",
|
||||
color: label.color ?? "",
|
||||
createdAt: label.createdAt,
|
||||
labelDescription: label.labelDescription
|
||||
)
|
||||
}
|
||||
return nil
|
||||
} ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkedItemLabel {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -3,6 +3,8 @@ import SwiftUI
|
||||
|
||||
public struct HighlightAnnotationSheet: View {
|
||||
@Binding var annotation: String
|
||||
@Binding var errorAlertMessage: String?
|
||||
@Binding var showErrorAlertMessage: Bool
|
||||
|
||||
let onSave: () -> Void
|
||||
let onCancel: () -> Void
|
||||
@ -10,11 +12,15 @@ public struct HighlightAnnotationSheet: View {
|
||||
public init(
|
||||
annotation: Binding<String>,
|
||||
onSave: @escaping () -> Void,
|
||||
onCancel: @escaping () -> Void
|
||||
onCancel: @escaping () -> Void,
|
||||
errorAlertMessage: Binding<String?>,
|
||||
showErrorAlertMessage: Binding<Bool>
|
||||
) {
|
||||
self._annotation = annotation
|
||||
self.onSave = onSave
|
||||
self.onCancel = onCancel
|
||||
self._errorAlertMessage = errorAlertMessage
|
||||
self._showErrorAlertMessage = showErrorAlertMessage
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
@ -43,5 +49,11 @@ public struct HighlightAnnotationSheet: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.alert(errorAlertMessage ?? "An error occurred", isPresented: $showErrorAlertMessage) {
|
||||
Button("Ok", role: .cancel, action: {
|
||||
errorAlertMessage = nil
|
||||
showErrorAlertMessage = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,25 @@ public final class OmnivoreWebView: WKWebView {
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
@objc public func setLabels(_: Any?) {
|
||||
do {
|
||||
try dispatchEvent(.setHighlightLabels)
|
||||
} catch {
|
||||
showErrorInSnackbar("Error setting labels for highlight")
|
||||
}
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -362,6 +375,7 @@ public enum WebViewDispatchEvent {
|
||||
case highlight
|
||||
case share
|
||||
case remove
|
||||
case setHighlightLabels
|
||||
case copyHighlight
|
||||
case dismissHighlight
|
||||
case speakingSection(anchorIdx: String)
|
||||
@ -399,6 +413,8 @@ public enum WebViewDispatchEvent {
|
||||
return "share"
|
||||
case .remove:
|
||||
return "remove"
|
||||
case .setHighlightLabels:
|
||||
return "setHighlightLabels"
|
||||
case .copyHighlight:
|
||||
return "copyHighlight"
|
||||
case .dismissHighlight:
|
||||
@ -435,7 +451,7 @@ public enum WebViewDispatchEvent {
|
||||
}
|
||||
case let .speakingSection(anchorIdx: anchorIdx):
|
||||
return "event.anchorIdx = '\(anchorIdx)';"
|
||||
case .annotate, .highlight, .share, .remove, .copyHighlight, .dismissHighlight:
|
||||
case .annotate, .highlight, .setHighlightLabels, .share, .remove, .copyHighlight, .dismissHighlight:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,15 +45,13 @@ public struct GridCard: View {
|
||||
|
||||
var contextMenuView: some View {
|
||||
Group {
|
||||
if (item.highlights?.count ?? 0) > 0 {
|
||||
Button(
|
||||
action: { menuActionHandler(.viewHighlights) },
|
||||
label: { Label("View Highlights & Notes", systemImage: "highlighter") }
|
||||
)
|
||||
}
|
||||
Button(
|
||||
action: { menuActionHandler(.viewHighlights) },
|
||||
label: { Label("Notebook", systemImage: "highlighter") }
|
||||
)
|
||||
Button(
|
||||
action: { menuActionHandler(.editTitle) },
|
||||
label: { Label("Edit Metadata", systemImage: "textbox") }
|
||||
label: { Label("Edit Info", systemImage: "info.circle") }
|
||||
)
|
||||
Button(
|
||||
action: { menuActionHandler(.editLabels) },
|
||||
|
||||
@ -23,7 +23,7 @@ public extension Font {
|
||||
}
|
||||
|
||||
static var textToSpeechRead: Font {
|
||||
Font.custom(InterFont.bold.rawValue, size: 24, relativeTo: .title2)
|
||||
Font.system(size: 24, weight: .bold)
|
||||
}
|
||||
|
||||
static var appNavbarIcon: Font {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -58,7 +58,7 @@ public struct TextChip: View {
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
color.opacity(colorScheme == .dark ? 0.08 : 1)
|
||||
color.opacity(colorScheme == .dark ? 0.2 : 1)
|
||||
}
|
||||
|
||||
var borderColor: Color {
|
||||
@ -73,13 +73,19 @@ public struct TextChip: View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Text(text)
|
||||
.strikethrough(color: negated ? textColor : .clear)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 5)
|
||||
.font(.appCaptionBold)
|
||||
.foregroundColor(textColor)
|
||||
.lineLimit(1)
|
||||
.background(Capsule().fill(backgroundColor))
|
||||
.overlay(Capsule().stroke(borderColor, lineWidth: 1))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(backgroundColor)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.stroke(borderColor, lineWidth: 1)
|
||||
)
|
||||
.padding(1)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
if checked {
|
||||
|
||||
@ -41,7 +41,7 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
||||
css={{
|
||||
display: 'inline-table',
|
||||
margin: '4px',
|
||||
borderRadius: '32px',
|
||||
borderRadius: '4px',
|
||||
color: isDarkMode ? darkThemeTextColor : lightThemeTextColor,
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
@ -49,7 +49,9 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'pointer',
|
||||
backgroundClip: 'padding-box',
|
||||
border: isDarkMode ? `1px solid ${darkThemeTextColor}` :`1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
|
||||
border: isDarkMode
|
||||
? `1px solid ${darkThemeTextColor}`
|
||||
: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
|
||||
backgroundColor: isDarkMode
|
||||
? `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`
|
||||
: props.color,
|
||||
|
||||
@ -22,6 +22,7 @@ export type HighlightAction =
|
||||
| 'share'
|
||||
| 'post'
|
||||
| 'unshare'
|
||||
| 'setHighlightLabels'
|
||||
|
||||
type HighlightBarProps = {
|
||||
anchorCoordinates: PageCoordinates
|
||||
@ -133,10 +134,7 @@ function BarContent(props: HighlightBarProps): JSX.Element {
|
||||
css={{ color: '$readerFont', height: '100%', m: 0, p: 0 }}
|
||||
>
|
||||
<HStack css={{ height: '100%', alignItems: 'center' }}>
|
||||
<Trash
|
||||
size={24}
|
||||
color={theme.colors.omnivoreRed.toString()}
|
||||
/>
|
||||
<Trash size={24} color={theme.colors.omnivoreRed.toString()} />
|
||||
<StyledText
|
||||
style="body"
|
||||
css={{
|
||||
@ -157,7 +155,7 @@ function BarContent(props: HighlightBarProps): JSX.Element {
|
||||
style="plainIcon"
|
||||
title="Add Note to Highlight"
|
||||
onClick={() => props.handleButtonClick('comment')}
|
||||
css={{ color: '$readerFont', height: '100%', m: 0, p: 0}}
|
||||
css={{ color: '$readerFont', height: '100%', m: 0, p: 0 }}
|
||||
>
|
||||
<HStack css={{ height: '100%', alignItems: 'center' }}>
|
||||
<Note size={24} color={theme.colors.readerFont.toString()} />
|
||||
|
||||
@ -196,6 +196,10 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
props.articleMutations
|
||||
)
|
||||
|
||||
if (result.errorMessage) {
|
||||
throw 'Failed to create highlight: ' + result.errorMessage
|
||||
}
|
||||
|
||||
if (!result.highlights || result.highlights.length == 0) {
|
||||
// TODO: show an error message
|
||||
console.error('Failed to create highlight')
|
||||
@ -218,26 +222,18 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
if (!selectionData) {
|
||||
return
|
||||
}
|
||||
const result = await createHighlightFromSelection(
|
||||
selectionData,
|
||||
annotation
|
||||
)
|
||||
if (!result) {
|
||||
showErrorToast('Error saving highlight', { position: 'bottom-right' })
|
||||
try {
|
||||
const result = await createHighlightFromSelection(
|
||||
selectionData,
|
||||
annotation
|
||||
)
|
||||
if (!result) {
|
||||
showErrorToast('Error saving highlight', { position: 'bottom-right' })
|
||||
throw 'Error creating highlight'
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
// if (successAction === 'share' && canShareNative) {
|
||||
// handleNativeShare(highlight.shortId)
|
||||
// return
|
||||
// } else {
|
||||
// setFocusedHighlight(undefined)
|
||||
// }
|
||||
|
||||
// if (successAction === 'addComment') {
|
||||
// openNoteModal({
|
||||
// highlightModalAction: 'addComment',
|
||||
// highlight,
|
||||
// })
|
||||
// }
|
||||
},
|
||||
[
|
||||
handleNativeShare,
|
||||
@ -328,13 +324,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
}, [handleClickHighlight])
|
||||
|
||||
const handleAction = useCallback(
|
||||
(action: HighlightAction) => {
|
||||
async (action: HighlightAction) => {
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
removeHighlightCallback()
|
||||
await removeHighlightCallback()
|
||||
break
|
||||
case 'create':
|
||||
createHighlightCallback('none')
|
||||
await createHighlightCallback('none')
|
||||
break
|
||||
case 'comment':
|
||||
if (props.highlightBarDisabled || focusedHighlight) {
|
||||
@ -375,12 +371,20 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
createHighlightCallback('share')
|
||||
await createHighlightCallback('share')
|
||||
}
|
||||
break
|
||||
case 'unshare':
|
||||
console.log('unshare')
|
||||
break // TODO: implement -- need to show confirmation dialog
|
||||
case 'setHighlightLabels':
|
||||
if (props.isAppleAppEmbed) {
|
||||
window?.webkit?.messageHandlers.highlightAction?.postMessage({
|
||||
actionID: 'setHighlightLabels',
|
||||
highlightID: focusedHighlight?.id,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
[
|
||||
@ -395,27 +399,59 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
]
|
||||
)
|
||||
|
||||
const dispatchHighlightError = (action: string, error: unknown) => {
|
||||
if (props.isAppleAppEmbed) {
|
||||
window?.webkit?.messageHandlers.highlightAction?.postMessage({
|
||||
actionID: 'highlightError',
|
||||
highlightAction: action,
|
||||
highlightID: focusedHighlight?.id,
|
||||
error: typeof error === 'string' ? error : JSON.stringify(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dispatchHighlightMessage = (actionID: string) => {
|
||||
if (props.isAppleAppEmbed) {
|
||||
window?.webkit?.messageHandlers.highlightAction?.postMessage({
|
||||
actionID: actionID,
|
||||
highlightID: focusedHighlight?.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const annotate = () => {
|
||||
handleAction('comment')
|
||||
const safeHandleAction = async (action: HighlightAction) => {
|
||||
try {
|
||||
await handleAction(action)
|
||||
} catch (error) {
|
||||
dispatchHighlightError(action, error)
|
||||
}
|
||||
}
|
||||
|
||||
const highlight = () => {
|
||||
handleAction('create')
|
||||
const annotate = async () => {
|
||||
await safeHandleAction('comment')
|
||||
}
|
||||
|
||||
const share = () => {
|
||||
handleAction('share')
|
||||
const highlight = async () => {
|
||||
await safeHandleAction('create')
|
||||
}
|
||||
|
||||
const remove = () => {
|
||||
handleAction('delete')
|
||||
const share = async () => {
|
||||
await safeHandleAction('share')
|
||||
}
|
||||
|
||||
const remove = async () => {
|
||||
await safeHandleAction('delete')
|
||||
}
|
||||
|
||||
const dismissHighlight = () => {
|
||||
setFocusedHighlight(undefined)
|
||||
}
|
||||
|
||||
const setHighlightLabels = () => {
|
||||
handleAction('setHighlightLabels')
|
||||
}
|
||||
|
||||
const copy = async () => {
|
||||
if (focusedHighlight) {
|
||||
await navigator.clipboard.writeText(focusedHighlight.quote)
|
||||
@ -453,10 +489,20 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
'failed to change annotation for highlight with id',
|
||||
focusedHighlight.id
|
||||
)
|
||||
dispatchHighlightError(
|
||||
'saveAnnotation',
|
||||
'Failed to create highlight.'
|
||||
)
|
||||
}
|
||||
setFocusedHighlight(undefined)
|
||||
dispatchHighlightMessage('noteCreated')
|
||||
} else {
|
||||
createHighlightCallback('none', event.annotation)
|
||||
try {
|
||||
await createHighlightCallback('none', event.annotation)
|
||||
dispatchHighlightMessage('noteCreated')
|
||||
} catch (error) {
|
||||
dispatchHighlightError('saveAnnotation', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -468,6 +514,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
document.addEventListener('dismissHighlight', dismissHighlight)
|
||||
document.addEventListener('saveAnnotation', saveAnnotation)
|
||||
document.addEventListener('speakingSection', speakingSection)
|
||||
document.addEventListener('setHighlightLabels', setHighlightLabels)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('annotate', annotate)
|
||||
@ -478,6 +525,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
|
||||
document.removeEventListener('dismissHighlight', dismissHighlight)
|
||||
document.removeEventListener('saveAnnotation', saveAnnotation)
|
||||
document.removeEventListener('speakingSection', speakingSection)
|
||||
document.removeEventListener('setHighlightLabels', setHighlightLabels)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user