diff --git a/firstfm.xcodeproj/project.pbxproj b/firstfm.xcodeproj/project.pbxproj index d15a729..e994bb6 100644 --- a/firstfm.xcodeproj/project.pbxproj +++ b/firstfm.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 82428B2826AECFC700AAC835 /* TopAlbum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82428B2726AECFC700AAC835 /* TopAlbum.swift */; }; 82428B2A26AED01F00AAC835 /* UserTopAlbumsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82428B2926AED01F00AAC835 /* UserTopAlbumsResponse.swift */; }; 82428B2C26AF216100AAC835 /* FriendProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82428B2B26AF216100AAC835 /* FriendProfileView.swift */; }; + 82428B3026AF440E00AAC835 /* UserFriendsHView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82428B2F26AF440E00AAC835 /* UserFriendsHView.swift */; }; 82505CDF2675074E00CCCB58 /* ArtistRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82505CDC2675074E00CCCB58 /* ArtistRow.swift */; }; 82505CE02675074E00CCCB58 /* ChartListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82505CDD2675074E00CCCB58 /* ChartListView.swift */; }; 82505CE12675074E00CCCB58 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82505CDE2675074E00CCCB58 /* ContentView.swift */; }; @@ -135,6 +136,7 @@ 82428B2726AECFC700AAC835 /* TopAlbum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopAlbum.swift; sourceTree = ""; }; 82428B2926AED01F00AAC835 /* UserTopAlbumsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTopAlbumsResponse.swift; sourceTree = ""; }; 82428B2B26AF216100AAC835 /* FriendProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendProfileView.swift; sourceTree = ""; }; + 82428B2F26AF440E00AAC835 /* UserFriendsHView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFriendsHView.swift; sourceTree = ""; }; 82505CDC2675074E00CCCB58 /* ArtistRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArtistRow.swift; sourceTree = ""; }; 82505CDD2675074E00CCCB58 /* ChartListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartListView.swift; sourceTree = ""; }; 82505CDE2675074E00CCCB58 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -344,6 +346,7 @@ 82428B2326AEC53C00AAC835 /* TopUserTracksView.swift */, 82428B2526AECDBC00AAC835 /* TopUserAlbumsView.swift */, 82428B2B26AF216100AAC835 /* FriendProfileView.swift */, + 82428B2F26AF440E00AAC835 /* UserFriendsHView.swift */, ); path = Profile; sourceTree = ""; @@ -621,6 +624,7 @@ 82505CE72675300400CCCB58 /* SpotifyArtistSearchResponse.swift in Sources */, 82BBD2482698B37F009B42FC /* TrendsViewModel.swift in Sources */, 82F0D54D2697AAC7007CEA98 /* SearchViewModel.swift in Sources */, + 82428B3026AF440E00AAC835 /* UserFriendsHView.swift in Sources */, 82D5B4D22696DBD700716931 /* ScrobblesViewModel.swift in Sources */, 82A006BE2679636F0009BD71 /* SpotifyTrackSearchResponse.swift in Sources */, 825F0FC126A6F72F007BA84B /* ArtistInfoView.swift in Sources */, diff --git a/firstfm/Models/LastFM/Friend.swift b/firstfm/Models/LastFM/Friend.swift index 06930c7..c179950 100644 --- a/firstfm/Models/LastFM/Friend.swift +++ b/firstfm/Models/LastFM/Friend.swift @@ -5,7 +5,7 @@ struct Friend: Codable, Identifiable { var id: String { name } let playlists, playcount, subscriber, name: String let country: String - let image: [LastFMImage ] + var image: [LastFMImage] let registered: Registered let url: String let realname, bootstrap: String diff --git a/firstfm/Service/LastFMAPI.swift b/firstfm/Service/LastFMAPI.swift index 5a63007..627693d 100644 --- a/firstfm/Service/LastFMAPI.swift +++ b/firstfm/Service/LastFMAPI.swift @@ -54,6 +54,11 @@ class LastFMAPI { if let statusCode = nsHTTPResponse?.statusCode { print("LastFMAPI status code = \(statusCode)") if statusCode != 200 { + if lastFMMethod == "user.getFriends" && statusCode == 400 { + // The API returns a 400 when the user has no friends 🤨 + callback(FriendsResponse(friends: Friends(user: [])) as! T, nil) + return + } let error = NSError(domain: "", code: statusCode, userInfo: [ NSLocalizedDescriptionKey: "Invalid API response 😢. Please try again"]) callback(callbackData, error as Error) return diff --git a/firstfm/ViewModel/ProfileViewModel.swift b/firstfm/ViewModel/ProfileViewModel.swift index d34b462..ddb7566 100644 --- a/firstfm/ViewModel/ProfileViewModel.swift +++ b/firstfm/ViewModel/ProfileViewModel.swift @@ -25,43 +25,51 @@ class ProfileViewModel: ObservableObject { self.getTopArtists(username: username, period: "overall") self.getTopTracks(username: username, period: "overall") self.getTopAlbums(username: username, period: "overall") + self.getFriends(username: username) } func getProfile(username: String) { - // self.isLoading = true - LastFMAPI.request(lastFMMethod: "user.getInfo", args: ["user": username]) { (data: UserInfoResponse?, error) -> Void in if error != nil { DispatchQueue.main.async { FloatingNotificationBanner(title: "Failed to load profile", subtitle: error?.localizedDescription, style: .danger).show() } } - // self.isLoading = false if let data = data { + var user = data.user + if user.image[3].url == "" { + user.image[3].url = "https://bonds-and-shares.com/wp-content/uploads/2019/07/placeholder-user.png" + } DispatchQueue.main.async { - self.user = data.user + self.user = user } } } } func getFriends(username: String) { - // self.isFriendsLoading = true - LastFMAPI.request(lastFMMethod: "user.getFriends", args: ["user": username]) { (data: FriendsResponse?, error) -> Void in if error != nil { DispatchQueue.main.async { FloatingNotificationBanner(title: "Failed to load friends", subtitle: error?.localizedDescription, style: .danger).show() } } - // self.isFriendsLoading = false if let data = data { + var friends = data.friends.user + + for index in friends.indices { + if friends[index].image[3].url == "" { + friends[index].image[3].url = "https://bonds-and-shares.com/wp-content/uploads/2019/07/placeholder-user.png" + } + } + DispatchQueue.main.async { - self.friends = data.friends.user + self.friends = friends } } + self.isFriendsLoading = false } } diff --git a/firstfm/Views/Profile/FriendProfileView.swift b/firstfm/Views/Profile/FriendProfileView.swift index 5d79fd2..0df0202 100644 --- a/firstfm/Views/Profile/FriendProfileView.swift +++ b/firstfm/Views/Profile/FriendProfileView.swift @@ -24,21 +24,19 @@ struct FriendProfileView: View { .resizable() .loadImmediately() .aspectRatio(contentMode: .fill) - .overlay(TintOverlayView().opacity(0.2)) + .overlay(TintOverlayView().opacity(0.3)) .frame(width: geometry.size.width, height: geometry.size.height) .offset(y: geometry.frame(in: .global).minY/9) .clipped() - .blur(radius: 3) } else { KFImage.url(URL(string: !self.profile.topArtists.isEmpty ? self.profile.topArtists[0].image[0].url : "https://www.nme.com/wp-content/uploads/2021/04/twice-betterconceptphoto-2020.jpg" )!) .resizable() .loadImmediately() .aspectRatio(contentMode: .fill) - .overlay(TintOverlayView().opacity(0.2)) + .overlay(TintOverlayView().opacity(0.3)) .frame(width: geometry.size.width, height: geometry.size.height + geometry.frame(in: .global).minY) .clipped() .offset(y: -geometry.frame(in: .global).minY) - .blur(radius: 3) } } .redacted(reason: self.profile.topArtists.isEmpty ? .placeholder : []) } @@ -90,7 +88,7 @@ struct FriendProfileView: View { .environmentObject(profile) .frame( width: g.size.width - 5, - height: g.size.height * 0.7, + height: g.size.height * 0.75, alignment: .center ) .offset(y: -100) @@ -98,12 +96,19 @@ struct FriendProfileView: View { TopUserAlbumsView(albums: profile.topAlbums) .environmentObject(profile) .offset(y: -120) - } + + if !profile.isFriendsLoading && !profile.friends.isEmpty { + UserFriendsHView() + .environmentObject(profile) + .offset(y: -120) + } + + }.padding(.bottom, -100) } } .onLoad { self.profile.getAll(username: friend.name) - }.navigationTitle("\(friend.name)'s profile") + }.navigationBarTitle("\(friend.name)'s profile", displayMode: .inline) } } diff --git a/firstfm/Views/Profile/ProfileView.swift b/firstfm/Views/Profile/ProfileView.swift index 770cadc..16488ce 100644 --- a/firstfm/Views/Profile/ProfileView.swift +++ b/firstfm/Views/Profile/ProfileView.swift @@ -29,7 +29,6 @@ struct ProfileView: View { .frame(width: geometry.size.width, height: geometry.size.height) .offset(y: geometry.frame(in: .global).minY/9) .clipped() - .blur(radius: 3) } else { KFImage.url(URL(string: !self.profile.topArtists.isEmpty ? self.profile.topArtists[0].image[0].url : "https://www.nme.com/wp-content/uploads/2021/04/twice-betterconceptphoto-2020.jpg" )!) .resizable() @@ -39,7 +38,6 @@ struct ProfileView: View { .frame(width: geometry.size.width, height: geometry.size.height + geometry.frame(in: .global).minY) .clipped() .offset(y: -geometry.frame(in: .global).minY) - .blur(radius: 3) } } .redacted(reason: self.profile.topArtists.isEmpty ? .placeholder : []) } @@ -99,7 +97,14 @@ struct ProfileView: View { TopUserAlbumsView(albums: profile.topAlbums) .environmentObject(profile) .offset(y: -120) + + if !profile.isFriendsLoading && !profile.friends.isEmpty { + UserFriendsHView() + .environmentObject(profile) + .offset(y: -120) + } } + .padding(.bottom, -100) .edgesIgnoringSafeArea(.top) }.edgesIgnoringSafeArea(.top) .navigationBarTitle("") diff --git a/firstfm/Views/Profile/UserFriendsHView.swift b/firstfm/Views/Profile/UserFriendsHView.swift new file mode 100644 index 0000000..42d14c5 --- /dev/null +++ b/firstfm/Views/Profile/UserFriendsHView.swift @@ -0,0 +1,69 @@ +import SwiftUI +import Kingfisher + +struct UserFriendsHView: View { + @EnvironmentObject var vm: ProfileViewModel + + var body: some View { + Section { + HStack { + Text("Friends").font(.headline).unredacted() + Spacer() + } + ScrollView(.horizontal, showsIndicators: false) { + HStack { + if !vm.friends.isEmpty { + ForEach(vm.friends, id: \.name) {friend in + ZStack { + Button("") {} + NavigationLink( + destination: FriendProfileView(friend: friend), + label: { + VStack { + if friend.image[3].url == "" { + KFImage.url(URL(string: "https://bonds-and-shares.com/wp-content/uploads/2019/07/placeholder-user.png" )!) + .resizable() + .loadImmediately() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 120) + .clipShape(Circle()) + .cornerRadius(.infinity) + } else { + KFImage.url(URL(string: friend.image[3].url )!) + .resizable() + .loadImmediately() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 120) + .clipShape(Circle()) + .cornerRadius(.infinity) + } + + Text(friend.name).font(.subheadline) + .foregroundColor(.gray).lineLimit(1) + }.padding(.horizontal, 10) + }) + } + + } + } else { + // Placeholder for redacted + ForEach((1...5), id: \.self) {_ in + VStack { + KFImage.url(URL(string: "https://lastfm.freetls.fastly.net/i/u/64s/4128a6eb29f94943c9d206c08e625904.webp")!) + .resizable() + .loadImmediately() + .aspectRatio(contentMode: .fill) + .frame(width: 120, height: 120) + .clipShape(Circle()) + .cornerRadius(.infinity) + + Text("Friend").font(.subheadline) + .foregroundColor(.gray).lineLimit(1) + } + } + } + } + } + }.padding() + } +}