Luminate/Sources/LuminateUI/Components/SearchView.swift

135 lines
3.7 KiB
Swift

//
// SearchView.swift
// LuminateUI
//
// Created by Brendan Szymanski on 6/5/26.
//
@preconcurrency import Adwaita
import Foundation
import LuminateCore
import LuminateDI
struct SearchView: View {
var client: JellyfinClient
var userId: String
@State private nonisolated(unsafe) var searchText = ""
@State private nonisolated(unsafe) var results: [Components.Schemas.SearchHint] = []
@State private nonisolated(unsafe) var isSearching = false
var view: Body {
VStack {
SearchEntry()
.text($searchText)
.placeholderText("Search")
if isSearching {
Spinner()
.padding(20)
} else {
ScrollView {
VStack {
ForEach(results) { hint in
SearchResultRow(hint: hint, client: client)
}
}
}
}
}
.padding()
}
private func performSearch() {
guard !searchText.isEmpty || searchText == "" else {
results = []
return
}
isSearching = true
Task { [self] in
let result = try? await client.getSearchHints(
searchTerm: searchText,
userId: userId,
limit: 50
)
results = result?.searchHints ?? []
isSearching = false
}
}
}
extension Components.Schemas.SearchHint: Identifiable {
public var id: String { id ?? itemId ?? String(describing: self) }
}
struct SearchResultRow: View {
var hint: Components.Schemas.SearchHint
var client: JellyfinClient
@Injected(\.pageAnimationTracker) var pageAnimationTracker
@State private nonisolated(unsafe) var imageData: Data?
var view: Body {
HStack {
if let data = imageData {
Picture()
.data(data)
.frame(minWidth: 80, minHeight: 120)
.frame(maxWidth: 80)
.frame(maxHeight: 120)
.card()
} else {
Box(spacing: 0) {}
.frame(minWidth: 80, minHeight: 120)
.frame(maxWidth: 80)
.frame(maxHeight: 120)
.card()
}
VStack {
Text(hint.name ?? "")
.body()
.halign(.start)
if let type = hint._type?.value1 {
Text("\(type)")
.caption()
.halign(.start)
}
if let year = hint.productionYear {
Text("\(year)")
.caption()
.halign(.start)
}
Text(hint.runtimeString)
.caption()
.halign(.start)
}
.hexpand(true)
}
.padding(5, .vertical)
.onAppear {
Idle {
loadImage()
}
}
}
private func loadImage() {
guard let tag = hint.primaryImageTag,
let itemId = hint.id ?? hint.itemId
else { return }
Task { [self] in
guard
let url = await client.imageURL(
itemId: itemId, imageType: .primary, tag: tag, maxWidth: 160
)
else { return }
let service = ImageService()
let data = try? await service.loadImage(url: url)
if pageAnimationTracker.isAnimating {
_imageData.rawValue = data
} else {
imageData = data
}
}
}
}