From 7a79b78e38314c7b7242f2a9379c83d8f404c571 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 26 Aug 2021 22:02:53 -0600 Subject: [PATCH 01/14] Move portrait header view --- JellyfinPlayer/ItemView.swift | 165 ++++++++++++++++++++++++++--- JellyfinPlayer/MovieItemView.swift | 6 +- 2 files changed, 151 insertions(+), 20 deletions(-) diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index a60937cc..2f0db724 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -25,6 +25,123 @@ struct ItemView: View { init(item: BaseItemDto) { self.item = item } + + var portraitHeaderView: some View { + ImageView(src: item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + bh: item.getBackdropImageBlurHash()) + .opacity(0.4) + .blur(radius: 2.0) + } + + var portraitHeaderOverlayView: some View { + VStack(alignment: .leading) { + HStack(alignment: .bottom, spacing: 12) { + ImageView(src: item.getPrimaryImage(maxWidth: 130)) + .frame(width: 130, height: 195) + .cornerRadius(10) + VStack(alignment: .leading) { + + Spacer() + + Text(item.name ?? "").font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + .offset(y: 5) + + HStack { + if item.productionYear != nil { + Text(String(item.productionYear ?? 0)).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + + Text(item.getItemRuntime()).font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + + if item.officialRating != nil { + Text(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)) + } + } + .padding(.top, 1) + } + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) + } + HStack { + // Play button + Button { + () +// self.playbackInfo.itemToPlay = item +// self.playbackInfo.shouldShowPlayer = true + } label: { + HStack { + Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + Text(item.getItemProgressString() == "" ? "Play" : item.getItemProgressString()) + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + } + .frame(width: 130, height: 40) + .background(Color.jellyfinPurple) + .cornerRadius(10) + } + + Spacer() + + Button { + print("Heart") + } label: { + Image(systemName: "heart").foregroundColor(.primary) + .font(.system(size: 20)) + } + + Button { + print("Check") + } label: { + Image(systemName: "checkmark.circle").foregroundColor(.primary) + .font(.system(size: 20)) + } + + +// Button { +// updateFavoriteState() +// () +// } label: { +// if viewModel.isFavorited { +// Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) +// .font(.system(size: 20)) +// } else { +// Image(systemName: "heart").foregroundColor(Color.primary) +// .font(.system(size: 20)) +// } +// } +// .disabled(viewModel.isLoading) +// Button { +// viewModel.updateWatchState() +// () +// } label: { +// if viewModel.isWatched { +// Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) +// .font(.system(size: 20)) +// } else { +// Image(systemName: "checkmark.circle").foregroundColor(Color.primary) +// .font(.system(size: 20)) +// } +// } +// .disabled(viewModel.isLoading) +// } + }.padding(.top, 8) + } + .padding(.horizontal, 16) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) + } var body: some View { VStack { @@ -37,25 +154,41 @@ struct ItemView: View { }, isActive: $videoPlayerItem.shouldShowPlayer) { EmptyView() } - VStack { - if item.type == "Movie" { - MovieItemView(viewModel: .init(item: item)) - } else if item.type == "Season" { - SeasonItemView(viewModel: .init(item: item)) - } else if item.type == "Series" { - SeriesItemView(viewModel: .init(item: item)) - } else if item.type == "Episode" { - EpisodeItemView(viewModel: .init(item: item)) - } else { - Text("Type: \(item.type ?? "") not implemented yet :(") + + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitHeaderOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds + .width * 0.5625) { + VStack { + Spacer() + .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) + + + + + + if item.type == "Movie" { +// MovieItemView(viewModel: .init(item: item)) + Text("Movie") + } else if item.type == "Season" { + SeasonItemView(viewModel: .init(item: item)) + } else if item.type == "Series" { + SeriesItemView(viewModel: .init(item: item)) + } else if item.type == "Episode" { + EpisodeItemView(viewModel: .init(item: item)) + } else { + Text("Type: \(item.type ?? "") not implemented yet :(") + } } - } - .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = false + .introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = false + } + .navigationBarBackButtonHidden(false) + .environmentObject(videoPlayerItem) } .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .environmentObject(videoPlayerItem) } } } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index bcef0c77..8dfad20d 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -10,10 +10,8 @@ import SwiftUI struct MovieItemView: View { @StateObject var viewModel: MovieItemViewModel @State private var orientation = UIDeviceOrientation.unknown - @Environment(\.horizontalSizeClass) - var hSizeClass - @Environment(\.verticalSizeClass) - var vSizeClass + @Environment(\.horizontalSizeClass) var hSizeClass + @Environment(\.verticalSizeClass) var vSizeClass @EnvironmentObject private var playbackInfo: VideoPlayerItem From 2dbb0bd8901fc4f8a9ff92fb2b2df9acbbc6a8c3 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Fri, 27 Aug 2021 23:31:13 -0600 Subject: [PATCH 02/14] Simplify item view creation --- JellyfinPlayer.xcodeproj/project.pbxproj | 100 ++++++++--- .../Components/PillHStackView.swift | 60 +++++++ .../Components/PortraitHStackView.swift | 76 ++++++++ .../PortraitHeaderOverlayView.swift | 132 ++++++++++++++ .../Components/PortraitItemView.swift | 2 + JellyfinPlayer/ItemView.swift | 169 +++++------------- .../BaseItemDtoExtensions.swift} | 65 ++++--- .../BaseItemPersonExtensions.swift | 47 +++++ .../NameGUIDPairExtensions.swift | 17 ++ Shared/Objects/DetailItem.swift | 28 +++ .../DeviceProfileBuilder.swift | 0 .../DeviceRotationViewModifier.swift | 0 .../Views}/ImageView.swift | 0 .../Views}/LazyView.swift | 0 .../Views}/MultiSelectorView.swift | 0 .../Views}/ParallaxHeader.swift | 0 .../Views}/SearchBarView.swift | 0 .../Views}/SearchablePickerView.swift | 0 18 files changed, 521 insertions(+), 175 deletions(-) create mode 100644 JellyfinPlayer/Components/PillHStackView.swift create mode 100644 JellyfinPlayer/Components/PortraitHStackView.swift create mode 100644 JellyfinPlayer/Components/PortraitHeaderOverlayView.swift rename Shared/Extensions/{APIExtensions.swift => JellyfinAPIExtensions/BaseItemDtoExtensions.swift} (82%) create mode 100644 Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift create mode 100644 Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift create mode 100644 Shared/Objects/DetailItem.swift rename Shared/{Extensions => Objects}/DeviceProfileBuilder.swift (100%) rename Shared/{Extensions => Objects}/DeviceRotationViewModifier.swift (100%) rename Shared/{Extensions => Objects/Views}/ImageView.swift (100%) rename Shared/{Extensions => Objects/Views}/LazyView.swift (100%) rename Shared/{Extensions => Objects/Views}/MultiSelectorView.swift (100%) rename Shared/{Extensions => Objects/Views}/ParallaxHeader.swift (100%) rename Shared/{Extensions => Objects/Views}/SearchBarView.swift (100%) rename Shared/{Extensions => Objects/Views}/SearchablePickerView.swift (100%) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 821cf188..01672051 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -78,8 +78,8 @@ 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AB4269D423A00A2D8B7 /* Puppy */; }; - 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; - 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; + 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; + 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D73267BA8170004248C /* BackgroundManager.swift */; }; 536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */; }; 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; }; @@ -188,7 +188,6 @@ 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; }; 6260FFF926A09754003FA968 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = 6260FFF826A09754003FA968 /* CombineExt */; }; 6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = 6261A0DF26A0AB710072EF1C /* CombineExt */; }; - 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; }; 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; }; @@ -242,6 +241,20 @@ E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; + E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; + E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; }; + E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; + E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; + E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; }; + E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */; }; + E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */; }; + E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; }; + E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; }; + E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; }; + E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; + E1AD106026D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; + E1AD106226D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */; }; + E1AD106326D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; @@ -347,7 +360,7 @@ 5362E4C6267D40F4000E2F71 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 5362E4C8267D40F7000E2F71 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 53649AB0269CFB1900A2D8B7 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; - 5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = ""; }; + 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemPersonExtensions.swift; sourceTree = ""; }; 536D3D73267BA8170004248C /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = ""; }; 536D3D75267BA9BB0004248C /* MainTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabViewModel.swift; sourceTree = ""; }; 536D3D7E267BDF100004248C /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; @@ -446,6 +459,12 @@ E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = ""; }; + E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = ""; }; + E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = ""; }; + E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = ""; }; + E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillHStackView.swift; sourceTree = ""; }; + E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = ""; }; + E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHeaderOverlayView.swift; sourceTree = ""; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = ""; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; @@ -658,7 +677,11 @@ 535870AB2669D8D300D05A09 /* Objects */ = { isa = PBXGroup; children = ( + E1AD105326D96F5A003E4A08 /* Views */, + 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 535870AC2669D8DD00D05A09 /* Typings.swift */, + E1AD104926D94822003E4A08 /* DetailItem.swift */, E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */, ); path = Objects; @@ -898,6 +921,9 @@ 53F866422687A45400DCD1D7 /* Components */ = { isa = PBXGroup; children = ( + E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */, + E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */, + E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, ); path = Components; @@ -906,21 +932,13 @@ 621338912660106C00A81A2A /* Extensions */ = { isa = PBXGroup; children = ( - 53DE4BD1267098F300739748 /* SearchBarView.swift */, - E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */, - 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, - 531AC8BE26750DE20091C7EB /* ImageView.swift */, - 5364F454266CA0DC0026ECBA /* APIExtensions.swift */, 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */, - 621338B22660A07800A81A2A /* LazyView.swift */, - 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, - 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, - 621338922660107500A81A2A /* StringExtensions.swift */, 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, - 6267B3D92671138200A7371D /* ImageExtensions.swift */, - 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */, 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */, - 624C21742685CF60007F1390 /* SearchablePickerView.swift */, + 6267B3D92671138200A7371D /* ImageExtensions.swift */, + E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, + 621338922660107500A81A2A /* StringExtensions.swift */, ); path = Extensions; sourceTree = ""; @@ -940,10 +958,10 @@ 62EC352A26766657000E9F2D /* Singleton */ = { isa = PBXGroup; children = ( - 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, - 62EC352E267666A5000E9F2D /* SessionManager.swift */, 536D3D73267BA8170004248C /* BackgroundManager.swift */, 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, + 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, + 62EC352E267666A5000E9F2D /* SessionManager.swift */, ); path = Singleton; sourceTree = ""; @@ -968,12 +986,35 @@ path = Pods; sourceTree = ""; }; + E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { + isa = PBXGroup; + children = ( + 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, + E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, + E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, + ); + path = JellyfinAPIExtensions; + sourceTree = ""; + }; + E1AD105326D96F5A003E4A08 /* Views */ = { + isa = PBXGroup; + children = ( + 531AC8BE26750DE20091C7EB /* ImageView.swift */, + 621338B22660A07800A81A2A /* LazyView.swift */, + 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, + 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */, + 624C21742685CF60007F1390 /* SearchablePickerView.swift */, + 53DE4BD1267098F300739748 /* SearchBarView.swift */, + ); + path = Views; + sourceTree = ""; + }; E1FCD08E26C466F3007C8DCF /* Errors */ = { isa = PBXGroup; children = ( - E1FCD08726C35A0D007C8DCF /* NetworkError.swift */, - E131691626C583BC0074BFEE /* LogConstructor.swift */, E1FCD09526C47118007C8DCF /* ErrorMessage.swift */, + E131691626C583BC0074BFEE /* LogConstructor.swift */, + E1FCD08726C35A0D007C8DCF /* NetworkError.swift */, ); path = Errors; sourceTree = ""; @@ -1346,6 +1387,7 @@ E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */, 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */, + E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */, 53272539268C20100035FBF1 /* EpisodeItemView.swift in Sources */, 531690F7267ACC00005D8AB9 /* LandscapeItemElement.swift in Sources */, 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */, @@ -1359,15 +1401,18 @@ 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + E1AD106326D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */, E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, + E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */, + E1AD106026D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, @@ -1391,9 +1436,11 @@ 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, - 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, + E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */, + E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, ); @@ -1403,7 +1450,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, @@ -1412,6 +1459,7 @@ 5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, + E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, @@ -1426,6 +1474,7 @@ 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, 0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */, 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */, + E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, 625CB56F2678C23300530A6E /* HomeView.swift in Sources */, E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, @@ -1447,9 +1496,11 @@ 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, + E1AD106226D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, + E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, @@ -1457,7 +1508,9 @@ 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, + E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, + E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, @@ -1481,11 +1534,12 @@ files = ( 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */, 62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */, - 6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */, 6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */, 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */, 6267B3DB2671139400A7371D /* ImageExtensions.swift in Sources */, + E1AD105926D9A543003E4A08 /* LazyView.swift in Sources */, 628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */, + E1AD105426D97161003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */, E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */, 628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */, diff --git a/JellyfinPlayer/Components/PillHStackView.swift b/JellyfinPlayer/Components/PillHStackView.swift new file mode 100644 index 00000000..fe0217bd --- /dev/null +++ b/JellyfinPlayer/Components/PillHStackView.swift @@ -0,0 +1,60 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +protocol PillStackable { + var title: String { get } +} + +struct PillHStackView: View { + + let title: String + let items: [ItemType] + let navigationView: (ItemType) -> NavigationView + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.callout) + .fontWeight(.semibold) + .padding(.top, 3) + .padding(.leading, 16) + + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(items, id: \.title) { item in + NavigationLink(destination: LazyView { + navigationView(item) + }) { + ZStack { + Color(UIColor.secondarySystemBackground) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .cornerRadius(10) + + Text(item.title) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.white) + .fixedSize() + .padding(.leading, 10) + .padding(.trailing, 10) + .padding(.top, 10) + .padding(.bottom, 10) + } + .fixedSize() + } + } + } + .padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + } + } +} diff --git a/JellyfinPlayer/Components/PortraitHStackView.swift b/JellyfinPlayer/Components/PortraitHStackView.swift new file mode 100644 index 00000000..5723aaec --- /dev/null +++ b/JellyfinPlayer/Components/PortraitHStackView.swift @@ -0,0 +1,76 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +public protocol PortraitImageStackable { + func imageURLContsructor(maxWidth: Int) -> URL + var title: String { get } + var description: String? { get } + var blurHash: String { get } +} + +struct PortraitImageHStackView: View { + + let title: String + let items: [ItemType] + let maxWidth: Int + let navigationView: (ItemType) -> NavigationView + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.callout) + .fontWeight(.semibold) + .padding(.top, 3) + .padding(.leading, 16) + + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack { + Spacer().frame(width: 16) + ForEach(items, id: \.title) { item in + NavigationLink( + destination: LazyView { + navigationView(item) + }, + label: { + VStack { + ImageView(src: item.imageURLContsructor(maxWidth: maxWidth), + bh: item.blurHash) + .frame(width: 100, height: CGFloat(maxWidth)) + .cornerRadius(10) + .shadow(radius: 4, y: 2) + + Text(item.title) + .font(.footnote) + .fontWeight(.regular) + .lineLimit(1) + .frame(width: 100) + .foregroundColor(.primary) + + if let description = item.description { + Text(description) + .font(.caption) + .fontWeight(.medium) + .lineLimit(1) + .frame(width: 100) + .foregroundColor(.secondary) + } + } + }) + } + Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + } + }.padding(.top, -3) + } + } +} diff --git a/JellyfinPlayer/Components/PortraitHeaderOverlayView.swift b/JellyfinPlayer/Components/PortraitHeaderOverlayView.swift new file mode 100644 index 00000000..dfae5798 --- /dev/null +++ b/JellyfinPlayer/Components/PortraitHeaderOverlayView.swift @@ -0,0 +1,132 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI + +struct PortraitHeaderOverlayView: View { + + @EnvironmentObject var viewModel: DetailItemViewModel + let item: BaseItemDto + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .bottom, spacing: 12) { + ImageView(src: item.portraitHeaderViewURL(maxWidth: 130)) + .frame(width: 130, height: 195) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 1) { + Spacer() + + Text(item.name ?? "") + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .offset(y: 5) + + Text(item.getItemRuntime()) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + .padding(.top, 10) + + HStack { + if let productionYear = item.productionYear { + Text(String(productionYear)) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + + if let officialRating = 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(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) + } + + HStack { + // Play button + Button { + () +// self.playbackInfo.itemToPlay = item +// self.playbackInfo.shouldShowPlayer = true + } label: { + HStack { + Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + Text(item.getItemProgressString() == "" ? "Play" : item.getItemProgressString()) + .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + } + .frame(width: 130, height: 40) + .background(Color.jellyfinPurple) + .cornerRadius(10) + } + + Spacer() + + Button { + print("Heart") + } label: { + Image(systemName: "heart").foregroundColor(.primary) + .font(.system(size: 20)) + } + + Button { + print("Check") + } label: { + Image(systemName: "checkmark.circle").foregroundColor(.primary) + .font(.system(size: 20)) + } + + +// Button { +// updateFavoriteState() +// () +// } label: { +// if viewModel.isFavorited { +// Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) +// .font(.system(size: 20)) +// } else { +// Image(systemName: "heart").foregroundColor(Color.primary) +// .font(.system(size: 20)) +// } +// } +// .disabled(viewModel.isLoading) +// Button { +// viewModel.updateWatchState() +// () +// } label: { +// if viewModel.isWatched { +// Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) +// .font(.system(size: 20)) +// } else { +// Image(systemName: "checkmark.circle").foregroundColor(Color.primary) +// .font(.system(size: 20)) +// } +// } +// .disabled(viewModel.isLoading) +// } + }.padding(.top, 8) + } + .padding(.horizontal, 16) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) + } +} diff --git a/JellyfinPlayer/Components/PortraitItemView.swift b/JellyfinPlayer/Components/PortraitItemView.swift index 60aa77a1..1e41226b 100644 --- a/JellyfinPlayer/Components/PortraitItemView.swift +++ b/JellyfinPlayer/Components/PortraitItemView.swift @@ -9,7 +9,9 @@ import SwiftUI import JellyfinAPI + struct PortraitItemView: View { + var item: BaseItemDto var body: some View { diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 2f0db724..e9716b5f 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -34,118 +34,15 @@ struct ItemView: View { } var portraitHeaderOverlayView: some View { - VStack(alignment: .leading) { - HStack(alignment: .bottom, spacing: 12) { - ImageView(src: item.getPrimaryImage(maxWidth: 130)) - .frame(width: 130, height: 195) - .cornerRadius(10) - VStack(alignment: .leading) { - - Spacer() - - Text(item.name ?? "").font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - .offset(y: 5) - - HStack { - if item.productionYear != nil { - Text(String(item.productionYear ?? 0)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - - Text(item.getItemRuntime()).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - - if item.officialRating != nil { - Text(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)) - } - } - .padding(.top, 1) - } - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) - } - HStack { - // Play button - Button { - () -// self.playbackInfo.itemToPlay = item -// self.playbackInfo.shouldShowPlayer = true - } label: { - HStack { - Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) - Text(item.getItemProgressString() == "" ? "Play" : item.getItemProgressString()) - .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) - } - .frame(width: 130, height: 40) - .background(Color.jellyfinPurple) - .cornerRadius(10) - } - - Spacer() - - Button { - print("Heart") - } label: { - Image(systemName: "heart").foregroundColor(.primary) - .font(.system(size: 20)) - } - - Button { - print("Check") - } label: { - Image(systemName: "checkmark.circle").foregroundColor(.primary) - .font(.system(size: 20)) - } - - -// Button { -// updateFavoriteState() -// () -// } label: { -// if viewModel.isFavorited { -// Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) -// .font(.system(size: 20)) -// } else { -// Image(systemName: "heart").foregroundColor(Color.primary) -// .font(.system(size: 20)) -// } -// } -// .disabled(viewModel.isLoading) -// Button { -// viewModel.updateWatchState() -// () -// } label: { -// if viewModel.isWatched { -// Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) -// .font(.system(size: 20)) -// } else { -// Image(systemName: "checkmark.circle").foregroundColor(Color.primary) -// .font(.system(size: 20)) -// } -// } -// .disabled(viewModel.isLoading) -// } - }.padding(.top, 8) - } - .padding(.horizontal, 16) - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) + PortraitHeaderOverlayView(item: item) } var body: some View { VStack { - NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) + NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { + VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, + loadBinding: $videoIsLoading, + pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) .navigationBarHidden(true) .navigationBarBackButtonHidden(true) .statusBar(hidden: true) @@ -158,29 +55,53 @@ struct ItemView: View { ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, - headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds - .width * 0.5625) { + headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { VStack { Spacer() .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) + // MARK: Overview + Text(item.overview ?? "") + .font(.footnote) + .padding(.top, 3) + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom, 3) + .padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - - - - if item.type == "Movie" { -// MovieItemView(viewModel: .init(item: item)) - Text("Movie") - } else if item.type == "Season" { - SeasonItemView(viewModel: .init(item: item)) - } else if item.type == "Series" { - SeriesItemView(viewModel: .init(item: item)) - } else if item.type == "Episode" { - EpisodeItemView(viewModel: .init(item: item)) - } else { - Text("Type: \(item.type ?? "") not implemented yet :(") + // MARK: Genres + PillHStackView(title: "Genres", items: item.genreItems ?? []) { genre in + LibraryView(viewModel: .init(genre: genre), title: genre.title) } + + // MARK: Studios + 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(viewModel: .init(studio: studio), title: studio.name ?? "") + }) { + Text(studio.name ?? "").font(.footnote) + } + } + } + .padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + } + + // MARK: Cast + PortraitImageHStackView(title: "Cast", + items: item.people!, + maxWidth: 150) { person in + LibraryView(viewModel: .init(person: person), title: person.title) + } + + // MARK: More Like This + } .introspectTabBarController { (UITabBarController) in UITabBarController.tabBar.isHidden = false diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift similarity index 82% rename from Shared/Extensions/APIExtensions.swift rename to Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index 24a8a5f4..44dd8ab0 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -1,9 +1,11 @@ -/* SwiftFin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ import Foundation import JellyfinAPI @@ -11,7 +13,8 @@ import UIKit // 001fC^ = dark grey plain blurhash -extension BaseItemDto { +public extension BaseItemDto { + // MARK: Images func getSeriesBackdropImageBlurHash() -> String { @@ -149,27 +152,33 @@ extension BaseItemDto { return "\(String(progminutes))m" } } -} - -func round(_ value: Double, toNearest: Double) -> Double { - return round(value / toNearest) * toNearest -} - -extension BaseItemPerson { - func getImage(baseURL: String, maxWidth: Int) -> URL { - let imageType = "Primary" - let imageTag = primaryImageTag ?? "" - - let x = UIScreen.main.nativeScale * CGFloat(maxWidth) - - let urlString = "\(baseURL)/Items/\(id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" - return URL(string: urlString)! + + // MARK: ItemType + + enum ItemType: String { + case movie = "Movie" + case season = "Season" + case episode = "Episode" + case series = "Series" + + case unknown } - - func getBlurHash() -> String { - let rawImgURL = getImage(baseURL: "", maxWidth: 1).absoluteString - let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] - - return imageBlurHashes?.primary?[imgTag] ?? "001fC^" + + var itemType: ItemType { + guard let originalType = self.type, let knownType = ItemType(rawValue: originalType) else { return .unknown } + return knownType + } + + // MARK: PortraitHeaderViewURL + + func portraitHeaderViewURL(maxWidth: Int) -> URL { + switch self.itemType { + case .movie, .season, .series: + return getPrimaryImage(maxWidth: maxWidth) + case .episode: + return getSeriesPrimaryImage(maxWidth: maxWidth) + case .unknown: + return getPrimaryImage(maxWidth: maxWidth) + } } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift new file mode 100644 index 00000000..ff1cf99d --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -0,0 +1,47 @@ +/* SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI +import UIKit + +extension BaseItemPerson { + func getImage(baseURL: String, maxWidth: Int) -> URL { + let imageType = "Primary" + let imageTag = primaryImageTag ?? "" + + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) + + let urlString = "\(baseURL)/Items/\(id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)" + return URL(string: urlString)! + } + + func getBlurHash() -> String { + let rawImgURL = getImage(baseURL: "", maxWidth: 1).absoluteString + let imgTag = rawImgURL.components(separatedBy: "&tag=")[1] + + return imageBlurHashes?.primary?[imgTag] ?? "001fC^" + } +} + +extension BaseItemPerson: PortraitImageStackable { + public func imageURLContsructor(maxWidth: Int) -> URL { + return self.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: maxWidth) + } + + public var title: String { + return self.name ?? "" + } + + public var description: String? { + return self.role + } + + public var blurHash: String { + return self.getBlurHash() + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift new file mode 100644 index 00000000..bc7311fa --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift @@ -0,0 +1,17 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +extension NameGuidPair: PillStackable { + var title: String { + return self.name ?? "" + } +} diff --git a/Shared/Objects/DetailItem.swift b/Shared/Objects/DetailItem.swift new file mode 100644 index 00000000..bf5b1792 --- /dev/null +++ b/Shared/Objects/DetailItem.swift @@ -0,0 +1,28 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +enum DetailItemType: String { + case movie = "Movie" + case season = "Season" + case series = "Series" + case episode = "Episode" +} + +struct DetailItem { + + let baseItem: BaseItemDto + let type: DetailItemType + + + + +} diff --git a/Shared/Extensions/DeviceProfileBuilder.swift b/Shared/Objects/DeviceProfileBuilder.swift similarity index 100% rename from Shared/Extensions/DeviceProfileBuilder.swift rename to Shared/Objects/DeviceProfileBuilder.swift diff --git a/Shared/Extensions/DeviceRotationViewModifier.swift b/Shared/Objects/DeviceRotationViewModifier.swift similarity index 100% rename from Shared/Extensions/DeviceRotationViewModifier.swift rename to Shared/Objects/DeviceRotationViewModifier.swift diff --git a/Shared/Extensions/ImageView.swift b/Shared/Objects/Views/ImageView.swift similarity index 100% rename from Shared/Extensions/ImageView.swift rename to Shared/Objects/Views/ImageView.swift diff --git a/Shared/Extensions/LazyView.swift b/Shared/Objects/Views/LazyView.swift similarity index 100% rename from Shared/Extensions/LazyView.swift rename to Shared/Objects/Views/LazyView.swift diff --git a/Shared/Extensions/MultiSelectorView.swift b/Shared/Objects/Views/MultiSelectorView.swift similarity index 100% rename from Shared/Extensions/MultiSelectorView.swift rename to Shared/Objects/Views/MultiSelectorView.swift diff --git a/Shared/Extensions/ParallaxHeader.swift b/Shared/Objects/Views/ParallaxHeader.swift similarity index 100% rename from Shared/Extensions/ParallaxHeader.swift rename to Shared/Objects/Views/ParallaxHeader.swift diff --git a/Shared/Extensions/SearchBarView.swift b/Shared/Objects/Views/SearchBarView.swift similarity index 100% rename from Shared/Extensions/SearchBarView.swift rename to Shared/Objects/Views/SearchBarView.swift diff --git a/Shared/Extensions/SearchablePickerView.swift b/Shared/Objects/Views/SearchablePickerView.swift similarity index 100% rename from Shared/Extensions/SearchablePickerView.swift rename to Shared/Objects/Views/SearchablePickerView.swift From f9114ae6bee7467c16a931bf899c20c6b7b47ba5 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 28 Aug 2021 22:25:43 -0600 Subject: [PATCH 03/14] Create explicit portrait and landscape views --- JellyfinPlayer.xcodeproj/project.pbxproj | 30 +++-- .../Components/PillHStackView.swift | 4 +- .../Components/PortraitHStackView.swift | 19 +-- JellyfinPlayer/ItemView.swift | 115 ------------------ .../ItemView/ItemLandscapeBodyView.swift | 16 +++ .../ItemView/ItemPortraitBodyView.swift | 103 ++++++++++++++++ .../ItemPortraitHeaderOverlayView.swift} | 79 ++++++------ JellyfinPlayer/ItemView/ItemView.swift | 50 ++++++++ 8 files changed, 243 insertions(+), 173 deletions(-) delete mode 100644 JellyfinPlayer/ItemView.swift create mode 100644 JellyfinPlayer/ItemView/ItemLandscapeBodyView.swift create mode 100644 JellyfinPlayer/ItemView/ItemPortraitBodyView.swift rename JellyfinPlayer/{Components/PortraitHeaderOverlayView.swift => ItemView/ItemPortraitHeaderOverlayView.swift} (61%) create mode 100644 JellyfinPlayer/ItemView/ItemView.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 01672051..348ea06d 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -238,6 +238,8 @@ E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; + E14F7D0726DB36EF007C3AE6 /* ItemPortraitBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitBodyView.swift */; }; + E14F7D0926DB36F7007C3AE6 /* ItemLandscapeBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeBodyView.swift */; }; E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; @@ -253,8 +255,8 @@ E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; }; E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E1AD106026D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; - E1AD106226D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */; }; - E1AD106326D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */; }; + E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; }; + E1AD106326D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; @@ -456,6 +458,8 @@ DE5004F745B19E28744A7DE7 /* Pods-JellyfinPlayer tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.debug.xcconfig"; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; + E14F7D0626DB36EF007C3AE6 /* ItemPortraitBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitBodyView.swift; sourceTree = ""; }; + E14F7D0826DB36F7007C3AE6 /* ItemLandscapeBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeBodyView.swift; sourceTree = ""; }; E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailViewModel.swift; sourceTree = ""; }; @@ -464,7 +468,7 @@ E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = ""; }; E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillHStackView.swift; sourceTree = ""; }; E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameGUIDPairExtensions.swift; sourceTree = ""; }; - E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHeaderOverlayView.swift; sourceTree = ""; }; + E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitHeaderOverlayView.swift; sourceTree = ""; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = ""; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; E1FCD09526C47118007C8DCF /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; @@ -734,7 +738,7 @@ 5389276D263C25100035E14B /* ContinueWatchingView.swift */, 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, 5377CC02263B596B003A4E83 /* Info.plist */, - 535BAE9E2649E569005FA86D /* ItemView.swift */, + E14F7D0A26DB3714007C3AE6 /* ItemView */, 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */, 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, @@ -922,7 +926,6 @@ isa = PBXGroup; children = ( E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */, - E1AD106126D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift */, E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, ); @@ -986,6 +989,17 @@ path = Pods; sourceTree = ""; }; + E14F7D0A26DB3714007C3AE6 /* ItemView */ = { + isa = PBXGroup; + children = ( + 535BAE9E2649E569005FA86D /* ItemView.swift */, + E14F7D0626DB36EF007C3AE6 /* ItemPortraitBodyView.swift */, + E14F7D0826DB36F7007C3AE6 /* ItemLandscapeBodyView.swift */, + E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */, + ); + path = ItemView; + sourceTree = ""; + }; E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { isa = PBXGroup; children = ( @@ -1401,7 +1415,7 @@ 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, - E1AD106326D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */, + E1AD106326D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 53272532268BF09D0035FBF1 /* MediaViewActionButton.swift in Sources */, @@ -1484,6 +1498,7 @@ E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, + E14F7D0926DB36F7007C3AE6 /* ItemLandscapeBodyView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, @@ -1496,7 +1511,8 @@ 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, - E1AD106226D9B7CD003E4A08 /* PortraitHeaderOverlayView.swift in Sources */, + E14F7D0726DB36EF007C3AE6 /* ItemPortraitBodyView.swift in Sources */, + E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, diff --git a/JellyfinPlayer/Components/PillHStackView.swift b/JellyfinPlayer/Components/PillHStackView.swift index fe0217bd..3c6fcf06 100644 --- a/JellyfinPlayer/Components/PillHStackView.swift +++ b/JellyfinPlayer/Components/PillHStackView.swift @@ -34,14 +34,14 @@ struct PillHStackView: View { navigationView(item) }) { ZStack { - Color(UIColor.secondarySystemBackground) + Color(UIColor.systemFill) .frame(maxWidth: .infinity, maxHeight: .infinity) .cornerRadius(10) Text(item.title) .font(.caption) .fontWeight(.semibold) - .foregroundColor(.white) + .foregroundColor(.primary) .fixedSize() .padding(.leading, 10) .padding(.trailing, 10) diff --git a/JellyfinPlayer/Components/PortraitHStackView.swift b/JellyfinPlayer/Components/PortraitHStackView.swift index 5723aaec..b6668419 100644 --- a/JellyfinPlayer/Components/PortraitHStackView.swift +++ b/JellyfinPlayer/Components/PortraitHStackView.swift @@ -16,20 +16,25 @@ public protocol PortraitImageStackable { var blurHash: String { get } } -struct PortraitImageHStackView: View { +struct PortraitImageHStackView: View { - let title: String let items: [ItemType] let maxWidth: Int + let horizontalAlignment: HorizontalAlignment + let topBarView: () -> TopBarView let navigationView: (ItemType) -> NavigationView + init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, navigationView: @escaping (ItemType) -> NavigationView) { + self.items = items + self.maxWidth = maxWidth + self.horizontalAlignment = horizontalAlignment + self.topBarView = topBarView + self.navigationView = navigationView + } + var body: some View { VStack(alignment: .leading) { - Text(title) - .font(.callout) - .fontWeight(.semibold) - .padding(.top, 3) - .padding(.leading, 16) + topBarView() ScrollView(.horizontal, showsIndicators: false) { VStack { diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift deleted file mode 100644 index e9716b5f..00000000 --- a/JellyfinPlayer/ItemView.swift +++ /dev/null @@ -1,115 +0,0 @@ -/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ - -import SwiftUI -import Introspect -import JellyfinAPI - -class VideoPlayerItem: ObservableObject { - @Published var shouldShowPlayer: Bool = false - @Published var itemToPlay: BaseItemDto = BaseItemDto() -} - -struct ItemView: View { - private var item: BaseItemDto - - @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() - @State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view. - @State private var isLoading: Bool = false - @State private var viewDidLoad: Bool = false - - init(item: BaseItemDto) { - self.item = item - } - - var portraitHeaderView: some View { - ImageView(src: item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), - bh: item.getBackdropImageBlurHash()) - .opacity(0.4) - .blur(radius: 2.0) - } - - var portraitHeaderOverlayView: some View { - PortraitHeaderOverlayView(item: item) - } - - var body: some View { - VStack { - NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { - VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, - loadBinding: $videoIsLoading, - pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .edgesIgnoringSafeArea(.all) - .prefersHomeIndicatorAutoHidden(true) - }, isActive: $videoPlayerItem.shouldShowPlayer) { - EmptyView() - } - - ParallaxHeaderScrollView(header: portraitHeaderView, - staticOverlayView: portraitHeaderOverlayView, - overlayAlignment: .bottomLeading, - headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { - VStack { - Spacer() - .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) - - // MARK: Overview - Text(item.overview ?? "") - .font(.footnote) - .padding(.top, 3) - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 3) - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - - // MARK: Genres - PillHStackView(title: "Genres", items: item.genreItems ?? []) { genre in - LibraryView(viewModel: .init(genre: genre), title: genre.title) - } - - // MARK: Studios - 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(viewModel: .init(studio: studio), title: studio.name ?? "") - }) { - Text(studio.name ?? "").font(.footnote) - } - } - } - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - - // MARK: Cast - PortraitImageHStackView(title: "Cast", - items: item.people!, - maxWidth: 150) { person in - LibraryView(viewModel: .init(person: person), title: person.title) - } - - // MARK: More Like This - - } - .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = false - } - .navigationBarBackButtonHidden(false) - .environmentObject(videoPlayerItem) - } - .navigationBarHidden(false) - } - } -} diff --git a/JellyfinPlayer/ItemView/ItemLandscapeBodyView.swift b/JellyfinPlayer/ItemView/ItemLandscapeBodyView.swift new file mode 100644 index 00000000..6e0cdc9a --- /dev/null +++ b/JellyfinPlayer/ItemView/ItemLandscapeBodyView.swift @@ -0,0 +1,16 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +struct ItemLandscapeBodyView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} diff --git a/JellyfinPlayer/ItemView/ItemPortraitBodyView.swift b/JellyfinPlayer/ItemView/ItemPortraitBodyView.swift new file mode 100644 index 00000000..619f8b88 --- /dev/null +++ b/JellyfinPlayer/ItemView/ItemPortraitBodyView.swift @@ -0,0 +1,103 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI + +struct ItemPortraitBodyView: View { + + @Binding var videoIsLoading: Bool + @EnvironmentObject var viewModel: MovieItemViewModel + @EnvironmentObject var videoPlayerItem: VideoPlayerItem + + private let item: BaseItemDto + private let portraitHeaderView: (BaseItemDto) -> PortraitHeaderView + private let portraitStaticOverlayView: (BaseItemDto) -> PortraitStaticOverlayView + + init(item: BaseItemDto, videoIsLoading: Binding, portraitHeaderView: @escaping (BaseItemDto) -> PortraitHeaderView, portraitStaticOverlayView: @escaping (BaseItemDto) -> PortraitStaticOverlayView) { + self.item = item + self._videoIsLoading = videoIsLoading + self.portraitHeaderView = portraitHeaderView + self.portraitStaticOverlayView = portraitStaticOverlayView + } + + var body: some View { + VStack(alignment: .leading) { + NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) { + VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, + loadBinding: $videoIsLoading, + pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .statusBar(hidden: true) + .edgesIgnoringSafeArea(.all) + .prefersHomeIndicatorAutoHidden(true) + }, isActive: $videoPlayerItem.shouldShowPlayer) { + EmptyView() + } + + // MARK: Body + ParallaxHeaderScrollView(header: portraitHeaderView(item), + staticOverlayView: portraitStaticOverlayView(item), + overlayAlignment: .bottomLeading, + headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) { + VStack { + Spacer() + .frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 54 : 24) + + // MARK: Overview + Text(item.overview ?? "") + .font(.footnote) + .padding(.top, 3) + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom, 3) + .padding(.leading, 16) + .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + + // MARK: Genres + PillHStackView(title: "Genres", + items: item.genreItems ?? []) { genre in + LibraryView(viewModel: .init(genre: genre), title: genre.title) + } + + // MARK: Studios + if let studios = item.studios { + PillHStackView(title: "Studios", + items: studios) { studio in + LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") + } + } + + // MARK: Cast + PortraitImageHStackView(items: item.people?.filter({ $0.type == "Actor" }) ?? [], + maxWidth: 150) { + Text("Cast") + .font(.callout) + .fontWeight(.semibold) + .padding(.top, 3) + .padding(.leading, 16) + } navigationView: { person in + LibraryView(viewModel: .init(person: person), title: person.title) + } + + // MARK: More Like This + + + Spacer(minLength: 10) + } +// .introspectTabBarController { (UITabBarController) in +// UITabBarController.tabBar.isHidden = false +// } +// .navigationBarBackButtonHidden(false) + .environmentObject(videoPlayerItem) + } + } + } +} diff --git a/JellyfinPlayer/Components/PortraitHeaderOverlayView.swift b/JellyfinPlayer/ItemView/ItemPortraitHeaderOverlayView.swift similarity index 61% rename from JellyfinPlayer/Components/PortraitHeaderOverlayView.swift rename to JellyfinPlayer/ItemView/ItemPortraitHeaderOverlayView.swift index dfae5798..d9ee3fd8 100644 --- a/JellyfinPlayer/Components/PortraitHeaderOverlayView.swift +++ b/JellyfinPlayer/ItemView/ItemPortraitHeaderOverlayView.swift @@ -12,7 +12,9 @@ import JellyfinAPI struct PortraitHeaderOverlayView: View { - @EnvironmentObject var viewModel: DetailItemViewModel + @EnvironmentObject private var viewModel: DetailItemViewModel + @EnvironmentObject private var videoPlayerItem: VideoPlayerItem + let item: BaseItemDto var body: some View { @@ -64,16 +66,20 @@ struct PortraitHeaderOverlayView: View { } HStack { - // Play button + + // MARK: Play Button { - () -// self.playbackInfo.itemToPlay = item -// self.playbackInfo.shouldShowPlayer = true + self.videoPlayerItem.itemToPlay = item + self.videoPlayerItem.shouldShowPlayer = true } label: { HStack { - Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20)) + Image(systemName: "play.fill") + .foregroundColor(Color.white) + .font(.system(size: 20)) Text(item.getItemProgressString() == "" ? "Play" : item.getItemProgressString()) - .foregroundColor(Color.white).font(.callout).fontWeight(.semibold) + .foregroundColor(Color.white) + .font(.callout) + .fontWeight(.semibold) } .frame(width: 130, height: 40) .background(Color.jellyfinPurple) @@ -82,48 +88,37 @@ struct PortraitHeaderOverlayView: View { Spacer() + // MARK: Favorite Button { - print("Heart") + viewModel.updateFavoriteState() } label: { - Image(systemName: "heart").foregroundColor(.primary) - .font(.system(size: 20)) + if viewModel.isFavorited { + Image(systemName: "heart.fill") + .foregroundColor(Color(UIColor.systemRed)) + .font(.system(size: 20)) + } else { + Image(systemName: "heart") + .foregroundColor(Color.primary) + .font(.system(size: 20)) + } } + .disabled(viewModel.isLoading) + // MARK: Watched Button { - print("Check") + viewModel.updateWatchState() } label: { - Image(systemName: "checkmark.circle").foregroundColor(.primary) - .font(.system(size: 20)) + if viewModel.isWatched { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color.jellyfinPurple) + .font(.system(size: 20)) + } else { + Image(systemName: "checkmark.circle") + .foregroundColor(Color.primary) + .font(.system(size: 20)) + } } - - -// Button { -// updateFavoriteState() -// () -// } label: { -// if viewModel.isFavorited { -// Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)) -// .font(.system(size: 20)) -// } else { -// Image(systemName: "heart").foregroundColor(Color.primary) -// .font(.system(size: 20)) -// } -// } -// .disabled(viewModel.isLoading) -// Button { -// viewModel.updateWatchState() -// () -// } label: { -// if viewModel.isWatched { -// Image(systemName: "checkmark.circle.fill").foregroundColor(Color.primary) -// .font(.system(size: 20)) -// } else { -// Image(systemName: "checkmark.circle").foregroundColor(Color.primary) -// .font(.system(size: 20)) -// } -// } -// .disabled(viewModel.isLoading) -// } + .disabled(viewModel.isLoading) }.padding(.top, 8) } .padding(.horizontal, 16) diff --git a/JellyfinPlayer/ItemView/ItemView.swift b/JellyfinPlayer/ItemView/ItemView.swift new file mode 100644 index 00000000..877f6d06 --- /dev/null +++ b/JellyfinPlayer/ItemView/ItemView.swift @@ -0,0 +1,50 @@ +/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import Introspect +import JellyfinAPI + +class VideoPlayerItem: ObservableObject { + @Published var shouldShowPlayer: Bool = false + @Published var itemToPlay: BaseItemDto = BaseItemDto() +} + +struct ItemView: View { + + @State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view. + @State private var viewDidLoad: Bool = false + @State private var orientation: UIDeviceOrientation = .unknown + @StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem() + @Environment(\.horizontalSizeClass) var hSizeClass + @Environment(\.verticalSizeClass) var vSizeClass + + private let item: BaseItemDto + + init(item: BaseItemDto) { + self.item = item + } + + var body: some View { + if hSizeClass == .compact && vSizeClass == .regular { + ItemPortraitBodyView(item: item, + videoIsLoading: $videoIsLoading, + portraitHeaderView: { item in + ImageView(src: item.getBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), + bh: item.getBackdropImageBlurHash()) + .opacity(0.4) + .blur(radius: 2.0) + }, + portraitStaticOverlayView: { item in + PortraitHeaderOverlayView(item: item) + .environmentObject(DetailItemViewModel(item: item)) + }).environmentObject(videoPlayerItem) + } else { + Text("Hello there") + } + } +} From acd12449cc2582bf16791589609b2d43bc73ec82 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 29 Aug 2021 23:34:37 -0600 Subject: [PATCH 04/14] add next up button constraints --- JellyfinPlayer/VideoPlayer.storyboard | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/JellyfinPlayer/VideoPlayer.storyboard b/JellyfinPlayer/VideoPlayer.storyboard index 25bca725..08b8107e 100644 --- a/JellyfinPlayer/VideoPlayer.storyboard +++ b/JellyfinPlayer/VideoPlayer.storyboard @@ -182,10 +182,13 @@ -