144 lines
6.0 KiB
Swift
144 lines
6.0 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 { 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)
|
|
}
|
|
}
|