From e66d0ce742c2da33443b7ee4ebc8efeb354b3ee8 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sun, 30 May 2021 18:35:00 +0900 Subject: [PATCH 1/5] add ParallaxHeaderScrollView --- JellyfinPlayer.xcodeproj/project.pbxproj | 8 +++- .../Extensions/ParallaxHeaderScrollView.swift | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 JellyfinPlayer/Extensions/ParallaxHeaderScrollView.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 8f7c94ec..89d913bf 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 621338932660107500A81A2A /* String++.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String++.swift */; }; 62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; }; 621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; + 6225FCCB2663841E00E067F6 /* ParallaxHeaderScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeaderScrollView.swift */; }; 6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; }; 6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; }; 6273DD48265F41B3009C1D0B /* JellyfinAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */; }; @@ -116,6 +117,7 @@ 621338922660107500A81A2A /* String++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String++.swift"; sourceTree = ""; }; 62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; + 6225FCCA2663841E00E067F6 /* ParallaxHeaderScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderScrollView.swift; sourceTree = ""; }; 6273DD47265F41B3009C1D0B /* JellyfinAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPI.swift; sourceTree = ""; }; 6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; AE8C3153265D60BF008AA076 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; @@ -218,6 +220,7 @@ 53892771263C8C6F0035E14B /* LoadingView.swift */, 621338B22660A07800A81A2A /* LazyView.swift */, 621338922660107500A81A2A /* String++.swift */, + 6225FCCA2663841E00E067F6 /* ParallaxHeaderScrollView.swift */, ); path = Extensions; sourceTree = ""; @@ -370,6 +373,7 @@ 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, + 6225FCCB2663841E00E067F6 /* ParallaxHeaderScrollView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, @@ -531,7 +535,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -558,7 +562,7 @@ CURRENT_PROJECT_VERSION = 29; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; diff --git a/JellyfinPlayer/Extensions/ParallaxHeaderScrollView.swift b/JellyfinPlayer/Extensions/ParallaxHeaderScrollView.swift new file mode 100644 index 00000000..1c57ffe8 --- /dev/null +++ b/JellyfinPlayer/Extensions/ParallaxHeaderScrollView.swift @@ -0,0 +1,44 @@ +// +// ParallaxHeaderScrollView.swift +// JellyfinPlayer +// +// Created by PangMo5 on 2021/05/30. +// + +import Foundation +import SwiftUI + +struct ParallaxHeaderScrollView: View { + var header: Header + var staticOverlayView: StaticOverlayView + var overlayAlignment: Alignment + var headerHeight: CGFloat + var content: () -> Content + + init(header: Header, + staticOverlayView: StaticOverlayView, + overlayAlignment: Alignment = .center, + headerHeight: CGFloat, + content: @escaping () -> Content) + { + self.header = header + self.staticOverlayView = staticOverlayView + self.overlayAlignment = overlayAlignment + self.headerHeight = headerHeight + self.content = content + } + + var body: some View { + ScrollView { + GeometryReader { proxy in + let yOffset = proxy.frame(in: .global).minY > 0 ? -proxy.frame(in: .global).minY : 0 + header + .frame(width: proxy.size.width, height: proxy.size.height - yOffset) + .overlay(staticOverlayView, alignment: overlayAlignment) + .offset(y: yOffset) + } + .frame(height: headerHeight) + content() + } + } +} From a16d51ae09692a6cadd2a77130cee83de175c8aa Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sun, 30 May 2021 18:35:49 +0900 Subject: [PATCH 2/5] Apply ParallaxHeaderScrollView to MovieItemView, SeasonItemView --- JellyfinPlayer/MovieItemView.swift | 442 ++++++++++++++-------------- JellyfinPlayer/SeasonItemView.swift | 85 +++--- 2 files changed, 261 insertions(+), 266 deletions(-) diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 54e12003..9085c0de 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -292,239 +292,237 @@ struct MovieItemView: View { } } + var portraitHeaderView: some View { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .BackdropBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + } + + .opacity(0.3) + .aspectRatio(contentMode: .fill) + .shadow(radius: 5) + } + + var portraitHeaderOverlayView: some View { + VStack(alignment: .leading) { + HStack(alignment: .bottom, spacing: 12) { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 120, height: 180) + .cornerRadius(10) + }.aspectRatio(contentMode: .fill) + .frame(width: 120, height: 180) + .cornerRadius(10) + VStack(alignment: .leading) { + Spacer() + Text(fullItem.Name).font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: -4) + HStack { + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + Text(fullItem.Runtime).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if fullItem.OfficialRating != "" { + Text(fullItem.OfficialRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + if fullItem.CommunityRating != "0" { + HStack { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -7, y: 0.7) + } + } + }.frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 26) + } + HStack { + // Play button + Button { + self.playbackInfo.itemToPlay = fullItem + self.playbackInfo.shouldPlay = true + } label: { + HStack { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + } + .frame(width: 120, height: 35) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .cornerRadius(10) + }.buttonStyle(PlainButtonStyle()) + .frame(width: 120, height: 35) + Spacer() + HStack { + Button { + favorite.toggle() + } label: { + if !favorite { + Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) + } else { + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) + } + } + Button { + watched.toggle() + } label: { + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) + } else { + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) + } + } + } + } + } + .padding(.horizontal, 16) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -135 : -60) + } + var body: some View { LoadingView(isShowing: $isLoading) { VStack(alignment: .leading) { if !isLoading { if orientationInfo.orientation == .portrait { - GeometryReader { geometry in - VStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: fullItem - .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .BackdropBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets - .trailing, - height: UIDevice.current - .userInterfaceIdiom == .pad ? 350 : - (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets - .trailing) * 0.5625) - } - - .opacity(0.3) - .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, - height: UIDevice.current - .userInterfaceIdiom == .pad ? 350 : - (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * - 0.5625) - .shadow(radius: 5) - .overlay(HStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - fullItem.PosterBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 120, height: 180) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { - Spacer() - Text(fullItem.Name).font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(y: -4) - HStack { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - Text(fullItem.Runtime).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - if fullItem.OfficialRating != "" { - Text(fullItem.OfficialRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - } - if fullItem.CommunityRating != "0" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - }.frame(maxWidth: .infinity, alignment: .leading) - }.offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46) - .padding(.trailing, 16) - }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40), - alignment: .bottomLeading) - VStack(alignment: .leading) { - HStack { - // Play button - Button { - self.playbackInfo.itemToPlay = fullItem - self.playbackInfo.shouldPlay = true - } label: { - HStack { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") - .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) - Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) - } - .frame(width: 120, height: 35) - .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) - .cornerRadius(10) - }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) - Spacer() + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIDevice.current + .userInterfaceIdiom == .pad ? 350 : + UIScreen.main.bounds.width * 0.5625) { + VStack(alignment: .leading) { + Spacer() + .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 60) + .padding(.bottom, 8) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, 16) + } + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + if !fullItem.Genres.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { HStack { - Button { - favorite.toggle() - } label: { - if !favorite { - Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) - } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) - .font(.system(size: 20)) + Text("Genres:").font(.callout).fontWeight(.semibold) + ForEach(fullItem.Genres, id: \.Id) { genre in + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(filter: Filter(genres: [ + genre + .Name, + ])), + title: genre.Name) + }) { + Text(genre.Name).font(.footnote) } } - Button { - watched.toggle() - } label: { - if watched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) - .font(.system(size: 20)) - } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - } - }.padding(.leading, 16).padding(.trailing, 16) - ScrollView { - VStack(alignment: .leading) { - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - if !fullItem.Genres.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(genres: [ - genre - .Name, - ])), - title: genre.Name) - }) { - Text(genre.Name).font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - if !fullItem.Cast.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(fullItem.Cast, id: \.Id) { cast in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(personIds: [ - cast - .Id, - ])), title: cast.Name) - }) { - VStack { - WebImage(url: cast.Image) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: cast - .ImageBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - cast.ImageBlurHash, - size: CGSize(width: 16, - height: 16))!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) - } - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) - .frame(width: 100).foregroundColor(Color.primary) - if cast.Role != "" { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) - .foregroundColor(Color.secondary).frame(width: 100) - } - } - } - Spacer().frame(width: 10) - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -3) - } - 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) - } - Spacer().frame(height: 3) - } + }.padding(.leading, 16).padding(.trailing, 16) } } - .padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, - trailing: 0)) + if !fullItem.Cast.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(fullItem.Cast, id: \.Id) { cast in + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(filter: Filter(personIds: [ + cast + .Id, + ])), title: cast.Name) + }) { + VStack { + WebImage(url: cast.Image) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: cast + .ImageBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + cast.ImageBlurHash, + size: CGSize(width: 16, + height: 16))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) + } + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) + Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if cast.Role != "" { + Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) + } + } + } + Spacer().frame(width: 10) + } + Spacer().frame(width: 16) + } + } + }.padding(.top, -3) + } + 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) + } + Spacer().frame(height: 3) } } } else { GeometryReader { geometry in - ZStack() { + ZStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { @@ -544,9 +542,9 @@ 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) - HStack() { - VStack() { + .blur(radius: 2) + HStack { + VStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index c2028be4..55ff0a5d 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -185,9 +185,8 @@ struct SeasonItemView: View { return result } - func portraitHeaderView(proxy: GeometryProxy) -> some View { - let yOffset = proxy.frame(in: .global).minY > 0 ? -proxy.frame(in: .global).minY : 0 - return WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=750&quality=80&tag=\(item.SeasonImage ?? "")")!) + var portraitHeaderView: some View { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=750&quality=80&tag=\(item.SeasonImage ?? "")")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { Image(uiImage: UIImage(blurHash: item @@ -195,50 +194,47 @@ struct SeasonItemView: View { .SeasonImageBlurHash ?? "", size: CGSize(width: 32, height: 32))!) .resizable() - .frame(width: proxy.size.width, height: proxy.size.height - yOffset) } - .opacity(0.4) .aspectRatio(contentMode: .fill) - .frame(width: proxy.size.width, height: proxy.size.height - yOffset) - .shadow(radius: 5) - .overlay(HStack(alignment: .bottom, spacing: 12) { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .PosterBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 120, height: 180) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { + } + + var portraitHeaderOverlayView: some View { + HStack(alignment: .bottom, spacing: 12) { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 120, height: 180) + .cornerRadius(10) + }.aspectRatio(contentMode: .fill) + .frame(width: 120, height: 180) + .cornerRadius(10) + VStack(alignment: .leading) { // Text(fullItem.SeriesName ?? "") // .font(.largeTitle) // .fontWeight(.bold) // .foregroundColor(.primary) // .padding(.bottom, 8) - Text(fullItem.Name).font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(y: -4) - if fullItem.ProductionYear != 0 { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } + Text(fullItem.Name).font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: -4) + if fullItem.ProductionYear != 0 { + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) } - }.padding(.horizontal, 16) - .padding(.bottom, -22), - alignment: .bottomLeading) - .offset(y: yOffset) + } + }.padding(.horizontal, 16) + .padding(.bottom, -22) } var body: some View { @@ -246,12 +242,13 @@ struct SeasonItemView: View { LoadingView(isShowing: $isLoading) { VStack(alignment: .leading) { if orientationInfo.orientation == .portrait { - ScrollView { - GeometryReader { proxy in - portraitHeaderView(proxy: proxy) - } - .frame(height: UIScreen.main.bounds.width * 0.5625) + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIScreen.main.bounds.width * 0.5625) { VStack(alignment: .leading) { + Spacer() + .frame(height: 22) if fullItem.Tagline != "" { Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) @@ -352,7 +349,7 @@ 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: 2) HStack { VStack(alignment: .leading) { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) @@ -383,7 +380,7 @@ struct SeasonItemView: View { .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) .padding(.trailing, 16) } - if(fullItem.Overview != "") { + 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) From 8bc24d1db8980d3dc3747c5abebd858c506d5c90 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sun, 30 May 2021 18:45:53 +0900 Subject: [PATCH 3/5] Apply ParallaxHeaderScrollView to EpisodeItem Fix layout of EpisodeItemView, MovieitemView --- JellyfinPlayer/EpisodeItemView.swift | 443 +++++++++++++-------------- JellyfinPlayer/MovieItemView.swift | 26 +- 2 files changed, 234 insertions(+), 235 deletions(-) diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 9ec089e6..05257d6d 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -202,239 +202,238 @@ struct EpisodeItemView: View { } } + var portraitHeaderView: some View { + VStack { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem + .BackdropBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + } + + .opacity(0.3) + .aspectRatio(contentMode: .fill) + .shadow(radius: 5) + } + } + + var portraitHeaderOverlayView: some View { + VStack(alignment: .leading) { + HStack(alignment: .bottom, spacing: 12) { + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: fullItem + .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + fullItem.PosterBlurHash, + size: CGSize(width: 32, height: 32))!) + .resizable() + .frame(width: 120, height: 180) + .cornerRadius(10) + }.aspectRatio(contentMode: .fill) + .frame(width: 120, height: 180) + .cornerRadius(10) + VStack(alignment: .leading) { + Spacer() + Text(fullItem.Name).font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: -4) + HStack { + Text(String(fullItem.ProductionYear)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + Text(fullItem.Runtime).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if fullItem.OfficialRating != "" { + Text(fullItem.OfficialRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + }.frame(maxWidth: .infinity, alignment: .leading) + if fullItem.CommunityRating != "0" { + HStack { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -7, y: 0.7) + } + } + }.frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 26) + } + HStack { + // Play button + Button { + self.playbackInfo.itemToPlay = fullItem + self.playbackInfo.shouldPlay = true + } label: { + HStack { + Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + } + .frame(width: 120, height: 35) + .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) + .cornerRadius(10) + }.buttonStyle(PlainButtonStyle()) + .frame(width: 120, height: 35) + Spacer() + HStack { + Button { + favorite.toggle() + } label: { + if !favorite { + Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) + } else { + Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) + } + } + Button { + watched.toggle() + } label: { + if watched { + Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) + .font(.system(size: 20)) + } else { + Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) + .font(.system(size: 20)) + } + } + } + } + } + .padding(.horizontal, 16) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) + } + var body: some View { LoadingView(isShowing: $isLoading) { VStack(alignment: .leading) { if !isLoading { if orientationInfo.orientation == .portrait { - GeometryReader { geometry in - VStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: fullItem - .BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem - .BackdropBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets - .trailing, - height: UIDevice.current - .userInterfaceIdiom == .pad ? 350 : - (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets - .trailing) * 0.5625) - } - - .opacity(0.3) - .aspectRatio(contentMode: .fill) - .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, - height: UIDevice.current - .userInterfaceIdiom == .pad ? 350 : - (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * - 0.5625) - .shadow(radius: 5) - .overlay(HStack { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: fullItem - .PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - fullItem.PosterBlurHash, - size: CGSize(width: 32, height: 32))!) - .resizable() - .frame(width: 120, height: 180) - .cornerRadius(10) - }.aspectRatio(contentMode: .fill) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { - Spacer() - Text(fullItem.Name).font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(y: -4) - HStack { - Text(String(fullItem.ProductionYear)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - Text(fullItem.Runtime).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - if fullItem.OfficialRating != "" { - Text(fullItem.OfficialRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - } - if fullItem.CommunityRating != "0" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - }.frame(maxWidth: .infinity, alignment: .leading) - }.frame(maxWidth: .infinity, alignment: .leading) - .offset(x: 0, y: UIDevice.current.userInterfaceIdiom == .pad ? -98 : -46) - .padding(.trailing, 16) - }.offset(x: 16, y: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40), - alignment: .bottomLeading) - VStack(alignment: .leading) { - HStack { - // Play button - Button { - self.playbackInfo.itemToPlay = fullItem - self.playbackInfo.shouldPlay = true - } label: { - HStack { - Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left") - .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) - Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) - } - .frame(width: 120, height: 35) - .background(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) - .cornerRadius(10) - }.buttonStyle(PlainButtonStyle()) - .frame(width: 120, height: 35) - Spacer() + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIDevice.current + .userInterfaceIdiom == .pad ? 350 : + UIScreen.main.bounds.width * 0.5625) { + VStack(alignment: .leading) { + Spacer() + .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) + if fullItem.Tagline != "" { + Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) + .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) + .padding(.trailing, 16) + } + Text(fullItem.Overview).font(.footnote).padding(.top, 3) + .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) + .padding(.trailing, 16) + if !fullItem.Genres.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { HStack { - Button { - favorite.toggle() - } label: { - if !favorite { - Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20)) - } else { - Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) - .font(.system(size: 20)) + Text("Genres:").font(.callout).fontWeight(.semibold) + ForEach(fullItem.Genres, id: \.Id) { genre in + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(filter: Filter(genres: [ + genre + .Name, + ])), title: genre.Name) + }) { + Text(genre.Name).font(.footnote) } } - Button { - watched.toggle() - } label: { - if watched { - Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary) - .font(.system(size: 20)) - } else { - Image(systemName: "xmark.rectangle").foregroundColor(Color.primary) - .font(.system(size: 20)) - } - } - } - }.padding(.leading, 16).padding(.trailing, 16) - ScrollView { - VStack(alignment: .leading) { - if fullItem.Tagline != "" { - Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(fullItem.Overview).font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - if !fullItem.Genres.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(fullItem.Genres, id: \.Id) { genre in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(genres: [ - genre - .Name, - ])), title: genre.Name) - }) { - Text(genre.Name).font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - if !fullItem.Cast.isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(fullItem.Cast, id: \.Id) { cast in - NavigationLink(destination: LazyView { - LibraryView(viewModel: .init(filter: Filter(personIds: [ - cast - .Id, - ])), title: cast.Name) - }) { - VStack { - WebImage(url: cast.Image) - .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size - .placeholder { - Image(uiImage: UIImage(blurHash: cast - .ImageBlurHash == "" ? - "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : - cast.ImageBlurHash, - size: CGSize(width: 16, - height: 16))!) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) - } - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(10) - Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) - .frame(width: 100).foregroundColor(Color.primary) - if cast.Role != "" { - Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) - .foregroundColor(Color.secondary).frame(width: 100) - } - } - } - Spacer().frame(width: 10) - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -3) - } - 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) - } - Spacer().frame(height: 3) - } + }.padding(.leading, 16).padding(.trailing, 16) } } - .padding(EdgeInsets(top: UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24, leading: 0, bottom: 0, - trailing: 0)) + if !fullItem.Cast.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(fullItem.Cast, id: \.Id) { cast in + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(filter: Filter(personIds: [ + cast + .Id, + ])), title: cast.Name) + }) { + VStack { + WebImage(url: cast.Image) + .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size + .placeholder { + Image(uiImage: UIImage(blurHash: cast + .ImageBlurHash == "" ? + "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : + cast.ImageBlurHash, + size: CGSize(width: 16, + height: 16))!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) + } + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(10) + Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if cast.Role != "" { + Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) + } + } + } + Spacer().frame(width: 10) + } + Spacer().frame(width: 16) + } + } + }.padding(.top, -3) + } + 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) + } + Spacer().frame(height: 3) } } } else { GeometryReader { geometry in - ZStack() { + ZStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { @@ -454,9 +453,9 @@ struct EpisodeItemView: 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) - HStack() { - VStack() { + .blur(radius: 2) + HStack { + VStack { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 9085c0de..fbae0647 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -349,17 +349,17 @@ struct MovieItemView: View { .overlay(RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1)) } - if fullItem.CommunityRating != "0" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } }.frame(maxWidth: .infinity, alignment: .leading) + if fullItem.CommunityRating != "0" { + HStack { + Image(systemName: "star").foregroundColor(.secondary) + Text(fullItem.CommunityRating).font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -7, y: 0.7) + } + } } .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 26) } @@ -406,7 +406,7 @@ struct MovieItemView: View { } } .padding(.horizontal, 16) - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -135 : -60) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) } var body: some View { @@ -422,8 +422,8 @@ struct MovieItemView: View { UIScreen.main.bounds.width * 0.5625) { VStack(alignment: .leading) { Spacer() - .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 60) - .padding(.bottom, 8) + .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) if fullItem.Tagline != "" { Text(fullItem.Tagline).font(.body).italic().padding(.top, 7) .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) From e9be25f44b8e09e73094e6815fd7126cbfc794b6 Mon Sep 17 00:00:00 2001 From: aiden Date: Sun, 30 May 2021 23:01:46 -0400 Subject: [PATCH 4/5] Revert team ID --- JellyfinPlayer.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 89d913bf..2c847beb 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -535,7 +535,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 29; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -562,7 +562,7 @@ CURRENT_PROJECT_VERSION = 29; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; From 07418a772f716a59b8b315c24f32c9a326b1790b Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Sun, 30 May 2021 23:09:31 -0400 Subject: [PATCH 5/5] Remove ratings --- JellyfinPlayer/EpisodeItemView.swift | 12 +----------- JellyfinPlayer/MovieItemView.swift | 10 ---------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 05257d6d..51db820f 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -261,18 +261,8 @@ struct EpisodeItemView: View { .overlay(RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1)) } - }.frame(maxWidth: .infinity, alignment: .leading) - if fullItem.CommunityRating != "0" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } } - }.frame(maxWidth: .infinity, alignment: .leading) + } .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 26) } HStack { diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index fbae0647..969a4c40 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -349,16 +349,6 @@ struct MovieItemView: View { .overlay(RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1)) } - }.frame(maxWidth: .infinity, alignment: .leading) - if fullItem.CommunityRating != "0" { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(fullItem.CommunityRating).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } } } .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 26)