summaryrefslogtreecommitdiff
path: root/Sora/Views/SearchSuggestionsView.swift
blob: e9593ecef291e5d77b3508ac2975d9b3ac33b253 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import SwiftUI

struct SearchSuggestionsView: View {
  private struct CachedTag {
    let original: Either<BooruTag, BooruSearchQuery>
    let names: [String]
  }

  var items: [Either<BooruTag, BooruSearchQuery>]
  @Binding var searchText: String
  @Binding var suppressNextSearchSubmit: Bool
  @State private var cachedTags: [CachedTag] = []

  private var lastSearchTag: String {
    String(searchText.split(separator: " ").last ?? "").lowercased()
  }

  private var itemsCacheKey: Int {
    items.reduce(into: Hasher()) { hasher, item in
      hasher.combine(item)
    }
    .finalize()
  }

  private func refreshCachedTags() {
    cachedTags = items.map { item in
      switch item {
      case .left(let tag):
        return CachedTag(original: item, names: [tag.name.lowercased()])

      case .right(let query):
        return CachedTag(original: item, names: query.tags.map { $0.lowercased() })
      }
    }
  }

  private var filteredItems: [Either<BooruTag, BooruSearchQuery>] {
    let matchCandidateTag = lastSearchTag
    let matchingTagsLimit = 50

    guard !matchCandidateTag.isEmpty else { return [] }

    var seenTags = Set<String>()
    var matchingTags: [Either<BooruTag, BooruSearchQuery>] = []

    matchingTags.reserveCapacity(matchingTagsLimit)

    for tag in cachedTags {
      if matchingTags.count >= matchingTagsLimit { break }

      for name in tag.names {
        if name.contains(matchCandidateTag), seenTags.insert(name).inserted {
          matchingTags.append(tag.original)

          break
        }
      }
    }

    return matchingTags
  }

  var body: some View {
    Group {
      ForEach(filteredItems, id: \.self) { item in
        switch item {
        case .left(let tag):
          Button {
            let previousTags = searchText.split(separator: " ").dropLast()

            suppressNextSearchSubmit = true
            searchText = (previousTags.map(String.init) + [tag.name]).joined(separator: " ")
          } label: {
            Text(tag.name)
          }

        case .right(let query):
          let matchingTag = query.tags.first { tag in
            tag.lowercased().contains(lastSearchTag)
          }

          Button {
            if let matchingTag {
              let previousTags = searchText.split(separator: " ").dropLast()

              suppressNextSearchSubmit = true
              searchText = (previousTags.map(String.init) + [matchingTag]).joined(separator: " ")
            }
          } label: {
            if let matchingTag {
              Text(matchingTag)
            } else {
              Text(query.tags.first ?? "")
            }
          }
        }
      }
    }
    .onAppear {
      refreshCachedTags()
    }
    .onChange(of: itemsCacheKey) {
      refreshCachedTags()
    }
  }
}