Label entry improvements for macos

This commit is contained in:
Jackson Harper 2023-11-13 20:14:31 +08:00
parent 50f1213e93
commit d33c975ffb
6 changed files with 188 additions and 88 deletions

View file

@ -31,14 +31,6 @@ public struct EditInfoSheet: View {
#endif
}
// func saveInfo() {
// if let linkedItem = viewModel.linkedItem {
// _ = viewModel.services.dataService.updateLinkedItemTitleAndDescription(itemID: linkedItem.unwrappedID, title: title, description: description, author: author)
// } else {
// // Maybe we shouldn't even allow this UI without linkeditem existing
// }
// }
public var body: some View {
if let item = viewModel.linkedItem {
LinkedItemMetadataEditView(item: item) { title, _ in

View file

@ -60,13 +60,12 @@ public struct ShareExtensionView: View {
}
var isSynced: Bool {
true
// switch viewModel.status {
// case .synced:
// return true
// default:
// return false
// }
switch viewModel.status {
case .synced:
return true
default:
return false
}
}
var articleInfoBox: some View {
@ -166,14 +165,16 @@ public struct ShareExtensionView: View {
var moreMenuButton: some View {
Menu {
Button(action: {
NotificationCenter.default.post(name: Notification.Name("ShowEditInfoSheet"), object: nil)
}, label: {
Label(
"Edit Info",
systemImage: "info.circle"
)
})
#if os(iOS)
Button(action: {
NotificationCenter.default.post(name: Notification.Name("ShowEditInfoSheet"), object: nil)
}, label: {
Label(
"Edit Info",
systemImage: "info.circle"
)
})
#endif
Button(action: {
if let linkedItem = self.viewModel.linkedItem {
self.viewModel.setLinkArchived(dataService: self.viewModel.services.dataService,
@ -263,8 +264,10 @@ public struct ShareExtensionView: View {
public var body: some View {
#if os(iOS)
iOSBody
.environmentObject(viewModel.services.dataService)
#else
macOSBody
.environmentObject(viewModel.services.dataService)
#endif
}
@ -324,10 +327,18 @@ public struct ShareExtensionView: View {
}
}
@State var notes = ""
@State var labelsSearch = ZWSP
@State var isLabelsEntryFocused = false
func save() {
if !viewModel.noteText.isEmpty {
viewModel.saveNote()
}
if let itemID = viewModel.linkedItem?.id {
labelsViewModel.saveItemLabelChanges(itemID: itemID, dataService: viewModel.services.dataService)
}
}
var macOSBody: some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 10) {
@ -342,7 +353,7 @@ public struct ShareExtensionView: View {
Divider()
ZStack(alignment: .topLeading) {
TextEditor(text: $notes)
TextEditor(text: $viewModel.noteText)
.frame(maxWidth: .infinity)
.font(Font.system(size: 14))
.accentColor(.blue)
@ -350,7 +361,7 @@ public struct ShareExtensionView: View {
textView.textContainerInset = NSSize(width: 10, height: 10)
}
.focused($focusedField, equals: .noteEditor)
if notes.isEmpty {
if viewModel.noteText.isEmpty {
Text("Notes")
.fontWeight(.light)
.font(Font.system(size: 14))
@ -366,7 +377,7 @@ public struct ShareExtensionView: View {
ZStack(alignment: .topLeading) {
LabelsEntryView(searchTerm: $labelsSearch, isFocused: $isLabelsEntryFocused, viewModel: labelsViewModel)
.frame(maxWidth: .infinity)
.padding(.horizontal, 10)
.padding(.horizontal, 8)
.focused($focusedField, equals: .labelEditor)
.onHover { isHovered in
DispatchQueue.main.async {
@ -377,8 +388,8 @@ public struct ShareExtensionView: View {
}
}
}
if !isLabelsEntryFocused, labelsViewModel.selectedLabels.isEmpty, labelsSearch == ZWSP {
Text("Add Labels")
if labelsViewModel.selectedLabels.isEmpty, labelsSearch == ZWSP {
Text("Type to add labels")
.fontWeight(.light)
.font(Font.system(size: 14))
.foregroundColor(.black.opacity(0.25))
@ -395,12 +406,14 @@ public struct ShareExtensionView: View {
.padding(.bottom, 15)
Spacer()
Button(action: {
save()
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}, label: {
Text("Dismiss")
})
.padding(.bottom, 15)
Button(action: {
save()
viewModel.handleReadNowAction(extensionContext: extensionContext)
}, label: {
Text("Read Now")
@ -412,9 +425,14 @@ public struct ShareExtensionView: View {
}.frame(maxWidth: .infinity)
.background(Color.isDarkMode ? Color.systemBackground : Color.white)
.onAppear {
viewModel.savePage(extensionContext: extensionContext)
if let extensionContext = extensionContext {
viewModel.savePage(extensionContext: extensionContext)
}
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
focusedField = .noteEditor
focusedField = .labelEditor
}
Task {
await labelsViewModel.loadLabels(dataService: viewModel.services.dataService, initiallySelectedLabels: [])
}
}
}

View file

@ -212,8 +212,11 @@ extension Sequence where Element == LinkedItemLabel {
if searchFilter.isEmpty || searchFilter == ZWSP {
return map { $0 } // return the identity of the sequence
}
let index = searchFilter.index(searchFilter.startIndex, offsetBy: 1)
let trimmed = searchFilter.suffix(from: index).lowercased()
return filter { ($0.name ?? "").lowercased().contains(trimmed) }
if searchFilter.starts(with: ZWSP) {
let index = searchFilter.index(searchFilter.startIndex, offsetBy: 1)
let trimmed = searchFilter.suffix(from: index).lowercased()
return filter { ($0.name ?? "").lowercased().contains(trimmed) }
}
return filter { ($0.name ?? "").lowercased().contains(searchFilter.lowercased()) }
}
}

View file

@ -31,6 +31,11 @@ public struct LabelsEntryView: View {
let entries: [Entry]
#if os(macOS)
@State var popoverIndex = -1
@State var presentPopover = false
#endif
@State private var totalHeight = CGFloat.zero
@FocusState private var textFieldFocused: Bool
@ -46,20 +51,28 @@ public struct LabelsEntryView: View {
self.entries = Array(viewModel.selectedLabels.map { LabelEntry(label: $0) })
}
func onTextSubmit() {
let index = searchTerm.index(searchTerm.startIndex, offsetBy: 1)
let trimmed = searchTerm.suffix(from: index).lowercased()
func getSearchTermText() -> String {
if searchTerm.starts(with: ZWSP) {
let index = searchTerm.index(searchTerm.startIndex, offsetBy: 1)
let trimmed = searchTerm.suffix(from: index)
return String(trimmed)
}
return searchTerm
}
func onTextSubmit() {
let trimmed = getSearchTermText()
if trimmed.count < 1 {
return
}
if let label = viewModel.labels.first(where: { $0.name?.lowercased() == trimmed }) {
let lowercased = trimmed.lowercased()
if let label = viewModel.labels.first(where: { $0.name?.lowercased() == lowercased }) {
if !viewModel.selectedLabels.contains(label) {
viewModel.selectedLabels.append(label)
}
searchTerm = ZWSP
reset()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
textFieldFocused = true
}
@ -70,7 +83,7 @@ public struct LabelsEntryView: View {
color: Gradient.randomColor(str: trimmed, offset: 1),
description: nil
)
searchTerm = ZWSP
reset()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
textFieldFocused = true
}
@ -95,17 +108,31 @@ public struct LabelsEntryView: View {
#endif
}
func reset() {
searchTerm = ZWSP
#if os(macOS)
popoverIndex = -1
presentPopover = false
#endif
}
var deletableTextField: some View {
// Round it up to avoid jitter when typing
let textWidth = max(25.0, Double(Int(textFieldString.size().width + 1)))
let result = TextField("", text: $searchTerm)
let textWidth = max(25.0, Double(Int(textFieldString.size().width + 28)))
var result = AnyView(TextField("", text: $searchTerm)
.id("deletableTextField")
.frame(alignment: .topLeading)
.frame(height: 25)
.frame(width: textWidth)
.padding(5)
.padding(.trailing, 5)
.padding(.vertical, 5)
.accentColor(.blue)
.font(Font.system(size: 14))
.multilineTextAlignment(.leading)
#if os(macOS)
.textFieldStyle(.plain)
.background(Color.clear)
#endif
.onChange(of: searchTerm, perform: { _ in
if searchTerm.count >= 64 {
searchTerm = String(searchTerm.prefix(64))
@ -113,59 +140,89 @@ public struct LabelsEntryView: View {
if searchTerm.isEmpty {
if viewModel.selectedLabels.count > 0 {
viewModel.selectedLabels.removeLast()
searchTerm = ZWSP
reset()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
textFieldFocused = true
}
} else {
searchTerm = ZWSP
reset()
}
}
})
.onSubmit {
onTextSubmit()
}
#if os(macOS)
.textFieldStyle(.plain)
.background(Color.clear)
#endif
return result
.onSubmit {
if popoverIndex == -1 || popoverIndex >= partialMatches.count {
onTextSubmit()
} else if popoverIndex >= 0, popoverIndex < partialMatches.count {
let matched = partialMatches[popoverIndex]
viewModel.selectedLabels.append(matched)
reset()
}
}
.submitScope()
)
if #available(macOS 14.0, *) {
result = AnyView(result
.onKeyPress(.downArrow) {
popoverIndex = ((popoverIndex + 1) % (partialMatches.count + 1))
return .handled
}
.onKeyPress(.upArrow) {
popoverIndex -= 1
return .handled
}
.onKeyPress(.tab) {
popoverIndex = ((popoverIndex + 1) % (partialMatches.count + 1))
return .handled
})
}
return AnyView(result)
}
// func onTextDelete() -> Bool { if searchTerm.isEmpty {
// if lastSelected {
// if viewModel.selectedLabels.count > 0 {
// viewModel.selectedLabels.removeLast()
// DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
// textFieldFocused = true
// }
// }
// } else {
// lastSelected = true
// }
// return true
// }
// return false
// }
public var body: some View {
// HStack(spacing: 0) {
VStack {
GeometryReader { geometry in
self.generateLabelsContent(in: geometry)
}
}.padding(0)
.frame(height: totalHeight)
.background(Color.extensionPanelBackground)
.cornerRadius(8)
.onAppear {
textFieldFocused = true
}
.onTapGesture {
textFieldFocused = true
}
.transaction { $0.animation = nil }
.onChange(of: textFieldFocused) { self.isFocused = $0 }
Form {
VStack {
GeometryReader { geometry in
self.generateLabelsContent(in: geometry)
}
}.padding(0)
.frame(height: totalHeight)
.background(Color.extensionPanelBackground)
.cornerRadius(8)
.onAppear {
textFieldFocused = true
}
.onTapGesture {
textFieldFocused = true
}
.onChange(of: textFieldFocused) { self.isFocused = $0 }
}
}
var partialMatches: [LinkedItemLabel] {
viewModel.labels.applySearchFilter(searchTerm)
}
private var createLabelButton: some View {
let count = partialMatches.count
return Button {
viewModel.createLabel(
dataService: dataService,
name: searchTerm,
color: Gradient.randomColor(str: searchTerm, offset: 1),
description: nil
)
reset()
} label: {
Text("Create new label")
.padding(6)
}
.background(popoverIndex == count ? Color.blue : Color.clear)
.frame(maxWidth: .infinity, alignment: .leading)
.cornerRadius(4)
.buttonStyle(.borderless)
.cornerRadius(4)
}
private func generateLabelsContent(in geom: GeometryProxy) -> some View {
@ -206,6 +263,36 @@ public struct LabelsEntryView: View {
height = 0
return result
}).focused($textFieldFocused)
#if os(macOS)
.onChange(of: searchTerm) { _ in
presentPopover = !searchTerm.isEmpty && searchTerm != ZWSP && partialMatches.count < 14
if popoverIndex >= partialMatches.count + 1 {
popoverIndex = partialMatches.count + 1
}
}
.popover(isPresented: $presentPopover, arrowEdge: .top) {
VStack(alignment: .leading, spacing: 4) {
ForEach(Array(partialMatches.enumerated()), id: \.offset) { idx, label in
if let name = label.name {
Button {
reset()
viewModel.selectedLabels.append(label)
} label: {
Text(name)
.padding(6)
.frame(maxWidth: .infinity, alignment: .leading)
}
.background(idx == popoverIndex ? Color.blue : Color.clear)
.frame(maxWidth: .infinity, alignment: .leading)
.cornerRadius(4)
.buttonStyle(.borderless)
.cornerRadius(4)
}
}
createLabelButton
}.padding(4)
}
#endif
}.background(viewHeightReader($totalHeight))
}

View file

@ -138,7 +138,7 @@ struct LinkedItemMetadataEditView: View {
)
}
}
.frame(minWidth: 400, minHeight: 600)
.frame(minWidth: 400, minHeight: 400)
}
#endif
}

View file

@ -66,7 +66,7 @@ public struct LibraryItemCard: View {
labels
}
if let note = item.noteText {
if let note = item.noteText, !note.isEmpty {
HStack(alignment: .top, spacing: 10) {
avatarImage
.frame(width: 20, height: 20)