From 8be5df69b0183dcaf595a3b6fdf6643062e6d4aa Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 21 May 2025 00:15:34 -0400 Subject: [PATCH] Cleanup `ItemView`s (#1543) * wip * wip * Update ItemView.swift * cleanup, fix images * cleanup * Update Package.resolved * Update Localizable.strings --- .../JellyfinAPI/BaseItemDto/BaseItemDto.swift | 3 +- Shared/Strings/Strings.swift | 2 - Swiftfin.xcodeproj/project.pbxproj | 158 ++---------------- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../CollectionItemContentView.swift | 19 +-- ...iew.swift => EpisodeItemContentView.swift} | 16 +- Swiftfin/Views/ItemView/ItemView.swift | 80 +++++---- .../MovieItemContentView.swift | 20 +-- .../ScrollViews/CinematicScrollView.swift | 62 ++++--- .../ScrollViews/CompactLogoScrollView.swift | 58 ++++--- .../CompactPortraitScrollView.swift | 45 +++-- .../ScrollViews/SimpleScrollView.swift | 140 ++++++++++++++++ .../iPadOSCinematicScrollView.swift | 34 ++-- .../SeriesItemContentView.swift | 19 +-- .../CollectionItemView.swift | 37 ---- .../EpisodeItemContentView.swift | 144 ---------------- .../iOS/EpisodeItemView/EpisodeItemView.swift | 23 --- .../iOS/MovieItemView/MovieItemView.swift | 37 ---- .../iOS/SeriesItemView/SeriesItemView.swift | 37 ---- .../iPadOSCollectionItemContentView.swift | 81 --------- .../iPadOSCollectionItemView.swift | 22 --- .../iPadOSEpisodeItemView.swift | 22 --- .../iPadOSMovieItemContentView.swift | 68 -------- .../MovieItemView/iPadOSMovieItemView.swift | 22 --- .../iPadOSSeriesItemContentView.swift | 72 -------- .../SeriesItemView/iPadOSSeriesItemView.swift | 22 --- Translations/en.lproj/Localizable.strings | Bin 106182 -> 105990 bytes 27 files changed, 339 insertions(+), 908 deletions(-) rename Swiftfin/Views/ItemView/{iOS/CollectionItemView => }/CollectionItemContentView.swift (88%) rename Swiftfin/Views/ItemView/{iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift => EpisodeItemContentView.swift} (83%) rename Swiftfin/Views/ItemView/{iOS/MovieItemView => }/MovieItemContentView.swift (81%) rename Swiftfin/Views/ItemView/{iOS => }/ScrollViews/CinematicScrollView.swift (76%) rename Swiftfin/Views/ItemView/{iOS => }/ScrollViews/CompactLogoScrollView.swift (75%) rename Swiftfin/Views/ItemView/{iOS => }/ScrollViews/CompactPortraitScrollView.swift (81%) create mode 100644 Swiftfin/Views/ItemView/ScrollViews/SimpleScrollView.swift rename Swiftfin/Views/ItemView/{iPadOS => }/ScrollViews/iPadOSCinematicScrollView.swift (86%) rename Swiftfin/Views/ItemView/{iOS/SeriesItemView => }/SeriesItemContentView.swift (85%) delete mode 100644 Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift delete mode 100644 Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift delete mode 100644 Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift index 1a4e9a2c..ab9774ee 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift @@ -17,7 +17,7 @@ import UIKit extension BaseItemDto: Displayable { var displayTitle: String { - name ?? .emptyDash + name ?? L10n.unknown } } @@ -268,7 +268,6 @@ extension BaseItemDto { album case .episode: seriesName - case .program: nil default: nil } diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 2e472822..c5f49cde 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -872,8 +872,6 @@ internal enum L10n { internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV programs") /// Live TV recording management internal static let liveTVRecordingManagement = L10n.tr("Localizable", "liveTVRecordingManagement", fallback: "Live TV recording management") - /// Live TV recording management - internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management") /// Loading user failed internal static let loadingUserFailed = L10n.tr("Localizable", "loadingUserFailed", fallback: "Loading user failed") /// Local diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index c4383d21..d72c65b5 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -943,23 +943,13 @@ E18D6AA62BAA96F000A0D167 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E18D6AA52BAA96F000A0D167 /* CollectionHStack */; }; E18E01AB288746AF0022598C /* PillHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A5288746AF0022598C /* PillHStack.swift */; }; E18E01AD288746AF0022598C /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A7288746AF0022598C /* DotHStack.swift */; }; - E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */; }; - E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */; }; E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */; }; - E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */; }; - E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */; }; - E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BE288747230022598C /* iPadOSMovieItemView.swift */; }; - E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */; }; E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C2288747230022598C /* EpisodeItemContentView.swift */; }; - E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C3288747230022598C /* EpisodeItemView.swift */; }; E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C5288747230022598C /* CompactPortraitScrollView.swift */; }; E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C6288747230022598C /* CompactLogoScrollView.swift */; }; E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C7288747230022598C /* CinematicScrollView.swift */; }; - E18E01E6288747230022598C /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C9288747230022598C /* CollectionItemView.swift */; }; E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CA288747230022598C /* CollectionItemContentView.swift */; }; E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CC288747230022598C /* SeriesItemContentView.swift */; }; - E18E01E9288747230022598C /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CD288747230022598C /* SeriesItemView.swift */; }; - E18E01EA288747230022598C /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CF288747230022598C /* MovieItemView.swift */; }; E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D0288747230022598C /* MovieItemContentView.swift */; }; E18E01EE288747230022598C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D5288747230022598C /* AboutView.swift */; }; E18E01F0288747230022598C /* AttributeHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D7288747230022598C /* AttributeHStack.swift */; }; @@ -1000,6 +990,7 @@ E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; }; E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */; }; E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; }; + E19523752DD8F18B00442F15 /* SimpleScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19523742DD8F18B00442F15 /* SimpleScrollView.swift */; }; E19D41A72BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */; }; E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */; }; E19D41AA2BF077130082B8B2 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A92BF077130082B8B2 /* Keychain.swift */; }; @@ -1250,8 +1241,6 @@ E1F5CF062CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; }; E1F5CF082CB0A04500607465 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF072CB0A04500607465 /* Text.swift */; }; E1F5CF092CB0A04500607465 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF072CB0A04500607465 /* Text.swift */; }; - E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */; }; - E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */; }; E1FAD1C62A0375BA007F5521 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E1FAD1C52A0375BA007F5521 /* UDPBroadcast */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; @@ -1937,23 +1926,13 @@ E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = ""; }; E18E01A5288746AF0022598C /* PillHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillHStack.swift; sourceTree = ""; }; E18E01A7288746AF0022598C /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = ""; }; - E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSEpisodeContentView.swift; sourceTree = ""; }; - E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSEpisodeItemView.swift; sourceTree = ""; }; E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSCinematicScrollView.swift; sourceTree = ""; }; - E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSSeriesItemContentView.swift; sourceTree = ""; }; - E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSSeriesItemView.swift; sourceTree = ""; }; - E18E01BE288747230022598C /* iPadOSMovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSMovieItemView.swift; sourceTree = ""; }; - E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSMovieItemContentView.swift; sourceTree = ""; }; E18E01C2288747230022598C /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = ""; }; - E18E01C3288747230022598C /* EpisodeItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = ""; }; E18E01C5288747230022598C /* CompactPortraitScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactPortraitScrollView.swift; sourceTree = ""; }; E18E01C6288747230022598C /* CompactLogoScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactLogoScrollView.swift; sourceTree = ""; }; E18E01C7288747230022598C /* CinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CinematicScrollView.swift; sourceTree = ""; }; - E18E01C9288747230022598C /* CollectionItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = ""; }; E18E01CA288747230022598C /* CollectionItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = ""; }; E18E01CC288747230022598C /* SeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemContentView.swift; sourceTree = ""; }; - E18E01CD288747230022598C /* SeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = ""; }; - E18E01CF288747230022598C /* MovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = ""; }; E18E01D0288747230022598C /* MovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemContentView.swift; sourceTree = ""; }; E18E01D5288747230022598C /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; E18E01D7288747230022598C /* AttributeHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeHStack.swift; sourceTree = ""; }; @@ -1975,6 +1954,7 @@ E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = ""; }; E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = ""; }; + E19523742DD8F18B00442F15 /* SimpleScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleScrollView.swift; sourceTree = ""; }; E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityViewModel.swift; sourceTree = ""; }; E19D41A92BF077130082B8B2 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; E19D41AB2BF288110082B8B2 /* ServerCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerCheckView.swift; sourceTree = ""; }; @@ -2152,8 +2132,6 @@ E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = ""; }; E1F5CF042CB09EA000607465 /* CurrentDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentDate.swift; sourceTree = ""; }; E1F5CF072CB0A04500607465 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; - E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemView.swift; sourceTree = ""; }; - E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemContentView.swift; sourceTree = ""; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; E1FE28C82DC16B2B00E1A23E /* RedrawOnNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnNotificationView.swift; sourceTree = ""; }; E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = ""; }; @@ -4655,10 +4633,13 @@ E14F7D0A26DB3714007C3AE6 /* ItemView */ = { isa = PBXGroup; children = ( - 535BAE9E2649E569005FA86D /* ItemView.swift */, + E18E01CA288747230022598C /* CollectionItemContentView.swift */, E18E01D4288747230022598C /* Components */, - E18E01C0288747230022598C /* iOS */, - E18E01B4288747230022598C /* iPadOS */, + E18E01C2288747230022598C /* EpisodeItemContentView.swift */, + 535BAE9E2649E569005FA86D /* ItemView.swift */, + E18E01D0288747230022598C /* MovieItemContentView.swift */, + E18E01C4288747230022598C /* ScrollViews */, + E18E01CC288747230022598C /* SeriesItemContentView.swift */, ); path = ItemView; sourceTree = ""; @@ -4918,111 +4899,18 @@ path = Components; sourceTree = ""; }; - E18E01B4288747230022598C /* iPadOS */ = { - isa = PBXGroup; - children = ( - E1FA891C289A302600176FEB /* CollectionItemView */, - E18E01B5288747230022598C /* EpisodeItemView */, - E18E01BD288747230022598C /* MovieItemView */, - E18E01B8288747230022598C /* ScrollViews */, - E18E01BA288747230022598C /* SeriesItemView */, - ); - path = iPadOS; - sourceTree = ""; - }; - E18E01B5288747230022598C /* EpisodeItemView */ = { - isa = PBXGroup; - children = ( - E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */, - E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */, - ); - path = EpisodeItemView; - sourceTree = ""; - }; - E18E01B8288747230022598C /* ScrollViews */ = { - isa = PBXGroup; - children = ( - E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */, - ); - path = ScrollViews; - sourceTree = ""; - }; - E18E01BA288747230022598C /* SeriesItemView */ = { - isa = PBXGroup; - children = ( - E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */, - E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */, - ); - path = SeriesItemView; - sourceTree = ""; - }; - E18E01BD288747230022598C /* MovieItemView */ = { - isa = PBXGroup; - children = ( - E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */, - E18E01BE288747230022598C /* iPadOSMovieItemView.swift */, - ); - path = MovieItemView; - sourceTree = ""; - }; - E18E01C0288747230022598C /* iOS */ = { - isa = PBXGroup; - children = ( - E18E01C8288747230022598C /* CollectionItemView */, - E18E01C1288747230022598C /* EpisodeItemView */, - E18E01CE288747230022598C /* MovieItemView */, - E18E01C4288747230022598C /* ScrollViews */, - E18E01CB288747230022598C /* SeriesItemView */, - ); - path = iOS; - sourceTree = ""; - }; - E18E01C1288747230022598C /* EpisodeItemView */ = { - isa = PBXGroup; - children = ( - E18E01C2288747230022598C /* EpisodeItemContentView.swift */, - E18E01C3288747230022598C /* EpisodeItemView.swift */, - ); - path = EpisodeItemView; - sourceTree = ""; - }; E18E01C4288747230022598C /* ScrollViews */ = { isa = PBXGroup; children = ( - E18E01C5288747230022598C /* CompactPortraitScrollView.swift */, - E18E01C6288747230022598C /* CompactLogoScrollView.swift */, E18E01C7288747230022598C /* CinematicScrollView.swift */, + E18E01C6288747230022598C /* CompactLogoScrollView.swift */, + E18E01C5288747230022598C /* CompactPortraitScrollView.swift */, + E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */, + E19523742DD8F18B00442F15 /* SimpleScrollView.swift */, ); path = ScrollViews; sourceTree = ""; }; - E18E01C8288747230022598C /* CollectionItemView */ = { - isa = PBXGroup; - children = ( - E18E01CA288747230022598C /* CollectionItemContentView.swift */, - E18E01C9288747230022598C /* CollectionItemView.swift */, - ); - path = CollectionItemView; - sourceTree = ""; - }; - E18E01CB288747230022598C /* SeriesItemView */ = { - isa = PBXGroup; - children = ( - E18E01CC288747230022598C /* SeriesItemContentView.swift */, - E18E01CD288747230022598C /* SeriesItemView.swift */, - ); - path = SeriesItemView; - sourceTree = ""; - }; - E18E01CE288747230022598C /* MovieItemView */ = { - isa = PBXGroup; - children = ( - E18E01D0288747230022598C /* MovieItemContentView.swift */, - E18E01CF288747230022598C /* MovieItemView.swift */, - ); - path = MovieItemView; - sourceTree = ""; - }; E18E01D4288747230022598C /* Components */ = { isa = PBXGroup; children = ( @@ -5518,15 +5406,6 @@ path = MediaSourceInfo; sourceTree = ""; }; - E1FA891C289A302600176FEB /* CollectionItemView */ = { - isa = PBXGroup; - children = ( - E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */, - E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */, - ); - path = CollectionItemView; - sourceTree = ""; - }; E1FCD08E26C466F3007C8DCF /* Errors */ = { isa = PBXGroup; children = ( @@ -6466,7 +6345,6 @@ 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, 4EC2B1A22CC96F6600D866BE /* ServerUsersViewModel.swift in Sources */, E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */, - E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */, 4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, 621338932660107500A81A2A /* String.swift in Sources */, @@ -6513,7 +6391,6 @@ C46DD8D22A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */, 4EB538C32CE3E21800EB72D5 /* SyncPlaySection.swift in Sources */, E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */, - E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */, E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */, 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, @@ -6585,7 +6462,6 @@ 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */, 4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */, 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */, - E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */, E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, @@ -6634,7 +6510,6 @@ E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */, 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */, E18E0204288749200022598C /* RowDivider.swift in Sources */, - E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */, E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */, E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */, @@ -6738,6 +6613,7 @@ E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */, E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, + E19523752DD8F18B00442F15 /* SimpleScrollView.swift in Sources */, 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */, 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, @@ -6771,7 +6647,6 @@ E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */, E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */, - E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */, E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */, 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */, @@ -6784,7 +6659,6 @@ E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */, 4E9654492D99C553006CB024 /* CollectionType.swift in Sources */, 4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */, - E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */, E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */, 4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */, E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */, @@ -6820,7 +6694,6 @@ E1FE28C92DC16B2B00E1A23E /* RedrawOnNotificationView.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */, 4EF36F642D962A430065BB79 /* ItemSortBy.swift in Sources */, - E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */, E1EA09672BED6815004CDE76 /* UserSignInSecurityView.swift in Sources */, @@ -6829,7 +6702,6 @@ E18A8E8028D6083700333B9A /* MediaSourceInfo+ItemVideoPlayerViewModel.swift in Sources */, E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */, E101ECD52CD40489001EA89E /* DeviceDetailViewModel.swift in Sources */, - E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */, 4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */, 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */, 4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */, @@ -6906,7 +6778,6 @@ BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, E1BDF2F529524E6400CC0294 /* PlayNextItemActionButton.swift in Sources */, BD3957772C112AD30078CEF8 /* SliderSection.swift in Sources */, - E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */, E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */, 4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */, E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */, @@ -6915,7 +6786,6 @@ E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */, E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */, E1CB758C2C80F9EC00217C76 /* CodecProfile.swift in Sources */, - E18E01E9288747230022598C /* SeriesItemView.swift in Sources */, E15756342936851D00976E1F /* NativeVideoPlayerSettingsView.swift in Sources */, E1D4BF7C2719D05000A11E64 /* AppSettingsView.swift in Sources */, 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */, @@ -6998,7 +6868,6 @@ 4EF36F662D9649050065BB79 /* SessionInfoDto.swift in Sources */, E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */, 4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */, - E18E01E6288747230022598C /* CollectionItemView.swift in Sources */, E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, @@ -7032,7 +6901,6 @@ 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, E1D8429529346C6400D1041A /* BasicStepper.swift in Sources */, 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */, - E18E01EA288747230022598C /* MovieItemView.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */, 4E8F74AF2CE03E2E00CC8969 /* RefreshMetadataButton.swift in Sources */, diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 606d4228..079c61ff 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "44d87a45bd21720bc457afc9350e1268808e629c432ce75d0e6c4c26ce3b67ce", + "originHash" : "66bff9f26defe8d2dfa92b4e65d0ae348e3b586d0fbb7de49c9c937459e6b55c", "pins" : [ { "identity" : "blurhashkit", @@ -24,6 +24,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/LePips/CollectionHStack", "state" : { + "branch" : "main", "revision" : "03dc666e8b20ec216fda60f55ccc0eeaabbc5fad" } }, @@ -32,6 +33,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/LePips/CollectionVGrid", "state" : { + "branch" : "main", "revision" : "70db2318ce64d49aa8b536e0623b96cb323fbdf1" } }, diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift b/Swiftfin/Views/ItemView/CollectionItemContentView.swift similarity index 88% rename from Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift rename to Swiftfin/Views/ItemView/CollectionItemContentView.swift index fd18f550..a532beef 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/CollectionItemContentView.swift @@ -10,9 +10,9 @@ import BlurHashKit import JellyfinAPI import SwiftUI -extension CollectionItemView { +extension ItemView { - struct ContentView: View { + struct CollectionItemContentView: View { @EnvironmentObject private var router: ItemCoordinator.Router @@ -21,9 +21,12 @@ extension CollectionItemView { var viewModel: CollectionItemViewModel var body: some View { - VStack(alignment: .leading, spacing: 20) { + SeparatorVStack(alignment: .leading) { + RowDivider() + .padding(.vertical, 10) + } content: { - // MARK: Items + // MARK: - Items ForEach(viewModel.collectionItems.elements, id: \.key) { element in if element.value.isNotEmpty { @@ -46,8 +49,6 @@ extension CollectionItemView { .onSelect { item in router.route(to: \.item, item) } - - RowDivider() } } @@ -55,24 +56,18 @@ extension CollectionItemView { if let genres = viewModel.item.itemGenres, genres.isNotEmpty { ItemView.GenresHStack(genres: genres) - - RowDivider() } // MARK: Studios if let studios = viewModel.item.studios, studios.isNotEmpty { ItemView.StudiosHStack(studios: studios) - - RowDivider() } // MARK: Similar if viewModel.similarItems.isNotEmpty { ItemView.SimilarItemsHStack(items: viewModel.similarItems) - - RowDivider() } ItemView.AboutView(viewModel: viewModel) diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift b/Swiftfin/Views/ItemView/EpisodeItemContentView.swift similarity index 83% rename from Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift rename to Swiftfin/Views/ItemView/EpisodeItemContentView.swift index 0a31f87c..1a9186d8 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift +++ b/Swiftfin/Views/ItemView/EpisodeItemContentView.swift @@ -6,12 +6,13 @@ // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // +import BlurHashKit import JellyfinAPI import SwiftUI -extension iPadOSEpisodeItemView { +extension ItemView { - struct ContentView: View { + struct EpisodeItemContentView: View { @EnvironmentObject private var router: ItemCoordinator.Router @@ -20,22 +21,21 @@ extension iPadOSEpisodeItemView { var viewModel: EpisodeItemViewModel var body: some View { - VStack(alignment: .leading, spacing: 10) { + SeparatorVStack(alignment: .leading) { + RowDivider() + .padding(.vertical, 10) + } content: { // MARK: Genres if let genres = viewModel.item.itemGenres, genres.isNotEmpty { ItemView.GenresHStack(genres: genres) - - RowDivider() } // MARK: Studios if let studios = viewModel.item.studios, studios.isNotEmpty { ItemView.StudiosHStack(studios: studios) - - RowDivider() } // MARK: Cast and Crew @@ -44,8 +44,6 @@ extension iPadOSEpisodeItemView { castAndCrew.isNotEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) - - RowDivider() } ItemView.AboutView(viewModel: viewModel) diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 5e651c83..e1ccf7c0 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -6,15 +6,22 @@ // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // +import Defaults import JellyfinAPI import SwiftUI -// TODO: try to make views simpler so there isn't one per media type, but per view type -// - basic (episodes, collection) vs more fancy (rest) -// - think about future for other media types - struct ItemView: View { + protocol ScrollContainerView: View { + + associatedtype Content: View + + init(viewModel: ItemViewModel, content: @escaping () -> Content) + } + + @Default(.Customization.itemViewType) + private var itemViewType + @EnvironmentObject private var router: ItemCoordinator.Router @@ -24,7 +31,7 @@ struct ItemView: View { private var deleteViewModel: DeleteItemViewModel @State - private var showConfirmationDialog = false + private var isPresentingConfirmationDialog = false @State private var isPresentingEventAlert = false @State @@ -73,51 +80,58 @@ struct ItemView: View { } @ViewBuilder - private var padView: some View { + private var scrollContentView: some View { switch viewModel.item.type { case .boxSet: - iPadOSCollectionItemView(viewModel: viewModel as! CollectionItemViewModel) + CollectionItemContentView(viewModel: viewModel as! CollectionItemViewModel) case .episode: - iPadOSEpisodeItemView(viewModel: viewModel as! EpisodeItemViewModel) + EpisodeItemContentView(viewModel: viewModel as! EpisodeItemViewModel) case .movie: - iPadOSMovieItemView(viewModel: viewModel as! MovieItemViewModel) + MovieItemContentView(viewModel: viewModel as! MovieItemViewModel) case .series: - iPadOSSeriesItemView(viewModel: viewModel as! SeriesItemViewModel) + SeriesItemContentView(viewModel: viewModel as! SeriesItemViewModel) default: Text(L10n.notImplementedYetWithType(viewModel.item.type ?? "--")) } } - @ViewBuilder - private var phoneView: some View { - switch viewModel.item.type { - case .boxSet: - CollectionItemView(viewModel: viewModel as! CollectionItemViewModel) - case .episode: - EpisodeItemView(viewModel: viewModel as! EpisodeItemViewModel) - case .movie: - MovieItemView(viewModel: viewModel as! MovieItemViewModel) - case .series: - SeriesItemView(viewModel: viewModel as! SeriesItemViewModel) - default: - Text(L10n.notImplementedYetWithType(viewModel.item.type ?? "--")) - } - } + // TODO: break out into pad vs phone views based on item type + private func scrollContainerView( + viewModel: ItemViewModel, + content: @escaping () -> Content + ) -> any ScrollContainerView { - @ViewBuilder - private var contentView: some View { if UIDevice.isPad { - padView - } else { - phoneView + return iPadOSCinematicScrollView(viewModel: viewModel, content: content) } + + if viewModel.item.type == .movie || viewModel.item.type == .series { + switch itemViewType { + case .compactPoster: + return CompactPosterScrollView(viewModel: viewModel, content: content) + case .compactLogo: + return CompactLogoScrollView(viewModel: viewModel, content: content) + case .cinematic: + return CinematicScrollView(viewModel: viewModel, content: content) + } + } + + return SimpleScrollView(viewModel: viewModel, content: content) + } + + @ViewBuilder + private var innerBody: some View { + scrollContainerView(viewModel: viewModel) { + scrollContentView + } + .eraseToAnyView() } var body: some View { ZStack { switch viewModel.state { case .content: - contentView + innerBody .navigationTitle(viewModel.item.displayTitle) case let .error(error): ErrorView(error: error) @@ -143,14 +157,14 @@ struct ItemView: View { if canDelete { Section { Button(L10n.delete, systemImage: "trash", role: .destructive) { - showConfirmationDialog = true + isPresentingConfirmationDialog = true } } } } .confirmationDialog( L10n.deleteItemConfirmationMessage, - isPresented: $showConfirmationDialog, + isPresented: $isPresentingConfirmationDialog, titleVisibility: .visible ) { Button(L10n.confirm, role: .destructive) { diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift b/Swiftfin/Views/ItemView/MovieItemContentView.swift similarity index 81% rename from Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift rename to Swiftfin/Views/ItemView/MovieItemContentView.swift index 47211ca3..17eec4a2 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/MovieItemContentView.swift @@ -10,30 +10,29 @@ import Defaults import JellyfinAPI import SwiftUI -extension MovieItemView { +extension ItemView { - struct ContentView: View { + struct MovieItemContentView: View { @ObservedObject var viewModel: MovieItemViewModel var body: some View { - VStack(alignment: .leading, spacing: 10) { + SeparatorVStack(alignment: .leading) { + RowDivider() + .padding(.vertical, 10) + } content: { // MARK: Genres if let genres = viewModel.item.itemGenres, genres.isNotEmpty { ItemView.GenresHStack(genres: genres) - - RowDivider() } // MARK: Studios if let studios = viewModel.item.studios, studios.isNotEmpty { ItemView.StudiosHStack(studios: studios) - - RowDivider() } // MARK: Cast and Crew @@ -42,29 +41,22 @@ extension MovieItemView { castAndCrew.isNotEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) - - RowDivider() } // MARK: Special Features if viewModel.specialFeatures.isNotEmpty { ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) - - RowDivider() } // MARK: Similar if viewModel.similarItems.isNotEmpty { ItemView.SimilarItemsHStack(items: viewModel.similarItems) - - RowDivider() } ItemView.AboutView(viewModel: viewModel) } - .animation(.linear(duration: 0.2), value: viewModel.item) } } } diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift b/Swiftfin/Views/ItemView/ScrollViews/CinematicScrollView.swift similarity index 76% rename from Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift rename to Swiftfin/Views/ItemView/ScrollViews/CinematicScrollView.swift index 8cc9097a..0ce8181f 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/ScrollViews/CinematicScrollView.swift @@ -12,7 +12,7 @@ import SwiftUI extension ItemView { - struct CinematicScrollView: View { + struct CinematicScrollView: ScrollContainerView { @Default(.Customization.CinematicItemViewType.usePrimaryImage) private var usePrimaryImage @@ -21,12 +21,29 @@ extension ItemView { private var router: ItemCoordinator.Router @ObservedObject - var viewModel: ItemViewModel + private var viewModel: ItemViewModel - @State - private var blurHashBottomEdgeColor: Color = .secondarySystemFill + private let blurHashBottomEdgeColor: Color + private let content: Content - let content: () -> Content + init( + viewModel: ItemViewModel, + content: @escaping () -> Content + ) { + if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { + let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB + blurHashBottomEdgeColor = Color( + red: Double(bottomRGB.0), + green: Double(bottomRGB.1), + blue: Double(bottomRGB.2) + ) + } else { + blurHashBottomEdgeColor = Color.secondarySystemFill + } + + self.content = content() + self.viewModel = viewModel + } @ViewBuilder private var headerView: some View { @@ -37,16 +54,6 @@ extension ItemView { .aspectRatio(usePrimaryImage ? (2 / 3) : 1.77, contentMode: .fill) .frame(height: UIScreen.main.bounds.height * 0.6) .bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) - .onAppear { - if let headerBlurHash = viewModel.item.blurHash(.backdrop) { - let bottomRGB = BlurHash(string: headerBlurHash)!.averageLinearRGB - blurHashBottomEdgeColor = Color( - red: Double(bottomRGB.0), - green: Double(bottomRGB.1), - blue: Double(bottomRGB.2) - ) - } - } } var body: some View { @@ -75,7 +82,7 @@ extension ItemView { } } } content: { - content() + content .edgePadding(.vertical) } } @@ -87,7 +94,7 @@ extension ItemView.CinematicScrollView { struct OverlayView: View { @Default(.Customization.CinematicItemViewType.usePrimaryImage) - private var cinematicItemViewTypeUsePrimaryImage + private var usePrimaryImage @EnvironmentObject private var router: ItemCoordinator.Router @@ -97,7 +104,7 @@ extension ItemView.CinematicScrollView { var body: some View { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .center, spacing: 10) { - if !cinematicItemViewTypeUsePrimaryImage { + if !usePrimaryImage { ImageView(viewModel.item.imageURL(.logo, maxHeight: 100)) .placeholder { _ in EmptyView() @@ -130,16 +137,17 @@ extension ItemView.CinematicScrollView { .foregroundColor(Color(UIColor.lightGray)) .padding(.horizontal) - if viewModel.presentPlayButton { - ItemView.PlayButton(viewModel: viewModel) - .frame(maxWidth: 300) - .frame(height: 50) - } + Group { + if viewModel.presentPlayButton { + ItemView.PlayButton(viewModel: viewModel) + .frame(height: 50) + } - ItemView.ActionButtonHStack(viewModel: viewModel) - .font(.title) - .frame(maxWidth: 300) - .foregroundColor(.white) + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + .foregroundColor(.white) + } + .frame(maxWidth: 300) } .frame(maxWidth: .infinity) diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift b/Swiftfin/Views/ItemView/ScrollViews/CompactLogoScrollView.swift similarity index 75% rename from Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift rename to Swiftfin/Views/ItemView/ScrollViews/CompactLogoScrollView.swift index 942be65d..67f92f37 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift +++ b/Swiftfin/Views/ItemView/ScrollViews/CompactLogoScrollView.swift @@ -11,20 +11,35 @@ import SwiftUI extension ItemView { - struct CompactLogoScrollView: View { + struct CompactLogoScrollView: ScrollContainerView { @EnvironmentObject private var router: ItemCoordinator.Router @ObservedObject - var viewModel: ItemViewModel + private var viewModel: ItemViewModel - @State - private var scrollViewOffset: CGFloat = 0 - @State - private var blurHashBottomEdgeColor: Color = .secondarySystemFill + private let blurHashBottomEdgeColor: Color + private let content: Content - let content: () -> Content + init( + viewModel: ItemViewModel, + content: @escaping () -> Content + ) { + if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { + let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB + blurHashBottomEdgeColor = Color( + red: Double(bottomRGB.0), + green: Double(bottomRGB.1), + blue: Double(bottomRGB.2) + ) + } else { + blurHashBottomEdgeColor = Color.secondarySystemFill + } + + self.content = content() + self.viewModel = viewModel + } @ViewBuilder private var headerView: some View { @@ -32,16 +47,6 @@ extension ItemView { .aspectRatio(1.77, contentMode: .fill) .frame(height: UIScreen.main.bounds.height * 0.35) .bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) - .onAppear { - if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { - let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB - blurHashBottomEdgeColor = Color( - red: Double(bottomRGB.0), - green: Double(bottomRGB.1), - blue: Double(bottomRGB.2) - ) - } - } } var body: some View { @@ -78,7 +83,7 @@ extension ItemView { RowDivider() - content() + content } .edgePadding(.vertical) } @@ -131,14 +136,17 @@ extension ItemView.CompactLogoScrollView { ItemView.AttributesHStack(viewModel: viewModel) - if viewModel.presentPlayButton { - ItemView.PlayButton(viewModel: viewModel) - .frame(height: 50) - } + Group { + if viewModel.presentPlayButton { + ItemView.PlayButton(viewModel: viewModel) + .frame(height: 50) + } - ItemView.ActionButtonHStack(viewModel: viewModel) - .font(.title) - .foregroundColor(.white) + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + .foregroundStyle(.white) + } + .frame(maxWidth: 300) } .frame(maxWidth: .infinity, alignment: .bottom) } diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift b/Swiftfin/Views/ItemView/ScrollViews/CompactPortraitScrollView.swift similarity index 81% rename from Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift rename to Swiftfin/Views/ItemView/ScrollViews/CompactPortraitScrollView.swift index 668b4212..fedf09c4 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift +++ b/Swiftfin/Views/ItemView/ScrollViews/CompactPortraitScrollView.swift @@ -11,27 +11,34 @@ import SwiftUI extension ItemView { - struct CompactPosterScrollView: View { + struct CompactPosterScrollView: ScrollContainerView { @EnvironmentObject private var router: ItemCoordinator.Router @ObservedObject - var viewModel: ItemViewModel + private var viewModel: ItemViewModel - @State - private var scrollViewOffset: CGFloat = 0 - @State - private var blurHashBottomEdgeColor: Color = .secondarySystemFill + private let blurHashBottomEdgeColor: Color + private let content: Content - let content: () -> Content + init( + viewModel: ItemViewModel, + content: @escaping () -> Content + ) { + if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { + let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB + blurHashBottomEdgeColor = Color( + red: Double(bottomRGB.0), + green: Double(bottomRGB.1), + blue: Double(bottomRGB.2) + ) + } else { + blurHashBottomEdgeColor = Color.secondarySystemFill + } - private var topOpacity: CGFloat { - let start = UIScreen.main.bounds.height * 0.20 - let end = UIScreen.main.bounds.height * 0.4 - let diff = end - start - let opacity = clamp((scrollViewOffset - start) / diff, min: 0, max: 1) - return opacity + self.content = content() + self.viewModel = viewModel } @ViewBuilder @@ -40,16 +47,6 @@ extension ItemView { .aspectRatio(1.77, contentMode: .fill) .frame(height: UIScreen.main.bounds.height * 0.35) .bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) - .onAppear { - if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { - let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB - blurHashBottomEdgeColor = Color( - red: Double(bottomRGB.0), - green: Double(bottomRGB.1), - blue: Double(bottomRGB.2) - ) - } - } } var body: some View { @@ -87,7 +84,7 @@ extension ItemView { RowDivider() - content() + content } .edgePadding(.vertical) } diff --git a/Swiftfin/Views/ItemView/ScrollViews/SimpleScrollView.swift b/Swiftfin/Views/ItemView/ScrollViews/SimpleScrollView.swift new file mode 100644 index 00000000..134650e5 --- /dev/null +++ b/Swiftfin/Views/ItemView/ScrollViews/SimpleScrollView.swift @@ -0,0 +1,140 @@ +// +// 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 (c) 2025 Jellyfin & Jellyfin Contributors +// + +import BlurHashKit +import JellyfinAPI +import SwiftUI + +extension ItemView { + + struct SimpleScrollView: ScrollContainerView { + + @EnvironmentObject + private var router: ItemCoordinator.Router + + @ObservedObject + private var viewModel: ItemViewModel + + private let content: Content + + init( + viewModel: ItemViewModel, + @ViewBuilder content: () -> Content + ) { + self.content = content() + self.viewModel = viewModel + } + + @ViewBuilder + private var shelfView: some View { + VStack(alignment: .center, spacing: 10) { + if let parentTitle = viewModel.item.parentTitle { + Text(parentTitle) + .font(.headline) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal) + .foregroundColor(.secondary) + } + + Text(viewModel.item.displayTitle) + .font(.title2) + .fontWeight(.bold) + .multilineTextAlignment(.center) + .lineLimit(2) + .padding(.horizontal) + + DotHStack { + if let seasonEpisodeLabel = viewModel.item.seasonEpisodeLabel { + Text(seasonEpisodeLabel) + } + + if let productionYear = viewModel.item.premiereDateYear { + Text(productionYear) + } + + if let runtime = viewModel.item.runTimeLabel { + Text(runtime) + } + } + .font(.caption) + .foregroundColor(.secondary) + .padding(.horizontal) + + ItemView.AttributesHStack(viewModel: viewModel, alignment: .center) + + Group { + if viewModel.presentPlayButton { + ItemView.PlayButton(viewModel: viewModel) + .frame(height: 50) + } + + ItemView.ActionButtonHStack(viewModel: viewModel) + .font(.title) + .foregroundStyle(.primary) + } + .frame(maxWidth: 300) + } + } + + private var imageType: ImageType { + if viewModel.item.type == .episode { + return .primary + } else { + return .backdrop + } + } + + @ViewBuilder + private var header: some View { + VStack(alignment: .center) { + ImageView(viewModel.item.imageSource(imageType, maxWidth: 600)) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash, size: .Square(length: 8)) + } else { + Color.secondarySystemFill + .opacity(0.75) + } + } + .failure { + SystemImageContentView(systemName: viewModel.item.systemImage) + } + .frame(maxHeight: 300) + .posterStyle(.landscape) + .posterShadow() + .padding(.horizontal) + + shelfView + } + } + + var body: some View { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 10) { + + header + + // MARK: Overview + + ItemView.OverviewView(item: viewModel.item) + .overviewLineLimit(4) + .padding(.horizontal) + + RowDivider() + + // MARK: Genres + + content + .edgePadding(.bottom) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift b/Swiftfin/Views/ItemView/ScrollViews/iPadOSCinematicScrollView.swift similarity index 86% rename from Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift rename to Swiftfin/Views/ItemView/ScrollViews/iPadOSCinematicScrollView.swift index 266f8ee0..62967205 100644 --- a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/ScrollViews/iPadOSCinematicScrollView.swift @@ -6,6 +6,7 @@ // Copyright (c) 2025 Jellyfin & Jellyfin Contributors // +import JellyfinAPI import SwiftUI // TODO: remove rest occurrences of `UIDevice.main` sizings @@ -17,33 +18,38 @@ import SwiftUI extension ItemView { - struct iPadOSCinematicScrollView: View { + struct iPadOSCinematicScrollView: ScrollContainerView { @ObservedObject - var viewModel: ItemViewModel + private var viewModel: ItemViewModel @State private var globalSize: CGSize = .zero - let content: () -> Content + private let content: Content - @ViewBuilder - private var headerView: some View { - Group { - if viewModel.item.type == .episode { - ImageView(viewModel.item.imageSource(.primary, maxWidth: 1920)) - } else { - ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920)) - } + init( + viewModel: ItemViewModel, + @ViewBuilder content: () -> Content + ) { + self.content = content() + self.viewModel = viewModel + } + + private var imageType: ImageType { + if viewModel.item.type == .episode { + return .primary + } else { + return .backdrop } - .aspectRatio(1.77, contentMode: .fill) } var body: some View { OffsetScrollView( headerHeight: globalSize.isLandscape ? 0.75 : 0.6 ) { - headerView + ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920)) + .aspectRatio(1.77, contentMode: .fill) } overlay: { VStack(spacing: 0) { Spacer() @@ -65,7 +71,7 @@ extension ItemView { } } } content: { - content() + content .edgePadding(.vertical) } .trackingSize($globalSize) diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift b/Swiftfin/Views/ItemView/SeriesItemContentView.swift similarity index 85% rename from Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift rename to Swiftfin/Views/ItemView/SeriesItemContentView.swift index 0fd2101a..381aa4c4 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/SeriesItemContentView.swift @@ -10,15 +10,18 @@ import Defaults import JellyfinAPI import SwiftUI -extension SeriesItemView { +extension ItemView { - struct ContentView: View { + struct SeriesItemContentView: View { @ObservedObject var viewModel: SeriesItemViewModel var body: some View { - VStack(alignment: .leading, spacing: 20) { + SeparatorVStack(alignment: .leading) { + RowDivider() + .padding(.vertical, 10) + } content: { // MARK: Episodes @@ -30,16 +33,12 @@ extension SeriesItemView { if let genres = viewModel.item.itemGenres, genres.isNotEmpty { ItemView.GenresHStack(genres: genres) - - RowDivider() } // MARK: Studios if let studios = viewModel.item.studios, studios.isNotEmpty { ItemView.StudiosHStack(studios: studios) - - RowDivider() } // MARK: Cast and Crew @@ -48,24 +47,18 @@ extension SeriesItemView { castAndCrew.isNotEmpty { ItemView.CastAndCrewHStack(people: castAndCrew) - - RowDivider() } // MARK: Special Features if viewModel.specialFeatures.isNotEmpty { ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) - - RowDivider() } // MARK: Similar if viewModel.similarItems.isNotEmpty { ItemView.SimilarItemsHStack(items: viewModel.similarItems) - - RowDivider() } ItemView.AboutView(viewModel: viewModel) diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift deleted file mode 100644 index f736b03b..00000000 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -struct CollectionItemView: View { - - @Default(.Customization.itemViewType) - private var itemViewType - - @ObservedObject - var viewModel: CollectionItemViewModel - - var body: some View { - switch itemViewType { - case .compactPoster: - ItemView.CompactPosterScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - case .compactLogo: - ItemView.CompactLogoScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - case .cinematic: - ItemView.CinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift deleted file mode 100644 index b8371ce9..00000000 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import BlurHashKit -import JellyfinAPI -import SwiftUI - -extension EpisodeItemView { - - struct ContentView: View { - - @EnvironmentObject - private var router: ItemCoordinator.Router - - @ObservedObject - var viewModel: EpisodeItemViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - - VStack(alignment: .center) { - ImageView(viewModel.item.imageSource(.primary, maxWidth: 600)) - .placeholder { source in - if let blurHash = source.blurHash { - BlurHashView(blurHash: blurHash, size: .Square(length: 8)) - } else { - Color.secondarySystemFill - .opacity(0.75) - } - } - .failure { - SystemImageContentView(systemName: viewModel.item.systemImage) - } - .frame(maxHeight: 300) - .posterStyle(.landscape) - .posterShadow() - .padding(.horizontal) - - ShelfView(viewModel: viewModel) - } - - // MARK: Overview - - ItemView.OverviewView(item: viewModel.item) - .overviewLineLimit(4) - .padding(.horizontal) - - RowDivider() - - // MARK: Genres - - if let genres = viewModel.item.itemGenres, genres.isNotEmpty { - ItemView.GenresHStack(genres: genres) - - RowDivider() - } - - // MARK: Studios - - if let studios = viewModel.item.studios, studios.isNotEmpty { - ItemView.StudiosHStack(studios: studios) - - RowDivider() - } - - // MARK: Cast and Crew - - if let castAndCrew = viewModel.item.people, - castAndCrew.isNotEmpty - { - ItemView.CastAndCrewHStack(people: castAndCrew) - - RowDivider() - } - - ItemView.AboutView(viewModel: viewModel) - } - } - } -} - -extension EpisodeItemView.ContentView { - - struct ShelfView: View { - - @EnvironmentObject - private var router: ItemCoordinator.Router - - @ObservedObject - var viewModel: EpisodeItemViewModel - - var body: some View { - VStack(alignment: .center, spacing: 10) { - Text(viewModel.item.seriesName ?? .emptyDash) - .font(.headline) - .fontWeight(.semibold) - .multilineTextAlignment(.center) - .lineLimit(2) - .padding(.horizontal) - .foregroundColor(.secondary) - - Text(viewModel.item.displayTitle) - .font(.title2) - .fontWeight(.bold) - .multilineTextAlignment(.center) - .lineLimit(2) - .padding(.horizontal) - - DotHStack { - if let seasonEpisodeLabel = viewModel.item.seasonEpisodeLabel { - Text(seasonEpisodeLabel) - } - - if let productionYear = viewModel.item.premiereDateYear { - Text(productionYear) - } - - if let runtime = viewModel.item.runTimeLabel { - Text(runtime) - } - } - .font(.caption) - .foregroundColor(.secondary) - .padding(.horizontal) - - ItemView.AttributesHStack(viewModel: viewModel, alignment: .center) - - ItemView.PlayButton(viewModel: viewModel) - .frame(maxWidth: 300) - .frame(height: 50) - - ItemView.ActionButtonHStack(viewModel: viewModel) - .font(.title) - .frame(maxWidth: 300) - .foregroundStyle(.primary) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift deleted file mode 100644 index 0b607492..00000000 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct EpisodeItemView: View { - - @ObservedObject - var viewModel: EpisodeItemViewModel - - var body: some View { - ScrollView(showsIndicators: false) { - ContentView(viewModel: viewModel) - .edgePadding(.bottom) - } - } -} diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift deleted file mode 100644 index 0e30eccd..00000000 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -struct MovieItemView: View { - - @Default(.Customization.itemViewType) - private var itemViewType - - @ObservedObject - var viewModel: MovieItemViewModel - - var body: some View { - switch itemViewType { - case .compactPoster: - ItemView.CompactPosterScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - case .compactLogo: - ItemView.CompactLogoScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - case .cinematic: - ItemView.CinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift deleted file mode 100644 index 36c771c4..00000000 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -struct SeriesItemView: View { - - @Default(.Customization.itemViewType) - private var itemViewType - - @ObservedObject - var viewModel: SeriesItemViewModel - - var body: some View { - switch itemViewType { - case .compactPoster: - ItemView.CompactPosterScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - case .compactLogo: - ItemView.CompactLogoScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - case .cinematic: - ItemView.CinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift deleted file mode 100644 index 4c701ea7..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -extension iPadOSCollectionItemView { - - struct ContentView: View { - - @EnvironmentObject - private var router: ItemCoordinator.Router - - @ObservedObject - var viewModel: CollectionItemViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 20) { - - // MARK: Items - - ForEach(viewModel.collectionItems.elements, id: \.key) { element in - if element.value.isNotEmpty { - PosterHStack( - title: element.key.pluralDisplayTitle, - type: .portrait, - items: element.value - ) - .trailing { - SeeAllButton() - .onSelect { - let viewModel = ItemLibraryViewModel( - title: viewModel.item.displayTitle, - id: viewModel.item.id, - element.value - ) - router.route(to: \.library, viewModel) - } - } - .onSelect { item in - router.route(to: \.item, item) - } - - RowDivider() - } - } - - // MARK: Genres - - if let genres = viewModel.item.itemGenres, genres.isNotEmpty { - ItemView.GenresHStack(genres: genres) - - RowDivider() - } - - // MARK: Studios - - if let studios = viewModel.item.studios, studios.isNotEmpty { - ItemView.StudiosHStack(studios: studios) - - RowDivider() - } - - // MARK: Similar - - if viewModel.similarItems.isNotEmpty { - ItemView.SimilarItemsHStack(items: viewModel.similarItems) - - RowDivider() - } - - ItemView.AboutView(viewModel: viewModel) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift deleted file mode 100644 index 4b73b2a8..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct iPadOSCollectionItemView: View { - - @ObservedObject - var viewModel: CollectionItemViewModel - - var body: some View { - ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift deleted file mode 100644 index 251d7488..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct iPadOSEpisodeItemView: View { - - @ObservedObject - var viewModel: EpisodeItemViewModel - - var body: some View { - ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift deleted file mode 100644 index e5341a8a..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -extension iPadOSMovieItemView { - - struct ContentView: View { - - @ObservedObject - var viewModel: MovieItemViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - - // MARK: Genres - - if let genres = viewModel.item.itemGenres, genres.isNotEmpty { - ItemView.GenresHStack(genres: genres) - - RowDivider() - } - - // MARK: Studios - - if let studios = viewModel.item.studios, studios.isNotEmpty { - ItemView.StudiosHStack(studios: studios) - - RowDivider() - } - - // MARK: Cast and Crew - - if let castAndCrew = viewModel.item.people, - castAndCrew.isNotEmpty - { - ItemView.CastAndCrewHStack(people: castAndCrew) - - RowDivider() - } - - // MARK: Special Features - - if viewModel.specialFeatures.isNotEmpty { - ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) - - RowDivider() - } - - // MARK: Similar - - if viewModel.similarItems.isNotEmpty { - ItemView.SimilarItemsHStack(items: viewModel.similarItems) - - RowDivider() - } - - ItemView.AboutView(viewModel: viewModel) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift deleted file mode 100644 index 04fd0ba5..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct iPadOSMovieItemView: View { - - @ObservedObject - var viewModel: MovieItemViewModel - - var body: some View { - ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift deleted file mode 100644 index d08781bd..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -extension iPadOSSeriesItemView { - - struct ContentView: View { - - @ObservedObject - var viewModel: SeriesItemViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 10) { - - // MARK: Episodes - - SeriesEpisodeSelector(viewModel: viewModel) - - // MARK: Genres - - if let genres = viewModel.item.itemGenres, genres.isNotEmpty { - ItemView.GenresHStack(genres: genres) - - RowDivider() - } - - // MARK: Studios - - if let studios = viewModel.item.studios, studios.isNotEmpty { - ItemView.StudiosHStack(studios: studios) - - RowDivider() - } - - // MARK: Cast and Crew - - if let castAndCrew = viewModel.item.people, - castAndCrew.isNotEmpty - { - ItemView.CastAndCrewHStack(people: castAndCrew) - - RowDivider() - } - - // MARK: Special Features - - if viewModel.specialFeatures.isNotEmpty { - ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) - - RowDivider() - } - - // MARK: Similar - - if viewModel.similarItems.isNotEmpty { - ItemView.SimilarItemsHStack(items: viewModel.similarItems) - - RowDivider() - } - - ItemView.AboutView(viewModel: viewModel) - } - } - } -} diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift deleted file mode 100644 index 0e73a1dc..00000000 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// 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 (c) 2025 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -struct iPadOSSeriesItemView: View { - - @ObservedObject - var viewModel: SeriesItemViewModel - - var body: some View { - ItemView.iPadOSCinematicScrollView(viewModel: viewModel) { - ContentView(viewModel: viewModel) - } - } -} diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 2e4eec56699383f6648f187f2a39f9adc10e0635..2d71f6889b7dd01da1ffd3e164226fcf2e84dfaa 100644 GIT binary patch delta 14 WcmX@Mm#u9N+lC+KHt#skcOC#d<_KQ^ delta 28 mcmV+%0OS9LyavX-2C(?f0d|x5&U=$g%?^_~&nUC%&K%F(Xb)%r