mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 05:59:51 +00:00
Clean Up Item Scroll Views (#1015)
This commit is contained in:
parent
d65c4e8454
commit
2387197021
@ -6,6 +6,7 @@
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Label generic not really necessary if just restricting to `Text`
|
||||
@ -18,7 +19,7 @@ enum SelectorType {
|
||||
|
||||
struct SelectorView<Element: Displayable & Hashable, Label: View>: View {
|
||||
|
||||
@Environment(\.accentColor)
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
@Binding
|
||||
|
@ -19,7 +19,7 @@ struct TruncatedText: View {
|
||||
case view
|
||||
}
|
||||
|
||||
@Environment(\.accentColor)
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
@State
|
||||
|
@ -36,6 +36,10 @@ extension EdgeInsets {
|
||||
}
|
||||
|
||||
static let zero: EdgeInsets = .init()
|
||||
|
||||
var vertical: CGFloat {
|
||||
top + bottom
|
||||
}
|
||||
}
|
||||
|
||||
extension NSDirectionalEdgeInsets {
|
||||
|
@ -11,10 +11,6 @@ import SwiftUI
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
||||
struct AccentColorKey: EnvironmentKey {
|
||||
static let defaultValue: Color = Defaults[.accentColor]
|
||||
}
|
||||
|
||||
struct AudioOffsetKey: EnvironmentKey {
|
||||
static let defaultValue: Binding<Int> = .constant(0)
|
||||
}
|
||||
|
@ -10,11 +10,6 @@ import SwiftUI
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
||||
var accentColor: Color {
|
||||
get { self[AccentColorKey.self] }
|
||||
set { self[AccentColorKey.self] = newValue }
|
||||
}
|
||||
|
||||
var audioOffset: Binding<Int> {
|
||||
get { self[AudioOffsetKey.self] }
|
||||
set { self[AudioOffsetKey.self] = newValue }
|
||||
|
@ -6,11 +6,12 @@
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct PaletteOverlayRenderingModifier: ViewModifier {
|
||||
|
||||
@Environment(\.accentColor)
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
let color: Color?
|
||||
|
@ -6,34 +6,34 @@
|
||||
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
|
||||
struct ScrollViewOffsetModifier: ViewModifier {
|
||||
|
||||
@Binding
|
||||
var scrollViewOffset: CGFloat
|
||||
|
||||
private let scrollViewDelegate: ScrollViewDelegate?
|
||||
@StateObject
|
||||
private var scrollViewDelegate: ScrollViewDelegate
|
||||
|
||||
init(scrollViewOffset: Binding<CGFloat>) {
|
||||
self._scrollViewOffset = scrollViewOffset
|
||||
self.scrollViewDelegate = ScrollViewDelegate()
|
||||
self.scrollViewDelegate?.parent = self
|
||||
self._scrollViewDelegate = StateObject(wrappedValue: ScrollViewDelegate(scrollViewOffset: scrollViewOffset))
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.introspectScrollView { scrollView in
|
||||
content.introspect(.scrollView, on: .iOS(.v15), .iOS(.v16), .iOS(.v17)) { scrollView in
|
||||
scrollView.delegate = scrollViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
private class ScrollViewDelegate: NSObject, UIScrollViewDelegate {
|
||||
private class ScrollViewDelegate: NSObject, ObservableObject, UIScrollViewDelegate {
|
||||
|
||||
var parent: ScrollViewOffsetModifier?
|
||||
let scrollViewOffset: Binding<CGFloat>
|
||||
|
||||
init(scrollViewOffset: Binding<CGFloat>) {
|
||||
self.scrollViewOffset = scrollViewOffset
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
parent?._scrollViewOffset.wrappedValue = scrollView.contentOffset.y
|
||||
scrollViewOffset.wrappedValue = scrollView.contentOffset.y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,12 +13,18 @@ struct FramePreferenceKey: PreferenceKey {
|
||||
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {}
|
||||
}
|
||||
|
||||
struct GeometryPrefenceKey: PreferenceKey {
|
||||
|
||||
static var defaultValue: Value = Value(size: .zero, safeAreaInsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
static func reduce(value: inout Value, nextValue: () -> Value) {}
|
||||
|
||||
struct Value: Equatable {
|
||||
let size: CGSize
|
||||
let safeAreaInsets: EdgeInsets
|
||||
}
|
||||
}
|
||||
|
||||
struct LocationPreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGPoint = .zero
|
||||
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
|
||||
}
|
||||
|
||||
struct SizePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGSize = .zero
|
||||
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
|
||||
}
|
||||
|
@ -183,14 +183,25 @@ extension View {
|
||||
|
||||
// TODO: have width/height tracked binding
|
||||
|
||||
func onSizeChanged(_ onChange: @escaping (CGSize) -> Void) -> some View {
|
||||
func onSizeChanged(perform action: @escaping (CGSize) -> Void) -> some View {
|
||||
onSizeChanged { size, _ in
|
||||
action(size)
|
||||
}
|
||||
}
|
||||
|
||||
func onSizeChanged(perform action: @escaping (CGSize, EdgeInsets) -> Void) -> some View {
|
||||
background {
|
||||
GeometryReader { reader in
|
||||
Color.clear
|
||||
.preference(key: SizePreferenceKey.self, value: reader.size)
|
||||
.preference(
|
||||
key: GeometryPrefenceKey.self,
|
||||
value: GeometryPrefenceKey.Value(size: reader.size, safeAreaInsets: reader.safeAreaInsets)
|
||||
)
|
||||
}
|
||||
}
|
||||
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
|
||||
.onPreferenceChange(GeometryPrefenceKey.self) { value in
|
||||
action(value.size, value.safeAreaInsets)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: probably rename since this doesn't set the size but tracks it
|
||||
|
@ -199,8 +199,8 @@
|
||||
E11245B128D919CD00D8A977 /* Overlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11245B028D919CD00D8A977 /* Overlay.swift */; };
|
||||
E11245B428D97D5D00D8A977 /* BottomBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11245B328D97D5D00D8A977 /* BottomBarView.swift */; };
|
||||
E11245B728D97ED200D8A977 /* TopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11245B628D97ED200D8A977 /* TopBarView.swift */; };
|
||||
E113132B28BDB4B500930F75 /* NavBarDrawerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */; };
|
||||
E113132F28BDB66A00930F75 /* NavBarDrawerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */; };
|
||||
E113132B28BDB4B500930F75 /* NavigationBarDrawerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132A28BDB4B500930F75 /* NavigationBarDrawerView.swift */; };
|
||||
E113132F28BDB66A00930F75 /* NavigationBarDrawerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132E28BDB66A00930F75 /* NavigationBarDrawerModifier.swift */; };
|
||||
E113133228BDC72000930F75 /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133128BDC72000930F75 /* FilterView.swift */; };
|
||||
E113133428BE988200930F75 /* NavigationBarFilterDrawer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133328BE988200930F75 /* NavigationBarFilterDrawer.swift */; };
|
||||
E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133528BE98AA00930F75 /* FilterDrawerButton.swift */; };
|
||||
@ -230,8 +230,8 @@
|
||||
E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; };
|
||||
E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; };
|
||||
E11895AA289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */; };
|
||||
E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */; };
|
||||
E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895AE2893840F0042947B /* NavBarOffsetView.swift */; };
|
||||
E11895AC289383EE0042947B /* NavigationBarOffsetModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895AB289383EE0042947B /* NavigationBarOffsetModifier.swift */; };
|
||||
E11895AF2893840F0042947B /* NavigationBarOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895AE2893840F0042947B /* NavigationBarOffsetView.swift */; };
|
||||
E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */; };
|
||||
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */; };
|
||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||
@ -243,7 +243,7 @@
|
||||
E11BDF972B865F550045C54A /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF962B865F550045C54A /* ItemTag.swift */; };
|
||||
E11BDF982B865F550045C54A /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF962B865F550045C54A /* ItemTag.swift */; };
|
||||
E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */; };
|
||||
E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */; };
|
||||
E11CEB8B28998552003E74C7 /* View-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8A28998552003E74C7 /* View-iOS.swift */; };
|
||||
E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8C28999B4A003E74C7 /* Font.swift */; };
|
||||
E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */; };
|
||||
E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */; };
|
||||
@ -590,7 +590,7 @@
|
||||
E1A3E4C72BB74E50005C59F8 /* EpisodeCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4C62BB74E50005C59F8 /* EpisodeCard.swift */; };
|
||||
E1A3E4C92BB74EA3005C59F8 /* LoadingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4C82BB74EA3005C59F8 /* LoadingCard.swift */; };
|
||||
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4CA2BB74EFD005C59F8 /* EpisodeHStack.swift */; };
|
||||
E1A3E4CD2BB7D8C8005C59F8 /* iOSLabelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4CC2BB7D8C8005C59F8 /* iOSLabelExtensions.swift */; };
|
||||
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4CC2BB7D8C8005C59F8 /* Label-iOS.swift */; };
|
||||
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */; };
|
||||
E1A3E4D12BB7F5BF005C59F8 /* ErrorCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A3E4D02BB7F5BF005C59F8 /* ErrorCard.swift */; };
|
||||
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */; };
|
||||
@ -618,6 +618,8 @@
|
||||
E1B5F7A929577BCE004B26CF /* PulseLogHandler in Frameworks */ = {isa = PBXBuildFile; productRef = E1B5F7A829577BCE004B26CF /* PulseLogHandler */; };
|
||||
E1B5F7AB29577BCE004B26CF /* PulseUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1B5F7AA29577BCE004B26CF /* PulseUI */; };
|
||||
E1B5F7AD29577BDD004B26CF /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E1B5F7AC29577BDD004B26CF /* OrderedCollections */; };
|
||||
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */; };
|
||||
E1B90C8A2BC475E7007027C8 /* ScalingButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */; };
|
||||
E1BA6FC529D25DBD007D98DC /* LandscapeItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BA6FC429D25DBD007D98DC /* LandscapeItemElement.swift */; };
|
||||
E1BDF2E52951475300CC0294 /* VideoPlayerActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */; };
|
||||
E1BDF2E62951475300CC0294 /* VideoPlayerActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */; };
|
||||
@ -964,8 +966,8 @@
|
||||
E11245B028D919CD00D8A977 /* Overlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overlay.swift; sourceTree = "<group>"; };
|
||||
E11245B328D97D5D00D8A977 /* BottomBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomBarView.swift; sourceTree = "<group>"; };
|
||||
E11245B628D97ED200D8A977 /* TopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBarView.swift; sourceTree = "<group>"; };
|
||||
E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarDrawerView.swift; sourceTree = "<group>"; };
|
||||
E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarDrawerModifier.swift; sourceTree = "<group>"; };
|
||||
E113132A28BDB4B500930F75 /* NavigationBarDrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarDrawerView.swift; sourceTree = "<group>"; };
|
||||
E113132E28BDB66A00930F75 /* NavigationBarDrawerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarDrawerModifier.swift; sourceTree = "<group>"; };
|
||||
E113133128BDC72000930F75 /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = "<group>"; };
|
||||
E113133328BE988200930F75 /* NavigationBarFilterDrawer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarFilterDrawer.swift; sourceTree = "<group>"; };
|
||||
E113133528BE98AA00930F75 /* FilterDrawerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterDrawerButton.swift; sourceTree = "<group>"; };
|
||||
@ -980,14 +982,14 @@
|
||||
E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = "<group>"; };
|
||||
E118959C289312020042947B /* BaseItemPerson+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemPerson+Poster.swift"; sourceTree = "<group>"; };
|
||||
E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetModifier.swift; sourceTree = "<group>"; };
|
||||
E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarOffsetModifier.swift; sourceTree = "<group>"; };
|
||||
E11895AE2893840F0042947B /* NavBarOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarOffsetView.swift; sourceTree = "<group>"; };
|
||||
E11895AB289383EE0042947B /* NavigationBarOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarOffsetModifier.swift; sourceTree = "<group>"; };
|
||||
E11895AE2893840F0042947B /* NavigationBarOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarOffsetView.swift; sourceTree = "<group>"; };
|
||||
E11895B22893844A0042947B /* BackgroundParallaxHeaderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundParallaxHeaderModifier.swift; sourceTree = "<group>"; };
|
||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||
E11BDF762B8513B40045C54A /* ItemGenre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemGenre.swift; sourceTree = "<group>"; };
|
||||
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedCaseIterable.swift; sourceTree = "<group>"; };
|
||||
E11BDF962B865F550045C54A /* ItemTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTag.swift; sourceTree = "<group>"; };
|
||||
E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSViewExtensions.swift; sourceTree = "<group>"; };
|
||||
E11CEB8A28998552003E74C7 /* View-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View-iOS.swift"; sourceTree = "<group>"; };
|
||||
E11CEB8C28999B4A003E74C7 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = "<group>"; };
|
||||
E11CEB8F28999D84003E74C7 /* EpisodeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemView.swift; sourceTree = "<group>"; };
|
||||
E11CEB9328999D9E003E74C7 /* EpisodeItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeItemContentView.swift; sourceTree = "<group>"; };
|
||||
@ -1185,7 +1187,7 @@
|
||||
E1A3E4C62BB74E50005C59F8 /* EpisodeCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCard.swift; sourceTree = "<group>"; };
|
||||
E1A3E4C82BB74EA3005C59F8 /* LoadingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingCard.swift; sourceTree = "<group>"; };
|
||||
E1A3E4CA2BB74EFD005C59F8 /* EpisodeHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeHStack.swift; sourceTree = "<group>"; };
|
||||
E1A3E4CC2BB7D8C8005C59F8 /* iOSLabelExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSLabelExtensions.swift; sourceTree = "<group>"; };
|
||||
E1A3E4CC2BB7D8C8005C59F8 /* Label-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Label-iOS.swift"; sourceTree = "<group>"; };
|
||||
E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelayedProgressView.swift; sourceTree = "<group>"; };
|
||||
E1A3E4D02BB7F5BF005C59F8 /* ErrorCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCard.swift; sourceTree = "<group>"; };
|
||||
E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = "<group>"; };
|
||||
@ -1203,6 +1205,7 @@
|
||||
E1B490462967E2E500D3EDCE /* CoreStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreStore.swift; sourceTree = "<group>"; };
|
||||
E1B5784028F8AFCB00D42911 /* WrappedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedView.swift; sourceTree = "<group>"; };
|
||||
E1B5861129E32EEF00E45D6E /* Sequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sequence.swift; sourceTree = "<group>"; };
|
||||
E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetScrollView.swift; sourceTree = "<group>"; };
|
||||
E1BA6FC429D25DBD007D98DC /* LandscapeItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapeItemElement.swift; sourceTree = "<group>"; };
|
||||
E1BDF2E42951475300CC0294 /* VideoPlayerActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerActionButton.swift; sourceTree = "<group>"; };
|
||||
E1BDF2E82951490400CC0294 /* ActionButtonSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonSelectorView.swift; sourceTree = "<group>"; };
|
||||
@ -1581,6 +1584,7 @@
|
||||
E1CCF12D28ABF989006CAC9E /* PosterType.swift */,
|
||||
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
|
||||
E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */,
|
||||
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
|
||||
E129429228F2845000796AC6 /* SliderType.swift */,
|
||||
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
||||
E11042742B8013DF00821020 /* Stateful.swift */,
|
||||
@ -1649,7 +1653,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E13DD3BB27163C3E009D4DAF /* App */,
|
||||
62ECA01926FA6D6900E8EBB7 /* AppURLHandler */,
|
||||
53F866422687A45400DCD1D7 /* Components */,
|
||||
E11CEB85289984F5003E74C7 /* Extensions */,
|
||||
E1DD1127271E7D15005BE12F /* Objects */,
|
||||
@ -1941,15 +1944,6 @@
|
||||
path = Coordinators;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
62ECA01926FA6D6900E8EBB7 /* AppURLHandler */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6220D0CB26D640C400B8E046 /* AppURLHandler.swift */,
|
||||
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */,
|
||||
);
|
||||
path = AppURLHandler;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AE8C3157265D6F5E008AA076 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2015,13 +2009,13 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E113133028BDB6D600930F75 /* NavBarDrawerButtons */ = {
|
||||
E113133028BDB6D600930F75 /* NavigationBarDrawerButtons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */,
|
||||
E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */,
|
||||
E113132E28BDB66A00930F75 /* NavigationBarDrawerModifier.swift */,
|
||||
E113132A28BDB4B500930F75 /* NavigationBarDrawerView.swift */,
|
||||
);
|
||||
path = NavBarDrawerButtons;
|
||||
path = NavigationBarDrawerButtons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1153D972BBA3E5300424D36 /* Components */ = {
|
||||
@ -2065,19 +2059,19 @@
|
||||
path = ViewExtensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E11895B12893842D0042947B /* NavBarOffset */ = {
|
||||
E11895B12893842D0042947B /* NavigationBarOffset */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */,
|
||||
E11895AE2893840F0042947B /* NavBarOffsetView.swift */,
|
||||
E11895AB289383EE0042947B /* NavigationBarOffsetModifier.swift */,
|
||||
E11895AE2893840F0042947B /* NavigationBarOffsetView.swift */,
|
||||
);
|
||||
path = NavBarOffset;
|
||||
path = NavigationBarOffset;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E11CEB85289984F5003E74C7 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1A3E4CC2BB7D8C8005C59F8 /* iOSLabelExtensions.swift */,
|
||||
E1A3E4CC2BB7D8C8005C59F8 /* Label-iOS.swift */,
|
||||
E11CEB8828998522003E74C7 /* View */,
|
||||
);
|
||||
path = Extensions;
|
||||
@ -2086,10 +2080,8 @@
|
||||
E11CEB8828998522003E74C7 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */,
|
||||
E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */,
|
||||
E113133028BDB6D600930F75 /* NavBarDrawerButtons */,
|
||||
E11895B12893842D0042947B /* NavBarOffset */,
|
||||
E1B90C892BC4563D007027C8 /* Modifiers */,
|
||||
E11CEB8A28998552003E74C7 /* View-iOS.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@ -2553,6 +2545,7 @@
|
||||
E17AC9722955007A003D2BC2 /* DownloadTaskButton.swift */,
|
||||
E172D3AF2BACA54A007B4647 /* EpisodeSelector */,
|
||||
E17FB55A28C1266400311DFE /* GenresHStack.swift */,
|
||||
E1B90C692BBE68D5007027C8 /* OffsetScrollView.swift */,
|
||||
E1D8424E2932F7C400D1041A /* OverviewView.swift */,
|
||||
E18E01D8288747230022598C /* PlayButton.swift */,
|
||||
E17FB55428C1250B00311DFE /* SimilarItemsHStack.swift */,
|
||||
@ -2663,6 +2656,16 @@
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1B90C892BC4563D007027C8 /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */,
|
||||
E113133028BDB6D600930F75 /* NavigationBarDrawerButtons */,
|
||||
E11895B12893842D0042947B /* NavigationBarOffset */,
|
||||
);
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1BDF2E7295148F400CC0294 /* VideoPlayerSettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2832,7 +2835,8 @@
|
||||
E1DD1127271E7D15005BE12F /* Objects */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */,
|
||||
6220D0CB26D640C400B8E046 /* AppURLHandler.swift */,
|
||||
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
@ -3556,6 +3560,7 @@
|
||||
E14EDECD2B8FB709000F00A4 /* ItemYear.swift in Sources */,
|
||||
E154965F296CA2EF00C4EF88 /* DownloadTask.swift in Sources */,
|
||||
E154967E296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift in Sources */,
|
||||
E1B90C8A2BC475E7007027C8 /* ScalingButtonStyle.swift in Sources */,
|
||||
E1DABAFE2A27B982008AC34A /* RatingsCard.swift in Sources */,
|
||||
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */,
|
||||
E104C873296E0D0A00C1C3F9 /* IndicatorSettingsView.swift in Sources */,
|
||||
@ -3578,6 +3583,7 @@
|
||||
E1A1528828FD229500600579 /* ChevronButton.swift in Sources */,
|
||||
E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
||||
E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */,
|
||||
E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */,
|
||||
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
|
||||
621338932660107500A81A2A /* String.swift in Sources */,
|
||||
@ -3603,7 +3609,7 @@
|
||||
E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */,
|
||||
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
||||
E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */,
|
||||
E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */,
|
||||
E11895AF2893840F0042947B /* NavigationBarOffsetView.swift in Sources */,
|
||||
E18E0208288749200022598C /* BlurView.swift in Sources */,
|
||||
E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */,
|
||||
E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */,
|
||||
@ -3630,7 +3636,7 @@
|
||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */,
|
||||
E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */,
|
||||
E1A3E4CD2BB7D8C8005C59F8 /* iOSLabelExtensions.swift in Sources */,
|
||||
E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */,
|
||||
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||
E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */,
|
||||
E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */,
|
||||
@ -3657,7 +3663,7 @@
|
||||
E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
|
||||
E17FB55228C119D400311DFE /* Displayable.swift in Sources */,
|
||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||
E113132B28BDB4B500930F75 /* NavBarDrawerView.swift in Sources */,
|
||||
E113132B28BDB4B500930F75 /* NavigationBarDrawerView.swift in Sources */,
|
||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||
E1559A76294D960C00C1FFBC /* MainOverlay.swift in Sources */,
|
||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */,
|
||||
@ -3771,7 +3777,7 @@
|
||||
E170D105294D21FA0017224C /* MediaSourceInfoView.swift in Sources */,
|
||||
E1D37F4B2B9CEA5C00343D2B /* ImageSource.swift in Sources */,
|
||||
E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */,
|
||||
E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */,
|
||||
E11895AC289383EE0042947B /* NavigationBarOffsetModifier.swift in Sources */,
|
||||
E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */,
|
||||
E157563029355B7900976E1F /* UpdateView.swift in Sources */,
|
||||
E1D8424F2932F7C400D1041A /* OverviewView.swift in Sources */,
|
||||
@ -3804,7 +3810,7 @@
|
||||
E1E7506A2A33E9B400B2C1EE /* RatingsCard.swift in Sources */,
|
||||
E1D842912933F87500D1041A /* ItemFields.swift in Sources */,
|
||||
E1BDF2F729524ECD00CC0294 /* PlaybackSpeedActionButton.swift in Sources */,
|
||||
E113132F28BDB66A00930F75 /* NavBarDrawerModifier.swift in Sources */,
|
||||
E113132F28BDB66A00930F75 /* NavigationBarDrawerModifier.swift in Sources */,
|
||||
E1E750692A33E9B400B2C1EE /* MediaSourcesCard.swift in Sources */,
|
||||
E1DE2B4C2B98389E00F6715F /* PaletteOverlayRenderingModifier.swift in Sources */,
|
||||
E18295E429CAC6F100F91ED0 /* BasicNavigationCoordinator.swift in Sources */,
|
||||
@ -3819,7 +3825,7 @@
|
||||
E1549662296CA2EF00C4EF88 /* NewSessionManager.swift in Sources */,
|
||||
E15756362936856700976E1F /* VideoPlayerType.swift in Sources */,
|
||||
E1DA654C28E69B0500592A73 /* SpecialFeatureType.swift in Sources */,
|
||||
E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */,
|
||||
E11CEB8B28998552003E74C7 /* View-iOS.swift in Sources */,
|
||||
E1401CA92938140700E8B599 /* DarkAppIcon.swift in Sources */,
|
||||
E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
|
||||
E11042752B8013DF00821020 /* Stateful.swift in Sources */,
|
||||
@ -4252,7 +4258,7 @@
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 78;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@ -4268,7 +4274,7 @@
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
OTHER_CFLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
@ -4292,7 +4298,7 @@
|
||||
CURRENT_PROJECT_VERSION = 78;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
DEVELOPMENT_TEAM = TY84JMYEFE;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@ -4308,7 +4314,7 @@
|
||||
);
|
||||
MARKETING_VERSION = 1.0.0;
|
||||
OTHER_CFLAGS = "";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = pip.jellyfin.swiftfin;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
|
@ -13,7 +13,7 @@ extension NavigationBarFilterDrawer {
|
||||
|
||||
struct FilterDrawerButton: View {
|
||||
|
||||
@Environment(\.accentColor)
|
||||
@Default(.accentColor)
|
||||
private var accentColor
|
||||
|
||||
private let systemName: String?
|
||||
|
@ -15,7 +15,7 @@ struct PillHStack<Item: Displayable>: View {
|
||||
private var onSelect: (Item) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(title)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NavBarDrawerModifier<Drawer: View>: ViewModifier {
|
||||
struct NavigationBarDrawerModifier<Drawer: View>: ViewModifier {
|
||||
|
||||
private let drawer: () -> Drawer
|
||||
|
||||
@ -17,7 +17,7 @@ struct NavBarDrawerModifier<Drawer: View>: ViewModifier {
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
NavBarDrawerView {
|
||||
NavigationBarDrawerView {
|
||||
drawer()
|
||||
.ignoresSafeArea()
|
||||
} content: {
|
@ -8,61 +8,55 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NavBarDrawerView: UIViewControllerRepresentable {
|
||||
struct NavigationBarDrawerView<Content: View, Drawer: View>: UIViewControllerRepresentable {
|
||||
|
||||
private let buttons: () -> any View
|
||||
private let content: () -> any View
|
||||
private let buttons: () -> Drawer
|
||||
private let content: () -> Content
|
||||
|
||||
init(
|
||||
@ViewBuilder buttons: @escaping () -> any View,
|
||||
@ViewBuilder content: @escaping () -> any View
|
||||
@ViewBuilder buttons: @escaping () -> Drawer,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.buttons = buttons
|
||||
self.content = content
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UINavBarDrawerHostingController {
|
||||
UINavBarDrawerHostingController(buttons: buttons, content: content)
|
||||
func makeUIViewController(context: Context) -> UINavigationBarDrawerHostingController<Content, Drawer> {
|
||||
UINavigationBarDrawerHostingController<Content, Drawer>(buttons: buttons, content: content)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UINavBarDrawerHostingController, context: Context) {}
|
||||
func updateUIViewController(_ uiViewController: UINavigationBarDrawerHostingController<Content, Drawer>, context: Context) {}
|
||||
}
|
||||
|
||||
class UINavBarDrawerHostingController: UIViewController {
|
||||
class UINavigationBarDrawerHostingController<Content: View, Drawer: View>: UIHostingController<Content> {
|
||||
|
||||
private let buttons: () -> any View
|
||||
private let content: () -> any View
|
||||
private let drawer: () -> Drawer
|
||||
private let content: () -> Content
|
||||
|
||||
// TODO: see if we can get the height instead from the view passed in
|
||||
private let drawerHeight: CGFloat = 36
|
||||
|
||||
private lazy var navBarBlurView: UIVisualEffectView = {
|
||||
private lazy var blurView: UIVisualEffectView = {
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return blurView
|
||||
}()
|
||||
|
||||
private lazy var contentView: UIHostingController<AnyView> = {
|
||||
let contentView = UIHostingController(rootView: content().eraseToAnyView())
|
||||
contentView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.view.backgroundColor = nil
|
||||
return contentView
|
||||
}()
|
||||
|
||||
private lazy var drawerButtonsView: UIHostingController<AnyView> = {
|
||||
let drawerButtonsView = UIHostingController(rootView: buttons().eraseToAnyView())
|
||||
private lazy var drawerButtonsView: UIHostingController<Drawer> = {
|
||||
let drawerButtonsView = UIHostingController(rootView: drawer())
|
||||
drawerButtonsView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
drawerButtonsView.view.backgroundColor = nil
|
||||
return drawerButtonsView
|
||||
}()
|
||||
|
||||
init(
|
||||
buttons: @escaping () -> any View,
|
||||
content: @escaping () -> any View
|
||||
buttons: @escaping () -> Drawer,
|
||||
content: @escaping () -> Content
|
||||
) {
|
||||
self.buttons = buttons
|
||||
self.drawer = buttons
|
||||
self.content = content
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
super.init(rootView: content())
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -75,11 +69,7 @@ class UINavBarDrawerHostingController: UIViewController {
|
||||
|
||||
view.backgroundColor = nil
|
||||
|
||||
addChild(contentView)
|
||||
view.addSubview(contentView.view)
|
||||
contentView.didMove(toParent: self)
|
||||
|
||||
view.addSubview(navBarBlurView)
|
||||
view.addSubview(blurView)
|
||||
|
||||
addChild(drawerButtonsView)
|
||||
view.addSubview(drawerButtonsView.view)
|
||||
@ -91,17 +81,12 @@ class UINavBarDrawerHostingController: UIViewController {
|
||||
drawerButtonsView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
drawerButtonsView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
navBarBlurView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
navBarBlurView.bottomAnchor.constraint(equalTo: drawerButtonsView.view.bottomAnchor),
|
||||
navBarBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navBarBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
NSLayoutConstraint.activate([
|
||||
contentView.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
contentView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
contentView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: drawerButtonsView.view.bottomAnchor),
|
||||
blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NavBarOffsetModifier: ViewModifier {
|
||||
struct NavigationBarOffsetModifier: ViewModifier {
|
||||
|
||||
@Binding
|
||||
var scrollViewOffset: CGFloat
|
||||
@ -17,7 +17,11 @@ struct NavBarOffsetModifier: ViewModifier {
|
||||
let end: CGFloat
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
NavBarOffsetView(scrollViewOffset: $scrollViewOffset, start: start, end: end) {
|
||||
NavigationBarOffsetView(
|
||||
scrollViewOffset: $scrollViewOffset,
|
||||
start: start,
|
||||
end: end
|
||||
) {
|
||||
content
|
||||
}
|
||||
.ignoresSafeArea()
|
@ -8,7 +8,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NavBarOffsetView<Content: View>: UIViewControllerRepresentable {
|
||||
struct NavigationBarOffsetView<Content: View>: UIViewControllerRepresentable {
|
||||
|
||||
@Binding
|
||||
private var scrollViewOffset: CGFloat
|
||||
@ -29,20 +29,20 @@ struct NavBarOffsetView<Content: View>: UIViewControllerRepresentable {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> UINavBarOffsetHostingController<Content> {
|
||||
UINavBarOffsetHostingController(rootView: content())
|
||||
func makeUIViewController(context: Context) -> UINavigationBarOffsetHostingController<Content> {
|
||||
UINavigationBarOffsetHostingController(rootView: content())
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UINavBarOffsetHostingController<Content>, context: Context) {
|
||||
func updateUIViewController(_ uiViewController: UINavigationBarOffsetHostingController<Content>, context: Context) {
|
||||
uiViewController.scrollViewDidScroll(scrollViewOffset, start: start, end: end)
|
||||
}
|
||||
}
|
||||
|
||||
class UINavBarOffsetHostingController<Content: View>: UIHostingController<Content> {
|
||||
class UINavigationBarOffsetHostingController<Content: View>: UIHostingController<Content> {
|
||||
|
||||
private var lastScrollViewOffset: CGFloat = 0
|
||||
private var lastAlpha: CGFloat = 0
|
||||
|
||||
private lazy var navBarBlurView: UIVisualEffectView = {
|
||||
private lazy var blurView: UIVisualEffectView = {
|
||||
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
return blurView
|
||||
@ -53,38 +53,41 @@ class UINavBarOffsetHostingController<Content: View>: UIHostingController<Conten
|
||||
|
||||
view.backgroundColor = nil
|
||||
|
||||
view.addSubview(navBarBlurView)
|
||||
navBarBlurView.alpha = 0
|
||||
view.addSubview(blurView)
|
||||
blurView.alpha = 0
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
navBarBlurView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
navBarBlurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
navBarBlurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navBarBlurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ offset: CGFloat, start: CGFloat, end: CGFloat) {
|
||||
|
||||
let diff = end - start
|
||||
let currentProgress = (offset - start) / diff
|
||||
let offset = min(max(currentProgress, 0), 1)
|
||||
let alpha = clamp(currentProgress, min: 0, max: 1)
|
||||
|
||||
navigationController?.navigationBar
|
||||
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(offset)]
|
||||
navBarBlurView.alpha = offset
|
||||
lastScrollViewOffset = offset
|
||||
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(alpha)]
|
||||
blurView.alpha = alpha
|
||||
lastAlpha = alpha
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
navigationController?.navigationBar
|
||||
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(lastScrollViewOffset)]
|
||||
.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label.withAlphaComponent(lastAlpha)]
|
||||
navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
|
||||
navigationController?.navigationBar.shadowImage = UIImage()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label]
|
||||
navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
|
||||
navigationController?.navigationBar.shadowImage = nil
|
@ -16,11 +16,11 @@ extension View {
|
||||
}
|
||||
|
||||
func navigationBarOffset(_ scrollViewOffset: Binding<CGFloat>, start: CGFloat, end: CGFloat) -> some View {
|
||||
modifier(NavBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end))
|
||||
modifier(NavigationBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end))
|
||||
}
|
||||
|
||||
func navigationBarDrawer<Drawer: View>(@ViewBuilder _ drawer: @escaping () -> Drawer) -> some View {
|
||||
modifier(NavBarDrawerModifier(drawer: drawer))
|
||||
modifier(NavigationBarDrawerModifier(drawer: drawer))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
@ -11,6 +11,8 @@ import SwiftUI
|
||||
|
||||
// Note: Keep all of the ItemFilterCollection/ItemFilter/AnyItemFilter KeyPath wackiness in this file
|
||||
|
||||
// TODO: multiple filter types?
|
||||
// - for sort order and sort by combined
|
||||
struct FilterView: View {
|
||||
|
||||
@Binding
|
||||
|
@ -29,10 +29,7 @@ extension ItemView {
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.accessibility(addTraits: [.isHeader])
|
||||
.padding(.horizontal)
|
||||
.if(UIDevice.isPad) { view in
|
||||
view.padding(.horizontal)
|
||||
}
|
||||
.edgePadding(.horizontal)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
@ -41,6 +38,7 @@ extension ItemView {
|
||||
.item.imageSource(.primary, maxWidth: 300)
|
||||
)
|
||||
.posterStyle(.portrait)
|
||||
.posterShadow()
|
||||
.frame(width: 130)
|
||||
.accessibilityIgnoresInvertColors()
|
||||
|
||||
@ -54,10 +52,7 @@ extension ItemView {
|
||||
|
||||
RatingsCard(item: viewModel.item)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.if(UIDevice.isPad) { view in
|
||||
view.padding(.horizontal)
|
||||
}
|
||||
.edgePadding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
86
Swiftfin/Views/ItemView/Components/OffsetScrollView.swift
Normal file
86
Swiftfin/Views/ItemView/Components/OffsetScrollView.swift
Normal file
@ -0,0 +1,86 @@
|
||||
//
|
||||
// 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) 2024 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: given height or height ratio options
|
||||
|
||||
// The fading values just "feel right" and is the same for iOS and iPadOS.
|
||||
// Adjust if necessary or if a more concrete design comes along.
|
||||
|
||||
extension ItemView {
|
||||
|
||||
struct OffsetScrollView<Header: View, Overlay: View, Content: View>: View {
|
||||
|
||||
@State
|
||||
private var scrollViewOffset: CGFloat = 0
|
||||
@State
|
||||
private var size: CGSize = .zero
|
||||
@State
|
||||
private var safeAreaInsets: EdgeInsets = .zero
|
||||
|
||||
private let header: () -> Header
|
||||
private let overlay: () -> Overlay
|
||||
private let content: () -> Content
|
||||
private let heightRatio: CGFloat
|
||||
|
||||
init(
|
||||
headerHeight: CGFloat = 0,
|
||||
@ViewBuilder header: @escaping () -> Header,
|
||||
@ViewBuilder overlay: @escaping () -> Overlay,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
) {
|
||||
self.header = header
|
||||
self.overlay = overlay
|
||||
self.content = content
|
||||
self.heightRatio = headerHeight
|
||||
}
|
||||
|
||||
private var headerOpacity: CGFloat {
|
||||
let start = (size.height + safeAreaInsets.vertical) * heightRatio - safeAreaInsets.top - 90
|
||||
let end = (size.height + safeAreaInsets.vertical) * heightRatio - safeAreaInsets.top - 40
|
||||
let diff = end - start
|
||||
let opacity = clamp((scrollViewOffset - start) / diff, min: 0, max: 1)
|
||||
return opacity
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
overlay()
|
||||
.frame(height: (size.height + safeAreaInsets.vertical) * heightRatio)
|
||||
.overlay {
|
||||
Color.systemBackground
|
||||
.opacity(headerOpacity)
|
||||
}
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.onSizeChanged { size, safeAreaInsets in
|
||||
self.size = size
|
||||
self.safeAreaInsets = safeAreaInsets
|
||||
}
|
||||
.scrollViewOffset($scrollViewOffset)
|
||||
.navigationBarOffset(
|
||||
$scrollViewOffset,
|
||||
start: (size.height + safeAreaInsets.vertical) * heightRatio - safeAreaInsets.top - 45,
|
||||
end: (size.height + safeAreaInsets.vertical) * heightRatio - safeAreaInsets.top - 5
|
||||
)
|
||||
.backgroundParallaxHeader(
|
||||
$scrollViewOffset,
|
||||
height: (size.height + safeAreaInsets.vertical) * heightRatio,
|
||||
multiplier: 0.3
|
||||
) {
|
||||
header()
|
||||
.frame(height: (size.height + safeAreaInsets.vertical) * heightRatio)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -28,9 +28,7 @@ extension ItemView {
|
||||
.font(.body)
|
||||
.fontWeight(.semibold)
|
||||
.multilineTextAlignment(.leading)
|
||||
.ifLet(taglineLineLimit) { view, lineLimit in
|
||||
view.lineLimit(lineLimit)
|
||||
}
|
||||
.lineLimit(taglineLineLimit)
|
||||
}
|
||||
|
||||
if let itemOverview = item.overview {
|
||||
@ -40,9 +38,7 @@ extension ItemView {
|
||||
}
|
||||
.seeMoreType(.view)
|
||||
.font(.footnote)
|
||||
.ifLet(overviewLineLimit) { view, lineLimit in
|
||||
view.lineLimit(lineLimit)
|
||||
}
|
||||
.lineLimit(overviewLineLimit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ import Defaults
|
||||
import Factory
|
||||
import SwiftUI
|
||||
|
||||
// TODO: fix play from beginning
|
||||
|
||||
extension ItemView {
|
||||
|
||||
struct PlayButton: View {
|
||||
|
@ -23,7 +23,7 @@ extension ItemView {
|
||||
PosterHStack(
|
||||
title: L10n.specialFeatures,
|
||||
type: .landscape,
|
||||
items: .constant(OrderedSet(items))
|
||||
items: items
|
||||
)
|
||||
.onSelect { item in
|
||||
guard let mediaSource = item.mediaSources?.first else { return }
|
||||
|
@ -11,6 +11,10 @@ import JellyfinAPI
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
// 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 {
|
||||
|
||||
@StateObject
|
||||
|
@ -22,6 +22,35 @@ extension CollectionItemView {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
ImageView(viewModel.item.imageSource(.backdrop, maxWidth: 600))
|
||||
.posterStyle(.landscape, contentMode: .fill)
|
||||
.frame(maxHeight: 300)
|
||||
.posterShadow()
|
||||
.edgePadding(.horizontal)
|
||||
|
||||
Text(viewModel.item.displayTitle)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(2)
|
||||
.padding(.horizontal)
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.frame(maxWidth: 300)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
|
||||
// MARK: Overview
|
||||
|
||||
ItemView.OverviewView(item: viewModel.item)
|
||||
.overviewLineLimit(4)
|
||||
.taglineLineLimit(2)
|
||||
.padding(.horizontal)
|
||||
|
||||
RowDivider()
|
||||
|
||||
// MARK: Genres
|
||||
|
||||
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
||||
|
@ -12,26 +12,13 @@ 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)
|
||||
}
|
||||
ScrollView {
|
||||
ContentView(viewModel: viewModel)
|
||||
.edgePadding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,8 @@ extension EpisodeItemView {
|
||||
.overviewLineLimit(4)
|
||||
.padding(.horizontal)
|
||||
|
||||
RowDivider()
|
||||
|
||||
// MARK: Genres
|
||||
|
||||
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {
|
||||
|
@ -11,25 +11,13 @@ import SwiftUI
|
||||
|
||||
struct EpisodeItemView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: ItemCoordinator.Router
|
||||
|
||||
@ObservedObject
|
||||
var viewModel: EpisodeItemViewModel
|
||||
|
||||
@State
|
||||
private var scrollViewOffset: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
ScrollView {
|
||||
ContentView(viewModel: viewModel)
|
||||
.edgePadding(.bottom)
|
||||
}
|
||||
.scrollViewOffset($scrollViewOffset)
|
||||
.navigationBarOffset(
|
||||
$scrollViewOffset,
|
||||
start: 0,
|
||||
end: 10
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ extension ItemView {
|
||||
struct CinematicScrollView<Content: View>: View {
|
||||
|
||||
@Default(.Customization.CinematicItemViewType.usePrimaryImage)
|
||||
private var cinematicItemViewTypeUsePrimaryImage
|
||||
private var usePrimaryImage
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: ItemCoordinator.Router
|
||||
@ -41,7 +41,7 @@ extension ItemView {
|
||||
@ViewBuilder
|
||||
private var headerView: some View {
|
||||
ImageView(viewModel.item.imageSource(
|
||||
cinematicItemViewTypeUsePrimaryImage ? .primary : .backdrop,
|
||||
usePrimaryImage ? .primary : .backdrop,
|
||||
maxWidth: UIScreen.main.bounds.width
|
||||
))
|
||||
.aspectRatio(contentMode: .fill)
|
||||
@ -60,59 +60,33 @@ extension ItemView {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
OverlayView(viewModel: viewModel)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0), location: 0),
|
||||
.init(color: .white, location: 0.3),
|
||||
.init(color: .white, location: 1),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
Color.systemBackground
|
||||
.opacity(topOpacity)
|
||||
}
|
||||
}
|
||||
.frame(height: UIScreen.main.bounds.height * 0.8)
|
||||
|
||||
content()
|
||||
.padding(.vertical)
|
||||
.background(Color.systemBackground)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.scrollViewOffset($scrollViewOffset)
|
||||
.navigationBarOffset(
|
||||
$scrollViewOffset,
|
||||
start: UIScreen.main.bounds.height * 0.66,
|
||||
end: UIScreen.main.bounds.height * 0.66 + 50
|
||||
)
|
||||
.backgroundParallaxHeader(
|
||||
$scrollViewOffset,
|
||||
height: UIScreen.main.bounds.height * 0.8,
|
||||
multiplier: 0.3
|
||||
) {
|
||||
OffsetScrollView(headerHeight: 0.75) {
|
||||
headerView
|
||||
}
|
||||
.topBarTrailing {
|
||||
if viewModel.state == .refreshing {
|
||||
ProgressView()
|
||||
} overlay: {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
OverlayView(viewModel: viewModel)
|
||||
.edgePadding(.horizontal)
|
||||
.padding(.bottom)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0), location: 0),
|
||||
.init(color: .white, location: 0.3),
|
||||
.init(color: .white, location: 1),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} content: {
|
||||
content()
|
||||
.edgePadding(.vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,10 +106,9 @@ extension ItemView.CinematicScrollView {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
VStack(alignment: .center, spacing: 10) {
|
||||
if !cinematicItemViewTypeUsePrimaryImage {
|
||||
ImageView(viewModel.item.imageURL(.logo, maxWidth: UIScreen.main.bounds.width))
|
||||
ImageView(viewModel.item.imageURL(.logo, maxHeight: 100))
|
||||
.placeholder {
|
||||
EmptyView()
|
||||
}
|
||||
@ -147,11 +120,7 @@ extension ItemView.CinematicScrollView {
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 100)
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Spacer()
|
||||
.frame(height: 50)
|
||||
.frame(height: 100, alignment: .bottom)
|
||||
}
|
||||
|
||||
DotHStack {
|
||||
@ -183,7 +152,7 @@ extension ItemView.CinematicScrollView {
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
ItemView.OverviewView(item: viewModel.item)
|
||||
.overviewLineLimit(4)
|
||||
.overviewLineLimit(3)
|
||||
.taglineLineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
@ -26,14 +26,6 @@ extension ItemView {
|
||||
|
||||
let content: () -> Content
|
||||
|
||||
private var topOpacity: CGFloat {
|
||||
let start = UIScreen.main.bounds.height * 0.25
|
||||
let end = UIScreen.main.bounds.height * 0.42 - 50
|
||||
let diff = end - start
|
||||
let opacity = clamp((scrollViewOffset - start) / diff, min: 0, max: 1)
|
||||
return opacity
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var headerView: some View {
|
||||
ImageView(viewModel.item.imageSource(.backdrop, maxHeight: UIScreen.main.bounds.height * 0.35))
|
||||
@ -53,57 +45,42 @@ extension ItemView {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
OffsetScrollView(headerHeight: 0.5) {
|
||||
headerView
|
||||
} overlay: {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
OverlayView(viewModel: viewModel)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .clear, location: 0),
|
||||
.init(color: .black, location: 0.3),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
Color.systemBackground
|
||||
.opacity(topOpacity)
|
||||
}
|
||||
}
|
||||
.frame(height: UIScreen.main.bounds.height * 0.5)
|
||||
OverlayView(viewModel: viewModel)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .clear, location: 0),
|
||||
.init(color: .black, location: 0.3),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} content: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
ItemView.OverviewView(item: viewModel.item)
|
||||
.overviewLineLimit(4)
|
||||
.taglineLineLimit(2)
|
||||
.edgePadding()
|
||||
.padding(.horizontal)
|
||||
|
||||
RowDivider()
|
||||
|
||||
content()
|
||||
.edgePadding(.bottom)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.scrollViewOffset($scrollViewOffset)
|
||||
.navigationBarOffset(
|
||||
$scrollViewOffset,
|
||||
start: UIScreen.main.bounds.height * 0.42 - 50,
|
||||
end: UIScreen.main.bounds.height * 0.42
|
||||
)
|
||||
.backgroundParallaxHeader(
|
||||
$scrollViewOffset,
|
||||
height: UIScreen.main.bounds.height * 0.5,
|
||||
multiplier: 0.3
|
||||
) {
|
||||
headerView
|
||||
.edgePadding(.vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,64 +53,43 @@ extension ItemView {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
OffsetScrollView(headerHeight: 0.45) {
|
||||
headerView
|
||||
} overlay: {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
OverlayView(viewModel: viewModel, scrollViewOffset: $scrollViewOffset)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0), location: 0.2),
|
||||
.init(color: .white.opacity(0.5), location: 0.3),
|
||||
.init(color: .white, location: 0.55),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
Color.systemBackground
|
||||
.opacity(topOpacity)
|
||||
}
|
||||
}
|
||||
.frame(height: UIScreen.main.bounds.height * 0.45)
|
||||
OverlayView(viewModel: viewModel)
|
||||
.edgePadding(.horizontal)
|
||||
.edgePadding(.bottom)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .white.opacity(0), location: 0.2),
|
||||
.init(color: .white.opacity(0.5), location: 0.3),
|
||||
.init(color: .white, location: 0.55),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} content: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
|
||||
ItemView.OverviewView(item: viewModel.item)
|
||||
.overviewLineLimit(4)
|
||||
.taglineLineLimit(2)
|
||||
.padding(.top)
|
||||
.padding(.horizontal)
|
||||
|
||||
RowDivider()
|
||||
|
||||
content()
|
||||
.padding(.vertical)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.scrollViewOffset($scrollViewOffset)
|
||||
.navigationBarOffset(
|
||||
$scrollViewOffset,
|
||||
start: UIScreen.main.bounds.height * 0.28,
|
||||
end: UIScreen.main.bounds.height * 0.28 + 50
|
||||
)
|
||||
.backgroundParallaxHeader(
|
||||
$scrollViewOffset,
|
||||
height: UIScreen.main.bounds.height * 0.45,
|
||||
multiplier: 0.8
|
||||
) {
|
||||
headerView
|
||||
}
|
||||
.topBarTrailing {
|
||||
if viewModel.state == .refreshing {
|
||||
ProgressView()
|
||||
}
|
||||
.edgePadding(.vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,22 +105,15 @@ extension ItemView.CompactPosterScrollView {
|
||||
@ObservedObject
|
||||
var viewModel: ItemViewModel
|
||||
|
||||
@Binding
|
||||
var scrollViewOffset: CGFloat
|
||||
|
||||
@ViewBuilder
|
||||
private var rightShelfView: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
// MARK: Name
|
||||
|
||||
Text(viewModel.item.displayTitle)
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
|
||||
// MARK: Details
|
||||
|
||||
DotHStack {
|
||||
if viewModel.item.isUnaired {
|
||||
if let premiereDateLabel = viewModel.item.airDateLabel {
|
||||
@ -169,8 +141,6 @@ extension ItemView.CompactPosterScrollView {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .bottom, spacing: 12) {
|
||||
|
||||
// MARK: Portrait Image
|
||||
|
||||
ImageView(viewModel.item.imageSource(.primary, maxWidth: 130))
|
||||
.failure {
|
||||
SystemImageContentView(systemName: viewModel.item.typeSystemImage)
|
||||
@ -183,8 +153,6 @@ extension ItemView.CompactPosterScrollView {
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
// MARK: Play
|
||||
|
||||
HStack(alignment: .center) {
|
||||
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
|
@ -8,31 +8,25 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: remove rest occurrences of `UIDevice.main` sizings
|
||||
// TODO: overlay spacing between overview and play button should be dynamic
|
||||
// - smaller spacing on smaller widths (iPad Mini, portrait)
|
||||
|
||||
// landscape vs portrait ratios just "feel right". Adjust if necessary
|
||||
// or if a concrete design comes along.
|
||||
|
||||
extension ItemView {
|
||||
|
||||
struct iPadOSCinematicScrollView<Content: View>: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var router: ItemCoordinator.Router
|
||||
|
||||
@ObservedObject
|
||||
var viewModel: ItemViewModel
|
||||
|
||||
@State
|
||||
private var globalSize: CGSize = .zero
|
||||
@State
|
||||
private var scrollViewOffset: CGFloat = 0
|
||||
|
||||
let content: () -> Content
|
||||
|
||||
private var topOpacity: CGFloat {
|
||||
let start = globalSize.isLandscape ? globalSize.height * 0.45 : globalSize.height * 0.25
|
||||
let end = globalSize.isLandscape ? globalSize.height * 0.65 : globalSize.height * 0.30
|
||||
let diff = end - start
|
||||
let opacity = clamp((scrollViewOffset - start) / diff, min: 0, max: 1)
|
||||
return opacity
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var headerView: some View {
|
||||
Group {
|
||||
@ -43,56 +37,36 @@ extension ItemView {
|
||||
}
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(height: globalSize.isLandscape ? globalSize.height * 0.8 : globalSize.height * 0.4)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
OverlayView(viewModel: viewModel)
|
||||
.edgePadding()
|
||||
}
|
||||
.frame(height: globalSize.isLandscape ? globalSize.height * 0.8 : globalSize.height * 0.4)
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .clear, location: 0.4),
|
||||
.init(color: .white, location: 0.8),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
Color.systemBackground
|
||||
.opacity(topOpacity)
|
||||
}
|
||||
|
||||
content()
|
||||
.padding(.vertical)
|
||||
.background(Color.systemBackground)
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.top)
|
||||
.edgesIgnoringSafeArea(.horizontal)
|
||||
.scrollViewOffset($scrollViewOffset)
|
||||
.navigationBarOffset(
|
||||
$scrollViewOffset,
|
||||
start: globalSize.isLandscape ? globalSize.height * 0.65 : globalSize.height * 0.30,
|
||||
end: globalSize.isLandscape ? globalSize.height * 0.65 + 50 : globalSize.height * 0.30 + 50
|
||||
)
|
||||
.backgroundParallaxHeader(
|
||||
$scrollViewOffset,
|
||||
height: globalSize.isLandscape ? globalSize.height * 0.8 : globalSize.height * 0.4,
|
||||
multiplier: 0.3
|
||||
OffsetScrollView(
|
||||
headerHeight: globalSize.isLandscape ? 0.75 : 0.6
|
||||
) {
|
||||
headerView
|
||||
} overlay: {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
OverlayView(viewModel: viewModel)
|
||||
.edgePadding()
|
||||
}
|
||||
.background {
|
||||
BlurView(style: .systemThinMaterialDark)
|
||||
.mask {
|
||||
LinearGradient(
|
||||
stops: [
|
||||
.init(color: .clear, location: 0.4),
|
||||
.init(color: .white, location: 0.8),
|
||||
],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
} content: {
|
||||
content()
|
||||
.edgePadding(.vertical)
|
||||
}
|
||||
.size($globalSize)
|
||||
}
|
||||
@ -135,7 +109,7 @@ extension ItemView.iPadOSCinematicScrollView {
|
||||
|
||||
ItemView.OverviewView(item: viewModel.item)
|
||||
.overviewLineLimit(3)
|
||||
.taglineLineLimit(1)
|
||||
.taglineLineLimit(2)
|
||||
.foregroundColor(.white)
|
||||
|
||||
HStack(spacing: 30) {
|
||||
@ -162,15 +136,22 @@ extension ItemView.iPadOSCinematicScrollView {
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(height: 50)
|
||||
// TODO: remove when/if collections have a different view
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
if !(viewModel is CollectionItemViewModel) {
|
||||
VStack(spacing: 10) {
|
||||
ItemView.PlayButton(viewModel: viewModel)
|
||||
.frame(height: 50)
|
||||
|
||||
ItemView.ActionButtonHStack(viewModel: viewModel)
|
||||
.font(.title)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.frame(width: 250)
|
||||
} else {
|
||||
Color.clear
|
||||
.frame(width: 250)
|
||||
}
|
||||
.frame(width: 250)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user