Files
omnivore/apple/OmnivoreKit/Sources/App/Views/LabelsEntryView.swift
2023-11-01 14:39:43 +08:00

199 lines
5.3 KiB
Swift

import Models
import Services
import SwiftUI
import Views
let ZWSP = "\u{200B}"
@MainActor
protocol Entry {
func item(parent: LabelsEntryView) -> AnyView
}
@MainActor
private struct LabelEntry: Entry {
let label: LinkedItemLabel
func item(parent _: LabelsEntryView) -> AnyView {
if let name = label.name, let hex = label.color, let color = Color(hex: hex) {
return AnyView(LibraryItemLabelView(text: name, color: color))
}
return AnyView(EmptyView())
}
}
@MainActor
public struct LabelsEntryView: View {
@Binding var searchTerm: String
@State var viewModel: LabelsViewModel
@EnvironmentObject var dataService: DataService
let entries: [Entry]
@State private var totalHeight = CGFloat.zero
@FocusState private var textFieldFocused: Bool
public init(
searchTerm: Binding<String>,
viewModel: LabelsViewModel
) {
self._searchTerm = searchTerm
self.viewModel = viewModel
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()
if trimmed.count < 1 {
return
}
if let label = viewModel.labels.first(where: { $0.name?.lowercased() == trimmed }) {
if !viewModel.selectedLabels.contains(label) {
viewModel.selectedLabels.append(label)
}
searchTerm = ZWSP
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
textFieldFocused = true
}
} else {
viewModel.createLabel(
dataService: dataService,
name: trimmed,
color: Gradient.randomColor(str: trimmed, offset: 1),
description: nil
)
searchTerm = ZWSP
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
textFieldFocused = true
}
}
}
var deletableTextField: some View {
let str = NSAttributedString(
string: searchTerm,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14)]
)
// Round it up to avoid jitter when typing
let textWidth = max(25.0, Double(Int(str.size().width + 1)))
let result = TextField("", text: $searchTerm)
.frame(alignment: .topLeading)
.frame(height: 25)
.frame(width: textWidth)
.padding(5)
.font(Font.system(size: 14))
.multilineTextAlignment(.leading)
.onChange(of: searchTerm, perform: { _ in
if searchTerm.count >= 64 {
searchTerm = String(searchTerm.prefix(64))
}
if searchTerm.isEmpty {
if viewModel.selectedLabels.count > 0 {
viewModel.selectedLabels.removeLast()
searchTerm = ZWSP
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
textFieldFocused = true
}
} else {
searchTerm = ZWSP
}
}
})
.onSubmit {
onTextSubmit()
}
return 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 }
}
private func generateLabelsContent(in geom: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(Array(self.entries.enumerated()), id: \.offset) { _, entry in
entry.item(parent: self)
.padding(5)
.alignmentGuide(.leading, computeValue: { dim in
if abs(width - dim.width) > geom.size.width {
width = 0
height -= dim.height
}
let result = width
width -= dim.width
return result
})
.alignmentGuide(.top, computeValue: { _ in
let result = height
return result
})
}
deletableTextField
.alignmentGuide(.leading, computeValue: { dim in
if abs(width - dim.width) > geom.size.width {
width = 0
height -= dim.height
}
let result = width
width = 0
return result
})
.alignmentGuide(.top, computeValue: { _ in
let result = height
height = 0
return result
}).focused($textFieldFocused)
}.background(viewHeightReader($totalHeight))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}