Merge pull request #2271 from omnivore-app/fix/ios-sort-filter-labels
iOS label sorting
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import CoreData
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
@ -11,6 +12,14 @@ import Views
|
||||
@Published var unselectedLabels = [LinkedItemLabel]()
|
||||
@Published var labelSearchFilter = ""
|
||||
|
||||
func setLabels(_ labels: [LinkedItemLabel]) {
|
||||
self.labels = labels.sorted { left, right in
|
||||
let aTrimmed = left.unwrappedName.trimmingCharacters(in: .whitespaces)
|
||||
let bTrimmed = right.unwrappedName.trimmingCharacters(in: .whitespaces)
|
||||
return aTrimmed.caseInsensitiveCompare(bTrimmed) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
func loadLabels(
|
||||
dataService: DataService,
|
||||
initiallySelectedLabels: [LinkedItemLabel],
|
||||
@ -18,22 +27,59 @@ import Views
|
||||
) async {
|
||||
isLoading = true
|
||||
|
||||
if let labelIDs = try? await dataService.labels() {
|
||||
dataService.viewContext.performAndWait {
|
||||
self.labels = labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel }
|
||||
await loadLabelsFromStore(dataService: dataService)
|
||||
for label in labels {
|
||||
if initiallySelectedLabels.contains(label) {
|
||||
selectedLabels.append(label)
|
||||
} else if initiallyNegatedLabels.contains(label) {
|
||||
negatedLabels.append(label)
|
||||
} else {
|
||||
unselectedLabels.append(label)
|
||||
}
|
||||
}
|
||||
|
||||
for label in labels {
|
||||
if initiallySelectedLabels.contains(label) {
|
||||
selectedLabels.append(label)
|
||||
} else if initiallyNegatedLabels.contains(label) {
|
||||
negatedLabels.append(label)
|
||||
} else {
|
||||
unselectedLabels.append(label)
|
||||
Task.detached(priority: .userInitiated) {
|
||||
if let labelIDs = try? await dataService.labels() {
|
||||
DispatchQueue.main.async {
|
||||
dataService.viewContext.performAndWait {
|
||||
self.setLabels(labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel })
|
||||
}
|
||||
for label in self.labels {
|
||||
if initiallySelectedLabels.contains(label) {
|
||||
self.selectedLabels.append(label)
|
||||
} else if initiallyNegatedLabels.contains(label) {
|
||||
self.negatedLabels.append(label)
|
||||
} else {
|
||||
self.unselectedLabels.append(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func loadLabelsFromStore(dataService: DataService) async {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItemLabel> = LinkedItemLabel.fetchRequest()
|
||||
|
||||
let fetchedLabels = await dataService.viewContext.perform {
|
||||
try? fetchRequest.execute()
|
||||
}
|
||||
|
||||
setLabels(fetchedLabels ?? [])
|
||||
unselectedLabels = fetchedLabels ?? []
|
||||
}
|
||||
|
||||
func fetchLabelsFromNetwork(dataService: DataService) async {
|
||||
let labelIDs = try? await dataService.labels()
|
||||
guard let labelIDs = labelIDs else { return }
|
||||
|
||||
let fetchedLabels = await dataService.viewContext.perform {
|
||||
labelIDs.compactMap { dataService.viewContext.object(with: $0) as? LinkedItemLabel }
|
||||
}
|
||||
|
||||
setLabels(fetchedLabels)
|
||||
unselectedLabels = fetchedLabels
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,9 +127,38 @@ public extension LinkedItem {
|
||||
}
|
||||
|
||||
var sortedLabels: [LinkedItemLabel] {
|
||||
labels.asArray(of: LinkedItemLabel.self).sorted {
|
||||
($0.name ?? "").lowercased() < ($1.name ?? "").lowercased()
|
||||
sortedLabels(labels: labels.asArray(of: LinkedItemLabel.self))
|
||||
}
|
||||
|
||||
func sortedLabels(labels: [LinkedItemLabel]?) -> [LinkedItemLabel] {
|
||||
guard let labels = labels else {
|
||||
return []
|
||||
}
|
||||
|
||||
var colors = [String: [LinkedItemLabel]]()
|
||||
for label in labels {
|
||||
if let color = label.color {
|
||||
var list = colors[color] ?? []
|
||||
list.append(label)
|
||||
colors[color] = list.sorted(by: { ($0.name ?? "").localizedCompare($1.name ?? "") == .orderedAscending })
|
||||
}
|
||||
}
|
||||
|
||||
let sortedColors = Array(colors.keys).sorted(by: { leftLabel, rightLabel -> Bool in
|
||||
let aname = colors[leftLabel]?.first?.name ?? leftLabel
|
||||
let bname = colors[rightLabel]?.first?.name ?? rightLabel
|
||||
return aname.localizedCompare(bname) == .orderedAscending
|
||||
})
|
||||
|
||||
var result = [LinkedItemLabel]()
|
||||
for key in sortedColors {
|
||||
if let items = colors[key] {
|
||||
let sorted = items.sorted(by: { ($0.name ?? "").localizedCompare($1.name ?? "") == .orderedAscending })
|
||||
result.append(contentsOf: sorted)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var sortedHighlights: [Highlight] {
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
//
|
||||
// based on: LabelsMasonaryView.swift we should try to combine the two
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
struct LabelsFlowLayout: View {
|
||||
@State private var totalHeight = CGFloat.zero
|
||||
private var labelItems: [LinkedItemLabel]
|
||||
|
||||
init(
|
||||
labels: [LinkedItemLabel]
|
||||
) {
|
||||
self.labelItems = labels
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
GeometryReader { geometry in
|
||||
self.generateContent(in: geometry)
|
||||
}
|
||||
}.padding(5)
|
||||
.frame(height: totalHeight)
|
||||
}
|
||||
|
||||
private func generateContent(in geom: GeometryProxy) -> some View {
|
||||
var width = CGFloat.zero
|
||||
var height = CGFloat.zero
|
||||
|
||||
return ZStack(alignment: .topLeading) {
|
||||
ForEach(self.labelItems, id: \.self) { label in
|
||||
self.item(for: label)
|
||||
.padding(.horizontal, 1)
|
||||
.padding(.vertical, 1)
|
||||
.alignmentGuide(.leading, computeValue: { dim in
|
||||
if abs(width - dim.width) > geom.size.width {
|
||||
width = 0
|
||||
height -= dim.height
|
||||
}
|
||||
let result = width
|
||||
if label == self.labelItems.last! {
|
||||
width = 0 // last item
|
||||
} else {
|
||||
width -= dim.width
|
||||
}
|
||||
return result
|
||||
})
|
||||
.alignmentGuide(.top, computeValue: { _ in
|
||||
let result = height
|
||||
if label == self.labelItems.last! {
|
||||
height = 0 // last item
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
.background(viewHeightReader($totalHeight))
|
||||
}
|
||||
|
||||
private func item(for item: LinkedItemLabel) -> some View {
|
||||
let chip = TextChip(feedItemLabel: item, padded: false, onTap: nil)
|
||||
return chip
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,19 +213,8 @@ public struct LibraryItemCard: View {
|
||||
}
|
||||
|
||||
var labels: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(item.sortedLabels, id: \.self) {
|
||||
TextChip(feedItemLabel: $0)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}.introspectScrollView { scrollView in
|
||||
#if os(iOS)
|
||||
scrollView.bounces = false
|
||||
#endif
|
||||
}
|
||||
.padding(.top, 0)
|
||||
LabelsFlowLayout(labels: item.sortedLabels)
|
||||
.padding(.top, 0)
|
||||
#if os(macOS)
|
||||
.onTapGesture {
|
||||
tapHandler()
|
||||
|
||||
Reference in New Issue
Block a user