Cleanup `ItemView`s (#1543)

* wip

* wip

* Update ItemView.swift

* cleanup, fix images

* cleanup

* Update Package.resolved

* Update Localizable.strings
This commit is contained in:
Ethan Pippin 2025-05-21 00:15:34 -04:00 committed by GitHub
parent 4402227ea2
commit 8be5df69b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 339 additions and 908 deletions

View File

@ -17,7 +17,7 @@ import UIKit
extension BaseItemDto: Displayable { extension BaseItemDto: Displayable {
var displayTitle: String { var displayTitle: String {
name ?? .emptyDash name ?? L10n.unknown
} }
} }
@ -268,7 +268,6 @@ extension BaseItemDto {
album album
case .episode: case .episode:
seriesName seriesName
case .program: nil
default: default:
nil nil
} }

View File

@ -872,8 +872,6 @@ internal enum L10n {
internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV programs") internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV programs")
/// Live TV recording management /// Live TV recording management
internal static let liveTVRecordingManagement = L10n.tr("Localizable", "liveTVRecordingManagement", fallback: "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 /// Loading user failed
internal static let loadingUserFailed = L10n.tr("Localizable", "loadingUserFailed", fallback: "Loading user failed") internal static let loadingUserFailed = L10n.tr("Localizable", "loadingUserFailed", fallback: "Loading user failed")
/// Local /// Local

View File

@ -943,23 +943,13 @@
E18D6AA62BAA96F000A0D167 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E18D6AA52BAA96F000A0D167 /* CollectionHStack */; }; E18D6AA62BAA96F000A0D167 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E18D6AA52BAA96F000A0D167 /* CollectionHStack */; };
E18E01AB288746AF0022598C /* PillHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A5288746AF0022598C /* PillHStack.swift */; }; E18E01AB288746AF0022598C /* PillHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A5288746AF0022598C /* PillHStack.swift */; };
E18E01AD288746AF0022598C /* DotHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01A7288746AF0022598C /* DotHStack.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 */; }; 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 */; }; 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 */; }; E18E01E3288747230022598C /* CompactPortraitScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C5288747230022598C /* CompactPortraitScrollView.swift */; };
E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C6288747230022598C /* CompactLogoScrollView.swift */; }; E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C6288747230022598C /* CompactLogoScrollView.swift */; };
E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01C7288747230022598C /* CinematicScrollView.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 */; }; E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CA288747230022598C /* CollectionItemContentView.swift */; };
E18E01E8288747230022598C /* SeriesItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01CC288747230022598C /* SeriesItemContentView.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 */; }; E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D0288747230022598C /* MovieItemContentView.swift */; };
E18E01EE288747230022598C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D5288747230022598C /* AboutView.swift */; }; E18E01EE288747230022598C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D5288747230022598C /* AboutView.swift */; };
E18E01F0288747230022598C /* AttributeHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01D7288747230022598C /* AttributeHStack.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 */; }; E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; };
E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */; }; E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */; };
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.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 */; }; E19D41A72BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */; };
E19D41A82BEEDC5F0082B8B2 /* 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 */; }; 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 */; }; E1F5CF062CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; };
E1F5CF082CB0A04500607465 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF072CB0A04500607465 /* Text.swift */; }; E1F5CF082CB0A04500607465 /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF072CB0A04500607465 /* Text.swift */; };
E1F5CF092CB0A04500607465 /* 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 */; }; E1FAD1C62A0375BA007F5521 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E1FAD1C52A0375BA007F5521 /* UDPBroadcast */; };
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; }; E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1FCD08726C35A0D007C8DCF /* NetworkError.swift */; };
E1FCD08926C35A0D007C8DCF /* 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 = "<group>"; }; E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeatingTimer.swift; sourceTree = "<group>"; };
E18E01A5288746AF0022598C /* PillHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillHStack.swift; sourceTree = "<group>"; }; E18E01A5288746AF0022598C /* PillHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PillHStack.swift; sourceTree = "<group>"; };
E18E01A7288746AF0022598C /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = "<group>"; }; E18E01A7288746AF0022598C /* DotHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DotHStack.swift; sourceTree = "<group>"; };
E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSEpisodeContentView.swift; sourceTree = "<group>"; };
E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSEpisodeItemView.swift; sourceTree = "<group>"; };
E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSCinematicScrollView.swift; sourceTree = "<group>"; }; E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSCinematicScrollView.swift; sourceTree = "<group>"; };
E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSSeriesItemContentView.swift; sourceTree = "<group>"; };
E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSSeriesItemView.swift; sourceTree = "<group>"; };
E18E01BE288747230022598C /* iPadOSMovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSMovieItemView.swift; sourceTree = "<group>"; };
E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = iPadOSMovieItemContentView.swift; sourceTree = "<group>"; };
E18E01C2288747230022598C /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = "<group>"; }; E18E01C2288747230022598C /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = "<group>"; };
E18E01C3288747230022598C /* EpisodeItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = "<group>"; };
E18E01C5288747230022598C /* CompactPortraitScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactPortraitScrollView.swift; sourceTree = "<group>"; }; E18E01C5288747230022598C /* CompactPortraitScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactPortraitScrollView.swift; sourceTree = "<group>"; };
E18E01C6288747230022598C /* CompactLogoScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactLogoScrollView.swift; sourceTree = "<group>"; }; E18E01C6288747230022598C /* CompactLogoScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactLogoScrollView.swift; sourceTree = "<group>"; };
E18E01C7288747230022598C /* CinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CinematicScrollView.swift; sourceTree = "<group>"; }; E18E01C7288747230022598C /* CinematicScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CinematicScrollView.swift; sourceTree = "<group>"; };
E18E01C9288747230022598C /* CollectionItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = "<group>"; };
E18E01CA288747230022598C /* CollectionItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = "<group>"; }; E18E01CA288747230022598C /* CollectionItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = "<group>"; };
E18E01CC288747230022598C /* SeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemContentView.swift; sourceTree = "<group>"; }; E18E01CC288747230022598C /* SeriesItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemContentView.swift; sourceTree = "<group>"; };
E18E01CD288747230022598C /* SeriesItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = "<group>"; };
E18E01CF288747230022598C /* MovieItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = "<group>"; };
E18E01D0288747230022598C /* MovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemContentView.swift; sourceTree = "<group>"; }; E18E01D0288747230022598C /* MovieItemContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MovieItemContentView.swift; sourceTree = "<group>"; };
E18E01D5288747230022598C /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; E18E01D5288747230022598C /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
E18E01D7288747230022598C /* AttributeHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeHStack.swift; sourceTree = "<group>"; }; E18E01D7288747230022598C /* AttributeHStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributeHStack.swift; sourceTree = "<group>"; };
@ -1975,6 +1954,7 @@
E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = "<group>"; }; E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = "<group>"; };
E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; }; E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = "<group>"; };
E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = "<group>"; }; E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = "<group>"; };
E19523742DD8F18B00442F15 /* SimpleScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleScrollView.swift; sourceTree = "<group>"; };
E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityViewModel.swift; sourceTree = "<group>"; }; E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityViewModel.swift; sourceTree = "<group>"; };
E19D41A92BF077130082B8B2 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; }; E19D41A92BF077130082B8B2 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
E19D41AB2BF288110082B8B2 /* ServerCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerCheckView.swift; sourceTree = "<group>"; }; E19D41AB2BF288110082B8B2 /* ServerCheckView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerCheckView.swift; sourceTree = "<group>"; };
@ -2152,8 +2132,6 @@
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; }; E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
E1F5CF042CB09EA000607465 /* CurrentDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentDate.swift; sourceTree = "<group>"; }; E1F5CF042CB09EA000607465 /* CurrentDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentDate.swift; sourceTree = "<group>"; };
E1F5CF072CB0A04500607465 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; }; E1F5CF072CB0A04500607465 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemView.swift; sourceTree = "<group>"; };
E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPadOSCollectionItemContentView.swift; sourceTree = "<group>"; };
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; }; E1FCD08726C35A0D007C8DCF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
E1FE28C82DC16B2B00E1A23E /* RedrawOnNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnNotificationView.swift; sourceTree = "<group>"; }; E1FE28C82DC16B2B00E1A23E /* RedrawOnNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnNotificationView.swift; sourceTree = "<group>"; };
E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; }; E1FE69A628C29B720021BC93 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = "<group>"; };
@ -4655,10 +4633,13 @@
E14F7D0A26DB3714007C3AE6 /* ItemView */ = { E14F7D0A26DB3714007C3AE6 /* ItemView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
535BAE9E2649E569005FA86D /* ItemView.swift */, E18E01CA288747230022598C /* CollectionItemContentView.swift */,
E18E01D4288747230022598C /* Components */, E18E01D4288747230022598C /* Components */,
E18E01C0288747230022598C /* iOS */, E18E01C2288747230022598C /* EpisodeItemContentView.swift */,
E18E01B4288747230022598C /* iPadOS */, 535BAE9E2649E569005FA86D /* ItemView.swift */,
E18E01D0288747230022598C /* MovieItemContentView.swift */,
E18E01C4288747230022598C /* ScrollViews */,
E18E01CC288747230022598C /* SeriesItemContentView.swift */,
); );
path = ItemView; path = ItemView;
sourceTree = "<group>"; sourceTree = "<group>";
@ -4918,111 +4899,18 @@
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E18E01B4288747230022598C /* iPadOS */ = {
isa = PBXGroup;
children = (
E1FA891C289A302600176FEB /* CollectionItemView */,
E18E01B5288747230022598C /* EpisodeItemView */,
E18E01BD288747230022598C /* MovieItemView */,
E18E01B8288747230022598C /* ScrollViews */,
E18E01BA288747230022598C /* SeriesItemView */,
);
path = iPadOS;
sourceTree = "<group>";
};
E18E01B5288747230022598C /* EpisodeItemView */ = {
isa = PBXGroup;
children = (
E18E01B6288747230022598C /* iPadOSEpisodeContentView.swift */,
E18E01B7288747230022598C /* iPadOSEpisodeItemView.swift */,
);
path = EpisodeItemView;
sourceTree = "<group>";
};
E18E01B8288747230022598C /* ScrollViews */ = {
isa = PBXGroup;
children = (
E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */,
);
path = ScrollViews;
sourceTree = "<group>";
};
E18E01BA288747230022598C /* SeriesItemView */ = {
isa = PBXGroup;
children = (
E18E01BB288747230022598C /* iPadOSSeriesItemContentView.swift */,
E18E01BC288747230022598C /* iPadOSSeriesItemView.swift */,
);
path = SeriesItemView;
sourceTree = "<group>";
};
E18E01BD288747230022598C /* MovieItemView */ = {
isa = PBXGroup;
children = (
E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */,
E18E01BE288747230022598C /* iPadOSMovieItemView.swift */,
);
path = MovieItemView;
sourceTree = "<group>";
};
E18E01C0288747230022598C /* iOS */ = {
isa = PBXGroup;
children = (
E18E01C8288747230022598C /* CollectionItemView */,
E18E01C1288747230022598C /* EpisodeItemView */,
E18E01CE288747230022598C /* MovieItemView */,
E18E01C4288747230022598C /* ScrollViews */,
E18E01CB288747230022598C /* SeriesItemView */,
);
path = iOS;
sourceTree = "<group>";
};
E18E01C1288747230022598C /* EpisodeItemView */ = {
isa = PBXGroup;
children = (
E18E01C2288747230022598C /* EpisodeItemContentView.swift */,
E18E01C3288747230022598C /* EpisodeItemView.swift */,
);
path = EpisodeItemView;
sourceTree = "<group>";
};
E18E01C4288747230022598C /* ScrollViews */ = { E18E01C4288747230022598C /* ScrollViews */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E18E01C5288747230022598C /* CompactPortraitScrollView.swift */,
E18E01C6288747230022598C /* CompactLogoScrollView.swift */,
E18E01C7288747230022598C /* CinematicScrollView.swift */, E18E01C7288747230022598C /* CinematicScrollView.swift */,
E18E01C6288747230022598C /* CompactLogoScrollView.swift */,
E18E01C5288747230022598C /* CompactPortraitScrollView.swift */,
E18E01B9288747230022598C /* iPadOSCinematicScrollView.swift */,
E19523742DD8F18B00442F15 /* SimpleScrollView.swift */,
); );
path = ScrollViews; path = ScrollViews;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E18E01C8288747230022598C /* CollectionItemView */ = {
isa = PBXGroup;
children = (
E18E01CA288747230022598C /* CollectionItemContentView.swift */,
E18E01C9288747230022598C /* CollectionItemView.swift */,
);
path = CollectionItemView;
sourceTree = "<group>";
};
E18E01CB288747230022598C /* SeriesItemView */ = {
isa = PBXGroup;
children = (
E18E01CC288747230022598C /* SeriesItemContentView.swift */,
E18E01CD288747230022598C /* SeriesItemView.swift */,
);
path = SeriesItemView;
sourceTree = "<group>";
};
E18E01CE288747230022598C /* MovieItemView */ = {
isa = PBXGroup;
children = (
E18E01D0288747230022598C /* MovieItemContentView.swift */,
E18E01CF288747230022598C /* MovieItemView.swift */,
);
path = MovieItemView;
sourceTree = "<group>";
};
E18E01D4288747230022598C /* Components */ = { E18E01D4288747230022598C /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -5518,15 +5406,6 @@
path = MediaSourceInfo; path = MediaSourceInfo;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E1FA891C289A302600176FEB /* CollectionItemView */ = {
isa = PBXGroup;
children = (
E1FA891D289A305D00176FEB /* iPadOSCollectionItemContentView.swift */,
E1FA891A289A302300176FEB /* iPadOSCollectionItemView.swift */,
);
path = CollectionItemView;
sourceTree = "<group>";
};
E1FCD08E26C466F3007C8DCF /* Errors */ = { E1FCD08E26C466F3007C8DCF /* Errors */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -6466,7 +6345,6 @@
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
4EC2B1A22CC96F6600D866BE /* ServerUsersViewModel.swift in Sources */, 4EC2B1A22CC96F6600D866BE /* ServerUsersViewModel.swift in Sources */,
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */, E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */,
E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */,
4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */, 4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */,
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
621338932660107500A81A2A /* String.swift in Sources */, 621338932660107500A81A2A /* String.swift in Sources */,
@ -6513,7 +6391,6 @@
C46DD8D22A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */, C46DD8D22A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift in Sources */,
4EB538C32CE3E21800EB72D5 /* SyncPlaySection.swift in Sources */, 4EB538C32CE3E21800EB72D5 /* SyncPlaySection.swift in Sources */,
E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */, E18ACA8B2A14301800BB4F35 /* ScalingButtonStyle.swift in Sources */,
E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */,
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */,
E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */, E154966E296CA2EF00C4EF88 /* LogManager.swift in Sources */,
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */, 62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
@ -6585,7 +6462,6 @@
4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */, 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */,
4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */, 4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */,
4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */, 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */,
E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */,
E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */,
E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */, E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */,
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
@ -6634,7 +6510,6 @@
E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */, E1EA09692BED78BB004CDE76 /* UserAccessPolicy.swift in Sources */,
4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */, 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */,
E18E0204288749200022598C /* RowDivider.swift in Sources */, E18E0204288749200022598C /* RowDivider.swift in Sources */,
E18E01DA288747230022598C /* iPadOSEpisodeContentView.swift in Sources */,
E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */,
E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */, E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */,
E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */, E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */,
@ -6738,6 +6613,7 @@
E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */, E18E01E4288747230022598C /* CompactLogoScrollView.swift in Sources */,
E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */, E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */,
E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */,
E19523752DD8F18B00442F15 /* SimpleScrollView.swift in Sources */,
4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */, 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */,
4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */, 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */,
C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */,
@ -6771,7 +6647,6 @@
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */, E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */, E1E0BEB729EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */,
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */,
E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */, E12CC1AE28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */,
E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */, E1549666296CA2EF00C4EF88 /* Notifications.swift in Sources */,
4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */, 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */,
@ -6784,7 +6659,6 @@
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */, E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
4E9654492D99C553006CB024 /* CollectionType.swift in Sources */, 4E9654492D99C553006CB024 /* CollectionType.swift in Sources */,
4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */, 4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */,
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */, E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */,
4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */, 4E0195E42CE0467B007844F4 /* ItemSection.swift in Sources */,
E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */, E10231442BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
@ -6820,7 +6694,6 @@
E1FE28C92DC16B2B00E1A23E /* RedrawOnNotificationView.swift in Sources */, E1FE28C92DC16B2B00E1A23E /* RedrawOnNotificationView.swift in Sources */,
E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */,
4EF36F642D962A430065BB79 /* ItemSortBy.swift in Sources */, 4EF36F642D962A430065BB79 /* ItemSortBy.swift in Sources */,
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */, E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */,
E1EA09672BED6815004CDE76 /* UserSignInSecurityView.swift in Sources */, E1EA09672BED6815004CDE76 /* UserSignInSecurityView.swift in Sources */,
@ -6829,7 +6702,6 @@
E18A8E8028D6083700333B9A /* MediaSourceInfo+ItemVideoPlayerViewModel.swift in Sources */, E18A8E8028D6083700333B9A /* MediaSourceInfo+ItemVideoPlayerViewModel.swift in Sources */,
E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */, E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */,
E101ECD52CD40489001EA89E /* DeviceDetailViewModel.swift in Sources */, E101ECD52CD40489001EA89E /* DeviceDetailViewModel.swift in Sources */,
E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */,
4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */, 4E35CE5C2CBED3F300DBD886 /* TimeRow.swift in Sources */,
4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */, 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */,
4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */, 4E35CE5D2CBED3F300DBD886 /* TriggerTypeRow.swift in Sources */,
@ -6906,7 +6778,6 @@
BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */,
E1BDF2F529524E6400CC0294 /* PlayNextItemActionButton.swift in Sources */, E1BDF2F529524E6400CC0294 /* PlayNextItemActionButton.swift in Sources */,
BD3957772C112AD30078CEF8 /* SliderSection.swift in Sources */, BD3957772C112AD30078CEF8 /* SliderSection.swift in Sources */,
E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */,
E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */, E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */,
4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */, 4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */,
E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */, E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */,
@ -6915,7 +6786,6 @@
E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */, E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */,
E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */, E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */,
E1CB758C2C80F9EC00217C76 /* CodecProfile.swift in Sources */, E1CB758C2C80F9EC00217C76 /* CodecProfile.swift in Sources */,
E18E01E9288747230022598C /* SeriesItemView.swift in Sources */,
E15756342936851D00976E1F /* NativeVideoPlayerSettingsView.swift in Sources */, E15756342936851D00976E1F /* NativeVideoPlayerSettingsView.swift in Sources */,
E1D4BF7C2719D05000A11E64 /* AppSettingsView.swift in Sources */, E1D4BF7C2719D05000A11E64 /* AppSettingsView.swift in Sources */,
4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */, 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */,
@ -6998,7 +6868,6 @@
4EF36F662D9649050065BB79 /* SessionInfoDto.swift in Sources */, 4EF36F662D9649050065BB79 /* SessionInfoDto.swift in Sources */,
E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */, E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */,
4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */, 4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */,
E18E01E6288747230022598C /* CollectionItemView.swift in Sources */,
E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */, E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */,
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */,
@ -7032,7 +6901,6 @@
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
E1D8429529346C6400D1041A /* BasicStepper.swift in Sources */, E1D8429529346C6400D1041A /* BasicStepper.swift in Sources */,
4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */, 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */,
E18E01EA288747230022598C /* MovieItemView.swift in Sources */,
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */, E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */,
4E8F74AF2CE03E2E00CC8969 /* RefreshMetadataButton.swift in Sources */, 4E8F74AF2CE03E2E00CC8969 /* RefreshMetadataButton.swift in Sources */,

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "44d87a45bd21720bc457afc9350e1268808e629c432ce75d0e6c4c26ce3b67ce", "originHash" : "66bff9f26defe8d2dfa92b4e65d0ae348e3b586d0fbb7de49c9c937459e6b55c",
"pins" : [ "pins" : [
{ {
"identity" : "blurhashkit", "identity" : "blurhashkit",
@ -24,6 +24,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/LePips/CollectionHStack", "location" : "https://github.com/LePips/CollectionHStack",
"state" : { "state" : {
"branch" : "main",
"revision" : "03dc666e8b20ec216fda60f55ccc0eeaabbc5fad" "revision" : "03dc666e8b20ec216fda60f55ccc0eeaabbc5fad"
} }
}, },
@ -32,6 +33,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/LePips/CollectionVGrid", "location" : "https://github.com/LePips/CollectionVGrid",
"state" : { "state" : {
"branch" : "main",
"revision" : "70db2318ce64d49aa8b536e0623b96cb323fbdf1" "revision" : "70db2318ce64d49aa8b536e0623b96cb323fbdf1"
} }
}, },

View File

@ -10,9 +10,9 @@ import BlurHashKit
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
extension CollectionItemView { extension ItemView {
struct ContentView: View { struct CollectionItemContentView: View {
@EnvironmentObject @EnvironmentObject
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ -21,9 +21,12 @@ extension CollectionItemView {
var viewModel: CollectionItemViewModel var viewModel: CollectionItemViewModel
var body: some View { 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 ForEach(viewModel.collectionItems.elements, id: \.key) { element in
if element.value.isNotEmpty { if element.value.isNotEmpty {
@ -46,8 +49,6 @@ extension CollectionItemView {
.onSelect { item in .onSelect { item in
router.route(to: \.item, item) router.route(to: \.item, item)
} }
RowDivider()
} }
} }
@ -55,24 +56,18 @@ extension CollectionItemView {
if let genres = viewModel.item.itemGenres, genres.isNotEmpty { if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
ItemView.GenresHStack(genres: genres) ItemView.GenresHStack(genres: genres)
RowDivider()
} }
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, studios.isNotEmpty { if let studios = viewModel.item.studios, studios.isNotEmpty {
ItemView.StudiosHStack(studios: studios) ItemView.StudiosHStack(studios: studios)
RowDivider()
} }
// MARK: Similar // MARK: Similar
if viewModel.similarItems.isNotEmpty { if viewModel.similarItems.isNotEmpty {
ItemView.SimilarItemsHStack(items: viewModel.similarItems) ItemView.SimilarItemsHStack(items: viewModel.similarItems)
RowDivider()
} }
ItemView.AboutView(viewModel: viewModel) ItemView.AboutView(viewModel: viewModel)

View File

@ -6,12 +6,13 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // Copyright (c) 2025 Jellyfin & Jellyfin Contributors
// //
import BlurHashKit
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
extension iPadOSEpisodeItemView { extension ItemView {
struct ContentView: View { struct EpisodeItemContentView: View {
@EnvironmentObject @EnvironmentObject
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ -20,22 +21,21 @@ extension iPadOSEpisodeItemView {
var viewModel: EpisodeItemViewModel var viewModel: EpisodeItemViewModel
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 10) { SeparatorVStack(alignment: .leading) {
RowDivider()
.padding(.vertical, 10)
} content: {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.itemGenres, genres.isNotEmpty { if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
ItemView.GenresHStack(genres: genres) ItemView.GenresHStack(genres: genres)
RowDivider()
} }
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, studios.isNotEmpty { if let studios = viewModel.item.studios, studios.isNotEmpty {
ItemView.StudiosHStack(studios: studios) ItemView.StudiosHStack(studios: studios)
RowDivider()
} }
// MARK: Cast and Crew // MARK: Cast and Crew
@ -44,8 +44,6 @@ extension iPadOSEpisodeItemView {
castAndCrew.isNotEmpty castAndCrew.isNotEmpty
{ {
ItemView.CastAndCrewHStack(people: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
RowDivider()
} }
ItemView.AboutView(viewModel: viewModel) ItemView.AboutView(viewModel: viewModel)

View File

@ -6,15 +6,22 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // Copyright (c) 2025 Jellyfin & Jellyfin Contributors
// //
import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI 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 { struct ItemView: View {
protocol ScrollContainerView: View {
associatedtype Content: View
init(viewModel: ItemViewModel, content: @escaping () -> Content)
}
@Default(.Customization.itemViewType)
private var itemViewType
@EnvironmentObject @EnvironmentObject
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ -24,7 +31,7 @@ struct ItemView: View {
private var deleteViewModel: DeleteItemViewModel private var deleteViewModel: DeleteItemViewModel
@State @State
private var showConfirmationDialog = false private var isPresentingConfirmationDialog = false
@State @State
private var isPresentingEventAlert = false private var isPresentingEventAlert = false
@State @State
@ -73,51 +80,58 @@ struct ItemView: View {
} }
@ViewBuilder @ViewBuilder
private var padView: some View { private var scrollContentView: some View {
switch viewModel.item.type { switch viewModel.item.type {
case .boxSet: case .boxSet:
iPadOSCollectionItemView(viewModel: viewModel as! CollectionItemViewModel) CollectionItemContentView(viewModel: viewModel as! CollectionItemViewModel)
case .episode: case .episode:
iPadOSEpisodeItemView(viewModel: viewModel as! EpisodeItemViewModel) EpisodeItemContentView(viewModel: viewModel as! EpisodeItemViewModel)
case .movie: case .movie:
iPadOSMovieItemView(viewModel: viewModel as! MovieItemViewModel) MovieItemContentView(viewModel: viewModel as! MovieItemViewModel)
case .series: case .series:
iPadOSSeriesItemView(viewModel: viewModel as! SeriesItemViewModel) SeriesItemContentView(viewModel: viewModel as! SeriesItemViewModel)
default: default:
Text(L10n.notImplementedYetWithType(viewModel.item.type ?? "--")) Text(L10n.notImplementedYetWithType(viewModel.item.type ?? "--"))
} }
} }
@ViewBuilder // TODO: break out into pad vs phone views based on item type
private var phoneView: some View { private func scrollContainerView<Content: View>(
switch viewModel.item.type { viewModel: ItemViewModel,
case .boxSet: content: @escaping () -> Content
CollectionItemView(viewModel: viewModel as! CollectionItemViewModel) ) -> any ScrollContainerView {
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 ?? "--"))
}
}
@ViewBuilder
private var contentView: some View {
if UIDevice.isPad { if UIDevice.isPad {
padView return iPadOSCinematicScrollView(viewModel: viewModel, content: content)
} else {
phoneView
} }
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 { var body: some View {
ZStack { ZStack {
switch viewModel.state { switch viewModel.state {
case .content: case .content:
contentView innerBody
.navigationTitle(viewModel.item.displayTitle) .navigationTitle(viewModel.item.displayTitle)
case let .error(error): case let .error(error):
ErrorView(error: error) ErrorView(error: error)
@ -143,14 +157,14 @@ struct ItemView: View {
if canDelete { if canDelete {
Section { Section {
Button(L10n.delete, systemImage: "trash", role: .destructive) { Button(L10n.delete, systemImage: "trash", role: .destructive) {
showConfirmationDialog = true isPresentingConfirmationDialog = true
} }
} }
} }
} }
.confirmationDialog( .confirmationDialog(
L10n.deleteItemConfirmationMessage, L10n.deleteItemConfirmationMessage,
isPresented: $showConfirmationDialog, isPresented: $isPresentingConfirmationDialog,
titleVisibility: .visible titleVisibility: .visible
) { ) {
Button(L10n.confirm, role: .destructive) { Button(L10n.confirm, role: .destructive) {

View File

@ -10,30 +10,29 @@ import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
extension MovieItemView { extension ItemView {
struct ContentView: View { struct MovieItemContentView: View {
@ObservedObject @ObservedObject
var viewModel: MovieItemViewModel var viewModel: MovieItemViewModel
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 10) { SeparatorVStack(alignment: .leading) {
RowDivider()
.padding(.vertical, 10)
} content: {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.itemGenres, genres.isNotEmpty { if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
ItemView.GenresHStack(genres: genres) ItemView.GenresHStack(genres: genres)
RowDivider()
} }
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, studios.isNotEmpty { if let studios = viewModel.item.studios, studios.isNotEmpty {
ItemView.StudiosHStack(studios: studios) ItemView.StudiosHStack(studios: studios)
RowDivider()
} }
// MARK: Cast and Crew // MARK: Cast and Crew
@ -42,29 +41,22 @@ extension MovieItemView {
castAndCrew.isNotEmpty castAndCrew.isNotEmpty
{ {
ItemView.CastAndCrewHStack(people: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
RowDivider()
} }
// MARK: Special Features // MARK: Special Features
if viewModel.specialFeatures.isNotEmpty { if viewModel.specialFeatures.isNotEmpty {
ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures)
RowDivider()
} }
// MARK: Similar // MARK: Similar
if viewModel.similarItems.isNotEmpty { if viewModel.similarItems.isNotEmpty {
ItemView.SimilarItemsHStack(items: viewModel.similarItems) ItemView.SimilarItemsHStack(items: viewModel.similarItems)
RowDivider()
} }
ItemView.AboutView(viewModel: viewModel) ItemView.AboutView(viewModel: viewModel)
} }
.animation(.linear(duration: 0.2), value: viewModel.item)
} }
} }
} }

View File

@ -12,7 +12,7 @@ import SwiftUI
extension ItemView { extension ItemView {
struct CinematicScrollView<Content: View>: View { struct CinematicScrollView<Content: View>: ScrollContainerView {
@Default(.Customization.CinematicItemViewType.usePrimaryImage) @Default(.Customization.CinematicItemViewType.usePrimaryImage)
private var usePrimaryImage private var usePrimaryImage
@ -21,12 +21,29 @@ extension ItemView {
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ObservedObject @ObservedObject
var viewModel: ItemViewModel private var viewModel: ItemViewModel
@State private let blurHashBottomEdgeColor: Color
private var blurHashBottomEdgeColor: Color = .secondarySystemFill 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 @ViewBuilder
private var headerView: some View { private var headerView: some View {
@ -37,16 +54,6 @@ extension ItemView {
.aspectRatio(usePrimaryImage ? (2 / 3) : 1.77, contentMode: .fill) .aspectRatio(usePrimaryImage ? (2 / 3) : 1.77, contentMode: .fill)
.frame(height: UIScreen.main.bounds.height * 0.6) .frame(height: UIScreen.main.bounds.height * 0.6)
.bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) .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 { var body: some View {
@ -75,7 +82,7 @@ extension ItemView {
} }
} }
} content: { } content: {
content() content
.edgePadding(.vertical) .edgePadding(.vertical)
} }
} }
@ -87,7 +94,7 @@ extension ItemView.CinematicScrollView {
struct OverlayView: View { struct OverlayView: View {
@Default(.Customization.CinematicItemViewType.usePrimaryImage) @Default(.Customization.CinematicItemViewType.usePrimaryImage)
private var cinematicItemViewTypeUsePrimaryImage private var usePrimaryImage
@EnvironmentObject @EnvironmentObject
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ -97,7 +104,7 @@ extension ItemView.CinematicScrollView {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .center, spacing: 10) { VStack(alignment: .center, spacing: 10) {
if !cinematicItemViewTypeUsePrimaryImage { if !usePrimaryImage {
ImageView(viewModel.item.imageURL(.logo, maxHeight: 100)) ImageView(viewModel.item.imageURL(.logo, maxHeight: 100))
.placeholder { _ in .placeholder { _ in
EmptyView() EmptyView()
@ -130,17 +137,18 @@ extension ItemView.CinematicScrollView {
.foregroundColor(Color(UIColor.lightGray)) .foregroundColor(Color(UIColor.lightGray))
.padding(.horizontal) .padding(.horizontal)
Group {
if viewModel.presentPlayButton { if viewModel.presentPlayButton {
ItemView.PlayButton(viewModel: viewModel) ItemView.PlayButton(viewModel: viewModel)
.frame(maxWidth: 300)
.frame(height: 50) .frame(height: 50)
} }
ItemView.ActionButtonHStack(viewModel: viewModel) ItemView.ActionButtonHStack(viewModel: viewModel)
.font(.title) .font(.title)
.frame(maxWidth: 300)
.foregroundColor(.white) .foregroundColor(.white)
} }
.frame(maxWidth: 300)
}
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
ItemView.OverviewView(item: viewModel.item) ItemView.OverviewView(item: viewModel.item)

View File

@ -11,28 +11,21 @@ import SwiftUI
extension ItemView { extension ItemView {
struct CompactLogoScrollView<Content: View>: View { struct CompactLogoScrollView<Content: View>: ScrollContainerView {
@EnvironmentObject @EnvironmentObject
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ObservedObject @ObservedObject
var viewModel: ItemViewModel private var viewModel: ItemViewModel
@State private let blurHashBottomEdgeColor: Color
private var scrollViewOffset: CGFloat = 0 private let content: Content
@State
private var blurHashBottomEdgeColor: Color = .secondarySystemFill
let content: () -> Content init(
viewModel: ItemViewModel,
@ViewBuilder content: @escaping () -> Content
private var headerView: some View { ) {
ImageView(viewModel.item.imageSource(.backdrop, maxHeight: UIScreen.main.bounds.height * 0.35))
.aspectRatio(1.77, contentMode: .fill)
.frame(height: UIScreen.main.bounds.height * 0.35)
.bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor)
.onAppear {
if let backdropBlurHash = viewModel.item.blurHash(.backdrop) { if let backdropBlurHash = viewModel.item.blurHash(.backdrop) {
let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB let bottomRGB = BlurHash(string: backdropBlurHash)!.averageLinearRGB
blurHashBottomEdgeColor = Color( blurHashBottomEdgeColor = Color(
@ -40,8 +33,20 @@ extension ItemView {
green: Double(bottomRGB.1), green: Double(bottomRGB.1),
blue: Double(bottomRGB.2) blue: Double(bottomRGB.2)
) )
} else {
blurHashBottomEdgeColor = Color.secondarySystemFill
} }
self.content = content()
self.viewModel = viewModel
} }
@ViewBuilder
private var headerView: some View {
ImageView(viewModel.item.imageSource(.backdrop, maxHeight: UIScreen.main.bounds.height * 0.35))
.aspectRatio(1.77, contentMode: .fill)
.frame(height: UIScreen.main.bounds.height * 0.35)
.bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor)
} }
var body: some View { var body: some View {
@ -78,7 +83,7 @@ extension ItemView {
RowDivider() RowDivider()
content() content
} }
.edgePadding(.vertical) .edgePadding(.vertical)
} }
@ -131,6 +136,7 @@ extension ItemView.CompactLogoScrollView {
ItemView.AttributesHStack(viewModel: viewModel) ItemView.AttributesHStack(viewModel: viewModel)
Group {
if viewModel.presentPlayButton { if viewModel.presentPlayButton {
ItemView.PlayButton(viewModel: viewModel) ItemView.PlayButton(viewModel: viewModel)
.frame(height: 50) .frame(height: 50)
@ -138,7 +144,9 @@ extension ItemView.CompactLogoScrollView {
ItemView.ActionButtonHStack(viewModel: viewModel) ItemView.ActionButtonHStack(viewModel: viewModel)
.font(.title) .font(.title)
.foregroundColor(.white) .foregroundStyle(.white)
}
.frame(maxWidth: 300)
} }
.frame(maxWidth: .infinity, alignment: .bottom) .frame(maxWidth: .infinity, alignment: .bottom)
} }

View File

@ -11,27 +11,34 @@ import SwiftUI
extension ItemView { extension ItemView {
struct CompactPosterScrollView<Content: View>: View { struct CompactPosterScrollView<Content: View>: ScrollContainerView {
@EnvironmentObject @EnvironmentObject
private var router: ItemCoordinator.Router private var router: ItemCoordinator.Router
@ObservedObject @ObservedObject
var viewModel: ItemViewModel private var viewModel: ItemViewModel
@State private let blurHashBottomEdgeColor: Color
private var scrollViewOffset: CGFloat = 0 private let content: Content
@State
private var blurHashBottomEdgeColor: Color = .secondarySystemFill
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 { self.content = content()
let start = UIScreen.main.bounds.height * 0.20 self.viewModel = viewModel
let end = UIScreen.main.bounds.height * 0.4
let diff = end - start
let opacity = clamp((scrollViewOffset - start) / diff, min: 0, max: 1)
return opacity
} }
@ViewBuilder @ViewBuilder
@ -40,16 +47,6 @@ extension ItemView {
.aspectRatio(1.77, contentMode: .fill) .aspectRatio(1.77, contentMode: .fill)
.frame(height: UIScreen.main.bounds.height * 0.35) .frame(height: UIScreen.main.bounds.height * 0.35)
.bottomEdgeGradient(bottomColor: blurHashBottomEdgeColor) .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 { var body: some View {
@ -87,7 +84,7 @@ extension ItemView {
RowDivider() RowDivider()
content() content
} }
.edgePadding(.vertical) .edgePadding(.vertical)
} }

View File

@ -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<Content: View>: 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)
}
}
}
}
}

View File

@ -6,6 +6,7 @@
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // Copyright (c) 2025 Jellyfin & Jellyfin Contributors
// //
import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: remove rest occurrences of `UIDevice.main` sizings // TODO: remove rest occurrences of `UIDevice.main` sizings
@ -17,33 +18,38 @@ import SwiftUI
extension ItemView { extension ItemView {
struct iPadOSCinematicScrollView<Content: View>: View { struct iPadOSCinematicScrollView<Content: View>: ScrollContainerView {
@ObservedObject @ObservedObject
var viewModel: ItemViewModel private var viewModel: ItemViewModel
@State @State
private var globalSize: CGSize = .zero private var globalSize: CGSize = .zero
let content: () -> Content private let content: Content
@ViewBuilder init(
private var headerView: some View { viewModel: ItemViewModel,
Group { @ViewBuilder content: () -> Content
) {
self.content = content()
self.viewModel = viewModel
}
private var imageType: ImageType {
if viewModel.item.type == .episode { if viewModel.item.type == .episode {
ImageView(viewModel.item.imageSource(.primary, maxWidth: 1920)) return .primary
} else { } else {
ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920)) return .backdrop
} }
} }
.aspectRatio(1.77, contentMode: .fill)
}
var body: some View { var body: some View {
OffsetScrollView( OffsetScrollView(
headerHeight: globalSize.isLandscape ? 0.75 : 0.6 headerHeight: globalSize.isLandscape ? 0.75 : 0.6
) { ) {
headerView ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 1920))
.aspectRatio(1.77, contentMode: .fill)
} overlay: { } overlay: {
VStack(spacing: 0) { VStack(spacing: 0) {
Spacer() Spacer()
@ -65,7 +71,7 @@ extension ItemView {
} }
} }
} content: { } content: {
content() content
.edgePadding(.vertical) .edgePadding(.vertical)
} }
.trackingSize($globalSize) .trackingSize($globalSize)

View File

@ -10,15 +10,18 @@ import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
extension SeriesItemView { extension ItemView {
struct ContentView: View { struct SeriesItemContentView: View {
@ObservedObject @ObservedObject
var viewModel: SeriesItemViewModel var viewModel: SeriesItemViewModel
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 20) { SeparatorVStack(alignment: .leading) {
RowDivider()
.padding(.vertical, 10)
} content: {
// MARK: Episodes // MARK: Episodes
@ -30,16 +33,12 @@ extension SeriesItemView {
if let genres = viewModel.item.itemGenres, genres.isNotEmpty { if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
ItemView.GenresHStack(genres: genres) ItemView.GenresHStack(genres: genres)
RowDivider()
} }
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, studios.isNotEmpty { if let studios = viewModel.item.studios, studios.isNotEmpty {
ItemView.StudiosHStack(studios: studios) ItemView.StudiosHStack(studios: studios)
RowDivider()
} }
// MARK: Cast and Crew // MARK: Cast and Crew
@ -48,24 +47,18 @@ extension SeriesItemView {
castAndCrew.isNotEmpty castAndCrew.isNotEmpty
{ {
ItemView.CastAndCrewHStack(people: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
RowDivider()
} }
// MARK: Special Features // MARK: Special Features
if viewModel.specialFeatures.isNotEmpty { if viewModel.specialFeatures.isNotEmpty {
ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures)
RowDivider()
} }
// MARK: Similar // MARK: Similar
if viewModel.similarItems.isNotEmpty { if viewModel.similarItems.isNotEmpty {
ItemView.SimilarItemsHStack(items: viewModel.similarItems) ItemView.SimilarItemsHStack(items: viewModel.similarItems)
RowDivider()
} }
ItemView.AboutView(viewModel: viewModel) ItemView.AboutView(viewModel: viewModel)

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}