From a535a11de4ca137cd83fedbd42ea5a09ffd0117e Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Tue, 8 Jun 2021 23:21:08 -0700 Subject: [PATCH] add seasonitemview --- JellyfinPlayer/ItemView.swift | 37 +++--- JellyfinPlayer/MovieItemView.swift | 2 +- JellyfinPlayer/SeasonItemView.swift | 164 +++++++++++--------------- JellyfinPlayer/SeriesItemView.swift | 2 +- Shared/Extensions/APIExtensions.swift | 19 +++ 5 files changed, 107 insertions(+), 117 deletions(-) diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 612e33b0..704f23db 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -36,20 +36,16 @@ struct ItemView: View { return } - if(item.type == "Movie" || item.type == "Episode") { - isLoading = true; - UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!) - .sink(receiveCompletion: { completion in - HandleAPIRequestCompletion(globalData: globalData, completion: completion) - }, receiveValue: { response in - isLoading = false - viewDidLoad = true - fullItem = response - }) - .store(in: &globalData.pendingAPIRequests) - } else { - viewDidLoad = true - } + isLoading = true; + UserLibraryAPI.getItem(userId: globalData.user.user_id!, itemId: item.id!) + .sink(receiveCompletion: { completion in + HandleAPIRequestCompletion(globalData: globalData, completion: completion) + }, receiveValue: { response in + isLoading = false + viewDidLoad = true + fullItem = response + }) + .store(in: &globalData.pendingAPIRequests) } var body: some View { @@ -70,14 +66,13 @@ struct ItemView: View { ProgressView() } else { VStack { - if(item.type == "Movie") { + if(fullItem.type == "Movie") { MovieItemView(item: fullItem) - } else if(item.type == "Season") { - EmptyView() - SeasonItemView(item: item) - } else if(item.type == "Series") { - SeriesItemView(item: item) - } else if(item.type == "Episode") { + } else if(fullItem.type == "Season") { + SeasonItemView(item: fullItem) + } else if(fullItem.type == "Series") { + SeriesItemView(item: fullItem) + } else if(fullItem.type == "Episode") { EmptyView() //EpisodeItemView(item: fullItem) } else { diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index d42eb9b0..65472da7 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -265,7 +265,7 @@ struct MovieItemView: View { .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .edgesIgnoringSafeArea(.all) - .blur(radius: 2) + .blur(radius: 4) HStack { VStack { LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120)) diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 01be3be3..39e4b81e 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -13,18 +13,24 @@ struct SeasonItemView: View { @EnvironmentObject var globalData: GlobalData @EnvironmentObject var orientationInfo: OrientationInfo - var item: BaseItemDto + var item: BaseItemDto = BaseItemDto() @State private var episodes: [BaseItemDto] = [] @State private var isLoading: Bool = true @State private var viewDidLoad: Bool = false + init(item: BaseItemDto) { + self.item = item + } + func onAppear() { if(viewDidLoad) { return } - TvShowsAPI.getEpisodes(seriesId: item.seriesId!, fields: [.primaryImageAspectRatio], seasonId: item.id!) + dump(item) + + TvShowsAPI.getEpisodes(seriesId: item.seriesId!, userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio, .seasonUserData, .itemCounts, .overview], seasonId: item.id!) .sink(receiveCompletion: { completion in HandleAPIRequestCompletion(globalData: globalData, completion: completion) isLoading = false @@ -40,9 +46,9 @@ struct SeasonItemView: View { if isLoading { EmptyView() } else { - LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 1500)) + LazyImage(source: item.getSeriesBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 1500)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), + Image(uiImage: UIImage(blurHash: item.getSeriesBackdropImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() } @@ -71,7 +77,7 @@ struct SeasonItemView: View { .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .offset(y: -4) - if item.productionYear != 0 { + if item.productionYear != nil { Text(String(item.productionYear!)).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) @@ -98,12 +104,12 @@ struct SeasonItemView: View { Text(item.overview ?? "").font(.footnote).padding(.top, 3) .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) .padding(.trailing, 16) - ForEach(episodes, id: \.Id) { episode in + ForEach(episodes, id: \.id) { episode in NavigationLink(destination: ItemView(item: episode)) { HStack { LazyImage(source: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: episode.getPrimaryImageBlurHash())) + Image(uiImage: UIImage(blurHash: episode.getPrimaryImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 150, height: 90) @@ -117,67 +123,60 @@ struct SeasonItemView: View { Rectangle() .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) .mask(ProgressBar()) - .frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7) + .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) .padding(0), alignment: .bottomLeading ) VStack(alignment: .leading) { HStack { - Text("S\(String(episode.ParentIndexNumber ?? 0)):E\(String(episode.IndexNumber ?? 0))").font(.subheadline) + Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) Spacer() - Text(episode.Name).font(.subheadline) + Text(episode.name ?? "").font(.subheadline) .fontWeight(.semibold) .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .lineLimit(1) Spacer() - Text(episode.Runtime).font(.subheadline) + Text(episode.getItemRuntime()).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) } Spacer() - Text(episode.Overview).font(.footnote).foregroundColor(.secondary) + Text(episode.overview ?? "").font(.footnote).foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true).lineLimit(4) Spacer() }.padding(.trailing, 20).offset(y: 2) }.offset(x: 12, y: 0) } } - if !fullItem.Directors.isEmpty { - HStack { - Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) + if !(item.studios ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Text("Studios:").font(.callout).fontWeight(.semibold) + ForEach(item.studios!, id: \.id) { studio in + NavigationLink(destination: LazyView { + LibraryView(withStudio: studio) + }) { + Text(studio.name ?? "").font(.footnote) + } + } + }.padding(.leading, 16).padding(.trailing, 16) + } } - if !fullItem.Writers.isEmpty { - HStack { - Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Studios.isEmpty { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - Spacer().frame(height: 6) - }.padding(.leading, 2) + Spacer().frame(height: 10) + } + .padding(.leading, 2) + .padding(.top, 20) } } else { GeometryReader { geometry in ZStack { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")")) + LazyImage(source: item.getSeriesBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: Int(geometry.size.width))) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: item - .SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item - .SeasonImageBlurHash ?? "", + Image(uiImage: UIImage(blurHash: item.getSeriesBackdropImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets @@ -191,15 +190,13 @@ struct SeasonItemView: View { .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) .edgesIgnoringSafeArea(.all) - .blur(radius: 2) + .blur(radius: 4) HStack { VStack(alignment: .leading) { Spacer().frame(height: 16) - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")) + LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - fullItem.PosterBlurHash, + Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 120, height: 180) @@ -209,8 +206,8 @@ struct SeasonItemView: View { .frame(width: 120, height: 180) .cornerRadius(10) Spacer().frame(height: 4) - if fullItem.ProductionYear != 0 { - Text(String(fullItem.ProductionYear)).font(.subheadline) + if item.productionYear != nil { + Text(String(item.productionYear!)).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) } @@ -219,25 +216,20 @@ struct SeasonItemView: View { ScrollView { Spacer().frame(height: 16) LazyVStack(alignment: .leading) { - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 3) + if !(item.taglines ?? []).isEmpty { + Text(item.taglines!.first!).font(.body).italic().padding(.top, 7) .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) .padding(.trailing, 16) } - if fullItem.Overview != "" { - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - } - ForEach(episodes, id: \.Id) { episode in - NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) { + Text(item.overview ?? "").font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + ForEach(episodes, id: \.id) { episode in + NavigationLink(destination: ItemView(item: episode)) { HStack { - LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)")) + LazyImage(source: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150)) .placeholderAndFailure { - Image(uiImage: UIImage(blurHash: episode - .PosterBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .PosterBlurHash, + Image(uiImage: UIImage(blurHash: episode.getPrimaryImageBlurHash(), size: CGSize(width: 32, height: 32))!) .resizable() .frame(width: 150, height: 90) @@ -251,64 +243,48 @@ struct SeasonItemView: View { Rectangle() .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) .mask(ProgressBar()) - .frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7) + .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) .padding(0), alignment: .bottomLeading ) VStack(alignment: .leading) { HStack { - Text("S\(String(episode.ParentIndexNumber ?? 0)):E\(String(episode.IndexNumber ?? 0))").font(.subheadline) + Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - if episode.OfficialRating != "" { - Text(episode.OfficialRating).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - } Spacer() - Text(episode.Name).font(.subheadline) + Text(episode.name ?? "").font(.subheadline) .fontWeight(.semibold) .foregroundColor(.primary) .fixedSize(horizontal: false, vertical: true) .lineLimit(1) Spacer() - Text(episode.Runtime).font(.subheadline) + Text(episode.getItemRuntime()).font(.subheadline) .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) } Spacer() - Text(episode.Overview).font(.footnote).foregroundColor(.secondary) + Text(episode.overview ?? "").font(.footnote).foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true).lineLimit(4) Spacer() }.padding(.trailing, 20).offset(y: 2) }.offset(x: 12, y: 0) } } - if !fullItem.Directors.isEmpty { - HStack { - Text("Directors:").font(.callout).fontWeight(.semibold) - Text(fullItem.Directors.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Writers.isEmpty { - HStack { - Text("Writers:").font(.callout).fontWeight(.semibold) - Text(fullItem.Writers.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) - } - if !fullItem.Studios.isEmpty { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - Text(fullItem.Studios.joined(separator: ", ")).font(.footnote).lineLimit(1) - .foregroundColor(Color.secondary) - }.padding(.leading, 16).padding(.trailing, 16) + if !(item.studios ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Text("Studios:").font(.callout).fontWeight(.semibold) + ForEach(item.studios!, id: \.id) { studio in + NavigationLink(destination: LazyView { + LibraryView(withStudio: studio) + }) { + Text(studio.name ?? "").font(.footnote) + } + } + }.padding(.leading, 16).padding(.trailing, 16) + } } Spacer().frame(height: 95) }.frame(maxHeight: .infinity) @@ -325,6 +301,6 @@ struct SeasonItemView: View { } .onAppear(perform: onAppear) .navigationBarTitleDisplayMode(.inline) - .navigationTitle("\(item.Name) - \(item.SeriesName ?? "")") + .navigationTitle("\(item.name ?? "") - \(item.seriesName ?? "")") } } diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 3cc1e90e..39014df4 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -54,7 +54,7 @@ struct SeriesItemView: View { Spacer().frame(height: 16) LazyVGrid(columns: tracks) { ForEach(seasons, id: \.id) { season in - NavigationLink(destination: ItemView(item: season )) { + NavigationLink(destination: ItemView(item: season)) { VStack(alignment: .leading) { LazyImage(source: season.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100)) .placeholderAndFailure { diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 718c197a..8ff2f1eb 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -14,6 +14,13 @@ import UIKit extension BaseItemDto { //MARK: Images + func getSeriesBackdropImageBlurHash() -> String { + let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString; + let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; + + return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"; + } + func getSeriesPrimaryImageBlurHash() -> String { let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString; let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]; @@ -60,6 +67,18 @@ extension BaseItemDto { return URL(string: urlString)! } + func getSeriesBackdropImage(baseURL: String, maxWidth: Int) -> URL { + let imageType = "Backdrop"; + let imageTag = (self.parentBackdropImageTags ?? [])[0]; + + print(imageType) + print(imageTag) + + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + let urlString = "\(baseURL)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + return URL(string: urlString)! + } + func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL { let imageType = "Primary"; let imageTag = self.seriesPrimaryImageTag ?? ""