Clean Up Item Scroll Views (#1015)

This commit is contained in:
Ethan Pippin 2024-04-10 07:31:10 -06:00 committed by GitHub
parent d65c4e8454
commit 2387197021
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 424 additions and 426 deletions

View File

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

View File

@ -19,7 +19,7 @@ struct TruncatedText: View {
case view
}
@Environment(\.accentColor)
@Default(.accentColor)
private var accentColor
@State

View File

@ -36,6 +36,10 @@ extension EdgeInsets {
}
static let zero: EdgeInsets = .init()
var vertical: CGFloat {
top + bottom
}
}
extension NSDirectionalEdgeInsets {

View File

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

View File

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

View File

@ -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?

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -13,7 +13,7 @@ extension NavigationBarFilterDrawer {
struct FilterDrawerButton: View {
@Environment(\.accentColor)
@Default(.accentColor)
private var accentColor
private let systemName: String?

View File

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

View File

@ -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: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -10,6 +10,8 @@ import Defaults
import Factory
import SwiftUI
// TODO: fix play from beginning
extension ItemView {
struct PlayButton: View {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -39,6 +39,8 @@ extension EpisodeItemView {
.overviewLineLimit(4)
.padding(.horizontal)
RowDivider()
// MARK: Genres
if let genres = viewModel.item.itemGenres, genres.isNotEmpty {

View File

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

View File

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

View File

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

View File

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

View File

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