Files
stemify/ios/Stemify/UI/Components/Rows.swift
T

151 lines
6.4 KiB
Swift

import SwiftUI
/// "NO STEMS" / "3/10 STEMS" for an album or artist, once the whole library has been matched.
struct StemFlag: View {
let cov: Coverage?
var body: some View {
if AppGraph.shared.coverage.ready, let cov {
if cov.none { Tag(text: "NO STEMS") } else if cov.partial { Tag(text: "\(cov.withStems)/\(cov.total) STEMS") }
}
}
}
/// Apple Music list row: art (or track number), title, artist, badges, and a ••• menu.
/// Stems are the norm once the server has worked through the library, so the badge marks the
/// songs without them, and only once the index has loaded.
struct TrackRow<Leading: View>: View {
let track: Track
var number: Int?
var showArt = true
var subtitle: String?
var menu: [SheetAction]?
let action: () -> Void
@ViewBuilder var leading: () -> Leading
@Environment(\.palette) private var p
var body: some View {
let g = AppGraph.shared
let pm = g.playback
let isCurrent = pm.current?.track.ratingKey == track.ratingKey
let noStems = g.stems.index.size > 0 && g.stems.find(track) == nil
let downloaded = g.downloads.records[track.ratingKey]?.done == true
let progress = g.downloads.progress[track.ratingKey]
HStack(spacing: 0) {
leading()
if showArt {
ZStack {
Artwork(thumb: track.thumb, size: 48, corner: 5)
if isCurrent {
Color.black.opacity(0.35).frame(width: 48, height: 48).clipShape(RoundedRectangle(cornerRadius: 5))
PlayingBars(playing: pm.playWhenReady, color: .white)
}
}
.padding(.trailing, 12)
} else if let number {
Group {
if isCurrent { PlayingBars(playing: pm.playWhenReady) } else { Text("\(number)").foregroundStyle(p.label2) }
}
.frame(width: 32, alignment: .leading)
}
VStack(alignment: .leading, spacing: 1) {
Text(track.title).foregroundStyle(isCurrent ? p.accent : p.label).lineLimit(1)
if let s = subtitle ?? (showArt ? track.artist : nil) {
Text(s).font(.subheadline).foregroundStyle(p.label2).lineLimit(1)
}
}
.padding(.vertical, 6)
Spacer(minLength: 6)
if noStems {
switch g.stems.state(track) {
case .stemming: Tag(text: "STEMMING").padding(.trailing, 6)
case .queued: Tag(text: "QUEUED").padding(.trailing, 6)
case .failed: Tag(text: "FAILED").padding(.trailing, 6)
case nil: Tag(text: "NO STEMS").padding(.trailing, 6)
}
}
if let progress { ProgressRing(progress: progress, size: 16).padding(.trailing, 4) }
else if downloaded { DownloadedMark().padding(.trailing, 4) }
if let menu { MoreMenu(items: menu).foregroundStyle(p.label) }
}
.frame(minHeight: showArt ? 56 : 40)
.contentShape(Rectangle())
.onTapGesture(perform: action)
.modifier(OptionalMenu(items: menu))
}
}
extension TrackRow where Leading == EmptyView {
init(track: Track, number: Int? = nil, showArt: Bool = true, subtitle: String? = nil, menu: [SheetAction]? = nil, action: @escaping () -> Void) {
self.init(track: track, number: number, showArt: showArt, subtitle: subtitle, menu: menu, action: action) { EmptyView() }
}
}
struct OptionalMenu: ViewModifier {
let items: [SheetAction]?
func body(content: Content) -> some View {
if let items { content.actionsMenu(items) } else { content }
}
}
/// Two-column album grid. [subtitle] replaces the artist (or year) line; [badge] sits on the art's corner.
struct AlbumGrid<Badge: View>: View {
let albums: [Album]
var showArtist = true
var subtitle: ((Album) -> String?)?
var menu: ((Album) -> [SheetAction])?
var columns = 2
let open: (Album) -> Void
@ViewBuilder var badge: (Album) -> Badge
@Environment(\.palette) private var p
var body: some View {
let cov = AppGraph.shared.coverage.albums
let small = columns >= 4
let gap: CGFloat = columns > 2 ? 10 : 16
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: gap, alignment: .top), count: columns), spacing: small ? 12 : 18) {
ForEach(albums) { a in
VStack(alignment: .leading, spacing: 0) {
GeometryReader { geo in
Artwork(thumb: a.thumb, size: geo.size.width, corner: 8, placeholder: "square.stack")
.overlay(alignment: .bottomTrailing) { badge(a).padding(8) }
}
.aspectRatio(1, contentMode: .fit)
HStack(spacing: 6) {
Text(a.title).font((small ? Font.caption : .subheadline).weight(.medium)).foregroundStyle(p.label).lineLimit(1)
StemFlag(cov: cov[a.ratingKey])
}
.padding(.top, 6)
let sub = subtitle?(a) ?? (showArtist ? a.artist : a.year.map(String.init) ?? "")
if !sub.isEmpty { Text(sub).font(small ? .caption : .subheadline).foregroundStyle(p.label2).lineLimit(1) }
}
.contentShape(Rectangle())
.onTapGesture { open(a) }
.modifier(OptionalMenu(items: menu?(a)))
}
}
.padding(.horizontal, MARGIN)
}
}
extension AlbumGrid where Badge == EmptyView {
init(albums: [Album], showArtist: Bool = true, subtitle: ((Album) -> String?)? = nil, menu: ((Album) -> [SheetAction])? = nil, columns: Int = 2, open: @escaping (Album) -> Void) {
self.init(albums: albums, showArtist: showArtist, subtitle: subtitle, menu: menu, columns: columns, open: open) { _ in EmptyView() }
}
}
struct ArtistRow: View {
let artist: Artist
@Environment(\.palette) private var p
var body: some View {
HStack(spacing: 14) {
Artwork(thumb: artist.thumb, size: 44, circle: true, placeholder: "person.fill")
Text(artist.title).foregroundStyle(p.label).lineLimit(1)
Spacer()
StemFlag(cov: AppGraph.shared.coverage.artists[artist.ratingKey])
}
.frame(minHeight: 52)
}
}