From 3fa97235b166fcd9b9a68857f1b221ca827d4f80 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 6 Aug 2021 20:00:00 +0900 Subject: [PATCH 1/4] new SeriesItemView UI (portrait) --- JellyfinPlayer/SeriesItemView.swift | 180 ++++++++++++++++++++++++++-- 1 file changed, 172 insertions(+), 8 deletions(-) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 84e65641..20c9023e 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -9,28 +9,192 @@ import SwiftUI struct SeriesItemView: View { @StateObject var viewModel: SeriesItemViewModel + @State private var orientation = UIDeviceOrientation.unknown + @Environment(\.horizontalSizeClass) var hSizeClass + @Environment(\.verticalSizeClass) var vSizeClass @State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) + @ViewBuilder + var portraitHeaderView: some View { + ImageView(src: viewModel.item + .getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.4) + .blur(radius: 2.0) + } + + var portraitHeaderOverlayView: some View { + HStack(alignment: .bottom, spacing: 12) { + ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120), bh: viewModel.item.getPrimaryImageBlurHash()) + .frame(width: 120, height: 180) + .cornerRadius(10) + VStack(alignment: .leading) { + Text(viewModel.item.name ?? "").font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: -4) + HStack { + Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if viewModel.item.officialRating != nil { + Text(viewModel.item.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)) + } + } + }.offset(y: -32) + }.padding(.horizontal, 16) + .offset(y: 22) + } + func recalcTracks() { tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) } + var innerBody: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 0) { + if !(viewModel.item.taglines ?? []).isEmpty { + Text(viewModel.item.taglines!.first!).font(.body).italic() + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom, 8) + } + if !(viewModel.item.genreItems ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 8) { + Text("Genres:").font(.callout).fontWeight(.semibold) + ForEach(viewModel.item.genreItems!, id: \.id) { genre in + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") + }) { + Text(genre.name ?? "").font(.footnote) + } + } + } + } + .padding(.bottom, 8) + } + Text(viewModel.item.overview ?? "") + .font(.footnote) + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom, 16) + Text("Seasons") + .font(.callout).fontWeight(.semibold) + } + .padding(.horizontal, 16) + .padding(.top, 24) + LazyVGrid(columns: tracks) { + ForEach(viewModel.seasons, id: \.id) { season in + PortraitItemView(item: season) + } + } + .padding(.bottom, 16) + LazyVStack(alignment: .leading, spacing: 0) { + if !(viewModel.item.people ?? []).isEmpty { + Text("CAST") + .font(.callout).fontWeight(.semibold) + .padding(.bottom, 8) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + ForEach(viewModel.item.people!, id: \.self) { person in + if person.type! == "Actor" { + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(person: person), title: person.name ?? "") + }) { + VStack { + ImageView(src: person + .getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), + bh: person.getBlurHash()) + .frame(width: 100, height: 100) + .cornerRadius(10) + Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) + .frame(width: 100).foregroundColor(Color.primary) + if person.role != "" { + Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1) + .foregroundColor(Color.secondary).frame(width: 100) + } + } + } + } + } + } + } + .padding(.bottom, 16) + } + if !(viewModel.item.studios ?? []).isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + Text("Studios:").font(.callout).fontWeight(.semibold) + ForEach(viewModel.item.studios!, id: \.id) { studio in + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") + }) { + Text(studio.name ?? "").font(.footnote) + } + } + } + } + .padding(.bottom, 16) + } + if !viewModel.similarItems.isEmpty { + Text("More Like This") + .font(.callout).fontWeight(.semibold) + .padding(.bottom, 8) + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 16) { + ForEach(viewModel.similarItems, id: \.self) { similarItem in + NavigationLink(destination: LazyView { ItemView(item: similarItem) }) { + PortraitItemView(item: similarItem) + } + } + } + } + .padding(.bottom, 16) + } + } + .padding(.horizontal, 16) + } + } + var body: some View { if viewModel.isLoading { ProgressView() } else { - ScrollView(.vertical) { - Spacer().frame(height: 16) - LazyVGrid(columns: tracks) { - ForEach(viewModel.seasons, id: \.id) { season in - PortraitItemView(item: season) + Group { + if hSizeClass == .compact && vSizeClass == .regular { + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIScreen.main.bounds.width * 0.5625) { + innerBody + } + } else { + GeometryReader { geometry in + ZStack { + ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200), + bh: viewModel.item.getSeriesBackdropImageBlurHash()) + .opacity(0.4) + .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: 4) + innerBody + } } - Spacer().frame(height: 2) - }.onRotate { _ in - recalcTracks() } } + .onRotate { + orientation = $0 + recalcTracks() + } .overrideViewPreference(.unspecified) .navigationTitle(viewModel.item.name ?? "") .navigationBarTitleDisplayMode(.inline) From 6e0bd58e1f10608f28433895f05bac1d15d84a08 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 6 Aug 2021 20:11:31 +0900 Subject: [PATCH 2/4] remove force wrapping --- JellyfinPlayer/SeriesItemView.swift | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 20c9023e..349b7b05 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -40,8 +40,8 @@ struct SeriesItemView: View { .fontWeight(.medium) .foregroundColor(.secondary) .lineLimit(1) - if viewModel.item.officialRating != nil { - Text(viewModel.item.officialRating!).font(.subheadline) + if let officialRating = viewModel.item.officialRating { + Text(officialRating).font(.subheadline) .fontWeight(.semibold) .foregroundColor(.secondary) .lineLimit(1) @@ -62,16 +62,17 @@ struct SeriesItemView: View { var innerBody: some View { ScrollView { LazyVStack(alignment: .leading, spacing: 0) { - if !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic() + if let firstTagline = viewModel.item.taglines?.first { + Text(firstTagline).font(.body).italic() .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 8) } - if !(viewModel.item.genreItems ?? []).isEmpty { + if let genreItems = viewModel.item.genreItems, + !genreItems.isEmpty { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 8) { Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.genreItems!, id: \.id) { genre in + ForEach(genreItems, id: \.id) { genre in NavigationLink(destination: LazyView { LibraryView(viewModel: .init(genre: genre), title: genre.name ?? "") }) { @@ -98,14 +99,15 @@ struct SeriesItemView: View { } .padding(.bottom, 16) LazyVStack(alignment: .leading, spacing: 0) { - if !(viewModel.item.people ?? []).isEmpty { + if let people = viewModel.item.people, + !people.isEmpty { Text("CAST") .font(.callout).fontWeight(.semibold) .padding(.bottom, 8) ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 16) { - ForEach(viewModel.item.people!, id: \.self) { person in - if person.type! == "Actor" { + ForEach(people, id: \.self) { person in + if person.type == "Actor" { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(person: person), title: person.name ?? "") }) { @@ -117,8 +119,9 @@ struct SeriesItemView: View { .cornerRadius(10) Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) .frame(width: 100).foregroundColor(Color.primary) - if person.role != "" { - Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1) + if let role = person.role, + !role.isEmpty { + Text(role).font(.caption).fontWeight(.medium).lineLimit(1) .foregroundColor(Color.secondary).frame(width: 100) } } @@ -129,11 +132,12 @@ struct SeriesItemView: View { } .padding(.bottom, 16) } - if !(viewModel.item.studios ?? []).isEmpty { + if let studios = viewModel.item.studios, + !studios.isEmpty { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 16) { Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in + ForEach(studios, id: \.id) { studio in NavigationLink(destination: LazyView { LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") }) { @@ -179,8 +183,8 @@ struct SeriesItemView: View { } else { GeometryReader { geometry in ZStack { - ImageView(src: viewModel.item.getSeriesBackdropImage(maxWidth: 200), - bh: viewModel.item.getSeriesBackdropImageBlurHash()) + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), + bh: viewModel.item.getBackdropImageBlurHash()) .opacity(0.4) .frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) From aa23216a1a7e43004b238547b9285b865eaa4e3e Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 6 Aug 2021 20:24:02 +0900 Subject: [PATCH 3/4] new SeriesItemView UI (landscape) --- JellyfinPlayer/SeriesItemView.swift | 64 +++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 349b7b05..6e26327c 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -68,7 +68,8 @@ struct SeriesItemView: View { .padding(.bottom, 8) } if let genreItems = viewModel.item.genreItems, - !genreItems.isEmpty { + !genreItems.isEmpty + { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 8) { Text("Genres:").font(.callout).fontWeight(.semibold) @@ -100,7 +101,8 @@ struct SeriesItemView: View { .padding(.bottom, 16) LazyVStack(alignment: .leading, spacing: 0) { if let people = viewModel.item.people, - !people.isEmpty { + !people.isEmpty + { Text("CAST") .font(.callout).fontWeight(.semibold) .padding(.bottom, 8) @@ -120,7 +122,8 @@ struct SeriesItemView: View { Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1) .frame(width: 100).foregroundColor(Color.primary) if let role = person.role, - !role.isEmpty { + !role.isEmpty + { Text(role).font(.caption).fontWeight(.medium).lineLimit(1) .foregroundColor(Color.secondary).frame(width: 100) } @@ -133,7 +136,8 @@ struct SeriesItemView: View { .padding(.bottom, 16) } if let studios = viewModel.item.studios, - !studios.isEmpty { + !studios.isEmpty + { ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 16) { Text("Studios:").font(.callout).fontWeight(.semibold) @@ -168,6 +172,45 @@ struct SeriesItemView: View { } } + var landscapeView: some View { + GeometryReader { geometry in + ZStack { + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), + bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.4) + .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: 4) + HStack(alignment: .top, spacing: 16) { + VStack(alignment: .leading) { + ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120), + bh: viewModel.item.getPrimaryImageBlurHash()) + .frame(width: 120, height: 180) + .cornerRadius(10) + HStack { + Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + if let officialRating = viewModel.item.officialRating { + Text(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)) + } + } + } + .padding(.top, 16) + innerBody + } + } + } + } + var body: some View { if viewModel.isLoading { ProgressView() @@ -181,18 +224,7 @@ struct SeriesItemView: View { innerBody } } else { - GeometryReader { geometry in - ZStack { - ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), - bh: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.4) - .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: 4) - innerBody - } - } + landscapeView } } .onRotate { From 6af599f2741e4c5f10c85b70eeac850a8a3a1a91 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 6 Aug 2021 20:32:08 +0900 Subject: [PATCH 4/4] fix some layout --- JellyfinPlayer/SeriesItemView.swift | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift index 6e26327c..3666f404 100644 --- a/JellyfinPlayer/SeriesItemView.swift +++ b/JellyfinPlayer/SeriesItemView.swift @@ -66,6 +66,7 @@ struct SeriesItemView: View { Text(firstTagline).font(.body).italic() .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 8) + .padding(.horizontal, 16) } if let genreItems = viewModel.item.genreItems, !genreItems.isEmpty @@ -81,6 +82,7 @@ struct SeriesItemView: View { } } } + .padding(.horizontal, 16) } .padding(.bottom, 8) } @@ -88,10 +90,11 @@ struct SeriesItemView: View { .font(.footnote) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 16) + .padding(.horizontal, 16) Text("Seasons") .font(.callout).fontWeight(.semibold) + .padding(.horizontal, 16) } - .padding(.horizontal, 16) .padding(.top, 24) LazyVGrid(columns: tracks) { ForEach(viewModel.seasons, id: \.id) { season in @@ -99,6 +102,7 @@ struct SeriesItemView: View { } } .padding(.bottom, 16) + .padding(.horizontal, 8) LazyVStack(alignment: .leading, spacing: 0) { if let people = viewModel.item.people, !people.isEmpty @@ -106,6 +110,7 @@ struct SeriesItemView: View { Text("CAST") .font(.callout).fontWeight(.semibold) .padding(.bottom, 8) + .padding(.horizontal, 16) ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 16) { ForEach(people, id: \.self) { person in @@ -132,6 +137,7 @@ struct SeriesItemView: View { } } } + .padding(.horizontal, 16) } .padding(.bottom, 16) } @@ -149,6 +155,7 @@ struct SeriesItemView: View { } } } + .padding(.horizontal, 16) } .padding(.bottom, 16) } @@ -156,6 +163,7 @@ struct SeriesItemView: View { Text("More Like This") .font(.callout).fontWeight(.semibold) .padding(.bottom, 8) + .padding(.horizontal, 16) ScrollView(.horizontal, showsIndicators: false) { LazyHStack(spacing: 16) { ForEach(viewModel.similarItems, id: \.self) { similarItem in @@ -164,11 +172,11 @@ struct SeriesItemView: View { } } } + .padding(.horizontal, 16) } .padding(.bottom, 16) } } - .padding(.horizontal, 16) } }