Luminate/Sources/LuminateUI/Components/SearchView.swift

151 lines
4.4 KiB
Swift

//
// SearchView.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Adwaita
import Foundation
import LuminateCore
import LuminateDI
struct SearchView: View {
var client: JellyfinClient
var userId: String
@State private var searchText = ""
@State private var results: [Components.Schemas.SearchHint] = []
@State private 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 {
let result = try? await client.getSearchHints(
searchTerm: searchText,
userId: userId,
limit: 50
)
await MainActor.run {
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 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 {
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
}
}
}
}