diff --git a/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift b/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift index 6613fec0..4b6987a0 100644 --- a/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift +++ b/JellyfinPlayer tvOS/Components/MediaPlayButtonRowView.swift @@ -11,7 +11,7 @@ import SwiftUI struct MediaPlayButtonRowView: View { - @ObservedObject var viewModel: DetailItemViewModel + @ObservedObject var viewModel: ItemViewModel @State var wrappedScrollView: UIScrollView? var body: some View { diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index e0ad0104..03d3805e 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 */; }; @@ -141,7 +141,6 @@ 5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 5398514726B64E4100101B49 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */; }; - 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; }; 53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BC266B0FF20016769F /* JellyfinAPI */; }; 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; }; 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A83C32268A309300DF3D92 /* LibraryView.swift */; }; @@ -156,11 +155,8 @@ 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDEA2679753200886593 /* ConnectToServerView.swift */; }; 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 53ABFDEC26799D7700886593 /* ActivityIndicator */; }; 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; - 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; }; - 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; }; 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A3F268A49C2002ABD4E /* ItemView.swift */; }; 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A41268A4B38002ABD4E /* MovieItemView.swift */; }; - 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; }; 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; @@ -202,7 +198,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 */; }; @@ -245,8 +240,8 @@ 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; }; 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; - 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* DetailItemViewModel.swift */; }; - 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* DetailItemViewModel.swift */; }; + 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; + 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; @@ -259,9 +254,31 @@ 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 /* ItemPortraitMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */; }; + E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.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 */; }; + E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; + E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; + E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; }; + E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; }; + E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* CardVStackView.swift */; }; + E188460526DEF04800B0C5B7 /* CardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* CardVStackView.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 /* 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 */; }; @@ -367,7 +384,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 = ""; }; @@ -398,11 +415,7 @@ 53913BEB26D323FE00EB3286 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = Localizable.strings; sourceTree = ""; }; 53913BEE26D323FE00EB3286 /* kk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kk; path = Localizable.strings; sourceTree = ""; }; 5398514426B64DA100101B49 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 53987CA326572C1300E7EA70 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = ""; }; - 53987CA526572F0700E7EA70 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; - 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 53A089CF264DA9DA00D57806 /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; 53A83C32268A309300DF3D92 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "JellyfinPlayer tvOS.entitlements"; sourceTree = ""; }; 53ABFDDB267972BF00886593 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.0.sdk/System/Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; @@ -466,7 +479,7 @@ 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemViewModel.swift; sourceTree = ""; }; 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemViewModel.swift; sourceTree = ""; }; 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterViewModel.swift; sourceTree = ""; }; - 62E632F2267D54030063E547 /* DetailItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItemViewModel.swift; sourceTree = ""; }; + 62E632F2267D54030063E547 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; 62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = ""; }; 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; @@ -476,9 +489,21 @@ 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 /* ItemPortraitMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPortraitMainView.swift; sourceTree = ""; }; + E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeMainView.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 = ""; }; + E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = ""; }; + E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = ""; }; + E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = ""; }; + E188460326DEF04800B0C5B7 /* CardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVStackView.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 /* 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 = ""; }; @@ -569,7 +594,7 @@ isa = PBXGroup; children = ( 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, - 62E632F2267D54030063E547 /* DetailItemViewModel.swift */, + 62E632F2267D54030063E547 /* ItemViewModel.swift */, 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, @@ -681,12 +706,13 @@ isa = PBXGroup; children = ( E1FCD08E26C466F3007C8DCF /* Errors */, + 621338912660106C00A81A2A /* Extensions */, + 535870AB2669D8D300D05A09 /* Objects */, + AE8C3157265D6F5E008AA076 /* Resources */, 091B5A852683142E00D78B61 /* ServerLocator */, 62EC352A26766657000E9F2D /* Singleton */, 532175392671BCED005491E6 /* ViewModels */, - 621338912660106C00A81A2A /* Extensions */, - AE8C3157265D6F5E008AA076 /* Resources */, - 535870AB2669D8D300D05A09 /* Objects */, + E1AD105326D96F5A003E4A08 /* Views */, ); path = Shared; sourceTree = ""; @@ -694,7 +720,10 @@ 535870AB2669D8D300D05A09 /* Objects */ = { isa = PBXGroup; children = ( + 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 535870AC2669D8DD00D05A09 /* Typings.swift */, + E1AD104926D94822003E4A08 /* DetailItem.swift */, E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */, ); path = Objects; @@ -746,9 +775,8 @@ 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, 5389276D263C25100035E14B /* ContinueWatchingView.swift */, - 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, 5377CC02263B596B003A4E83 /* Info.plist */, - 535BAE9E2649E569005FA86D /* ItemView.swift */, + E14F7D0A26DB3714007C3AE6 /* ItemView */, 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */, 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */, 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, @@ -756,12 +784,9 @@ 53EE24E5265060780068F029 /* LibrarySearchView.swift */, 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, 53892771263C8C6F0035E14B /* LoadingView.swift */, - 53A089CF264DA9DA00D57806 /* MovieItemView.swift */, 5389276F263C25230035E14B /* NextUpView.swift */, 5377CBFD263B596B003A4E83 /* PersistenceController.swift */, 5377CBFA263B596B003A4E83 /* Preview Content */, - 53987CA326572C1300E7EA70 /* SeasonItemView.swift */, - 53987CA526572F0700E7EA70 /* SeriesItemView.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, @@ -934,7 +959,10 @@ 53F866422687A45400DCD1D7 /* Components */ = { isa = PBXGroup; children = ( + E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */, + E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */, 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */, + E188460326DEF04800B0C5B7 /* CardVStackView.swift */, ); path = Components; sourceTree = ""; @@ -942,20 +970,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 */, + 6267B3D92671138200A7371D /* ImageExtensions.swift */, + E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */, + 621338922660107500A81A2A /* StringExtensions.swift */, 624C21742685CF60007F1390 /* SearchablePickerView.swift */, 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, ); @@ -995,10 +1016,10 @@ 62EC352A26766657000E9F2D /* Singleton */ = { isa = PBXGroup; children = ( - 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, - 62EC352E267666A5000E9F2D /* SessionManager.swift */, 536D3D73267BA8170004248C /* BackgroundManager.swift */, 53649AB0269CFB1900A2D8B7 /* LogManager.swift */, + 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, + 62EC352E267666A5000E9F2D /* SessionManager.swift */, 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */, ); path = Singleton; @@ -1024,12 +1045,65 @@ path = Pods; sourceTree = ""; }; + E14F7D0A26DB3714007C3AE6 /* ItemView */ = { + isa = PBXGroup; + children = ( + 535BAE9E2649E569005FA86D /* ItemView.swift */, + E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */, + E18845FB26DEACC400B0C5B7 /* Landscape */, + E18845FA26DEACBE00B0C5B7 /* Portrait */, + ); + path = ItemView; + sourceTree = ""; + }; + E18845FA26DEACBE00B0C5B7 /* Portrait */ = { + isa = PBXGroup; + children = ( + E14F7D0626DB36EF007C3AE6 /* ItemPortraitMainView.swift */, + E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */, + ); + path = Portrait; + sourceTree = ""; + }; + E18845FB26DEACC400B0C5B7 /* Landscape */ = { + isa = PBXGroup; + children = ( + E14F7D0826DB36F7007C3AE6 /* ItemLandscapeMainView.swift */, + E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */, + ); + path = Landscape; + sourceTree = ""; + }; + E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { + isa = PBXGroup; + children = ( + 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, + E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, + E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.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 = ""; @@ -1385,6 +1459,7 @@ buildActionMask = 2147483647; files = ( 531069572684E7EE00CFFDBA /* InfoTabBarViewController.swift in Sources */, + E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, 6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */, 531069592684E7EE00CFFDBA /* SubtitlesView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, @@ -1406,6 +1481,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 */, @@ -1416,18 +1492,21 @@ 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 5310695A2684E7EE00CFFDBA /* VideoPlayer.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, - 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */, + 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + E1AD106326D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.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 */, @@ -1453,10 +1532,14 @@ 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, + 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, + E1AD105D26D9ABDD003E4A08 /* PillHStackView.swift in Sources */, + E188460526DEF04800B0C5B7 /* CardVStackView.swift in Sources */, 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 6220D0BE26D60D6600B8E046 /* ItemViewModel.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 */, ); @@ -1466,6 +1549,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, + E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */, 5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */, 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, @@ -1480,6 +1565,7 @@ 62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */, 5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */, 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */, + E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */, 53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */, 62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, @@ -1489,14 +1575,16 @@ 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, - 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */, + E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */, 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.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 */, @@ -1506,25 +1594,30 @@ E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 532E68CF267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift in Sources */, + E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, + E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, - 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */, + 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */, 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, + E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, + E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */, 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.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 */, @@ -1532,7 +1625,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 */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */, 6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */, @@ -1557,11 +1652,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/CardVStackView.swift b/JellyfinPlayer/Components/CardVStackView.swift new file mode 100644 index 00000000..84583dd9 --- /dev/null +++ b/JellyfinPlayer/Components/CardVStackView.swift @@ -0,0 +1,105 @@ +// + /* + * 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 CardVStackView: View { + + let items: [BaseItemDto] + + private func buildCardOverlayView(item: BaseItemDto) -> some View { + HStack { + ZStack { + if item.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) + } + } + .padding(.leading, 2) + .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) + .opacity(1) + + ZStack { + if item.userData?.played ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.jellyfinPurple) + } + }.padding(2) + .opacity(1) + } + } + + var body: some View { + VStack { + ForEach(items, id: \.id) { item in + NavigationLink(destination: ItemNavigationView(item: item)) { + + HStack { + + // MARK: Image + ImageView(src: item.getPrimaryImage(maxWidth: 150), + bh: item.getPrimaryImageBlurHash(), + failureInitials: item.failureInitials) + .frame(width: 150, height: 100) + .cornerRadius(10) + .overlay( + Rectangle() + .fill(Color.jellyfinPurple) + .mask(ProgressBar()) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0 * 1.5), height: 7) + .padding(0), alignment: .bottomLeading + ) + .overlay(buildCardOverlayView(item: item), alignment: .topTrailing) + + VStack(alignment: .leading) { + + // MARK: Title + Text(item.title) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.primary) + .lineLimit(2) + + HStack { + Text(item.getEpisodeLocator() ?? "") + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + + Text(item.getItemRuntime()) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + + Spacer() + } + + // MARK: Overview + Text(item.overview ?? "") + .font(.footnote) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .lineLimit(4) + + Spacer() + } + } + .padding(.horizontal, 16) + } + } + } + } +} diff --git a/JellyfinPlayer/Components/PillHStackView.swift b/JellyfinPlayer/Components/PillHStackView.swift new file mode 100644 index 00000000..3c6fcf06 --- /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.systemFill) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .cornerRadius(10) + + Text(item.title) + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(.primary) + .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..96e32645 --- /dev/null +++ b/JellyfinPlayer/Components/PortraitHStackView.swift @@ -0,0 +1,87 @@ +// + /* + * 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 } + var failureInitials: String { get } +} + +struct PortraitImageHStackView: View { + + 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) { + topBarView() + + ScrollView(.horizontal, showsIndicators: false) { + VStack { + Spacer().frame(height: 8) + HStack(alignment: .top) { + + 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, + failureInitials: item.failureInitials) + .frame(width: 100, height: CGFloat(maxWidth)) + .cornerRadius(10) + .shadow(radius: 4, y: 2) + + Text(item.title) + .font(.footnote) + .fontWeight(.regular) + .frame(width: 100) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + .lineLimit(2) + + if let description = item.description { + Text(description) + .font(.caption) + .fontWeight(.medium) + .frame(width: 100) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .lineLimit(2) + } + } + }) + } + Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) + } + } + }.padding(.top, -3) + } + } +} diff --git a/JellyfinPlayer/Components/PortraitItemView.swift b/JellyfinPlayer/Components/PortraitItemView.swift index a2441314..90dc7738 100644 --- a/JellyfinPlayer/Components/PortraitItemView.swift +++ b/JellyfinPlayer/Components/PortraitItemView.swift @@ -1,86 +1,91 @@ // -/* - * 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 JellyfinAPI -import Stinsen import SwiftUI +import JellyfinAPI struct PortraitItemView: View { + var item: BaseItemDto var body: some View { - VStack(alignment: .leading) { - ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), - bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) - .frame(width: 100, height: 150) - .cornerRadius(10) - .shadow(radius: 4, y: 2) - .shadow(radius: 4, y: 2) - .overlay(Rectangle() - .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) - .mask(ProgressBar()) - .frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7) - .padding(0), alignment: .bottomLeading) - .overlay(ZStack { - if item.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } - } - .padding(.leading, 2) - .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) - .opacity(1), alignment: .bottomLeading) - .overlay(ZStack { - if item.userData?.played ?? false { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.accentColor) - .background(Color(.white)) - .clipShape(Circle().scale(0.8)) - } else { - if item.userData?.unplayedItemCount != nil { - Capsule() - .fill(Color.accentColor) - .frame(minWidth: 20, minHeight: 20, maxHeight: 20) - Text(String(item.userData!.unplayedItemCount ?? 0)) - .foregroundColor(.white) - .font(.caption2) - .padding(2) + NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) { + VStack(alignment: .leading) { + ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) + .frame(width: 100, height: 150) + .cornerRadius(10) + .shadow(radius: 4, y: 2) + .shadow(radius: 4, y: 2) + .overlay( + Rectangle() + .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) + .mask(ProgressBar()) + .frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7) + .padding(0), alignment: .bottomLeading + ) + .overlay( + ZStack { + if item.userData?.isFavorite ?? false { + Image(systemName: "circle.fill") + .foregroundColor(.white) + .opacity(0.6) + Image(systemName: "heart.fill") + .foregroundColor(Color(.systemRed)) + .font(.system(size: 10)) + } } - } - }.padding(2) - .fixedSize() - .opacity(1), alignment: .topTrailing).opacity(1) - Text(item.seriesName ?? item.name ?? "") - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - if item.type == "Movie" || item.type == "Series" { - Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") - .foregroundColor(.secondary) + .padding(.leading, 2) + .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) + .opacity(1), alignment: .bottomLeading) + .overlay( + ZStack { + if item.userData?.played ?? false { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.accentColor) + .background(Color(.white)) + .clipShape(Circle().scale(0.8)) + } else { + if item.userData?.unplayedItemCount != nil { + Capsule() + .fill(Color.accentColor) + .frame(minWidth: 20, minHeight: 20, maxHeight: 20) + Text(String(item.userData!.unplayedItemCount ?? 0)) + .foregroundColor(.white) + .font(.caption2) + .padding(2) + } + } + }.padding(2) + .fixedSize() + .opacity(1), alignment: .topTrailing).opacity(1) + Text(item.seriesName ?? item.name ?? "") .font(.caption) - .fontWeight(.medium) - } else if item.type == "Season" { - Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } else { - Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") - .foregroundColor(.secondary) - .font(.caption) - .fontWeight(.medium) - } - }.frame(width: 100) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + if item.type == "Movie" || item.type == "Series" { + Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else if item.type == "Season" { + Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } else { + Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") + .foregroundColor(.secondary) + .font(.caption) + .fontWeight(.medium) + } + }.frame(width: 100) + } } } diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index bc5c8698..b0ed52d9 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -8,7 +8,6 @@ import SwiftUI import JellyfinAPI -import Stinsen struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { @@ -32,17 +31,13 @@ struct ProgressBar: Shape { } struct ContinueWatchingView: View { - @EnvironmentObject var homeRouter: NavigationRouter - var items: [BaseItemDto] var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { ForEach(items, id: \.id) { item in - Button { - homeRouter.route(to: .item(viewModel: .init(id: item.id!))) - } label: { + NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) { VStack(alignment: .leading) { ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash()) .frame(width: 320, height: 180) @@ -63,7 +58,7 @@ struct ContinueWatchingView: View { .foregroundColor(.primary) .lineLimit(1) if item.type == "Episode" { - Text("• \(item.getEpisodeLocator()) - \(item.name ?? "")") + Text("• \(item.getEpisodeLocator() ?? "") - \(item.name ?? "")") .font(.callout) .fontWeight(.semibold) .foregroundColor(.secondary) diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift deleted file mode 100644 index a65390a6..00000000 --- a/JellyfinPlayer/EpisodeItemView.swift +++ /dev/null @@ -1,431 +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 Stinsen -import SwiftUI - -struct EpisodeItemView: View { - @EnvironmentObject var itemRouter: NavigationRouter - @StateObject var viewModel: EpisodeItemViewModel - @State private var orientation = UIDeviceOrientation.unknown - @Environment(\.horizontalSizeClass) var hSizeClass - @Environment(\.verticalSizeClass) var vSizeClass - @EnvironmentObject private var playbackInfo: VideoPlayerItem - - 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 { - VStack(alignment: .leading) { - HStack(alignment: .bottom, spacing: 12) { - ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), bh: viewModel.item.getSeriesPrimaryImageBlurHash()) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { - Spacer() - Text(viewModel.item.name ?? "").font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(y: 5) - HStack { - Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - Text(viewModel.item.getItemRuntime()).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)) - } - } - .padding(.top, 1) - } - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) - } - HStack { - // Play button - Button { - self.playbackInfo.shouldShowPlayer = true - } label: { - HStack { - Text(viewModel.item.getItemProgressString() == "" ? "Play" : viewModel.item.getItemProgressString()) - .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 { - viewModel.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(alignment: .leading) { - if hSizeClass == .compact && vSizeClass == .regular { - 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 !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - if !(viewModel.item.genreItems ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.genreItems!, id: \.id) { genre in - Button { - itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) - } label: { - Text(genre.name ?? "").font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - if !(viewModel.item.people ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(viewModel.item.people!, id: \.self) { person in - if person.type! == "Actor" { - Button { - itemRouter.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) - } label: { - 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) - } - } - } - Spacer().frame(width: 10) - } - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -3) - } - if !(viewModel.item.studios ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - if !viewModel.similarItems.isEmpty { - Text("More Like This") - .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(viewModel.similarItems, id: \.self) { similarItem in - Button { - itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) - } label: { - PortraitItemView(item: similarItem) - } - Spacer().frame(width: 10) - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -5) - } - Spacer().frame(height: 3) - } - } - } else { - GeometryReader { geometry in - ZStack { - ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), bh: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.3) - .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 { - VStack { - ImageView(src: viewModel.item.getSeriesPrimaryImage(maxWidth: 120), - bh: viewModel.item.getSeriesPrimaryImageBlurHash()) - .frame(width: 120, height: 180) - .cornerRadius(10) - Spacer().frame(height: 15) - Button { - self.playbackInfo.shouldShowPlayer = true - } label: { - HStack { - Text(viewModel.item.getItemProgressString() == "" ? "Play" : viewModel.item.getItemProgressString()) - .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() - } - ScrollView { - VStack(alignment: .leading) { - HStack { - VStack(alignment: .leading) { - Text(viewModel.item.name ?? "").font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(x: 14, y: 0) - Spacer().frame(height: 1) - HStack { - Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - Text(viewModel.item.getItemRuntime()).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)) - } - if viewModel.item.communityRating != nil { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(String(viewModel.item.communityRating!)).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - Spacer() - }.frame(maxWidth: .infinity, alignment: .leading) - .offset(x: 14) - .padding(.top, 1) - - }.frame(maxWidth: .infinity, alignment: .leading) - Spacer() - HStack { - Button { - viewModel.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(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - Text(viewModel.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 !(viewModel.item.genreItems ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.genreItems!, id: \.id) { genre in - Button { - itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) - } label: { - Text(genre.name ?? "").font(.footnote) - } - } - } - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - if !(viewModel.item.people ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(viewModel.item.people!, id: \.self) { person in - if person.type! == "Actor" { - Button { - itemRouter - .route(to: .library(viewModel: .init(person: person), - title: person.name ?? "")) - } label: { - 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) - } - } - } - Spacer().frame(width: 10) - } - } - Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - }.padding(.top, -3) - } - if !(viewModel.item.studios ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - } - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - if !viewModel.similarItems.isEmpty { - Text("More Like This") - .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(viewModel.similarItems, id: \.self) { similarItem in - Button { - itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) - } label: { - PortraitItemView(item: similarItem) - } - Spacer().frame(width: 10) - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -5) - } - Spacer().frame(height: 195) - }.frame(maxHeight: .infinity) - } - }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - .edgesIgnoringSafeArea(.leading) - } - } - } - } - .onRotate(perform: { orientation in - self.orientation = orientation - }) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("\(viewModel.item.seriesName ?? "") - S\(String(viewModel.item.parentIndexNumber ?? 0)):E\(String(viewModel.item.indexNumber ?? 0))") - } -} diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index cd580b91..797f8e06 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -8,12 +8,19 @@ */ import Foundation -import Stinsen import SwiftUI struct HomeView: View { - @EnvironmentObject var homeRouter: NavigationRouter @StateObject var viewModel = HomeViewModel() + @State var showingSettings = false + + init() { + let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill") + let barAppearance = UINavigationBar.appearance() + barAppearance.backIndicatorImage = backButtonBackgroundImage + barAppearance.backIndicatorTransitionMaskImage = backButtonBackgroundImage + barAppearance.tintColor = UIColor(Color.jellyfinPurple) + } @ViewBuilder var innerBody: some View { @@ -21,7 +28,7 @@ struct HomeView: View { ProgressView() } else { ScrollView { - LazyVStack(alignment: .leading) { + VStack(alignment: .leading) { if !viewModel.resumeItems.isEmpty { ContinueWatchingView(items: viewModel.resumeItems) } @@ -36,18 +43,16 @@ struct HomeView: View { .font(.title2) .fontWeight(.bold) Spacer() - Button { - homeRouter - .route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), - title: library?.name ?? "")) - } label: { + NavigationLink(destination: LazyView { + LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "") + }) { HStack { Text("See All").font(.subheadline).fontWeight(.bold) Image(systemName: "chevron.right").font(Font.subheadline.bold()) } } }.padding(.leading, 16) - .padding(.trailing, 16) + .padding(.trailing, 16) LatestMediaView(viewModel: .init(libraryID: libraryID)) } } @@ -63,17 +68,14 @@ struct HomeView: View { .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { - homeRouter.route(to: .settings) + showingSettings = true } label: { Image(systemName: "gear") } } } - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - AppURLHandler.shared.appURLState = .allowed - AppURLHandler.shared.processLaunchedURLIfNeeded() - } + .fullScreenCover(isPresented: $showingSettings) { + SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) } } } diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift deleted file mode 100644 index 2b6e8719..00000000 --- a/JellyfinPlayer/ItemView.swift +++ /dev/null @@ -1,58 +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 Introspect -import JellyfinAPI -import Stinsen -import SwiftUI - -class VideoPlayerItem: ObservableObject { - @Published var shouldShowPlayer: Bool = false -} - -struct ItemView: View { - @EnvironmentObject var itemRouter: NavigationRouter - @StateObject var viewModel: ItemViewModel - @StateObject private var 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 - - var body: some View { - ZStack { - Group { - if let item = viewModel.item { - 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 :(") - } - } - } - .introspectTabBarController { UITabBarController in - UITabBarController.tabBar.isHidden = false - } - .navigationBarHidden(false) - .navigationBarBackButtonHidden(false) - .environmentObject(videoPlayerItem) - } - .onReceive(videoPlayerItem.$shouldShowPlayer) { flag in - guard flag, - let item = viewModel.item else { return } - self.itemRouter.route(to: .videoPlayer(item: item)) - } - if viewModel.isLoading { - ProgressView() - } - } -} diff --git a/JellyfinPlayer/ItemView/ItemView.swift b/JellyfinPlayer/ItemView/ItemView.swift new file mode 100644 index 00000000..e3669681 --- /dev/null +++ b/JellyfinPlayer/ItemView/ItemView.swift @@ -0,0 +1,69 @@ +/* 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() +} + +// Intermediary view for ItemView to set navigation bar settings +struct ItemNavigationView: View { + + private let item: BaseItemDto + + init(item: BaseItemDto) { + self.item = item + } + + var body: some View { + ItemView(item: item) + .navigationBarTitle("", displayMode: .inline) + } +} + +fileprivate 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) private var hSizeClass + @Environment(\.verticalSizeClass) private var vSizeClass + + private let viewModel: ItemViewModel + + init(item: BaseItemDto) { + switch item.itemType { + case .movie: + self.viewModel = MovieItemViewModel(item: item) + case .season: + self.viewModel = SeasonItemViewModel(item: item) + case .episode: + self.viewModel = EpisodeItemViewModel(item: item) + case .series: + self.viewModel = SeriesItemViewModel(item: item) + default: + self.viewModel = ItemViewModel(item: item) + } + } + + var body: some View { + if hSizeClass == .compact && vSizeClass == .regular { + ItemPortraitMainView(videoIsLoading: $videoIsLoading) + .environmentObject(videoPlayerItem) + .environmentObject(viewModel) + } else { + ItemLandscapeMainView(videoIsLoading: $videoIsLoading) + .environmentObject(videoPlayerItem) + .environmentObject(viewModel) + } + } +} diff --git a/JellyfinPlayer/ItemView/ItemViewBody.swift b/JellyfinPlayer/ItemView/ItemViewBody.swift new file mode 100644 index 00000000..0050b01c --- /dev/null +++ b/JellyfinPlayer/ItemView/ItemViewBody.swift @@ -0,0 +1,88 @@ +// + /* + * 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 ItemViewBody: View { + + @EnvironmentObject private var viewModel: ItemViewModel + + var body: some View { + VStack(alignment: .leading) { + + // MARK: Overview + Text(viewModel.item.overview ?? "") + .font(.footnote) + .padding(.horizontal, 16) + .padding(.vertical, 3) + + // MARK: Seasons + if let seriesViewModel = viewModel as? SeriesItemViewModel { + PortraitImageHStackView(items: seriesViewModel.seasons, + maxWidth: 150, + topBarView: { + Text("Seasons") + .font(.callout) + .fontWeight(.semibold) + .padding(.top, 3) + .padding(.leading, 16) + }, navigationView: { season in + ItemNavigationView(item: season) + }) + } + + // MARK: Genres + PillHStackView(title: "Genres", + items: viewModel.item.genreItems ?? []) { genre in + LibraryView(viewModel: .init(genre: genre), title: genre.title) + } + + // MARK: Studios + if let studios = viewModel.item.studios { + PillHStackView(title: "Studios", + items: studios) { studio in + LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "") + } + } + + // MARK: Cast & Crew + if let castAndCrew = viewModel.item.people { + PortraitImageHStackView(items: castAndCrew.filter({ BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") }), + maxWidth: 150, + topBarView: { + Text("Cast & Crew") + .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 + if !viewModel.similarItems.isEmpty { + PortraitImageHStackView(items: viewModel.similarItems, + maxWidth: 150, + topBarView: { + Text("More Like This") + .font(.callout) + .fontWeight(.semibold) + .padding(.top, 3) + .padding(.leading, 16) + }, + navigationView: { item in + ItemNavigationView(item: item) + }) + } + } + } +} diff --git a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift new file mode 100644 index 00000000..4ebb9061 --- /dev/null +++ b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeMainView.swift @@ -0,0 +1,114 @@ +// + /* + * 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 ItemLandscapeMainView: View { + + @Binding private var videoIsLoading: Bool + @EnvironmentObject private var viewModel: ItemViewModel + @EnvironmentObject private var videoPlayerItem: VideoPlayerItem + + init(videoIsLoading: Binding) { + self._videoIsLoading = videoIsLoading + } + + // MARK: innerBody + private var innerBody: some View { + HStack { + + // MARK: Sidebar Image + VStack { + ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130), + bh: viewModel.item.getPrimaryImageBlurHash()) + .frame(width: 130, height: 195) + .cornerRadius(10) + + Spacer().frame(height: 15) + + Button { + if let playButtonItem = viewModel.playButtonItem { + self.videoPlayerItem.itemToPlay = playButtonItem + self.videoPlayerItem.shouldShowPlayer = true + } + } label: { + + // MARK: Play + HStack { + Image(systemName: "play.fill") + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) + .font(.system(size: 20)) + Text(viewModel.playButtonText()) + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) + .font(.callout) + .fontWeight(.semibold) + } + .frame(width: 130, height: 40) + .background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) + .cornerRadius(10) + }.disabled(viewModel.playButtonItem == nil) + + Spacer() + } + + ScrollView { + VStack(alignment: .leading) { + + // MARK: ItemLandscapeTopBarView + ItemLandscapeTopBarView() + .environmentObject(viewModel) + + // MARK: ItemViewBody + if let episodeViewModel = viewModel as? SeasonItemViewModel { + CardVStackView(items: episodeViewModel.episodes) + } else { + ItemViewBody() + .environmentObject(viewModel) + } + } + } + } + } + + // MARK: body + 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() + } + + ZStack { + + // MARK: Backdrop + ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), + bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.3) + .edgesIgnoringSafeArea(.all) + .blur(radius: 4) + + // iPadOS is making the view go all the way to the edge. + // We have to accomodate this here + if UIDevice.current.userInterfaceIdiom == .pad { + innerBody.padding(.horizontal, 25) + } else { + innerBody + } + } + } + } +} diff --git a/JellyfinPlayer/ItemView/Landscape/ItemLandscapeTopBarView.swift b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeTopBarView.swift new file mode 100644 index 00000000..24a9e15a --- /dev/null +++ b/JellyfinPlayer/ItemView/Landscape/ItemLandscapeTopBarView.swift @@ -0,0 +1,93 @@ +// + /* + * 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 ItemLandscapeTopBarView: View { + + @EnvironmentObject private var viewModel: ItemViewModel + + var body: some View { + HStack { + VStack(alignment: .leading) { + + // MARK: Name + + Text(viewModel.getItemDisplayName()) + .font(.title) + .fontWeight(.semibold) + .foregroundColor(.primary) + .padding(.leading, 16) + .padding(.bottom, 10) + + if viewModel.item.itemType.showDetails { + // MARK: Runtime + Text(viewModel.item.getItemRuntime()) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .padding(.leading, 16) + } + + // MARK: Details + HStack { + if viewModel.item.productionYear != nil { + Text(String(viewModel.item.productionYear ?? 0)) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + } + + if viewModel.item.officialRating != nil { + Text(viewModel.item.officialRating!) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + } + + Spacer() + + if viewModel.item.itemType.showDetails { + // MARK: Favorite + Button { + viewModel.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) + + // MARK: Watched + 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(.leading, 16) + } + } + } +} diff --git a/JellyfinPlayer/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift b/JellyfinPlayer/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift new file mode 100644 index 00000000..cb7c28a5 --- /dev/null +++ b/JellyfinPlayer/ItemView/Portrait/ItemPortraitHeaderOverlayView.swift @@ -0,0 +1,137 @@ +// + /* + * 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 private var viewModel: ItemViewModel + @EnvironmentObject private var videoPlayerItem: VideoPlayerItem + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .bottom, spacing: 12) { + + // MARK: Portrait Image + ImageView(src: viewModel.item.portraitHeaderViewURL(maxWidth: 130)) + .frame(width: 130, height: 195) + .cornerRadius(10) + + VStack(alignment: .leading, spacing: 1) { + Spacer() + + // MARK: Name + Text(viewModel.getItemDisplayName()) + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom, 10) + + if viewModel.item.itemType.showDetails { + // MARK: Runtime + if viewModel.shouldDisplayRuntime() { + Text(viewModel.item.getItemRuntime()) + .font(.subheadline) + .fontWeight(.medium) + .foregroundColor(.secondary) + .lineLimit(1) + } + } + + // MARK: Details + HStack { + if let productionYear = viewModel.item.productionYear { + Text(String(productionYear)) + .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(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) + } + + HStack { + + // MARK: Play + Button { + if let playButtonItem = viewModel.playButtonItem { + self.videoPlayerItem.itemToPlay = playButtonItem + self.videoPlayerItem.shouldShowPlayer = true + } + } label: { + HStack { + Image(systemName: "play.fill") + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) + .font(.system(size: 20)) + Text(viewModel.playButtonText()) + .foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white) + .font(.callout) + .fontWeight(.semibold) + } + .frame(width: 130, height: 40) + .background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple) + .cornerRadius(10) + }.disabled(viewModel.playButtonItem == nil) + + Spacer() + + if viewModel.item.itemType.showDetails { + // MARK: Favorite + Button { + viewModel.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) + + // MARK: Watched + Button { + viewModel.updateWatchState() + } label: { + 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)) + } + } + .disabled(viewModel.isLoading) + } + }.padding(.top, 8) + } + .padding(.horizontal, 16) + .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? -189 : -64) + } +} diff --git a/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift b/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift new file mode 100644 index 00000000..7ccac0ee --- /dev/null +++ b/JellyfinPlayer/ItemView/Portrait/ItemPortraitMainView.swift @@ -0,0 +1,75 @@ +// + /* + * 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 ItemPortraitMainView: View { + + @Binding private var videoIsLoading: Bool + @EnvironmentObject private var viewModel: ItemViewModel + @EnvironmentObject private var videoPlayerItem: VideoPlayerItem + + init(videoIsLoading: Binding) { + self._videoIsLoading = videoIsLoading + } + + // MARK: portraitHeaderView + var portraitHeaderView: some View { + ImageView(src: viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)), + bh: viewModel.item.getBackdropImageBlurHash()) + .opacity(0.4) + .blur(radius: 2.0) + } + + // MARK: portraitStaticOverlayView + var portraitStaticOverlayView: some View { + PortraitHeaderOverlayView() + .environmentObject(viewModel) + } + + // MARK: body + 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: ParallaxScrollView + ParallaxHeaderScrollView(header: portraitHeaderView, + staticOverlayView: portraitStaticOverlayView, + overlayAlignment: .bottomLeading, + headerHeight: UIScreen.main.bounds.width * 0.5625) { + + VStack { + Spacer() + .frame(height: 70) + + if let episodeViewModel = viewModel as? SeasonItemViewModel { + Spacer() + CardVStackView(items: episodeViewModel.episodes) + .padding(.top, 5) + } else { + ItemViewBody() + .environmentObject(viewModel) + } + } + } + } + } +} diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index cb7d08c9..58f50b03 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -241,8 +241,7 @@ struct JellyfinPlayerApp: App { } private func setupAppearance() { - guard let storedAppearance = AppAppearance(rawValue: appAppearance) else { return } - UIApplication.shared.windows.first?.overrideUserInterfaceStyle = storedAppearance.style + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style } } diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift deleted file mode 100644 index 2e183b71..00000000 --- a/JellyfinPlayer/MovieItemView.swift +++ /dev/null @@ -1,439 +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 Stinsen -import SwiftUI - -struct MovieItemView: View { - @EnvironmentObject var itemRouter: NavigationRouter - @StateObject var viewModel: MovieItemViewModel - @State private var orientation = UIDeviceOrientation.unknown - @Environment(\.horizontalSizeClass) - var hSizeClass - @Environment(\.verticalSizeClass) - var vSizeClass - @EnvironmentObject - private var playbackInfo: VideoPlayerItem - - 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 { - VStack(alignment: .leading) { - HStack(alignment: .bottom, spacing: 12) { - ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120)) - .frame(width: 120, height: 180) - .cornerRadius(10) - VStack(alignment: .leading) { - Spacer() - Text(viewModel.item.name ?? "").font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - .offset(y: 5) - HStack { - if viewModel.item.productionYear != nil { - Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - Text(viewModel.item.getItemRuntime()).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)) - } - } - .padding(.top, 1) - } - .padding(.bottom, UIDevice.current.userInterfaceIdiom == .pad ? 98 : 30) - } - HStack { - // Play button - Button { - self.playbackInfo.shouldShowPlayer = true - } label: { - HStack { - Text(viewModel.item.getItemProgressString() == "" ? "Play" : viewModel.item.getItemProgressString()) - .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 { - viewModel.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(alignment: .leading) { - if hSizeClass == .compact && vSizeClass == .regular { - 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 !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - if !(viewModel.item.genreItems ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.genreItems!, id: \.id) { genre in - Button { - itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) - } label: { - Text(genre.name ?? "").font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - if !(viewModel.item.people ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(viewModel.item.people!, id: \.self) { person in - if person.type ?? "" == "Actor" { - Button { - itemRouter.route(to: .library(viewModel: .init(person: person), title: person.name ?? "")) - } label: { - 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 != nil { - Text(person.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 !(viewModel.item.studios ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - if !viewModel.similarItems.isEmpty { - Text("More Like This") - .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(viewModel.similarItems, id: \.self) { similarItem in - Button { - itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) - } label: { - PortraitItemView(item: similarItem) - } - Spacer().frame(width: 10) - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -5) - } - Spacer().frame(height: 16) - } - } - } else { - GeometryReader { geometry in - ZStack { - ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200), - bh: viewModel.item.getBackdropImageBlurHash()) - .opacity(0.3) - .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 { - VStack { - ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120), - bh: viewModel.item.getPrimaryImageBlurHash()) - .frame(width: 120, height: 180) - .cornerRadius(10) - Spacer().frame(height: 15) - Button { - self.playbackInfo.shouldShowPlayer = true - } label: { - HStack { - Text(viewModel.item - .getItemProgressString() == "" ? "Play" : "\(viewModel.item.getItemProgressString()) 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() - } - ScrollView { - VStack(alignment: .leading) { - HStack { - VStack(alignment: .leading) { - Text(viewModel.item.name ?? "").font(.headline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .offset(x: 14, y: 0) - Spacer().frame(height: 1) - HStack { - if viewModel.item.productionYear != nil { - Text(String(viewModel.item.productionYear ?? 0)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - Text(viewModel.item.getItemRuntime()).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)) - } - if viewModel.item.communityRating != nil { - HStack { - Image(systemName: "star").foregroundColor(.secondary) - Text(String(viewModel.item.communityRating!)).font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.secondary) - .lineLimit(1) - .offset(x: -7, y: 0.7) - } - } - Spacer() - }.frame(maxWidth: .infinity, alignment: .leading) - .offset(x: 14) - .padding(.top, 1) - }.frame(maxWidth: .infinity, alignment: .leading) - Spacer() - HStack { - Button { - viewModel.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(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - if !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - Text(viewModel.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 !(viewModel.item.genreItems ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.genreItems!, id: \.id) { genre in - Button { - itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) - } label: { - Text(genre.name ?? "").font(.footnote) - } - } - } - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - if !(viewModel.item.people ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - VStack { - Spacer().frame(height: 8) - HStack { - Spacer().frame(width: 16) - ForEach(viewModel.item.people!, id: \.self) { person in - if person.type! == "Actor" { - Button { - itemRouter - .route(to: .library(viewModel: .init(person: person), - title: person.name ?? "")) - } label: { - 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) - } - } - } - Spacer().frame(width: 10) - } - } - Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - }.padding(.top, -3) - } - if !(viewModel.item.studios ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - } - .padding(.leading, 16) - .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - } - } - if !viewModel.similarItems.isEmpty { - Text("More Like This") - .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(viewModel.similarItems, id: \.self) { similarItem in - Button { - itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) - } label: { - PortraitItemView(item: similarItem) - } - Spacer().frame(width: 10) - } - Spacer().frame(width: 16) - } - } - }.padding(.top, -5) - } - Spacer().frame(height: 105) - }.frame(maxHeight: .infinity) - } - }.padding(.top, 16).padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - .edgesIgnoringSafeArea(.leading) - } - } - } - } - .onRotate { - orientation = $0 - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(viewModel.item.name ?? "") - } -} diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift deleted file mode 100644 index 8c52b78c..00000000 --- a/JellyfinPlayer/SeasonItemView.swift +++ /dev/null @@ -1,261 +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 Stinsen -import SwiftUI - -struct SeasonItemView: View { - @EnvironmentObject var itemRouter: NavigationRouter - @StateObject var viewModel: SeasonItemViewModel - @State private var orientation = UIDeviceOrientation.unknown - @Environment(\.horizontalSizeClass) var hSizeClass - @Environment(\.verticalSizeClass) var vSizeClass - - @ViewBuilder - var portraitHeaderView: some View { - if viewModel.isLoading { - EmptyView() - } else { - ImageView(src: viewModel.item - .getSeriesBackdropImage(maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), - bh: viewModel.item.getSeriesBackdropImageBlurHash()) - .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) - if viewModel.item.productionYear != nil { - Text(String(viewModel.item.productionYear!)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - }.offset(y: -32) - }.padding(.horizontal, 16) - .offset(y: 22) - } - - @ViewBuilder - var innerBody: some View { - if hSizeClass == .compact && vSizeClass == .regular { - ParallaxHeaderScrollView(header: portraitHeaderView, - staticOverlayView: portraitHeaderOverlayView, - overlayAlignment: .bottomLeading, - headerHeight: UIScreen.main.bounds.width * 0.5625) { - LazyVStack(alignment: .leading) { - if !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - ForEach(viewModel.episodes, id: \.id) { episode in - Button { - itemRouter.route(to: .item(viewModel: .init(id: episode.id!))) - } label: { - HStack { - ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) - .shadow(radius: 5) - .frame(width: 150, height: 90) - .cornerRadius(10) - .overlay(Rectangle() - .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) - .mask(ProgressBar()) - .frame(width: CGFloat(episode.userData?.playedPercentage ?? 0 * 1.5), height: 7) - .padding(0), alignment: .bottomLeading) - .overlay(ZStack { - if episode.userData?.isFavorite ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - .opacity(0.6) - Image(systemName: "heart.fill") - .foregroundColor(Color(.systemRed)) - .font(.system(size: 10)) - } - } - .padding(.leading, 2) - .padding(.bottom, episode.userData?.playedPercentage == nil ? 2 : 9) - .opacity(1), alignment: .bottomLeading) - .overlay(ZStack { - if episode.userData?.played ?? false { - Image(systemName: "circle.fill") - .foregroundColor(.white) - Image(systemName: "checkmark.circle.fill") - .foregroundColor(Color(.systemBlue)) - } - }.padding(2) - .opacity(1), alignment: .topTrailing).opacity(1) - VStack(alignment: .leading) { - HStack { - Text(episode.getEpisodeLocator()).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - Spacer() - Text(episode.name ?? "").font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(1) - Spacer() - Text(episode.getItemRuntime()).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - Spacer() - Text(episode.overview ?? "").font(.footnote).foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true).lineLimit(4) - Spacer() - }.padding(.trailing, 20).offset(y: 2) - }.offset(x: 12, y: 0) - } - } - if !(viewModel.item.studios ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - Spacer().frame(height: 10) - } - .padding(.leading, 2) - .padding(.top, 20) - } - } 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) - HStack { - VStack(alignment: .leading) { - Spacer().frame(height: 16) - ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 120), bh: viewModel.item.getPrimaryImageBlurHash()) - .frame(width: 120, height: 180) - .cornerRadius(10) - Spacer().frame(height: 4) - if viewModel.item.productionYear != nil { - Text(String(viewModel.item.productionYear!)).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - } - Spacer() - } - ScrollView { - Spacer().frame(height: 16) - LazyVStack(alignment: .leading) { - if !(viewModel.item.taglines ?? []).isEmpty { - Text(viewModel.item.taglines!.first!).font(.body).italic().padding(.top, 7) - .fixedSize(horizontal: false, vertical: true).padding(.leading, 16) - .padding(.trailing, 16) - } - Text(viewModel.item.overview ?? "").font(.footnote).padding(.top, 3) - .fixedSize(horizontal: false, vertical: true).padding(.bottom, 3).padding(.leading, 16) - .padding(.trailing, 16) - ForEach(viewModel.episodes, id: \.id) { episode in - Button { - itemRouter.route(to: .item(viewModel: .init(id: episode.id!))) - } label: { - HStack { - ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash()) - .shadow(radius: 5) - .frame(width: 150, height: 90) - .cornerRadius(10) - .overlay(Rectangle() - .fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255)) - .mask(ProgressBar()) - .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) - .padding(0), alignment: .bottomLeading) - VStack(alignment: .leading) { - HStack { - Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))") - .font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - Spacer() - Text(episode.name ?? "").font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(1) - Spacer() - Text(episode.getItemRuntime()).font(.subheadline) - .fontWeight(.medium) - .foregroundColor(.secondary) - .lineLimit(1) - } - Spacer() - Text(episode.overview ?? "").font(.footnote).foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true).lineLimit(4) - Spacer() - }.padding(.trailing, 20).offset(y: 2) - }.offset(x: 12, y: 0) - } - } - if !(viewModel.item.studios ?? []).isEmpty { - ScrollView(.horizontal, showsIndicators: false) { - HStack { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(viewModel.item.studios!, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - }.padding(.leading, 16).padding(.trailing, 16) - } - } - Spacer().frame(height: 95) - }.frame(maxHeight: .infinity) - }.padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) - }.padding(.leading, UIDevice.current.userInterfaceIdiom == .pad ? 16 : 0) - } - } - } - } - - var body: some View { - if viewModel.isLoading { - ProgressView() - } else { - innerBody - .onRotate { - orientation = $0 - } - .navigationBarTitleDisplayMode(.inline) - .navigationTitle("\(viewModel.item.name ?? "") - \(viewModel.item.seriesName ?? "")") - } - } -} diff --git a/JellyfinPlayer/SeriesItemView.swift b/JellyfinPlayer/SeriesItemView.swift deleted file mode 100644 index 76db1495..00000000 --- a/JellyfinPlayer/SeriesItemView.swift +++ /dev/null @@ -1,257 +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 Stinsen -import SwiftUI - -struct SeriesItemView: View { - @EnvironmentObject var itemRouter: NavigationRouter - @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 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)) - } - } - }.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 let firstTagline = viewModel.item.taglines?.first { - Text(firstTagline).font(.body).italic() - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 8) - .padding(.horizontal, 16) - } - if let genreItems = viewModel.item.genreItems, - !genreItems.isEmpty - { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 8) { - Text("Genres:").font(.callout).fontWeight(.semibold) - ForEach(genreItems, id: \.id) { genre in - Button { - itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? "")) - } label: { - Text(genre.name ?? "").font(.footnote) - } - } - } - .padding(.horizontal, 16) - } - .padding(.bottom, 16) - } - Text(viewModel.item.overview ?? "") - .font(.footnote) - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 16) - .padding(.horizontal, 16) - if let studios = viewModel.item.studios, - !studios.isEmpty - { - ScrollView(.horizontal, showsIndicators: false) { - LazyHStack(spacing: 16) { - Text("Studios:").font(.callout).fontWeight(.semibold) - ForEach(studios, id: \.id) { studio in - Button { - itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? "")) - } label: { - Text(studio.name ?? "").font(.footnote) - } - } - } - .padding(.horizontal, 16) - } - .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 - Button { - itemRouter.route(to: .item(viewModel: .init(id: season.id!))) - } label: { - PortraitItemView(item: season) - } - } - } - .padding(.bottom, 16) - .padding(.horizontal, 8) - LazyVStack(alignment: .leading, spacing: 0) { - if let people = viewModel.item.people, - !people.isEmpty - { - 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 - if person.type == "Actor" { - Button { - itemRouter - .route(to: .library(viewModel: .init(person: person), - title: person.name ?? "")) - } label: { - 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 let role = person.role, - !role.isEmpty - { - Text(role).font(.caption).fontWeight(.medium).lineLimit(1) - .foregroundColor(Color.secondary).frame(width: 100) - } - } - } - } - } - } - .padding(.horizontal, 16) - } - .padding(.bottom, 16) - } - if !viewModel.similarItems.isEmpty { - 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 - Button { - itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!))) - } label: { - PortraitItemView(item: similarItem) - } - } - } - .padding(.horizontal, 16) - } - .padding(.bottom, 16) - } - } - } - } - - 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, .leading], 16) - innerBody - } - } - } - } - - var body: some View { - if viewModel.isLoading { - ProgressView() - } else { - Group { - if hSizeClass == .compact && vSizeClass == .regular { - ParallaxHeaderScrollView(header: portraitHeaderView, - staticOverlayView: portraitHeaderOverlayView, - overlayAlignment: .bottomLeading, - headerHeight: UIScreen.main.bounds.width * 0.5625) { - innerBody - } - } else { - landscapeView - } - } - .onRotate { - orientation = $0 - recalcTracks() - } - .overrideViewPreference(.unspecified) - .navigationTitle(viewModel.item.name ?? "") - .navigationBarTitleDisplayMode(.inline) - } - } -} diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 2f3d491e..47c98512 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -6,17 +6,15 @@ */ import CoreData -import Defaults -import Stinsen import SwiftUI +import Defaults struct SettingsView: View { - @RouterObject var mainRouter: ViewRouter? - @EnvironmentObject var settingsRouter: NavigationRouter @Environment(\.managedObjectContext) private var viewContext @ObservedObject var viewModel: SettingsViewModel + @Binding var close: Bool @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles @@ -27,102 +25,101 @@ struct SettingsView: View { @Default(.videoPlayerJumpBackward) var jumpBackwardLength var body: some View { - Form { - Section { - HStack { - Text("User") - Spacer() - Text(SessionManager.current.user.username ?? "") - .foregroundColor(.jellyfinPurple) - } - - Button { - settingsRouter.route(to: .serverDetail) - } label: { + NavigationView { + Form { + Section(header: EmptyView()) { HStack { - Text("Server") + Text("User") Spacer() - Text(ServerEnvironment.current.server.name ?? "") + Text(SessionManager.current.user.username ?? "") .foregroundColor(.jellyfinPurple) - Image(systemName: "chevron.right") + } + + NavigationLink( + destination: ServerDetailView(), + label: { + HStack { + Text("Server") + Spacer() + Text(ServerEnvironment.current.server.name ?? "") + .foregroundColor(.jellyfinPurple) + } + }) + + Button { + close = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + SessionManager.current.logout() + let nc = NotificationCenter.default + nc.post(name: Notification.Name("didSignOut"), object: nil) + } + } label: { + Text("Sign out") + .font(.callout) + } + } + Section(header: Text("Playback")) { + Picker("Default local quality", selection: $inNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + } + } + + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { + ForEach(self.viewModel.bitrates, id: \.self) { bitrate in + Text(bitrate.name).tag(bitrate.value) + } + } + + Picker("Jump Forward Length", selection: $jumpForwardLength) { + ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } + + Picker("Jump Backward Length", selection: $jumpBackwardLength) { + ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } } } - Button { - settingsRouter.dismiss { - SessionManager.current.logout() - mainRouter?.route(to: .connectToServer) - } - } label: { - Text("Sign out") - .font(.callout) + Section(header: Text("Accessibility")) { + Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) + SearchablePicker(label: "Preferred subtitle language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding( + get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, + set: {autoSelectSubtitlesLangcode = $0.isoCode} + ) + ) + SearchablePicker(label: "Preferred audio language", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding( + get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, + set: { autoSelectAudioLangcode = $0.isoCode} + ) + ) + Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { + ForEach(self.viewModel.appearances, id: \.self) { appearance in + Text(appearance.localizedName).tag(appearance.rawValue) + } + }.onChange(of: appAppearance, perform: { value in + UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style + }) } } - Section(header: Text("Playback")) { - Picker("Default local quality", selection: $inNetworkStreamBitrate) { - ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - Text(bitrate.name).tag(bitrate.value) + .navigationBarTitle("Settings", displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + close = false + } label: { + Image(systemName: "xmark") } } - - Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { - ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - Text(bitrate.name).tag(bitrate.value) - } - } - - Picker("Jump Forward Length", selection: $jumpForwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } - - Picker("Jump Backward Length", selection: $jumpBackwardLength) { - ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } - } - - Section(header: Text("Accessibility")) { - Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - SearchablePicker(label: "Preferred subtitle language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding(get: { - viewModel.langs - .first(where: { $0.isoCode == autoSelectSubtitlesLangcode - }) ?? - .auto - }, - set: { autoSelectSubtitlesLangcode = $0.isoCode })) - SearchablePicker(label: "Preferred audio language", - options: viewModel.langs, - optionToString: { $0.name }, - selected: Binding(get: { - viewModel.langs - .first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? - .auto - }, - set: { autoSelectAudioLangcode = $0.isoCode })) - Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { - ForEach(self.viewModel.appearances, id: \.self) { appearance in - Text(appearance.localizedName).tag(appearance.rawValue) - } - }.onChange(of: appAppearance, perform: { value in - guard let appearance = AppAppearance(rawValue: value) else { return } - UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appearance.style - }) - } - } - .navigationBarTitle("Settings", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - settingsRouter.dismiss() - } label: { - Image(systemName: "xmark") - } } } } 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 @@ -