Merge pull request #1283 from omnivore-app/feature/highlights-list-view-ios

This commit is contained in:
Jackson Harper
2022-10-06 10:05:15 +08:00
committed by GitHub
14 changed files with 337 additions and 5 deletions

View File

@ -0,0 +1,136 @@
import Models
import SwiftUI
import Views
struct HighlightsListCard: View {
@State var isContextMenuOpen = false
@State var annotation = String()
@State var showAnnotationModal = false
let highlightParams: HighlightListItemParams
@Binding var hasHighlightMutations: Bool
let onSaveAnnotation: (String) -> Void
let onDeleteHighlight: () -> Void
var contextMenuView: some View {
Group {
Button(
action: {
#if os(iOS)
UIPasteboard.general.string = highlightParams.quote
#endif
#if os(macOS)
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.writeObjects([highlightParams.quote as NSString])
#endif
Snackbar.show(message: "Highlight copied")
},
label: { Label("Copy", systemImage: "doc.on.doc") }
)
Button(
action: onDeleteHighlight,
label: { Label("Delete", systemImage: "trash") }
)
}
}
var noteSection: some View {
Group {
HStack {
Image(systemName: "note.text")
Text("Note")
.font(.appSubheadline)
.foregroundColor(.appGrayTextContrast)
.lineLimit(1)
Spacer()
}
Text(highlightParams.annotation)
}
.onTapGesture {
annotation = highlightParams.annotation
showAnnotationModal = true
}
}
var addNoteSection: some View {
HStack {
Image(systemName: "note.text.badge.plus").foregroundColor(.appGrayTextContrast)
Text("Add a Note")
.font(.appSubheadline)
.foregroundColor(.appGrayTextContrast)
.lineLimit(1)
Spacer()
}
.onTapGesture {
annotation = highlightParams.annotation
showAnnotationModal = true
}
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemName: "highlighter")
Text(highlightParams.title)
.font(.appHeadline)
.foregroundColor(.appGrayTextContrast)
.lineLimit(1)
Spacer()
Menu(
content: { contextMenuView },
label: {
Image(systemName: "ellipsis")
.foregroundColor(.appGrayTextContrast)
.padding()
}
)
.frame(width: 16, height: 16, alignment: .center)
.onTapGesture { isContextMenuOpen = true }
}
.padding(.top, 8)
HStack {
Divider()
.frame(width: 6)
.overlay(Color.appYellow48)
VStack(alignment: .leading, spacing: 8) {
Text(highlightParams.quote)
Divider()
if highlightParams.annotation.isEmpty {
addNoteSection
} else {
noteSection
}
}
}
.padding(.bottom, 8)
}
.sheet(isPresented: $showAnnotationModal) {
HighlightAnnotationSheet(
annotation: $annotation,
onSave: {
onSaveAnnotation(annotation)
showAnnotationModal = false
hasHighlightMutations = true
},
onCancel: {
showAnnotationModal = false
}
)
}
}
}

View File

@ -0,0 +1,81 @@
import CoreData
import Models
import Services
import SwiftUI
import Views
struct HighlightsListView: View {
@EnvironmentObject var dataService: DataService
@Environment(\.presentationMode) private var presentationMode
@StateObject var viewModel = HighlightsListViewModel()
let itemObjectID: NSManagedObjectID
@Binding var hasHighlightMutations: Bool
var innerBody: some View {
List {
Section {
ForEach(viewModel.highlightItems) { highlightParams in
HighlightsListCard(
highlightParams: highlightParams,
hasHighlightMutations: $hasHighlightMutations,
onSaveAnnotation: {
viewModel.updateAnnotation(
highlightID: highlightParams.highlightID,
annotation: $0,
dataService: dataService
)
},
onDeleteHighlight: {
hasHighlightMutations = true
viewModel.deleteHighlight(
highlightID: highlightParams.highlightID,
dataService: dataService
)
}
)
}
}
}
.navigationTitle("Highlights")
.listStyle(PlainListStyle())
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
dismissButton
}
}
#else
.toolbar {
ToolbarItemGroup {
dismissButton
}
}
#endif
}
var dismissButton: some View {
Button(
action: { presentationMode.wrappedValue.dismiss() },
label: { Text("Done").foregroundColor(.appGrayTextContrast) }
)
}
var body: some View {
Group {
#if os(iOS)
NavigationView {
innerBody
}
#elseif os(macOS)
innerBody
.frame(minWidth: 400, minHeight: 400)
#endif
}
.task {
viewModel.load(itemObjectID: itemObjectID, dataService: dataService)
}
}
}

View File

@ -0,0 +1,58 @@
import CoreData
import Models
import Services
import SwiftUI
import Views
struct HighlightListItemParams: Identifiable {
let id = UUID()
let highlightID: String
let title: String
let annotation: String
let quote: String
}
@MainActor final class HighlightsListViewModel: ObservableObject {
@Published var highlightItems = [HighlightListItemParams]()
func load(itemObjectID: NSManagedObjectID, dataService: DataService) {
if let linkedItem = dataService.viewContext.object(with: itemObjectID) as? LinkedItem {
loadHighlights(item: linkedItem)
}
}
func updateAnnotation(highlightID: String, annotation: String, dataService: DataService) {
dataService.updateHighlightAttributes(highlightID: highlightID, annotation: annotation)
if let index = highlightItems.firstIndex(where: { $0.highlightID == highlightID }) {
highlightItems[index] = HighlightListItemParams(
highlightID: highlightID,
title: highlightItems[index].title,
annotation: annotation,
quote: highlightItems[index].quote
)
}
}
func deleteHighlight(highlightID: String, dataService: DataService) {
dataService.deleteHighlight(highlightID: highlightID)
highlightItems.removeAll { $0.highlightID == highlightID }
}
private func loadHighlights(item: LinkedItem) {
let unsortedHighlights = item.highlights.asArray(of: Highlight.self)
let highlights = unsortedHighlights.sorted {
($0.createdAt ?? Date()) < ($1.createdAt ?? Date())
}
highlightItems = highlights.map {
HighlightListItemParams(
highlightID: $0.unwrappedID,
title: "Highlight",
annotation: $0.annotation ?? "",
quote: $0.quote ?? ""
)
}
}
}

View File

@ -11,6 +11,7 @@ import Views
private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
struct HomeFeedContainerView: View {
@State var hasHighlightMutations = false
@EnvironmentObject var dataService: DataService
@EnvironmentObject var audioController: AudioController
@ -62,6 +63,9 @@ import Views
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
LinkedItemTitleEditView(item: item)
}
.sheet(item: $viewModel.itemForHighlightsView) { item in
HighlightsListView(itemObjectID: item.objectID, hasHighlightMutations: $hasHighlightMutations)
}
.toolbar {
ToolbarItem(placement: .barTrailing) {
Button("", action: {})
@ -255,6 +259,10 @@ import Views
viewModel: viewModel
)
.contextMenu {
Button(
action: { viewModel.itemForHighlightsView = item },
label: { Label("View Highlights", systemImage: "highlighter") }
)
Button(
action: { viewModel.itemUnderTitleEdit = item },
label: { Label("Edit Title/Description", systemImage: "textbox") }
@ -372,6 +380,8 @@ import Views
func contextMenuActionHandler(item: LinkedItem, action: GridCardAction) {
switch action {
case .viewHighlights:
viewModel.itemForHighlightsView = item
case .toggleArchiveStatus:
viewModel.setLinkArchived(dataService: dataService, objectID: item.objectID, archived: !item.isArchived)
case .delete:

View File

@ -36,6 +36,7 @@ import Views
viewModel: viewModel
)
.contextMenu {
// TODO: add highlights view button
Button(
action: { viewModel.itemUnderTitleEdit = item },
label: { Label("Edit Title/Description", systemImage: "textbox") }
@ -137,6 +138,7 @@ import Views
.sheet(item: $viewModel.itemUnderTitleEdit) { item in
LinkedItemTitleEditView(item: item)
}
// TODO: add highlights view sheet
.task {
if viewModel.items.isEmpty {
loadItems(isRefresh: true)

View File

@ -17,6 +17,7 @@ import Views
@Published var showPushNotificationPrimer = false
@Published var itemUnderLabelEdit: LinkedItem?
@Published var itemUnderTitleEdit: LinkedItem?
@Published var itemForHighlightsView: LinkedItem?
@Published var searchTerm = ""
@Published var selectedLabels = [LinkedItemLabel]()
@Published var negatedLabels = [LinkedItemLabel]()

View File

@ -70,6 +70,7 @@ struct WebReader: PlatformViewRepresentable {
context.coordinator.linkHandler = openLinkAction
context.coordinator.webViewActionHandler = webViewActionHandler
context.coordinator.updateNavBarVisibilityRatio = navBarVisibilityRatioUpdater
context.coordinator.articleContentID = articleContent.id
loadContent(webView: webView)
return webView
@ -101,8 +102,10 @@ struct WebReader: PlatformViewRepresentable {
}
// If the webview had been terminated `needsReload` will have been set to true
if context.coordinator.needsReload {
// Or if the articleContent value has changed then it's id will be different from the coordinator's
if context.coordinator.needsReload || context.coordinator.articleContentID != articleContent.id {
loadContent(webView: webView)
context.coordinator.articleContentID = articleContent.id
context.coordinator.needsReload = false
return
}

View File

@ -12,6 +12,8 @@ struct WebReaderContainerView: View {
@State private var showPreferencesPopover = false
@State private var showLabelsModal = false
@State private var showTitleEdit = false
@State private var showHighlightsView = false
@State private var hasPerformedHighlightMutations = false
@State var showHighlightAnnotationModal = false
@State var safariWebLink: SafariWebLink?
@State private var navBarVisibilityRatio = 1.0
@ -43,6 +45,23 @@ struct WebReaderContainerView: View {
}
}
func onHighlightListViewDismissal() {
// Reload the web view if mutation happened in highlights list modal
guard hasPerformedHighlightMutations else { return }
hasPerformedHighlightMutations.toggle()
Task {
if let username = dataService.currentViewer?.username {
await viewModel.loadContent(
dataService: dataService,
username: username,
itemID: item.unwrappedID
)
}
}
}
private func handleHighlightAction(message: WKScriptMessage) {
guard let messageBody = message.body as? [String: String] else { return }
guard let actionID = messageBody["actionID"] else { return }
@ -131,6 +150,10 @@ struct WebReaderContainerView: View {
Menu(
content: {
Group {
Button(
action: { showHighlightsView = true },
label: { Label("View Highlights", systemImage: "highlighter") }
)
Button(
action: { showTitleEdit = true },
label: { Label("Edit Title/Description", systemImage: "textbox") }
@ -198,6 +221,12 @@ struct WebReaderContainerView: View {
.sheet(isPresented: $showTitleEdit) {
LinkedItemTitleEditView(item: item)
}
.sheet(isPresented: $showHighlightsView, onDismiss: onHighlightListViewDismissal) {
HighlightsListView(
itemObjectID: item.objectID,
hasHighlightMutations: $hasPerformedHighlightMutations
)
}
#if os(macOS)
.buttonStyle(PlainButtonStyle())
#endif

View File

@ -19,6 +19,7 @@ final class WebReaderCoordinator: NSObject {
var previousShowNavBarActionID: UUID?
var previousShareActionID: UUID?
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in }
var articleContentID = UUID()
private var yOffsetAtStartOfDrag: Double?
private var lastYOffset: Double = 0
private var hasDragged = false

View File

@ -9,6 +9,7 @@ public enum ArticleContentStatus: String {
}
public struct ArticleContent {
public let id = UUID()
public let title: String
public let htmlContent: String
public let highlightsJSONString: String

View File

@ -87,6 +87,12 @@ public extension LinkedItem {
}
}
var sortedHighlights: [Highlight] {
highlights.asArray(of: Highlight.self).sorted {
($0.createdAt ?? Date()) < ($1.createdAt ?? Date())
}
}
var labelsJSONString: String {
let labels = self.labels.asArray(of: LinkedItemLabel.self).map { label in
[

View File

@ -15,4 +15,5 @@ public enum FeatureFlag {
public static let enableSnooze = false
public static let enableGridCardsOnPhone = false
public static let enableTextToSpeechButton = true
public static let enableHighlightsView = true
}

View File

@ -22,15 +22,13 @@ public struct HighlightAnnotationSheet: View {
HStack {
Button("Cancel", action: onCancel)
Spacer()
HStack {
Image(systemName: "note.text")
Text("Note")
}
Label("Note", systemImage: "note.text")
Spacer()
Button("Save") {
onSave()
}
}
.foregroundColor(.appGrayTextContrast)
ScrollView {
TextEditor(text: $annotation)

View File

@ -8,6 +8,7 @@ public enum GridCardAction {
case editLabels
case editTitle
case downloadAudio
case viewHighlights
}
public struct GridCard: View {
@ -45,6 +46,10 @@ public struct GridCard: View {
var contextMenuView: some View {
Group {
Button(
action: { menuActionHandler(.viewHighlights) },
label: { Label("View Highlights", systemImage: "highlighter") }
)
Button(
action: { menuActionHandler(.editTitle) },
label: { Label("Edit Title/Description", systemImage: "textbox") }