From c3f7bd31e7e545e34eecaba102e285b7ef622ddf Mon Sep 17 00:00:00 2001 From: Lee Jun Kit Date: Sat, 19 Jun 2021 08:39:13 +0800 Subject: [PATCH] implement shortcut grid --- Spottie.xcodeproj/project.pbxproj | 14 +++- .../Types/API/RecommendationLink.swift | 4 +- Spottie/Views/Carousel/CarouselRow.swift | 78 ------------------- Spottie/Views/Components/CarouselRow.swift | 78 +++++++++++++++++++ .../CarouselRowItem.swift | 9 +-- Spottie/Views/Components/ShortcutGrid.swift | 43 ++++++++++ Spottie/Views/Components/ShortcutItem.swift | 74 ++++++++++++++++++ Spottie/Views/Home.swift | 25 ++++-- 8 files changed, 231 insertions(+), 94 deletions(-) delete mode 100644 Spottie/Views/Carousel/CarouselRow.swift create mode 100644 Spottie/Views/Components/CarouselRow.swift rename Spottie/Views/{Carousel => Components}/CarouselRowItem.swift (82%) create mode 100644 Spottie/Views/Components/ShortcutGrid.swift create mode 100644 Spottie/Views/Components/ShortcutItem.swift diff --git a/Spottie.xcodeproj/project.pbxproj b/Spottie.xcodeproj/project.pbxproj index bd9de96..4e5ae34 100644 --- a/Spottie.xcodeproj/project.pbxproj +++ b/Spottie.xcodeproj/project.pbxproj @@ -68,6 +68,8 @@ 475798D1266F103100AADF2F /* WebAPIPlaylistTrackObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475798D0266F103100AADF2F /* WebAPIPlaylistTrackObject.swift */; }; 475798D3266F12EB00AADF2F /* PersonalizedRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475798D2266F12EB00AADF2F /* PersonalizedRecommendationsResponse.swift */; }; 475798D5266F1B9B00AADF2F /* WebAPIPlaylistTracksRefObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475798D4266F1B9B00AADF2F /* WebAPIPlaylistTracksRefObject.swift */; }; + 475EE242267C9306007BEBDC /* ShortcutGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475EE241267C9306007BEBDC /* ShortcutGrid.swift */; }; + 475EE244267D6659007BEBDC /* ShortcutItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475EE243267D6659007BEBDC /* ShortcutItem.swift */; }; 47C56F602679A789003EA20A /* PlayerCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C56F5F2679A789003EA20A /* PlayerCommands.swift */; }; /* End PBXBuildFile section */ @@ -135,6 +137,8 @@ 475798D0266F103100AADF2F /* WebAPIPlaylistTrackObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIPlaylistTrackObject.swift; sourceTree = ""; }; 475798D2266F12EB00AADF2F /* PersonalizedRecommendationsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizedRecommendationsResponse.swift; sourceTree = ""; }; 475798D4266F1B9B00AADF2F /* WebAPIPlaylistTracksRefObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAPIPlaylistTracksRefObject.swift; sourceTree = ""; }; + 475EE241267C9306007BEBDC /* ShortcutGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutGrid.swift; sourceTree = ""; }; + 475EE243267D6659007BEBDC /* ShortcutItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutItem.swift; sourceTree = ""; }; 47C56F5F2679A789003EA20A /* PlayerCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerCommands.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -193,7 +197,7 @@ 4730615D26565706001E3A1F /* Views */ = { isa = PBXGroup; children = ( - 474BAFBD266B79900006EB16 /* Carousel */, + 475EE240267C92EC007BEBDC /* Components */, 4730616626565AF8001E3A1F /* BottomBar */, 4730614F265656ED001E3A1F /* ContentView.swift */, 4730615E2656573B001E3A1F /* Sidebar.swift */, @@ -307,13 +311,15 @@ path = API; sourceTree = ""; }; - 474BAFBD266B79900006EB16 /* Carousel */ = { + 475EE240267C92EC007BEBDC /* Components */ = { isa = PBXGroup; children = ( 474BAFC0266B84CD0006EB16 /* CarouselRow.swift */, 474BAFC2266B85440006EB16 /* CarouselRowItem.swift */, + 475EE241267C9306007BEBDC /* ShortcutGrid.swift */, + 475EE243267D6659007BEBDC /* ShortcutItem.swift */, ); - path = Carousel; + path = Components; sourceTree = ""; }; 47C56F5E2679A779003EA20A /* Commands */ = { @@ -401,6 +407,7 @@ files = ( 475798D5266F1B9B00AADF2F /* WebAPIPlaylistTracksRefObject.swift in Sources */, 47306150265656ED001E3A1F /* ContentView.swift in Sources */, + 475EE244267D6659007BEBDC /* ShortcutItem.swift in Sources */, 473061792658A02E001E3A1F /* SpotifyEvent.swift in Sources */, 470201BD265B80970030ECA9 /* FakePlayerViewModel.swift in Sources */, 4730618326590931001E3A1F /* TrackObject.swift in Sources */, @@ -410,6 +417,7 @@ 4730615F2656573B001E3A1F /* Sidebar.swift in Sources */, 470201CC265CF9610030ECA9 /* RepeatButton.swift in Sources */, 4730618B26591E36001E3A1F /* CoverGroupObject.swift in Sources */, + 475EE242267C9306007BEBDC /* ShortcutGrid.swift in Sources */, 4730618926591E16001E3A1F /* DateObject.swift in Sources */, 4730616826565B03001E3A1F /* PlayPauseButton.swift in Sources */, 4730616326565828001E3A1F /* Search.swift in Sources */, diff --git a/Spottie/Backend/Types/API/RecommendationLink.swift b/Spottie/Backend/Types/API/RecommendationLink.swift index 7d28ba9..769f67f 100644 --- a/Spottie/Backend/Types/API/RecommendationLink.swift +++ b/Spottie/Backend/Types/API/RecommendationLink.swift @@ -8,5 +8,7 @@ import Foundation struct RecommendationLink: Decodable { - + var uri: String + var name: String + var images: [WebAPIImageObject] } diff --git a/Spottie/Views/Carousel/CarouselRow.swift b/Spottie/Views/Carousel/CarouselRow.swift deleted file mode 100644 index 2281cc0..0000000 --- a/Spottie/Views/Carousel/CarouselRow.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// CarouselRow.swift -// Spottie -// -// Created by Lee Jun Kit on 5/6/21. -// - -import SwiftUI - -struct CarouselRow: View { - var viewModel: ViewModel - var body: some View { - VStack(alignment: .leading) { - Text(viewModel.title) - .font(.title) - .padding(.leading) - viewModel.subtitle.map({ - Text($0) - .font(.body) - .foregroundColor(.secondary) - .padding(.leading) - .padding(.bottom, 8) - }) - HStack(alignment: .top, spacing: 40) { - ForEach(viewModel.items) { item in - let vm = CarouselRowItem.ViewModel.init(item, artworkIsCircular: viewModel.renderMode == .circular) - CarouselRowItem(viewModel: vm) - .onTapGesture { - viewModel.onItemPressed(item.id) - } - } - } - .padding([.leading, .trailing]) - } - } -} - -extension CarouselRow { - enum RenderMode { - case circular - case normal - } - - class ViewModel: ObservableObject { - @Published var renderMode: RenderMode - @Published var title: String - @Published var subtitle: String? - @Published var items: [RecommendationItem] - let onItemPressed: (String) -> Void - - init(_ recommendationGroup: RecommendationGroup, numberOfItemsToShow: Int, onItemPressed: @escaping (String) -> Void) { - self.onItemPressed = onItemPressed - - title = recommendationGroup.name - subtitle = recommendationGroup.tagline - - if (recommendationGroup.id == "home-personalized[favorite-artists]") { - renderMode = .circular - } else { - renderMode = .normal - } - - let numItems = recommendationGroup.items.count - if (numItems < numberOfItemsToShow) { - items = recommendationGroup.items - } else { - items = Array(recommendationGroup.items[0...numberOfItemsToShow - 1]) - } - } - } -} - -//struct CarouselRow_Previews: PreviewProvider { -//// static let group = RecommendationGroup() -//// static var previews: some View { -//// CarouselRow(viewModel: <#T##CarouselRow.ViewModel#>.init()) -//// } -//} diff --git a/Spottie/Views/Components/CarouselRow.swift b/Spottie/Views/Components/CarouselRow.swift new file mode 100644 index 0000000..f43af77 --- /dev/null +++ b/Spottie/Views/Components/CarouselRow.swift @@ -0,0 +1,78 @@ +// +// CarouselRow.swift +// Spottie +// +// Created by Lee Jun Kit on 5/6/21. +// + +import SwiftUI + +struct CarouselRow: View { + let viewModel: ViewModel + let onItemPressed: (String) -> Void + + var body: some View { + if viewModel.groupID.hasPrefix("podcast") { + EmptyView() + } else { + VStack(alignment: .leading) { + Text(viewModel.title) + .font(.title).bold() + .padding(.leading) + viewModel.subtitle.map({ + Text($0) + .font(.body) + .foregroundColor(.secondary) + .padding(.leading) + .padding(.bottom, 8) + }) + HStack(alignment: .top, spacing: 40) { + ForEach(viewModel.items) { item in + let vm = CarouselRowItem.ViewModel.init(item) + CarouselRowItem(viewModel: vm) + .onTapGesture { + onItemPressed(item.id) + } + } + } + .padding([.leading, .trailing]) + } + } + + } +} + +extension CarouselRow { + enum RenderMode { + case circular + case normal + } + + class ViewModel: ObservableObject { + var groupID: String + var title: String + var subtitle: String? + var items: [RecommendationItem] + + init(_ recommendationGroup: RecommendationGroup, numberOfItemsToShow: Int) { + self.groupID = recommendationGroup.id + + title = recommendationGroup.name + subtitle = recommendationGroup.tagline + + let numItems = recommendationGroup.items.count + if (numItems < numberOfItemsToShow) { + items = recommendationGroup.items + } else { + items = Array(recommendationGroup.items[0...numberOfItemsToShow - 1]) + } + } + } +} + +//struct CarouselRow_Previews: PreviewProvider { +//// static let group = RecommendationGroup() +//// static var previews: some View { +//// CarouselRow(viewModel: <#T##CarouselRow.ViewModel#>.init()) +//// } +//} diff --git a/Spottie/Views/Carousel/CarouselRowItem.swift b/Spottie/Views/Components/CarouselRowItem.swift similarity index 82% rename from Spottie/Views/Carousel/CarouselRowItem.swift rename to Spottie/Views/Components/CarouselRowItem.swift index 0ccbec1..c6cb5af 100644 --- a/Spottie/Views/Carousel/CarouselRowItem.swift +++ b/Spottie/Views/Components/CarouselRowItem.swift @@ -55,9 +55,7 @@ extension CarouselRowItem { @Published var isHovering = false @Published var artworkIsCircle = false - init(_ item: RecommendationItem, artworkIsCircular: Bool) { - artworkIsCircle = artworkIsCircular - + init(_ item: RecommendationItem) { if let data = item.data { switch data { case let .album(album): @@ -68,6 +66,7 @@ extension CarouselRowItem { self.title = artist.name self.subtitle = "Artist" self.artworkURL = URL(string: artist.images[0].url)! + self.artworkIsCircle = true case let.playlist(playlist): self.title = playlist.name self.subtitle = playlist.description ?? "" @@ -75,12 +74,12 @@ extension CarouselRowItem { default: self.title = "" self.subtitle = "" - self.artworkURL = URL(string: "https://mosaic.scdn.co/640/ab67616d00001e020b7c6d46885f7434c99e6d8bab67616d00001e0244bb39545f5d7176080d17aeab67616d00001e027679b9d0724cc7421189ac44ab67616d00001e028863bc11d2aa12b54f5aeb36")! + self.artworkURL = URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")! } } else { self.title = "" self.subtitle = "" - self.artworkURL = URL(string: "https://mosaic.scdn.co/640/ab67616d00001e020b7c6d46885f7434c99e6d8bab67616d00001e0244bb39545f5d7176080d17aeab67616d00001e027679b9d0724cc7421189ac44ab67616d00001e028863bc11d2aa12b54f5aeb36")! + self.artworkURL = URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")! } } } diff --git a/Spottie/Views/Components/ShortcutGrid.swift b/Spottie/Views/Components/ShortcutGrid.swift new file mode 100644 index 0000000..29b0223 --- /dev/null +++ b/Spottie/Views/Components/ShortcutGrid.swift @@ -0,0 +1,43 @@ +// +// ShortcutsGrid.swift +// Spottie +// +// Created by Lee Jun Kit on 18/6/21. +// + +import SwiftUI +import SDWebImageSwiftUI + +struct ShortcutGrid: View { + let items: [RecommendationItem] + let onItemPressed: (String) -> Void + var gridItemLayout = Array(repeating: GridItem(.flexible(), spacing: 20), count: 3) + + var body: some View { + VStack(alignment: .leading) { + Text("Good Morning") + .font(.largeTitle).bold() + .padding(.leading) + LazyVGrid(columns: gridItemLayout, spacing: 20) { + ForEach(items) { item in + ShortcutItem( + itemHeight: 80, + viewModel: ShortcutItem.ViewModel(item) + ) + .onTapGesture { + onItemPressed(item.id) + } + .frame(height: 80) + } + } + .padding([.leading, .trailing]) + } + + } +} + +//struct ShortcutsGrid_Previews: PreviewProvider { +// static var previews: some View { +// ShortcutGrid() +// } +//} diff --git a/Spottie/Views/Components/ShortcutItem.swift b/Spottie/Views/Components/ShortcutItem.swift new file mode 100644 index 0000000..9edc163 --- /dev/null +++ b/Spottie/Views/Components/ShortcutItem.swift @@ -0,0 +1,74 @@ +// +// ShortcutItem.swift +// Spottie +// +// Created by Lee Jun Kit on 19/6/21. +// + +import SwiftUI +import SDWebImageSwiftUI + +struct ShortcutItem: View { + var itemHeight: CGFloat + var viewModel: ViewModel + + @State private var isHovering = false + + var body: some View { + HStack { + WebImage(url: viewModel.artworkURL) + .resizable() + .frame(width: itemHeight, height: itemHeight) + .aspectRatio(1.0, contentMode: .fill) + Text(viewModel.title).bold() + .padding(.leading) + Spacer() + } + .background( + Color(NSColor.alternatingContentBackgroundColors[1]) + .opacity(isHovering ? 1.0 : 0.3) + ) + .onHover { hovering in + isHovering = hovering + } + } +} + +extension ShortcutItem { + class ViewModel { + var title: String + var artworkURL: URL + + init(_ item: RecommendationItem) { + if let data = item.data { + switch data { + case let .album(album): + self.title = album.name + self.artworkURL = album.getArtworkURL()! + case let .artist(artist): + self.title = artist.name + self.artworkURL = URL(string: artist.images[0].url)! + case let.playlist(playlist): + self.title = playlist.name + self.artworkURL = URL(string: playlist.images[0].url)! + case let .link(link): + self.title = link.name + self.artworkURL = URL(string: link.images[0].url)! + } + } else { + self.title = "Unknown" + self.artworkURL = URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")! + } + } + } +} + +//struct ShortcutItem_Previews: PreviewProvider { +// static var previews: some View { +// ShortcutItem( +// itemHeight: 80, +// title: "Liked Songs", +// imageURL: URL(string: "https://misc.scdn.co/liked-songs/liked-songs-640.png")! +// ) +// } +//} diff --git a/Spottie/Views/Home.swift b/Spottie/Views/Home.swift index 54024a6..78583a3 100644 --- a/Spottie/Views/Home.swift +++ b/Spottie/Views/Home.swift @@ -15,12 +15,23 @@ struct Home: View { ScrollView(.vertical) { LazyVStack(alignment: .leading) { ForEach(viewModel.recommendationGroups) { group in - CarouselRow(viewModel: CarouselRow.ViewModel.init( - group, - numberOfItemsToShow: numberOfItemsToShowInRow(reader), - onItemPressed: viewModel.load - )) - .padding() + if group.id == "shortcuts" { + ShortcutGrid( + items: group.items, + onItemPressed: viewModel.load + ) + .padding() + } else if group.items.count > 0 { + let vm = CarouselRow.ViewModel.init(group, + numberOfItemsToShow: numberOfItemsToShowInRow(reader) + ) + + CarouselRow( + viewModel: vm, + onItemPressed: viewModel.load + ) + .padding() + } } } } @@ -42,7 +53,7 @@ extension Home { private var cancellables = [AnyCancellable]() init() { SpotifyAPI.getPersonalizedRecommendations().sink { _ in } receiveValue: { response in - self.recommendationGroups = Array(response!.content.items.dropFirst(2)) + self.recommendationGroups = response!.content.items }.store(in: &cancellables) }