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: 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: View { let albums: [Album] var showArtist = true var subtitle: ((Album) -> String?)? var menu: ((Album) -> [SheetAction])? let open: (Album) -> Void @ViewBuilder var badge: (Album) -> Badge @Environment(\.palette) private var p var body: some View { let cov = AppGraph.shared.coverage.albums LazyVGrid(columns: [GridItem(.flexible(), spacing: 16, alignment: .top), GridItem(.flexible(), spacing: 16, alignment: .top)], spacing: 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(.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(.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, open: @escaping (Album) -> Void) { self.init(albums: albums, showArtist: showArtist, subtitle: subtitle, menu: menu, 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) } }