From 8345707d1fd72c14c2bc51c6616011c0b13a8361 Mon Sep 17 00:00:00 2001 From: Robbe Van Petegem Date: Sun, 4 Feb 2024 12:15:26 +0100 Subject: [PATCH] Improve artist view (#64) * Improve click target for context button * Make number optional * Add a better track list to artist view * Make whole album view scrollable * Remove list usage * Improve padding and layout of view * Imrpove spacing in artists view --- accentor/Model/Track.swift | 2 +- accentor/View/Albums/AlbumView.swift | 62 +++++++++++---------- accentor/View/Artists/ArtistView.swift | 35 ++++++++---- accentor/View/Artists/ArtistViewModel.swift | 12 +++- accentor/View/Artists/ArtistsView.swift | 4 +- accentor/View/TrackRow/TrackRowView.swift | 9 ++- 6 files changed, 75 insertions(+), 49 deletions(-) diff --git a/accentor/Model/Track.swift b/accentor/Model/Track.swift index 08079ff..ac2e474 100644 --- a/accentor/Model/Track.swift +++ b/accentor/Model/Track.swift @@ -8,7 +8,7 @@ import Foundation import GRDB -struct Track: Identifiable, Equatable, Codable, FetchableRecord, PersistableRecord { +struct Track: Identifiable, Equatable, Hashable, Codable, FetchableRecord, PersistableRecord { // Generic properties var id: Int64 var createdAt: Date diff --git a/accentor/View/Albums/AlbumView.swift b/accentor/View/Albums/AlbumView.swift index 61d3611..e9e2ebb 100644 --- a/accentor/View/Albums/AlbumView.swift +++ b/accentor/View/Albums/AlbumView.swift @@ -10,6 +10,7 @@ import GRDBQuery struct AlbumView: View { @EnvironmentStateObject private var viewModel: AlbumViewModel + @Environment(\.defaultMinListRowHeight) var minRowHeight init(id: Album.ID) { _viewModel = EnvironmentStateObject { @@ -19,42 +20,44 @@ struct AlbumView: View { var body: some View { if let album = viewModel.albumInfo?.album { - VStack { - CachedImage(imageURL: album.image250) { - GeometryReader { geometry in - ZStack { - Rectangle().fill(.gray) - Image(systemName: "music.note").font(.largeTitle) - }.frame(width: geometry.size.width, height: geometry.size.width) - } - }.scaledToFit() - .frame(maxWidth: 250) - .clipShape(RoundedRectangle(cornerRadius: 4)) - .shadow(radius: 6, x: -3, y: 3) - - Text(album.title).font(.title) - Text(AlbumArtist.constructAlbumArtistText(viewModel.albumInfo?.albumArtists)).foregroundStyle(Color.accentColor) - - HStack { - Button(action: viewModel.playAlbum, label: { - Label("Play", systemImage: "play.fill") - }) + ScrollView { + VStack { + CachedImage(imageURL: album.image250) { + GeometryReader { geometry in + ZStack { + Rectangle().fill(.gray) + Image(systemName: "music.note").font(.largeTitle) + }.frame(width: geometry.size.width, height: geometry.size.width) + } + }.scaledToFit() + .frame(maxWidth: 250) + .clipShape(RoundedRectangle(cornerRadius: 4)) + .shadow(radius: 6, x: -3, y: 3) - Button(action: viewModel.shuffleAlbum, label: { - Label("Shuffle", systemImage: "shuffle") - }) - }.buttonStyle(.borderedProminent) - List { + Text(album.title).font(.title) + Text(AlbumArtist.constructAlbumArtistText(viewModel.albumInfo?.albumArtists)).foregroundStyle(Color.accentColor) + + HStack { + Button(action: viewModel.playAlbum, label: { + Label("Play", systemImage: "play.fill") + }) + + Button(action: viewModel.shuffleAlbum, label: { + Label("Shuffle", systemImage: "shuffle") + }) + }.buttonStyle(.borderedProminent) Section(content: { + Divider() ForEach(viewModel.albumInfo!.tracks, id: \.track.id) { trackInfo in - TrackRowView(track: trackInfo.track, trackArtists: trackInfo.trackArtists) + TrackRowView(track: trackInfo.track, trackArtists: trackInfo.trackArtists, showNumber: true).padding(.horizontal, 12) + Divider() } }, footer: { // Add padding here, so the view scrolls under the player - Text(viewModel.tracksStats).padding(.bottom, 75) + Text(viewModel.tracksStats).frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal, 12).font(.caption2).foregroundStyle(Color.gray) }) - }.listStyle(.plain) - }.padding(.top, 10) + }.padding(EdgeInsets(top: 10, leading: 0, bottom: 75, trailing: 0)) + } .navigationTitle(album.title) #if os(iOS) .navigationBarTitleDisplayMode(.inline) @@ -71,7 +74,6 @@ struct AlbumView: View { } }) } - } else { ProgressView() } diff --git a/accentor/View/Artists/ArtistView.swift b/accentor/View/Artists/ArtistView.swift index 1a5ef2d..459c726 100644 --- a/accentor/View/Artists/ArtistView.swift +++ b/accentor/View/Artists/ArtistView.swift @@ -10,18 +10,19 @@ import GRDBQuery struct ArtistView: View { @EnvironmentStateObject private var viewModel: ArtistViewModel + @Environment(\.defaultMinListRowHeight) var minRowHeight init(id: Artist.ID) { _viewModel = EnvironmentStateObject { ArtistViewModel(database: $0.appDatabase, id: id) } } - + var body: some View { if let artist = viewModel.artistInfo?.artist { ScrollView { VStack(alignment: .leading) { - HStack { + HStack(alignment: .bottom) { if (artist.image250 != nil) { CachedImage(imageURL: artist.image250) { ZStack { @@ -30,23 +31,33 @@ struct ArtistView: View { } }.frame(maxWidth: 250, maxHeight: 250) } - Text(artist.name) - } - Section("Albums") { + Text(artist.name).font(.headline) + }.padding(10) + Section(content: { ScrollView(.horizontal) { LazyHStack(spacing: 5) { ForEach(viewModel.artistInfo!.albums) { item in AlbumCard(id: item.id).frame(width: 200, height: 250) } - } + }.padding(.horizontal, 8) + } + }, header: { + Text("Albums").padding(.horizontal, 12).font(.subheadline) + }) + Section(content: { + ForEach(viewModel.artistInfo!.tracks, id: \.track.id) { trackInfo in + TrackRowView(track: trackInfo.track, trackArtists: trackInfo.trackArtists).padding(.horizontal, 12) + Divider() } - } - Text("Tracks") - ForEach(viewModel.artistInfo!.tracks) { track in - Text(track.title) - } - } + }, header: { + Text("Tracks").padding(.horizontal, 12).font(.subheadline) + Divider() + }) + }.padding(EdgeInsets(top: 10, leading: 0, bottom: 75, trailing: 0)) }.navigationTitle(artist.name) +#if os(iOS) + .navigationBarTitleDisplayMode(.inline) +#endif } else { ProgressView() } diff --git a/accentor/View/Artists/ArtistViewModel.swift b/accentor/View/Artists/ArtistViewModel.swift index b8c3a27..007a3d3 100644 --- a/accentor/View/Artists/ArtistViewModel.swift +++ b/accentor/View/Artists/ArtistViewModel.swift @@ -9,9 +9,14 @@ import Combine import GRDB final class ArtistViewModel: ObservableObject { + struct TrackInfo: Decodable { + var track: Track + var trackArtists: [TrackArtist] + } + struct ArtistInfo: Decodable, FetchableRecord { var artist: Artist - var tracks: [Track] + var tracks: [TrackInfo] var albums: [Album] } @@ -23,7 +28,10 @@ final class ArtistViewModel: ObservableObject { init(database: AppDatabase, id: Album.ID) { self.database = database self.observationCancellable = ValueObservation - .tracking(Artist.filter(key: id).including(all: Artist.tracks.order(Track.Columns.normalizedTitle)).including(all: Artist.albums.all().orderByRelease()).asRequest(of: ArtistInfo.self).fetchOne) + .tracking(Artist.filter(key: id) + .including(all: Artist.tracks.order(Track.Columns.normalizedTitle).distinct().including(all: Track.trackArtists)) + .including(all: Artist.albums.all().orderByRelease()) + .asRequest(of: ArtistInfo.self).fetchOne) .publisher(in: database.reader, scheduling: .immediate) .sink( receiveCompletion: { _ in /* ignore error */ }, diff --git a/accentor/View/Artists/ArtistsView.swift b/accentor/View/Artists/ArtistsView.swift index cb28f1c..e0ce74d 100644 --- a/accentor/View/Artists/ArtistsView.swift +++ b/accentor/View/Artists/ArtistsView.swift @@ -19,7 +19,7 @@ struct ArtistsView: View { } let columns = [ - GridItem(.adaptive(minimum: 130)) + GridItem(.adaptive(minimum: 162, maximum: 252)) ] var body: some View { @@ -30,7 +30,7 @@ struct ArtistsView: View { ArtistCard(artist: artist) }.buttonStyle(PlainButtonStyle()) } - } + }.padding(EdgeInsets(top: 20, leading: 30, bottom: 65, trailing: 30)) }.navigationTitle("Artists") .searchable(text: $viewModel.searchTerm) } diff --git a/accentor/View/TrackRow/TrackRowView.swift b/accentor/View/TrackRow/TrackRowView.swift index 59f333c..6f4e38b 100644 --- a/accentor/View/TrackRow/TrackRowView.swift +++ b/accentor/View/TrackRow/TrackRowView.swift @@ -10,8 +10,10 @@ import GRDBQuery struct TrackRowView: View { @EnvironmentStateObject private var viewModel: TrackRowViewModel + let showNumber: Bool - init(track: Track, trackArtists: [TrackArtist]) { + init(track: Track, trackArtists: [TrackArtist], showNumber: Bool = false) { + self.showNumber = showNumber _viewModel = EnvironmentStateObject { TrackRowViewModel(database: $0.appDatabase, player: $0.player, track: track, trackArtists: trackArtists) } @@ -19,7 +21,9 @@ struct TrackRowView: View { var body: some View { HStack { - Text(String(viewModel.track.number)).padding(.trailing, 20) + if (showNumber) { + Text(String(viewModel.track.number)).padding(.trailing, 20) + } VStack(alignment: .leading) { Text(viewModel.track.title).foregroundStyle(Color.black) Text(TrackArtist.constructTrackArtistText(viewModel.trackArtists)) @@ -50,6 +54,7 @@ struct TrackRowView: View { TrackActions(viewModel: viewModel) } label: { ZStack { + Circle().fill(Color(white: 0, opacity: 0)).frame(width: 30, height: 30) Label("Actions", systemImage: "ellipsis") .foregroundStyle(Color.accentColor) .labelStyle(.iconOnly)