diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdfb1210..a8270bcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,9 @@ jobs: - name: Install SwiftGen run: brew install swiftgen + - name: Set Xcode Version + run: sudo xcode-select -s "/Applications/Xcode_15.2.app" + - name: Cache Carthage uses: actions/cache@v4 id: carthage-cache @@ -41,12 +44,14 @@ jobs: - name: Update Carthage run: carthage update --use-xcframeworks --cache-builds - - name: Cache Swift packages - uses: actions/cache@v4 - with: - path: PackageCache - key: ${{ runner.os }}-${{ matrix.scheme }}-spm-${{ hashFiles('**/Package.resolved') }} - restore-keys: ${{ runner.os }}-${{ matrix.scheme }}-spm- + # FIXME: caches would keep failed compiles? + + # - name: Cache Swift packages + # uses: actions/cache@v4 + # with: + # path: PackageCache + # key: ${{ runner.os }}-${{ matrix.scheme }}-spm-${{ hashFiles('**/Package.resolved') }} + # restore-keys: ${{ runner.os }}-${{ matrix.scheme }}-spm- - name: Build uses: nick-fields/retry@v2 diff --git a/Shared/Components/TypeSystemNameView.swift b/Shared/Components/TypeSystemNameView.swift index 6d182d36..a6351657 100644 --- a/Shared/Components/TypeSystemNameView.swift +++ b/Shared/Components/TypeSystemNameView.swift @@ -8,26 +8,28 @@ import SwiftUI -struct TypeSystemNameView: View { +struct SystemImageContentView: View { @State private var contentSize: CGSize = .zero - let item: Item + private let systemName: String + + init(systemName: String?) { + self.systemName = systemName ?? "circle" + } var body: some View { ZStack { Color.secondarySystemFill .opacity(0.5) - if let typeSystemImage = item.typeSystemName { - Image(systemName: typeSystemImage) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.secondary) - .accessibilityHidden(true) - .frame(width: contentSize.width / 3.5, height: contentSize.height / 3) - } + Image(systemName: systemName) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(.secondary) + .accessibilityHidden(true) + .frame(width: contentSize.width / 3.5, height: contentSize.height / 3) } .size($contentSize) } diff --git a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift index 1efe4d94..013dd14f 100644 --- a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift +++ b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift @@ -35,18 +35,13 @@ extension EnvironmentValues { static let defaultValue: Binding = .constant(1) } - // TODO: does this actually do anything useful? - // should instead use view safe area? + // TODO: See if we can use a root `GeometryReader` that sets the environment value struct SafeAreaInsetsKey: EnvironmentKey { static var defaultValue: EdgeInsets { UIApplication.shared.keyWindow?.safeAreaInsets.asEdgeInsets ?? .zero } } - struct ShowsLibraryFiltersKey: EnvironmentKey { - static let defaultValue: Bool = true - } - struct SubtitleOffsetKey: EnvironmentKey { static let defaultValue: Binding = .constant(0) } diff --git a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift index 55f49a07..3a337fd8 100644 --- a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift +++ b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift @@ -49,13 +49,6 @@ extension EnvironmentValues { self[SafeAreaInsetsKey.self] } - // TODO: remove and make a parameter instead, isn't necessarily an - // environment value - var showsLibraryFilters: Bool { - get { self[ShowsLibraryFiltersKey.self] } - set { self[ShowsLibraryFiltersKey.self] = newValue } - } - var subtitleOffset: Binding { get { self[SubtitleOffsetKey.self] } set { self[SubtitleOffsetKey.self] = newValue } diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift index 8934a85a..74e4fd40 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift @@ -44,7 +44,7 @@ extension BaseItemDto: Poster { } } - var typeSystemName: String? { + var typeSystemImage: String? { switch type { case .episode, .movie, .series: "film" diff --git a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift index 93900d32..869b93a5 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift @@ -21,7 +21,7 @@ extension BaseItemPerson: Poster { true } - var typeSystemName: String? { + var typeSystemImage: String? { "person.fill" } diff --git a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift index 0658274b..6c260906 100644 --- a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift @@ -31,7 +31,7 @@ extension ChapterInfo { extension ChapterInfo { - struct FullInfo: Poster { + struct FullInfo: Poster, Equatable { var id: Int { chapterInfo.hashValue @@ -45,7 +45,7 @@ extension ChapterInfo { chapterInfo.displayTitle } - let typeSystemName: String? = "film" + let typeSystemImage: String? = "film" var subtitle: String? var showTitle: Bool = true diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift index 21a82958..8edc1838 100644 --- a/Shared/Objects/Poster.swift +++ b/Shared/Objects/Poster.swift @@ -9,13 +9,13 @@ import Foundation // TODO: remove `showTitle` and `subtitle` since the PosterButton can define custom supplementary views? -// TODO: instead of the below image functions, have variables that match `ImageType` +// TODO: instead of the below image functions, have functions that match `ImageType` // - allows caller to choose images protocol Poster: Displayable, Hashable, Identifiable { var subtitle: String? { get } var showTitle: Bool { get } - var typeSystemName: String? { get } + var typeSystemImage: String? { get } func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] diff --git a/Swiftfin tvOS/Components/CinematicItemSelector.swift b/Swiftfin tvOS/Components/CinematicItemSelector.swift index f5c6b4da..7721f456 100644 --- a/Swiftfin tvOS/Components/CinematicItemSelector.swift +++ b/Swiftfin tvOS/Components/CinematicItemSelector.swift @@ -16,8 +16,6 @@ struct CinematicItemSelector: View { @State private var focusedItem: Item? - @State - private var posterHStackSize: CGSize = .zero @StateObject private var viewModel: CinematicBackgroundView.ViewModel = .init() @@ -47,25 +45,13 @@ struct CinematicItemSelector: View { .transition(.opacity) } - // By design, PosterHStack/CollectionHStack requires being in a ScrollView - ScrollView { - PosterHStack(type: .landscape, items: items) - .content(itemContent) - .imageOverlay(itemImageOverlay) - .contextMenu(itemContextMenu) - .trailing(trailingContent) - .onSelect(onSelect) - .focusedItem($focusedItem) - .size($posterHStackSize) - } - .frame(height: posterHStackSize.height) - .if(true) { view in - if #available(tvOS 16, *) { - view.scrollDisabled(true) - } else { - view - } - } + PosterHStack(type: .landscape, items: items) + .content(itemContent) + .imageOverlay(itemImageOverlay) + .contextMenu(itemContextMenu) + .trailing(trailingContent) + .onSelect(onSelect) + .focusedItem($focusedItem) } } .background(alignment: .top) { diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index 60f0f3fc..1ec4795c 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -30,19 +30,12 @@ struct PosterButton: View { // Only set if desiring focus changes private var onFocusChanged: ((Bool) -> Void)? - @ViewBuilder - private func poster(from item: Item) -> some View { + private func imageView(from item: Item) -> ImageView { switch type { case .portrait: ImageView(item.portraitPosterImageSource(maxWidth: 500)) - .failure { - TypeSystemNameView(item: item) - } case .landscape: ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage)) - .failure { - TypeSystemNameView(item: item) - } } } @@ -54,7 +47,10 @@ struct PosterButton: View { ZStack { Color.clear - poster(from: item) + imageView(from: item) + .failure { + SystemImageContentView(systemName: item.typeSystemImage) + } imageOverlay() .eraseToAnyView() diff --git a/Swiftfin tvOS/Components/PosterHStack.swift b/Swiftfin tvOS/Components/PosterHStack.swift index a3ee7f84..c490d3f6 100644 --- a/Swiftfin tvOS/Components/PosterHStack.swift +++ b/Swiftfin tvOS/Components/PosterHStack.swift @@ -58,8 +58,7 @@ struct PosterHStack: View { } .clipsToBounds(false) .dataPrefix(20) - .horizontalInset(EdgeInsets.defaultEdgePadding) - .verticalInsets(top: 20, bottom: 20) + .insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: 20) .itemSpacing(EdgeInsets.defaultEdgePadding - 20) .scrollBehavior(.continuousLeadingEdge) } diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift index 9c88e0f2..bbbd13f2 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/Components/SeriesEpisodeSelector.swift @@ -111,7 +111,7 @@ extension SeriesEpisodeSelector { EpisodeCard(episode: item) .focused($focusedEpisodeID, equals: item.id) } - .verticalInsets(top: 20, bottom: 20) + .insets(vertical: 20) .mask { VStack(spacing: 0) { Color.white diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 0c82d1ef..43514d13 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -180,9 +180,7 @@ E1047E2327E5880000CB0D4A /* TypeSystemNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* TypeSystemNameView.swift */; }; E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */; }; E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */; }; - E104DC8D2B9D8979008F506D /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E104DC8C2B9D8979008F506D /* CollectionHStack */; }; E104DC902B9D8995008F506D /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E104DC8F2B9D8995008F506D /* CollectionVGrid */; }; - E104DC922B9D89A2008F506D /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E104DC912B9D89A2008F506D /* CollectionHStack */; }; E104DC942B9D89A2008F506D /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E104DC932B9D89A2008F506D /* CollectionVGrid */; }; E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104DC952B9E7E29008F506D /* AssertionFailureView.swift */; }; E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104DC952B9E7E29008F506D /* AssertionFailureView.swift */; }; @@ -288,6 +286,9 @@ E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A40293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift */; }; E1388A43293F0AAD009721B1 /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A41293F0AAD009721B1 /* PreferenceUIHostingController.swift */; }; E1388A46293F0ABA009721B1 /* SwizzleSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E1388A45293F0ABA009721B1 /* SwizzleSwift */; }; + E1392FED2BA218A80034110D /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = E1392FEC2BA218A80034110D /* SwiftUIIntrospect */; }; + E1392FF22BA21B360034110D /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1392FF12BA21B360034110D /* CollectionHStack */; }; + E1392FF42BA21B470034110D /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E1392FF32BA21B470034110D /* CollectionHStack */; }; E139CC1D28EC836F00688DE2 /* ChapterOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E139CC1C28EC836F00688DE2 /* ChapterOverlay.swift */; }; E139CC1F28EC83E400688DE2 /* Int.swift in Sources */ = {isa = PBXBuildFile; fileRef = E139CC1E28EC83E400688DE2 /* Int.swift */; }; E13AF3B628A0C598009093AB /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B528A0C598009093AB /* Nuke */; }; @@ -429,6 +430,8 @@ E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F062B1B12C300442DB8 /* Backport.swift */; }; E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; }; E15D4F0B2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; }; + E15EFA842BA167350080E926 /* CollectionHStack in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA832BA167350080E926 /* CollectionHStack */; }; + E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */; }; E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; }; E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */; }; E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; }; @@ -1303,6 +1306,7 @@ 62666E2327E501EB00EC0ECD /* Foundation.framework in Frameworks */, 62666E2127E501E400EC0ECD /* CoreVideo.framework in Frameworks */, 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, + E1392FED2BA218A80034110D /* SwiftUIIntrospect in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */, E1575E58293E7685001665B1 /* Files in Frameworks */, @@ -1311,7 +1315,6 @@ E1B5F7AB29577BCE004B26CF /* PulseUI in Frameworks */, E1B5F7A929577BCE004B26CF /* PulseLogHandler in Frameworks */, 62666E1B27E501D400EC0ECD /* CoreGraphics.framework in Frameworks */, - E104DC922B9D89A2008F506D /* CollectionHStack in Frameworks */, E1388A46293F0ABA009721B1 /* SwizzleSwift in Frameworks */, 62666E2C27E5021000EC0ECD /* QuartzCore.framework in Frameworks */, 62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */, @@ -1325,6 +1328,7 @@ E104DC942B9D89A2008F506D /* CollectionVGrid in Frameworks */, 62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */, E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */, + E1392FF42BA21B470034110D /* CollectionHStack in Frameworks */, 62666E1327E501C300EC0ECD /* AudioToolbox.framework in Frameworks */, E13AF3B628A0C598009093AB /* Nuke in Frameworks */, E12186DE2718F1C50010884C /* Defaults in Frameworks */, @@ -1372,10 +1376,12 @@ E15210562946DF1B00375CC2 /* PulseLogHandler in Frameworks */, 62666E0427E5017500EC0ECD /* CoreText.framework in Frameworks */, E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */, + E1392FF22BA21B360034110D /* CollectionHStack in Frameworks */, 62666E0E27E501AF00EC0ECD /* Security.framework in Frameworks */, E1DC9814296DC06200982F06 /* PulseLogHandler in Frameworks */, E1DC9821296DDBE600982F06 /* CollectionView in Frameworks */, - E104DC8D2B9D8979008F506D /* CollectionHStack in Frameworks */, + E15EFA842BA167350080E926 /* CollectionHStack in Frameworks */, + E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */, 62666DFE27E5015700EC0ECD /* AVFoundation.framework in Frameworks */, 62666DFD27E5014F00EC0ECD /* AudioToolbox.framework in Frameworks */, E19E6E0528A0B958005C10C8 /* Nuke in Frameworks */, @@ -2897,8 +2903,9 @@ E18443CA2A037773002DDDC8 /* UDPBroadcast */, E14CB6872A9FF71F001586C6 /* JellyfinAPI */, E1A7B1642B9A9F7800152546 /* PreferencesView */, - E104DC912B9D89A2008F506D /* CollectionHStack */, E104DC932B9D89A2008F506D /* CollectionVGrid */, + E1392FEC2BA218A80034110D /* SwiftUIIntrospect */, + E1392FF32BA21B470034110D /* CollectionHStack */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; @@ -2946,8 +2953,10 @@ E15D4F042B1B0C3C00442DB8 /* PreferencesView */, E113A2A62B5A178D009CAAAA /* CollectionHStack */, E113A2A92B5A179A009CAAAA /* CollectionVGrid */, - E104DC8C2B9D8979008F506D /* CollectionHStack */, E104DC8F2B9D8995008F506D /* CollectionVGrid */, + E15EFA832BA167350080E926 /* CollectionHStack */, + E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */, + E1392FF12BA21B360034110D /* CollectionHStack */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; @@ -3017,8 +3026,8 @@ E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */, E14CB6842A9FF62A001586C6 /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, E15D4F032B1B0C3C00442DB8 /* XCLocalSwiftPackageReference "PreferencesView" */, - E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */, E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */, + E1392FF02BA21B360034110D /* XCRemoteSwiftPackageReference "CollectionHStack" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -4261,14 +4270,6 @@ minimumVersion = 1.0.0; }; }; - E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/LePips/CollectionHStack"; - requirement = { - branch = main; - kind = branch; - }; - }; E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/LePips/CollectionVGrid"; @@ -4285,6 +4286,14 @@ minimumVersion = 2.0.0; }; }; + E1392FF02BA21B360034110D /* XCRemoteSwiftPackageReference "CollectionHStack" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/LePips/CollectionHStack"; + requirement = { + branch = main; + kind = branch; + }; + }; E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; @@ -4427,21 +4436,11 @@ package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */; productName = Algorithms; }; - E104DC8C2B9D8979008F506D /* CollectionHStack */ = { - isa = XCSwiftPackageProductDependency; - package = E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */; - productName = CollectionHStack; - }; E104DC8F2B9D8995008F506D /* CollectionVGrid */ = { isa = XCSwiftPackageProductDependency; package = E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */; productName = CollectionVGrid; }; - E104DC912B9D89A2008F506D /* CollectionHStack */ = { - isa = XCSwiftPackageProductDependency; - package = E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */; - productName = CollectionHStack; - }; E104DC932B9D89A2008F506D /* CollectionVGrid */ = { isa = XCSwiftPackageProductDependency; package = E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */; @@ -4484,6 +4483,21 @@ package = 62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */; productName = SwizzleSwift; }; + E1392FEC2BA218A80034110D /* SwiftUIIntrospect */ = { + isa = XCSwiftPackageProductDependency; + package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = SwiftUIIntrospect; + }; + E1392FF12BA21B360034110D /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + package = E1392FF02BA21B360034110D /* XCRemoteSwiftPackageReference "CollectionHStack" */; + productName = CollectionHStack; + }; + E1392FF32BA21B470034110D /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + package = E1392FF02BA21B360034110D /* XCRemoteSwiftPackageReference "CollectionHStack" */; + productName = CollectionHStack; + }; E13AF3B528A0C598009093AB /* Nuke */ = { isa = XCSwiftPackageProductDependency; package = E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */; @@ -4567,6 +4581,15 @@ isa = XCSwiftPackageProductDependency; productName = PreferencesView; }; + E15EFA832BA167350080E926 /* CollectionHStack */ = { + isa = XCSwiftPackageProductDependency; + productName = CollectionHStack; + }; + E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */ = { + isa = XCSwiftPackageProductDependency; + package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = SwiftUIIntrospect; + }; E18443CA2A037773002DDDC8 /* UDPBroadcast */ = { isa = XCSwiftPackageProductDependency; package = E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */; diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5087afbf..f6ab9b41 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,7 +15,7 @@ "location" : "https://github.com/LePips/CollectionHStack", "state" : { "branch" : "main", - "revision" : "ff54c0ea655840a12e7faaabb31aa66f50cc4767" + "revision" : "e192023a2f2ce9351cbe7fb6f01c47043de209a8" } }, { diff --git a/Swiftfin/Components/PosterButton.swift b/Swiftfin/Components/PosterButton.swift index e63feec4..0610c99b 100644 --- a/Swiftfin/Components/PosterButton.swift +++ b/Swiftfin/Components/PosterButton.swift @@ -10,6 +10,8 @@ import Defaults import JellyfinAPI import SwiftUI +// TODO: image aspect fill/fit + struct PosterButton: View { private var item: Item @@ -20,19 +22,12 @@ struct PosterButton: View { private var onSelect: () -> Void private var singleImage: Bool - @ViewBuilder - private func poster(from item: Item) -> some View { + private func imageView(from item: Item) -> ImageView { switch type { case .portrait: ImageView(item.portraitPosterImageSource(maxWidth: 200)) - .failure { - TypeSystemNameView(item: item) - } case .landscape: ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage)) - .failure { - TypeSystemNameView(item: item) - } } } @@ -44,7 +39,10 @@ struct PosterButton: View { ZStack { Color.clear - poster(from: item) + imageView(from: item) + .failure { + SystemImageContentView(systemName: item.typeSystemImage) + } imageOverlay() .eraseToAnyView() diff --git a/Swiftfin/Components/PosterHStack.swift b/Swiftfin/Components/PosterHStack.swift index ae346922..19bf89e2 100644 --- a/Swiftfin/Components/PosterHStack.swift +++ b/Swiftfin/Components/PosterHStack.swift @@ -41,7 +41,7 @@ struct PosterHStack: View { } .clipsToBounds(false) .dataPrefix(20) - .horizontalInset(EdgeInsets.defaultEdgePadding) + .insets(horizontal: EdgeInsets.defaultEdgePadding) .itemSpacing(EdgeInsets.defaultEdgePadding / 2) .scrollBehavior(.continuousLeadingEdge) } @@ -64,7 +64,7 @@ struct PosterHStack: View { } .clipsToBounds(false) .dataPrefix(20) - .horizontalInset(EdgeInsets.defaultEdgePadding) + .insets(horizontal: EdgeInsets.defaultEdgePadding) .itemSpacing(EdgeInsets.defaultEdgePadding / 2) .scrollBehavior(.continuousLeadingEdge) } diff --git a/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift b/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift index 8a210aae..a620f1da 100644 --- a/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift +++ b/Swiftfin/Views/ItemView/Components/SeriesEpisodeSelector.swift @@ -76,7 +76,7 @@ struct SeriesEpisodeSelector: View { } } .scrollBehavior(.continuousLeadingEdge) - .horizontalInset(16) + .insets(horizontal: EdgeInsets.defaultEdgePadding) .itemSpacing(8) } } diff --git a/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift b/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift index e1f94860..5a5ba11b 100644 --- a/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift +++ b/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift @@ -21,6 +21,15 @@ extension PagingLibraryView { private var onSelect: () -> Void private let posterType: PosterType + private func imageView(from element: Element) -> ImageView { + switch posterType { + case .portrait: + ImageView(element.portraitPosterImageSource(maxWidth: 60)) + case .landscape: + ImageView(element.landscapePosterImageSources(maxWidth: 110, single: false)) + } + } + @ViewBuilder private func itemAccessoryView(item: BaseItemDto) -> some View { DotHStack { @@ -70,18 +79,10 @@ extension PagingLibraryView { ZStack { Color.clear - switch posterType { - case .portrait: - ImageView(item.portraitPosterImageSource(maxWidth: 60)) - .failure { - TypeSystemNameView(item: item) - } - case .landscape: - ImageView(item.landscapePosterImageSources(maxWidth: 110, single: false)) - .failure { - TypeSystemNameView(item: item) - } - } + imageView(from: item) + .failure { + SystemImageContentView(systemName: item.typeSystemImage) + } } .posterStyle(posterType) .frame(width: posterType == .landscape ? 110 : 60) diff --git a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 4c412385..5927d1ea 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -36,11 +36,11 @@ extension VideoPlayer.Overlay { @EnvironmentObject private var viewModel: VideoPlayerViewModel - @State - private var scrollViewProxy: ScrollViewProxy? = nil + @StateObject + private var collectionHStackProxy: CollectionHStackProxy = .init() var body: some View { - VStack { + VStack(spacing: 0) { Spacer(minLength: 0) .allowsHitTesting(false) @@ -56,9 +56,8 @@ extension VideoPlayer.Overlay { Button { if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) { - withAnimation { - scrollViewProxy?.scrollTo(currentChapter.hashValue, anchor: .center) - } + let index = viewModel.chapters.firstIndex(of: currentChapter)! + collectionHStackProxy.scrollTo(index: index) } } label: { Text(L10n.current) @@ -67,101 +66,24 @@ extension VideoPlayer.Overlay { } } .padding(.horizontal, safeAreaInsets.leading) - .if(UIDevice.isPad) { view in - view.padding(.horizontal) - } + .edgePadding(.horizontal) -// ScrollViewReader { proxy in CollectionHStack( viewModel.chapters, minWidth: 200 ) { chapter in - PosterButton( - item: chapter, - type: .landscape - ) - .content { - VStack(alignment: .leading, spacing: 5) { - Text(chapter.chapterInfo.displayTitle) - .font(.subheadline) - .fontWeight(.semibold) - .lineLimit(1) - .foregroundColor(.white) + ChapterButton(chapter: chapter) + } + .insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: EdgeInsets.defaultEdgePadding) + .proxy(collectionHStackProxy) + .onChange(of: currentOverlayType) { newValue in + guard newValue == .chapters else { return } - Text(chapter.chapterInfo.timestampLabel) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(Color(UIColor.systemBlue)) - .padding(.vertical, 2) - .padding(.horizontal, 4) - .background { - Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) - } - } + if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) { + let index = viewModel.chapters.firstIndex(of: currentChapter)! + collectionHStackProxy.scrollTo(index: index, animated: false) } } - .scrollBehavior(.continuousLeadingEdge) - .horizontalInset(safeAreaInsets.leading) - -// ScrollView(.horizontal, showsIndicators: false) { -// HStack(alignment: .top, spacing: 15) { -// ForEach(viewModel.chapters, id: \.self) { chapter in -// PosterButton( -// item: chapter, -// type: .landscape -// ) -// .imageOverlay { -// if chapter.secondsRange.contains(currentProgressHandler.seconds) { -// RoundedRectangle(cornerRadius: 6) -// .stroke(accentColor, lineWidth: 8) -// } -// } -// .content { -// VStack(alignment: .leading, spacing: 5) { -// Text(chapter.chapterInfo.displayTitle) -// .font(.subheadline) -// .fontWeight(.semibold) -// .lineLimit(1) -// .foregroundColor(.white) -// -// Text(chapter.chapterInfo.timestampLabel) -// .font(.subheadline) -// .fontWeight(.semibold) -// .foregroundColor(Color(UIColor.systemBlue)) -// .padding(.vertical, 2) -// .padding(.horizontal, 4) -// .background { -// Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) -// } -// } -// } -// .onSelect { -// let seconds = chapter.chapterInfo.startTimeSeconds -// videoPlayerProxy.setTime(.seconds(seconds)) -// -// if videoPlayerManager.state != .playing { -// videoPlayerProxy.play() -// } -// } -// } -// } -// .padding(.leading, safeAreaInsets.leading) -// .padding(.trailing, safeAreaInsets.trailing) -// .padding(.bottom) -// .if(UIDevice.isPad) { view in -// view.padding(.horizontal) -// } -// } -// .onChange(of: currentOverlayType) { newValue in -// guard newValue == .chapters else { return } -// if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) { -// scrollViewProxy?.scrollTo(currentChapter.hashValue, anchor: .center) -// } -// } -// .onAppear { -// scrollViewProxy = proxy -// } -// } } .background { LinearGradient( @@ -178,3 +100,73 @@ extension VideoPlayer.Overlay { } } } + +extension VideoPlayer.Overlay.ChapterOverlay { + + struct ChapterButton: View { + + @Default(.accentColor) + private var accentColor + + @EnvironmentObject + private var currentProgressHandler: VideoPlayerManager.CurrentProgressHandler + @EnvironmentObject + private var overlayTimer: TimerProxy + @EnvironmentObject + private var videoPlayerManager: VideoPlayerManager + @EnvironmentObject + private var videoPlayerProxy: VLCVideoPlayer.Proxy + + let chapter: ChapterInfo.FullInfo + + var body: some View { + Button { + let seconds = chapter.chapterInfo.startTimeSeconds + videoPlayerProxy.setTime(.seconds(seconds)) + + if videoPlayerManager.state != .playing { + videoPlayerProxy.play() + } + } label: { + VStack(alignment: .leading) { + ZStack { + Color.black + + ImageView(chapter.landscapePosterImageSources(maxWidth: 500, single: false)) + .failure { + SystemImageContentView(systemName: chapter.typeSystemImage) + } + .aspectRatio(contentMode: .fit) + } + .posterStyle(.landscape) + .overlay { + if chapter.secondsRange.contains(currentProgressHandler.seconds) { + RoundedRectangle(cornerRadius: 1) + .stroke(accentColor, lineWidth: 5) + .transition(.opacity.animation(.linear(duration: 0.1))) + } + } + + VStack(alignment: .leading, spacing: 5) { + Text(chapter.chapterInfo.displayTitle) + .font(.subheadline) + .fontWeight(.semibold) + .lineLimit(1) + .foregroundColor(.white) + + Text(chapter.chapterInfo.timestampLabel) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(Color(UIColor.systemBlue)) + .padding(.vertical, 2) + .padding(.horizontal, 4) + .background { + Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) + } + } + } + } + .buttonStyle(.plain) + } + } +}