luminate-old/Sources/LuminateUI/Components/HomePosterCell.swift

166 lines
4.4 KiB
Swift

//
// HomePosterCell.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 HomePosterCell: View {
let itemId: String?
let title: String?
let subtitle: String?
let width: Int
let imageAspectRatio: Float
let onClick: (() -> Void)?
@Injected(\.client) private var client
@Injected(\.imageService) private var imageService
@Injected(\.viewUpdateScheduler) private var viewUpdateScheduler
@State private var imageData: Data?
private struct Constants {
static let padding = 8
}
init(
itemId: String? = nil,
title: String? = nil,
subtitle: String? = nil,
width: Int = 200,
imageAspectRatio: Float = 1.5,
onClick: @escaping () -> Void
) {
self.itemId = itemId
self.title = title
self.subtitle = subtitle
self.width = width
self.imageAspectRatio = imageAspectRatio
self.onClick = onClick
}
var view: Body {
Bin {
VStack(spacing: showTextSection ? 6 : 0) {
imageSection
if showTextSection {
textSection
}
}
.padding(padding)
}
.onClick(handler: onClick ?? {})
.onAppear {
Idle {
loadImage()
}
}
.frame(minWidth: width)
.halign(.fill)
.valign(.start)
.hexpand(false)
.style("activatable")
.card()
}
@ViewBuilder
private var imageSection: Body {
AspectContainer(aspectRatio: imageAspectRatio)
.child {
image
.halign(.fill)
.hexpand()
.overflow(.hidden)
.card()
}
.frame(minWidth: width - (padding * 2))
}
@ViewBuilder
private var image: Body {
if let imageData {
Picture()
.contentFit(.cover)
.data(imageData)
.valign(.fill)
.halign(.fill)
.vexpand()
.hexpand()
.transition(.crossfade)
} else {
Spinner()
.frame(minWidth: 64, minHeight: 64)
.transition(.crossfade)
}
}
@ViewBuilder
private var textSection: Body {
Bin {
VStack(spacing: 2) {
if let title, !title.isEmpty {
Text(title)
.maxWidthChars(0)
.ellipsize()
.heading()
}
if let subtitle, !subtitle.isEmpty {
Text(subtitle)
.maxWidthChars(0)
.ellipsize()
.caption()
.dimLabel()
}
}
.valign(.center)
}
.frame(minHeight: 40)
.hexpand()
}
private var showTextSection: Bool {
(title?.isEmpty == false) && (subtitle?.isEmpty == false)
}
private var padding: Int {
showTextSection ? Constants.padding : 0
}
private func loadImage() {
guard let itemId else { return }
guard
let url = client.imageURL(
itemId: itemId,
imageType: .primary,
maxWidth: width.cInt * 2
)
else { return }
Task {
let data = try? await imageService.loadImage(url: url)
_imageData.rawValue = data
viewUpdateScheduler.scheduleFlush()
}
}
}