Fix iOS Chapter Overlay (#992)

This commit is contained in:
Ethan Pippin 2024-03-13 23:08:43 -06:00 committed by GitHub
parent 876ffba417
commit 1bd18ef8b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 201 additions and 211 deletions

View File

@ -30,6 +30,9 @@ jobs:
- name: Install SwiftGen - name: Install SwiftGen
run: brew install swiftgen run: brew install swiftgen
- name: Set Xcode Version
run: sudo xcode-select -s "/Applications/Xcode_15.2.app"
- name: Cache Carthage - name: Cache Carthage
uses: actions/cache@v4 uses: actions/cache@v4
id: carthage-cache id: carthage-cache
@ -41,12 +44,14 @@ jobs:
- name: Update Carthage - name: Update Carthage
run: carthage update --use-xcframeworks --cache-builds run: carthage update --use-xcframeworks --cache-builds
- name: Cache Swift packages # FIXME: caches would keep failed compiles?
uses: actions/cache@v4
with: # - name: Cache Swift packages
path: PackageCache # uses: actions/cache@v4
key: ${{ runner.os }}-${{ matrix.scheme }}-spm-${{ hashFiles('**/Package.resolved') }} # with:
restore-keys: ${{ runner.os }}-${{ matrix.scheme }}-spm- # path: PackageCache
# key: ${{ runner.os }}-${{ matrix.scheme }}-spm-${{ hashFiles('**/Package.resolved') }}
# restore-keys: ${{ runner.os }}-${{ matrix.scheme }}-spm-
- name: Build - name: Build
uses: nick-fields/retry@v2 uses: nick-fields/retry@v2

View File

@ -8,26 +8,28 @@
import SwiftUI import SwiftUI
struct TypeSystemNameView<Item: Poster>: View { struct SystemImageContentView: View {
@State @State
private var contentSize: CGSize = .zero private var contentSize: CGSize = .zero
let item: Item private let systemName: String
init(systemName: String?) {
self.systemName = systemName ?? "circle"
}
var body: some View { var body: some View {
ZStack { ZStack {
Color.secondarySystemFill Color.secondarySystemFill
.opacity(0.5) .opacity(0.5)
if let typeSystemImage = item.typeSystemName { Image(systemName: systemName)
Image(systemName: typeSystemImage) .resizable()
.resizable() .aspectRatio(contentMode: .fit)
.aspectRatio(contentMode: .fit) .foregroundColor(.secondary)
.foregroundColor(.secondary) .accessibilityHidden(true)
.accessibilityHidden(true) .frame(width: contentSize.width / 3.5, height: contentSize.height / 3)
.frame(width: contentSize.width / 3.5, height: contentSize.height / 3)
}
} }
.size($contentSize) .size($contentSize)
} }

View File

@ -35,18 +35,13 @@ extension EnvironmentValues {
static let defaultValue: Binding<Double> = .constant(1) static let defaultValue: Binding<Double> = .constant(1)
} }
// TODO: does this actually do anything useful? // TODO: See if we can use a root `GeometryReader` that sets the environment value
// should instead use view safe area?
struct SafeAreaInsetsKey: EnvironmentKey { struct SafeAreaInsetsKey: EnvironmentKey {
static var defaultValue: EdgeInsets { static var defaultValue: EdgeInsets {
UIApplication.shared.keyWindow?.safeAreaInsets.asEdgeInsets ?? .zero UIApplication.shared.keyWindow?.safeAreaInsets.asEdgeInsets ?? .zero
} }
} }
struct ShowsLibraryFiltersKey: EnvironmentKey {
static let defaultValue: Bool = true
}
struct SubtitleOffsetKey: EnvironmentKey { struct SubtitleOffsetKey: EnvironmentKey {
static let defaultValue: Binding<Int> = .constant(0) static let defaultValue: Binding<Int> = .constant(0)
} }

View File

@ -49,13 +49,6 @@ extension EnvironmentValues {
self[SafeAreaInsetsKey.self] 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<Int> { var subtitleOffset: Binding<Int> {
get { self[SubtitleOffsetKey.self] } get { self[SubtitleOffsetKey.self] }
set { self[SubtitleOffsetKey.self] = newValue } set { self[SubtitleOffsetKey.self] = newValue }

View File

@ -44,7 +44,7 @@ extension BaseItemDto: Poster {
} }
} }
var typeSystemName: String? { var typeSystemImage: String? {
switch type { switch type {
case .episode, .movie, .series: case .episode, .movie, .series:
"film" "film"

View File

@ -21,7 +21,7 @@ extension BaseItemPerson: Poster {
true true
} }
var typeSystemName: String? { var typeSystemImage: String? {
"person.fill" "person.fill"
} }

View File

@ -31,7 +31,7 @@ extension ChapterInfo {
extension ChapterInfo { extension ChapterInfo {
struct FullInfo: Poster { struct FullInfo: Poster, Equatable {
var id: Int { var id: Int {
chapterInfo.hashValue chapterInfo.hashValue
@ -45,7 +45,7 @@ extension ChapterInfo {
chapterInfo.displayTitle chapterInfo.displayTitle
} }
let typeSystemName: String? = "film" let typeSystemImage: String? = "film"
var subtitle: String? var subtitle: String?
var showTitle: Bool = true var showTitle: Bool = true

View File

@ -9,13 +9,13 @@
import Foundation import Foundation
// TODO: remove `showTitle` and `subtitle` since the PosterButton can define custom supplementary views? // 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 // - allows caller to choose images
protocol Poster: Displayable, Hashable, Identifiable { protocol Poster: Displayable, Hashable, Identifiable {
var subtitle: String? { get } var subtitle: String? { get }
var showTitle: Bool { get } var showTitle: Bool { get }
var typeSystemName: String? { get } var typeSystemImage: String? { get }
func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource func portraitPosterImageSource(maxWidth: CGFloat) -> ImageSource
func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource] func landscapePosterImageSources(maxWidth: CGFloat, single: Bool) -> [ImageSource]

View File

@ -16,8 +16,6 @@ struct CinematicItemSelector<Item: Poster>: View {
@State @State
private var focusedItem: Item? private var focusedItem: Item?
@State
private var posterHStackSize: CGSize = .zero
@StateObject @StateObject
private var viewModel: CinematicBackgroundView<Item>.ViewModel = .init() private var viewModel: CinematicBackgroundView<Item>.ViewModel = .init()
@ -47,25 +45,13 @@ struct CinematicItemSelector<Item: Poster>: View {
.transition(.opacity) .transition(.opacity)
} }
// By design, PosterHStack/CollectionHStack requires being in a ScrollView PosterHStack(type: .landscape, items: items)
ScrollView { .content(itemContent)
PosterHStack(type: .landscape, items: items) .imageOverlay(itemImageOverlay)
.content(itemContent) .contextMenu(itemContextMenu)
.imageOverlay(itemImageOverlay) .trailing(trailingContent)
.contextMenu(itemContextMenu) .onSelect(onSelect)
.trailing(trailingContent) .focusedItem($focusedItem)
.onSelect(onSelect)
.focusedItem($focusedItem)
.size($posterHStackSize)
}
.frame(height: posterHStackSize.height)
.if(true) { view in
if #available(tvOS 16, *) {
view.scrollDisabled(true)
} else {
view
}
}
} }
} }
.background(alignment: .top) { .background(alignment: .top) {

View File

@ -30,19 +30,12 @@ struct PosterButton<Item: Poster>: View {
// Only set if desiring focus changes // Only set if desiring focus changes
private var onFocusChanged: ((Bool) -> Void)? private var onFocusChanged: ((Bool) -> Void)?
@ViewBuilder private func imageView(from item: Item) -> ImageView {
private func poster(from item: Item) -> some View {
switch type { switch type {
case .portrait: case .portrait:
ImageView(item.portraitPosterImageSource(maxWidth: 500)) ImageView(item.portraitPosterImageSource(maxWidth: 500))
.failure {
TypeSystemNameView(item: item)
}
case .landscape: case .landscape:
ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage)) ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage))
.failure {
TypeSystemNameView(item: item)
}
} }
} }
@ -54,7 +47,10 @@ struct PosterButton<Item: Poster>: View {
ZStack { ZStack {
Color.clear Color.clear
poster(from: item) imageView(from: item)
.failure {
SystemImageContentView(systemName: item.typeSystemImage)
}
imageOverlay() imageOverlay()
.eraseToAnyView() .eraseToAnyView()

View File

@ -58,8 +58,7 @@ struct PosterHStack<Item: Poster>: View {
} }
.clipsToBounds(false) .clipsToBounds(false)
.dataPrefix(20) .dataPrefix(20)
.horizontalInset(EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: 20)
.verticalInsets(top: 20, bottom: 20)
.itemSpacing(EdgeInsets.defaultEdgePadding - 20) .itemSpacing(EdgeInsets.defaultEdgePadding - 20)
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
} }

View File

@ -111,7 +111,7 @@ extension SeriesEpisodeSelector {
EpisodeCard(episode: item) EpisodeCard(episode: item)
.focused($focusedEpisodeID, equals: item.id) .focused($focusedEpisodeID, equals: item.id)
} }
.verticalInsets(top: 20, bottom: 20) .insets(vertical: 20)
.mask { .mask {
VStack(spacing: 0) { VStack(spacing: 0) {
Color.white Color.white

View File

@ -180,9 +180,7 @@
E1047E2327E5880000CB0D4A /* TypeSystemNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* TypeSystemNameView.swift */; }; E1047E2327E5880000CB0D4A /* TypeSystemNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* TypeSystemNameView.swift */; };
E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */; }; E104C870296E087200C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */; };
E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104C872296E0D0A00C1C3F9 /* 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 */; }; 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 */; }; E104DC942B9D89A2008F506D /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E104DC932B9D89A2008F506D /* CollectionVGrid */; };
E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104DC952B9E7E29008F506D /* AssertionFailureView.swift */; }; E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E104DC952B9E7E29008F506D /* AssertionFailureView.swift */; };
E104DC972B9E7E29008F506D /* 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 */; }; E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A40293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift */; };
E1388A43293F0AAD009721B1 /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A41293F0AAD009721B1 /* PreferenceUIHostingController.swift */; }; E1388A43293F0AAD009721B1 /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1388A41293F0AAD009721B1 /* PreferenceUIHostingController.swift */; };
E1388A46293F0ABA009721B1 /* SwizzleSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E1388A45293F0ABA009721B1 /* SwizzleSwift */; }; 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 */; }; E139CC1D28EC836F00688DE2 /* ChapterOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E139CC1C28EC836F00688DE2 /* ChapterOverlay.swift */; };
E139CC1F28EC83E400688DE2 /* Int.swift in Sources */ = {isa = PBXBuildFile; fileRef = E139CC1E28EC83E400688DE2 /* Int.swift */; }; E139CC1F28EC83E400688DE2 /* Int.swift in Sources */ = {isa = PBXBuildFile; fileRef = E139CC1E28EC83E400688DE2 /* Int.swift */; };
E13AF3B628A0C598009093AB /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B528A0C598009093AB /* Nuke */; }; E13AF3B628A0C598009093AB /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E13AF3B528A0C598009093AB /* Nuke */; };
@ -429,6 +430,8 @@
E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F062B1B12C300442DB8 /* Backport.swift */; }; E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F062B1B12C300442DB8 /* Backport.swift */; };
E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; }; E15D4F0A2B1BD88900442DB8 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E15D4F092B1BD88900442DB8 /* Edge.swift */; };
E15D4F0B2B1BD88900442DB8 /* 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 */; }; E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; };
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */; }; E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0D289A4162001A6922 /* ContinueWatchingView.swift */; };
E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; }; E168BD14289A4162001A6922 /* LatestInLibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD0E289A4162001A6922 /* LatestInLibraryView.swift */; };
@ -1303,6 +1306,7 @@
62666E2327E501EB00EC0ECD /* Foundation.framework in Frameworks */, 62666E2327E501EB00EC0ECD /* Foundation.framework in Frameworks */,
62666E2127E501E400EC0ECD /* CoreVideo.framework in Frameworks */, 62666E2127E501E400EC0ECD /* CoreVideo.framework in Frameworks */,
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */,
E1392FED2BA218A80034110D /* SwiftUIIntrospect in Frameworks */,
535870912669D7A800D05A09 /* Introspect in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */,
E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */, E13AF3B828A0C598009093AB /* NukeExtensions in Frameworks */,
E1575E58293E7685001665B1 /* Files in Frameworks */, E1575E58293E7685001665B1 /* Files in Frameworks */,
@ -1311,7 +1315,6 @@
E1B5F7AB29577BCE004B26CF /* PulseUI in Frameworks */, E1B5F7AB29577BCE004B26CF /* PulseUI in Frameworks */,
E1B5F7A929577BCE004B26CF /* PulseLogHandler in Frameworks */, E1B5F7A929577BCE004B26CF /* PulseLogHandler in Frameworks */,
62666E1B27E501D400EC0ECD /* CoreGraphics.framework in Frameworks */, 62666E1B27E501D400EC0ECD /* CoreGraphics.framework in Frameworks */,
E104DC922B9D89A2008F506D /* CollectionHStack in Frameworks */,
E1388A46293F0ABA009721B1 /* SwizzleSwift in Frameworks */, E1388A46293F0ABA009721B1 /* SwizzleSwift in Frameworks */,
62666E2C27E5021000EC0ECD /* QuartzCore.framework in Frameworks */, 62666E2C27E5021000EC0ECD /* QuartzCore.framework in Frameworks */,
62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */, 62666E1927E501D000EC0ECD /* CoreFoundation.framework in Frameworks */,
@ -1325,6 +1328,7 @@
E104DC942B9D89A2008F506D /* CollectionVGrid in Frameworks */, E104DC942B9D89A2008F506D /* CollectionVGrid in Frameworks */,
62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */, 62666E1527E501C800EC0ECD /* AVFoundation.framework in Frameworks */,
E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */, E13AF3BC28A0C59E009093AB /* BlurHashKit in Frameworks */,
E1392FF42BA21B470034110D /* CollectionHStack in Frameworks */,
62666E1327E501C300EC0ECD /* AudioToolbox.framework in Frameworks */, 62666E1327E501C300EC0ECD /* AudioToolbox.framework in Frameworks */,
E13AF3B628A0C598009093AB /* Nuke in Frameworks */, E13AF3B628A0C598009093AB /* Nuke in Frameworks */,
E12186DE2718F1C50010884C /* Defaults in Frameworks */, E12186DE2718F1C50010884C /* Defaults in Frameworks */,
@ -1372,10 +1376,12 @@
E15210562946DF1B00375CC2 /* PulseLogHandler in Frameworks */, E15210562946DF1B00375CC2 /* PulseLogHandler in Frameworks */,
62666E0427E5017500EC0ECD /* CoreText.framework in Frameworks */, 62666E0427E5017500EC0ECD /* CoreText.framework in Frameworks */,
E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */, E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */,
E1392FF22BA21B360034110D /* CollectionHStack in Frameworks */,
62666E0E27E501AF00EC0ECD /* Security.framework in Frameworks */, 62666E0E27E501AF00EC0ECD /* Security.framework in Frameworks */,
E1DC9814296DC06200982F06 /* PulseLogHandler in Frameworks */, E1DC9814296DC06200982F06 /* PulseLogHandler in Frameworks */,
E1DC9821296DDBE600982F06 /* CollectionView in Frameworks */, E1DC9821296DDBE600982F06 /* CollectionView in Frameworks */,
E104DC8D2B9D8979008F506D /* CollectionHStack in Frameworks */, E15EFA842BA167350080E926 /* CollectionHStack in Frameworks */,
E15EFA862BA1685F0080E926 /* SwiftUIIntrospect in Frameworks */,
62666DFE27E5015700EC0ECD /* AVFoundation.framework in Frameworks */, 62666DFE27E5015700EC0ECD /* AVFoundation.framework in Frameworks */,
62666DFD27E5014F00EC0ECD /* AudioToolbox.framework in Frameworks */, 62666DFD27E5014F00EC0ECD /* AudioToolbox.framework in Frameworks */,
E19E6E0528A0B958005C10C8 /* Nuke in Frameworks */, E19E6E0528A0B958005C10C8 /* Nuke in Frameworks */,
@ -2897,8 +2903,9 @@
E18443CA2A037773002DDDC8 /* UDPBroadcast */, E18443CA2A037773002DDDC8 /* UDPBroadcast */,
E14CB6872A9FF71F001586C6 /* JellyfinAPI */, E14CB6872A9FF71F001586C6 /* JellyfinAPI */,
E1A7B1642B9A9F7800152546 /* PreferencesView */, E1A7B1642B9A9F7800152546 /* PreferencesView */,
E104DC912B9D89A2008F506D /* CollectionHStack */,
E104DC932B9D89A2008F506D /* CollectionVGrid */, E104DC932B9D89A2008F506D /* CollectionVGrid */,
E1392FEC2BA218A80034110D /* SwiftUIIntrospect */,
E1392FF32BA21B470034110D /* CollectionHStack */,
); );
productName = "JellyfinPlayer tvOS"; productName = "JellyfinPlayer tvOS";
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
@ -2946,8 +2953,10 @@
E15D4F042B1B0C3C00442DB8 /* PreferencesView */, E15D4F042B1B0C3C00442DB8 /* PreferencesView */,
E113A2A62B5A178D009CAAAA /* CollectionHStack */, E113A2A62B5A178D009CAAAA /* CollectionHStack */,
E113A2A92B5A179A009CAAAA /* CollectionVGrid */, E113A2A92B5A179A009CAAAA /* CollectionVGrid */,
E104DC8C2B9D8979008F506D /* CollectionHStack */,
E104DC8F2B9D8995008F506D /* CollectionVGrid */, E104DC8F2B9D8995008F506D /* CollectionVGrid */,
E15EFA832BA167350080E926 /* CollectionHStack */,
E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */,
E1392FF12BA21B360034110D /* CollectionHStack */,
); );
productName = JellyfinPlayer; productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
@ -3017,8 +3026,8 @@
E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */, E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */,
E14CB6842A9FF62A001586C6 /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, E14CB6842A9FF62A001586C6 /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
E15D4F032B1B0C3C00442DB8 /* XCLocalSwiftPackageReference "PreferencesView" */, E15D4F032B1B0C3C00442DB8 /* XCLocalSwiftPackageReference "PreferencesView" */,
E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */,
E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */, E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */,
E1392FF02BA21B360034110D /* XCRemoteSwiftPackageReference "CollectionHStack" */,
); );
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -4261,14 +4270,6 @@
minimumVersion = 1.0.0; minimumVersion = 1.0.0;
}; };
}; };
E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/LePips/CollectionHStack";
requirement = {
branch = main;
kind = branch;
};
};
E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = { E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/LePips/CollectionVGrid"; repositoryURL = "https://github.com/LePips/CollectionVGrid";
@ -4285,6 +4286,14 @@
minimumVersion = 2.0.0; minimumVersion = 2.0.0;
}; };
}; };
E1392FF02BA21B360034110D /* XCRemoteSwiftPackageReference "CollectionHStack" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/LePips/CollectionHStack";
requirement = {
branch = main;
kind = branch;
};
};
E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; repositoryURL = "https://github.com/JohnEstropia/CoreStore.git";
@ -4427,21 +4436,11 @@
package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */; package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */;
productName = Algorithms; productName = Algorithms;
}; };
E104DC8C2B9D8979008F506D /* CollectionHStack */ = {
isa = XCSwiftPackageProductDependency;
package = E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */;
productName = CollectionHStack;
};
E104DC8F2B9D8995008F506D /* CollectionVGrid */ = { E104DC8F2B9D8995008F506D /* CollectionVGrid */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */; package = E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */;
productName = CollectionVGrid; productName = CollectionVGrid;
}; };
E104DC912B9D89A2008F506D /* CollectionHStack */ = {
isa = XCSwiftPackageProductDependency;
package = E104DC8B2B9D8979008F506D /* XCRemoteSwiftPackageReference "CollectionHStack" */;
productName = CollectionHStack;
};
E104DC932B9D89A2008F506D /* CollectionVGrid */ = { E104DC932B9D89A2008F506D /* CollectionVGrid */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */; package = E104DC8E2B9D8995008F506D /* XCRemoteSwiftPackageReference "CollectionVGrid" */;
@ -4484,6 +4483,21 @@
package = 62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */; package = 62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */;
productName = 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 */ = { E13AF3B528A0C598009093AB /* Nuke */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */; package = E19E6E0328A0B958005C10C8 /* XCRemoteSwiftPackageReference "Nuke" */;
@ -4567,6 +4581,15 @@
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = PreferencesView; productName = PreferencesView;
}; };
E15EFA832BA167350080E926 /* CollectionHStack */ = {
isa = XCSwiftPackageProductDependency;
productName = CollectionHStack;
};
E15EFA852BA1685F0080E926 /* SwiftUIIntrospect */ = {
isa = XCSwiftPackageProductDependency;
package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
productName = SwiftUIIntrospect;
};
E18443CA2A037773002DDDC8 /* UDPBroadcast */ = { E18443CA2A037773002DDDC8 /* UDPBroadcast */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */; package = E1FAD1C42A0375BA007F5521 /* XCRemoteSwiftPackageReference "UDPBroadcastConnection" */;

View File

@ -15,7 +15,7 @@
"location" : "https://github.com/LePips/CollectionHStack", "location" : "https://github.com/LePips/CollectionHStack",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "ff54c0ea655840a12e7faaabb31aa66f50cc4767" "revision" : "e192023a2f2ce9351cbe7fb6f01c47043de209a8"
} }
}, },
{ {

View File

@ -10,6 +10,8 @@ import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: image aspect fill/fit
struct PosterButton<Item: Poster>: View { struct PosterButton<Item: Poster>: View {
private var item: Item private var item: Item
@ -20,19 +22,12 @@ struct PosterButton<Item: Poster>: View {
private var onSelect: () -> Void private var onSelect: () -> Void
private var singleImage: Bool private var singleImage: Bool
@ViewBuilder private func imageView(from item: Item) -> ImageView {
private func poster(from item: Item) -> some View {
switch type { switch type {
case .portrait: case .portrait:
ImageView(item.portraitPosterImageSource(maxWidth: 200)) ImageView(item.portraitPosterImageSource(maxWidth: 200))
.failure {
TypeSystemNameView(item: item)
}
case .landscape: case .landscape:
ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage)) ImageView(item.landscapePosterImageSources(maxWidth: 500, single: singleImage))
.failure {
TypeSystemNameView(item: item)
}
} }
} }
@ -44,7 +39,10 @@ struct PosterButton<Item: Poster>: View {
ZStack { ZStack {
Color.clear Color.clear
poster(from: item) imageView(from: item)
.failure {
SystemImageContentView(systemName: item.typeSystemImage)
}
imageOverlay() imageOverlay()
.eraseToAnyView() .eraseToAnyView()

View File

@ -41,7 +41,7 @@ struct PosterHStack<Item: Poster>: View {
} }
.clipsToBounds(false) .clipsToBounds(false)
.dataPrefix(20) .dataPrefix(20)
.horizontalInset(EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.defaultEdgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.defaultEdgePadding / 2)
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
} }
@ -64,7 +64,7 @@ struct PosterHStack<Item: Poster>: View {
} }
.clipsToBounds(false) .clipsToBounds(false)
.dataPrefix(20) .dataPrefix(20)
.horizontalInset(EdgeInsets.defaultEdgePadding) .insets(horizontal: EdgeInsets.defaultEdgePadding)
.itemSpacing(EdgeInsets.defaultEdgePadding / 2) .itemSpacing(EdgeInsets.defaultEdgePadding / 2)
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
} }

View File

@ -76,7 +76,7 @@ struct SeriesEpisodeSelector: View {
} }
} }
.scrollBehavior(.continuousLeadingEdge) .scrollBehavior(.continuousLeadingEdge)
.horizontalInset(16) .insets(horizontal: EdgeInsets.defaultEdgePadding)
.itemSpacing(8) .itemSpacing(8)
} }
} }

View File

@ -21,6 +21,15 @@ extension PagingLibraryView {
private var onSelect: () -> Void private var onSelect: () -> Void
private let posterType: PosterType 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 @ViewBuilder
private func itemAccessoryView(item: BaseItemDto) -> some View { private func itemAccessoryView(item: BaseItemDto) -> some View {
DotHStack { DotHStack {
@ -70,18 +79,10 @@ extension PagingLibraryView {
ZStack { ZStack {
Color.clear Color.clear
switch posterType { imageView(from: item)
case .portrait: .failure {
ImageView(item.portraitPosterImageSource(maxWidth: 60)) SystemImageContentView(systemName: item.typeSystemImage)
.failure { }
TypeSystemNameView(item: item)
}
case .landscape:
ImageView(item.landscapePosterImageSources(maxWidth: 110, single: false))
.failure {
TypeSystemNameView(item: item)
}
}
} }
.posterStyle(posterType) .posterStyle(posterType)
.frame(width: posterType == .landscape ? 110 : 60) .frame(width: posterType == .landscape ? 110 : 60)

View File

@ -36,11 +36,11 @@ extension VideoPlayer.Overlay {
@EnvironmentObject @EnvironmentObject
private var viewModel: VideoPlayerViewModel private var viewModel: VideoPlayerViewModel
@State @StateObject
private var scrollViewProxy: ScrollViewProxy? = nil private var collectionHStackProxy: CollectionHStackProxy<ChapterInfo.FullInfo> = .init()
var body: some View { var body: some View {
VStack { VStack(spacing: 0) {
Spacer(minLength: 0) Spacer(minLength: 0)
.allowsHitTesting(false) .allowsHitTesting(false)
@ -56,9 +56,8 @@ extension VideoPlayer.Overlay {
Button { Button {
if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) { if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) {
withAnimation { let index = viewModel.chapters.firstIndex(of: currentChapter)!
scrollViewProxy?.scrollTo(currentChapter.hashValue, anchor: .center) collectionHStackProxy.scrollTo(index: index)
}
} }
} label: { } label: {
Text(L10n.current) Text(L10n.current)
@ -67,101 +66,24 @@ extension VideoPlayer.Overlay {
} }
} }
.padding(.horizontal, safeAreaInsets.leading) .padding(.horizontal, safeAreaInsets.leading)
.if(UIDevice.isPad) { view in .edgePadding(.horizontal)
view.padding(.horizontal)
}
// ScrollViewReader { proxy in
CollectionHStack( CollectionHStack(
viewModel.chapters, viewModel.chapters,
minWidth: 200 minWidth: 200
) { chapter in ) { chapter in
PosterButton( ChapterButton(chapter: chapter)
item: chapter, }
type: .landscape .insets(horizontal: EdgeInsets.defaultEdgePadding, vertical: EdgeInsets.defaultEdgePadding)
) .proxy(collectionHStackProxy)
.content { .onChange(of: currentOverlayType) { newValue in
VStack(alignment: .leading, spacing: 5) { guard newValue == .chapters else { return }
Text(chapter.chapterInfo.displayTitle)
.font(.subheadline)
.fontWeight(.semibold)
.lineLimit(1)
.foregroundColor(.white)
Text(chapter.chapterInfo.timestampLabel) if let currentChapter = viewModel.chapter(from: currentProgressHandler.seconds) {
.font(.subheadline) let index = viewModel.chapters.firstIndex(of: currentChapter)!
.fontWeight(.semibold) collectionHStackProxy.scrollTo(index: index, animated: false)
.foregroundColor(Color(UIColor.systemBlue))
.padding(.vertical, 2)
.padding(.horizontal, 4)
.background {
Color(UIColor.darkGray).opacity(0.2).cornerRadius(4)
}
}
} }
} }
.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 { .background {
LinearGradient( 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)
}
}
}