diff --git a/Shared/Components/LetterPickerOrientation.swift b/Shared/Components/LetterPickerOrientation.swift index 60cbdc9c..c5e55ecb 100644 --- a/Shared/Components/LetterPickerOrientation.swift +++ b/Shared/Components/LetterPickerOrientation.swift @@ -22,4 +22,22 @@ enum LetterPickerOrientation: String, CaseIterable, Defaults.Serializable, Displ return L10n.right } } + + var alignment: Alignment { + switch self { + case .leading: + .leading + case .trailing: + .trailing + } + } + + var edge: Edge.Set { + switch self { + case .leading: + .leading + case .trailing: + .trailing + } + } } diff --git a/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift b/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift new file mode 100644 index 00000000..875c5f95 --- /dev/null +++ b/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift @@ -0,0 +1,31 @@ +// +// 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 Foundation +import SwiftUI + +extension Backport { + + enum ScrollIndicatorVisibility { + + case automatic + case visible + case hidden + case never + + @available(iOS 16, tvOS 16, *) + var supportedValue: SwiftUI.ScrollIndicatorVisibility { + switch self { + case .automatic: .automatic + case .visible: .visible + case .hidden: .hidden + case .never: .never + } + } + } +} diff --git a/Shared/Extensions/ViewExtensions/Backport.swift b/Shared/Extensions/ViewExtensions/Backport/Backport.swift similarity index 84% rename from Shared/Extensions/ViewExtensions/Backport.swift rename to Shared/Extensions/ViewExtensions/Backport/Backport.swift index 7bbd9929..cecb99d7 100644 --- a/Shared/Extensions/ViewExtensions/Backport.swift +++ b/Shared/Extensions/ViewExtensions/Backport/Backport.swift @@ -51,6 +51,18 @@ extension Backport where Content: View { } } + @ViewBuilder + func scrollIndicators(_ visibility: Backport.ScrollIndicatorVisibility) -> some View { + if #available(iOS 16, tvOS 16, *) { + content.scrollIndicators(visibility.supportedValue) + } else { + content.introspect(.scrollView, on: .iOS(.v15), .tvOS(.v15)) { scrollView in + scrollView.showsHorizontalScrollIndicator = visibility == .visible + scrollView.showsVerticalScrollIndicator = visibility == .visible + } + } + } + #if os(iOS) // TODO: - remove comment when migrated away from Stinsen diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift new file mode 100644 index 00000000..5a524d70 --- /dev/null +++ b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift @@ -0,0 +1,38 @@ +// +// 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: both axes + +struct ScrollIfLargerThanContainerModifier: ViewModifier { + + @State + private var contentSize: CGSize = .zero + @State + private var layoutSize: CGSize = .zero + + let padding: CGFloat + + func body(content: Content) -> some View { + AlternateLayoutView { + Color.clear + .trackingSize($layoutSize) + } content: { + ScrollView { + content + .trackingSize($contentSize) + } + .frame(maxHeight: contentSize.height >= layoutSize.height ? .infinity : contentSize.height) + .backport + .scrollDisabled(contentSize.height < layoutSize.height) + .backport + .scrollIndicators(.never) + } + } +} diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanModifier.swift deleted file mode 100644 index 2ea99797..00000000 --- a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanModifier.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct ScrollIfLargerThanModifier: ViewModifier { - - @State - private var contentSize: CGSize = .zero - - let height: CGFloat - - func body(content: Content) -> some View { - ScrollView { - content - .trackingSize($contentSize) - } - .backport - .scrollDisabled(contentSize.height < height) - .frame(maxHeight: contentSize.height >= height ? .infinity : contentSize.height) - } -} diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift index 643d2c5f..6339353d 100644 --- a/Shared/Extensions/ViewExtensions/ViewExtensions.swift +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -328,13 +328,13 @@ extension View { ) } - func scroll(ifLargerThan height: CGFloat) -> some View { - modifier(ScrollIfLargerThanModifier(height: height)) + func scrollIfLargerThanContainer(padding: CGFloat = 0) -> some View { + modifier(ScrollIfLargerThanContainerModifier(padding: padding)) } // MARK: debug - // Useful modifiers during development for layout + // Useful modifiers during development for layout without RocketSim #if DEBUG func debugBackground(_ fill: S = .red.opacity(0.5)) -> some View { diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index 27f55765..976fd6ff 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -190,7 +190,7 @@ struct SelectUserView: View { gridContentView } - .scroll(ifLargerThan: contentSize.height - 100) + .scrollIfLargerThanContainer(padding: 100) .scrollViewOffset($scrollViewOffset) } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index ae09875e..cb857f1d 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; - 4EF7A3E22C031FEB00CC58A2 /* LetterPickerOverflow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF7A3E12C031FEB00CC58A2 /* LetterPickerOverflow.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; @@ -378,7 +377,7 @@ E145EB422BE0A6EE003BF6F3 /* ServerSelectionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB412BE0A6EE003BF6F3 /* ServerSelectionMenu.swift */; }; E145EB452BE0AD4E003BF6F3 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB442BE0AD4E003BF6F3 /* Set.swift */; }; E145EB462BE0AD4E003BF6F3 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB442BE0AD4E003BF6F3 /* Set.swift */; }; - E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanModifier.swift */; }; + E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift */; }; E145EB4B2BE16849003BF6F3 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E145EB4A2BE16849003BF6F3 /* KeychainSwift */; }; E145EB4D2BE1688E003BF6F3 /* SwiftinStore+UserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB4C2BE1688E003BF6F3 /* SwiftinStore+UserState.swift */; }; E145EB4F2BE168AC003BF6F3 /* SwiftfinStore+ServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB4E2BE168AC003BF6F3 /* SwiftfinStore+ServerState.swift */; }; @@ -545,7 +544,7 @@ E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */; }; E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */; }; E17639F82BF2E25B004DF6AB /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19D41A92BF077130082B8B2 /* Keychain.swift */; }; - E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanModifier.swift */; }; + E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanContainerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift */; }; E1763A272BF303C9004DF6AB /* ServerSelectionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */; }; E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; }; E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; }; @@ -806,6 +805,8 @@ E1D842912933F87500D1041A /* ItemFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D842902933F87500D1041A /* ItemFields.swift */; }; E1D8429329340B8300D1041A /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8429229340B8300D1041A /* Utilities.swift */; }; E1D8429529346C6400D1041A /* BasicStepper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D8429429346C6400D1041A /* BasicStepper.swift */; }; + E1D90D762C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D90D752C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift */; }; + E1D90D772C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D90D752C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift */; }; E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D9F474296E86D400129AF3 /* NativeVideoPlayer.swift */; }; E1DA654C28E69B0500592A73 /* SpecialFeatureType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */; }; E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DA656E28E78C9900592A73 /* EpisodeSelector.swift */; }; @@ -927,7 +928,6 @@ 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerOrientation.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = ""; }; - 4EF7A3E12C031FEB00CC58A2 /* LetterPickerOverflow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerOverflow.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; @@ -1198,7 +1198,7 @@ E145EB242BE055AD003BF6F3 /* ServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerResponse.swift; sourceTree = ""; }; E145EB412BE0A6EE003BF6F3 /* ServerSelectionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionMenu.swift; sourceTree = ""; }; E145EB442BE0AD4E003BF6F3 /* Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Set.swift; sourceTree = ""; }; - E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollIfLargerThanModifier.swift; sourceTree = ""; }; + E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollIfLargerThanContainerModifier.swift; sourceTree = ""; }; E145EB4C2BE1688E003BF6F3 /* SwiftinStore+UserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftinStore+UserState.swift"; sourceTree = ""; }; E145EB4E2BE168AC003BF6F3 /* SwiftfinStore+ServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+ServerState.swift"; sourceTree = ""; }; E146A9D72BE6E9830034DA1E /* StoredValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredValue.swift; sourceTree = ""; }; @@ -1458,6 +1458,7 @@ E1D842902933F87500D1041A /* ItemFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemFields.swift; sourceTree = ""; }; E1D8429229340B8300D1041A /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; E1D8429429346C6400D1041A /* BasicStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicStepper.swift; sourceTree = ""; }; + E1D90D752C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BackPort+ScrollIndicatorVisibility.swift"; sourceTree = ""; }; E1D9F474296E86D400129AF3 /* NativeVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeVideoPlayer.swift; sourceTree = ""; }; E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialFeatureType.swift; sourceTree = ""; }; E1DA656E28E78C9900592A73 /* EpisodeSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeSelector.swift; sourceTree = ""; }; @@ -1656,7 +1657,6 @@ isa = PBXGroup; children = ( 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */, - 4EF7A3E12C031FEB00CC58A2 /* LetterPickerOverflow.swift */, ); path = Components; sourceTree = ""; @@ -2499,7 +2499,7 @@ isa = PBXGroup; children = ( E170D101294CE4C10017224C /* Modifiers */, - E15D4F062B1B12C300442DB8 /* Backport.swift */, + E1D90D742C051D3B000EA787 /* Backport */, E1E1E24C28DF8A2E000DF5FD /* PreferenceKeys.swift */, 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, ); @@ -2839,7 +2839,7 @@ E129428428F080B500796AC6 /* OnReceiveNotificationModifier.swift */, E43918652AD5C8310045A18C /* OnScenePhaseChangedModifier.swift */, E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */, - E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanModifier.swift */, + E145EB472BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift */, E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */, E1E2F8442B757E3400B75998 /* SinceLastDisappearModifier.swift */, ); @@ -3367,6 +3367,15 @@ path = Slider; sourceTree = ""; }; + E1D90D742C051D3B000EA787 /* Backport */ = { + isa = PBXGroup; + children = ( + E15D4F062B1B12C300442DB8 /* Backport.swift */, + E1D90D752C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift */, + ); + path = Backport; + sourceTree = ""; + }; E1DABAD62A26E28E008AC34A /* Resources */ = { isa = PBXGroup; children = ( @@ -4102,7 +4111,7 @@ E1575E5D293E77B5001665B1 /* ItemViewType.swift in Sources */, E12CC1AF28D0FAEA00678D5D /* NextUpLibraryViewModel.swift in Sources */, E1575E7A293E77B5001665B1 /* TimeStampType.swift in Sources */, - E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanModifier.swift in Sources */, + E1763A252BF2F77B004DF6AB /* ScrollIfLargerThanContainerModifier.swift in Sources */, E11E374E293E7F08009EF240 /* MediaSourceInfo.swift in Sources */, E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */, E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */, @@ -4152,6 +4161,7 @@ E10B1ECB2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, E154966B296CA2EF00C4EF88 /* DownloadManager.swift in Sources */, 535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */, + E1D90D772C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */, E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */, E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */, E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, @@ -4253,7 +4263,6 @@ E18CE0AF28A222240092E7F1 /* PublicUserRow.swift in Sources */, E129429828F4785200796AC6 /* CaseIterablePicker.swift in Sources */, E18E01E5288747230022598C /* CinematicScrollView.swift in Sources */, - 4EF7A3E22C031FEB00CC58A2 /* LetterPickerOverflow.swift in Sources */, E154965E296CA2EF00C4EF88 /* DownloadTask.swift in Sources */, 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */, E1E2F8422B757E0900B75998 /* OnFirstAppearModifier.swift in Sources */, @@ -4343,6 +4352,7 @@ E1B5784128F8AFCB00D42911 /* WrappedView.swift in Sources */, E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */, E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */, + E1D90D762C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */, 6264E88C273850380081A12A /* Strings.swift in Sources */, E145EB252BE055AD003BF6F3 /* ServerResponse.swift in Sources */, E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */, @@ -4392,7 +4402,7 @@ E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */, E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */, E1937A61288F32DB00CB80AA /* Poster.swift in Sources */, - E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanModifier.swift in Sources */, + E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */, E1CAF65F2BA345830087D991 /* MediaViewModel.swift in Sources */, E1EA9F6A28F8A79E00BEC442 /* VideoPlayerManager.swift in Sources */, E133328D2953AE4B00EE76AB /* CircularProgressView.swift in Sources */, diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0ae77f3d..d5600c5e 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -34,7 +34,7 @@ "location" : "https://github.com/LePips/CollectionVGrid", "state" : { "branch" : "main", - "revision" : "b50b5241df5fc1d71e5a09f6a87731c67c2a79e5" + "revision" : "91ba930a502761924204ae74a59ded05f3b7ef89" } }, { diff --git a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift index 5114ce4a..10d27ef1 100644 --- a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift +++ b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift @@ -19,36 +19,31 @@ extension LetterPickerBar { @Environment(\.isSelected) private var isSelected - private let filterLetter: ItemLetter + private let letter: ItemLetter private let viewModel: FilterViewModel - init(filterLetter: ItemLetter, viewModel: FilterViewModel) { - self.filterLetter = filterLetter + init(letter: ItemLetter, viewModel: FilterViewModel) { + self.letter = letter self.viewModel = viewModel } var body: some View { Button { - if !viewModel.currentFilters.letter.contains(filterLetter) { - viewModel.currentFilters.letter = [ItemLetter(stringLiteral: filterLetter.value)] + if !viewModel.currentFilters.letter.contains(letter) { + viewModel.currentFilters.letter = [ItemLetter(stringLiteral: letter.value)] } else { viewModel.currentFilters.letter = [] } } label: { - Text( - filterLetter.value - ) - .environment(\.isSelected, viewModel.currentFilters.letter.contains(filterLetter)) - .font(.headline) - .frame(width: 15, height: 15) - .foregroundStyle(isSelected ? accentColor.overlayColor : accentColor) - .padding(.vertical, 2) - .fixedSize(horizontal: false, vertical: true) - .background { + ZStack { RoundedRectangle(cornerRadius: 5) - .frame(width: 20, height: 20) - .foregroundStyle(isSelected ? accentColor.opacity(0.5) : Color.clear) + .foregroundStyle(isSelected ? accentColor : Color.clear) + + Text(letter.value) + .font(.headline) + .foregroundStyle(isSelected ? accentColor.overlayColor : accentColor) } + .frame(width: 20, height: 20) } } } diff --git a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift deleted file mode 100644 index 844687c7..00000000 --- a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Foundation -import SwiftUI - -struct LetterPickerOverflow: ViewModifier { - @State - private var contentOverflow: Bool = false - - func body(content: Content) -> some View { - GeometryReader { geometry in - content - .background( - GeometryReader { contentGeometry in - Color.clear.onAppear { - contentOverflow = contentGeometry.size.height > geometry.size.height - } - } - ) - .wrappedInScrollView(when: contentOverflow) - } - } -} - -extension View { - @ViewBuilder - func wrappedInScrollView(when condition: Bool) -> some View { - if condition { - ScrollView(showsIndicators: false) { - self - } - .frame(maxWidth: .infinity, alignment: .center) - } else { - self - .frame(width: 30, alignment: .center) - } - } -} - -extension View { - func scrollOnOverflow() -> some View { - modifier(LetterPickerOverflow()) - } -} diff --git a/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift index e8390f68..6eacb2bf 100644 --- a/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift +++ b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift @@ -10,28 +10,26 @@ import Defaults import SwiftUI struct LetterPickerBar: View { - @ObservedObject - private var viewModel: FilterViewModel - init(viewModel: FilterViewModel) { - self.viewModel = viewModel - } + @ObservedObject + var viewModel: FilterViewModel @ViewBuilder var body: some View { VStack(spacing: 0) { Spacer() + ForEach(ItemLetter.allCases, id: \.hashValue) { filterLetter in LetterPickerButton( - filterLetter: filterLetter, + letter: filterLetter, viewModel: viewModel ) .environment(\.isSelected, viewModel.currentFilters.letter.contains(filterLetter)) - .frame(maxWidth: .infinity) } + Spacer() } - .scrollOnOverflow() + .scrollIfLargerThanContainer() .frame(width: 30, alignment: .center) } } diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index eb915bb2..9d68c042 100644 --- a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift @@ -180,6 +180,7 @@ struct PagingLibraryView: View { // Note: if parent is a folders then other items will have labels, // so an empty content view is necessary + @ViewBuilder private func landscapeGridItemView(item: Element) -> some View { PosterButton(item: item, type: .landscape) .content { @@ -199,6 +200,7 @@ struct PagingLibraryView: View { } } + @ViewBuilder private func portraitGridItemView(item: Element) -> some View { PosterButton(item: item, type: .portrait) .content { @@ -218,6 +220,7 @@ struct PagingLibraryView: View { } } + @ViewBuilder private func listItemView(item: Element, posterType: PosterDisplayType) -> some View { LibraryRow(item: item, posterType: posterType) .onSelect { @@ -233,7 +236,8 @@ struct PagingLibraryView: View { } } - private var contentView: some View { + @ViewBuilder + private var gridView: some View { CollectionVGrid( $viewModel.elements, layout: $layout @@ -256,33 +260,40 @@ struct PagingLibraryView: View { viewModel.send(.getNextPage) } .proxy(collectionVGridProxy) + .scrollIndicatorsVisible(false) } @ViewBuilder - private func contentLetterBarView(content: some View) -> some View { + private var innerContent: some View { + switch viewModel.state { + case .content: + if viewModel.elements.isEmpty { + L10n.noResults.text + } else { + gridView + } + case .initial, .refreshing: + DelayedProgressView() + default: + AssertionFailureView("Expected view for unexpected state") + } + } + + @ViewBuilder + private var contentView: some View { if letterPickerEnabled, let filterViewModel = viewModel.filterViewModel { - switch letterPickerOrientation { - case .trailing: - HStack(spacing: 0) { - content - .frame(maxWidth: .infinity) + ZStack(alignment: letterPickerOrientation.alignment) { + innerContent + .padding(letterPickerOrientation.edge, 35) + .frame(maxWidth: .infinity) - LetterPickerBar(viewModel: filterViewModel) - .padding(.top, safeArea.top) - .padding(.bottom, safeArea.bottom) - } - case .leading: - HStack(spacing: 0) { - LetterPickerBar(viewModel: filterViewModel) - .padding(.top, safeArea.top) - .padding(.bottom, safeArea.bottom) - - content - .frame(maxWidth: .infinity) - } + LetterPickerBar(viewModel: filterViewModel) + .padding(.top, safeArea.top) + .padding(.bottom, safeArea.bottom) + .padding(letterPickerOrientation.edge, 10) } } else { - content + innerContent } } @@ -292,17 +303,13 @@ struct PagingLibraryView: View { var body: some View { ZStack { + Color.clear + switch viewModel.state { - case .content: - if viewModel.elements.isEmpty { - contentLetterBarView(content: L10n.noResults.text) - } else { - contentLetterBarView(content: contentView) - } + case .content, .initial, .refreshing: + contentView case let .error(error): errorView(with: error) - case .initial, .refreshing: - contentLetterBarView(content: DelayedProgressView()) } } .animation(.linear(duration: 0.1), value: viewModel.state) diff --git a/Swiftfin/Views/SelectUserView/SelectUserView.swift b/Swiftfin/Views/SelectUserView/SelectUserView.swift index 57bf6293..9fa4ea8f 100644 --- a/Swiftfin/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin/Views/SelectUserView/SelectUserView.swift @@ -20,8 +20,6 @@ import SwiftUI // TODO: user ordering // - name // - last signed in date -// TODO: for random splash screen, instead have a random sorted array -// for failure cases struct SelectUserView: View { @@ -45,8 +43,6 @@ struct SelectUserView: View { @EnvironmentObject private var router: SelectUserCoordinator.Router - @State - private var contentSafeAreaInsets: EdgeInsets = .zero @State private var contentSize: CGSize = .zero @State @@ -70,7 +66,7 @@ struct SelectUserView: View { @State private var selectedUsers: Set = [] @State - private var splashScreenImageSource: ImageSource? = nil + private var splashScreenImageSources: [ImageSource] = [] @StateObject private var viewModel = SelectUserViewModel() @@ -115,25 +111,27 @@ struct SelectUserView: View { } // For all server selection, .all is random - private func makeSplashScreenImageSource( + private func makeSplashScreenImageSources( serverSelection: SelectUserServerSelection, allServersSelection: SelectUserServerSelection - ) -> ImageSource? { + ) -> [ImageSource] { switch (serverSelection, allServersSelection) { case (.all, .all): return viewModel .servers .keys - .randomElement()? - .splashScreenImageSource() + .shuffled() + .map { $0.splashScreenImageSource() } // need to evaluate server with id selection first case let (.server(id), _), let (.all, .server(id)): - return viewModel - .servers - .keys - .first { $0.id == id }? - .splashScreenImageSource() + return [ + viewModel + .servers + .keys + .first { $0.id == id }? + .splashScreenImageSource() ?? .init(), + ] } } @@ -252,7 +250,7 @@ struct SelectUserView: View { } } .edgePadding() - .scroll(ifLargerThan: contentSize.height - 100) + .scrollIfLargerThanContainer(padding: 100) .onChange(of: gridItemSize) { newValue in let columns = Int(contentSize.width / (newValue.width + EdgeInsets.edgePadding)) @@ -274,7 +272,7 @@ struct SelectUserView: View { } } .edgePadding() - .scroll(ifLargerThan: contentSize.height - 100) + .scrollIfLargerThanContainer(padding: 100) } @ViewBuilder @@ -386,9 +384,8 @@ struct SelectUserView: View { VStack(spacing: 0) { ZStack { Color.clear - .onSizeChanged { size, safeAreaInsets in + .onSizeChanged { size, _ in contentSize = size - contentSafeAreaInsets = safeAreaInsets } switch userListDisplayType { @@ -433,16 +430,16 @@ struct SelectUserView: View { } } .background { - if selectUserUseSplashscreen, let splashScreenImageSource { + if selectUserUseSplashscreen, splashScreenImageSources.isNotEmpty { ZStack { Color.clear - ImageView(splashScreenImageSource) + ImageView(splashScreenImageSources) .pipeline(.Swiftfin.branding) .aspectRatio(contentMode: .fill) - .id(splashScreenImageSource) + .id(splashScreenImageSources) .transition(.opacity) - .animation(.linear, value: splashScreenImageSource) + .animation(.linear, value: splashScreenImageSources) Color.black .opacity(0.9) @@ -515,7 +512,7 @@ struct SelectUserView: View { .onAppear { viewModel.send(.getServers) - splashScreenImageSource = makeSplashScreenImageSource( + splashScreenImageSources = makeSplashScreenImageSources( serverSelection: serverSelection, allServersSelection: selectUserAllServersSplashscreen ) @@ -537,7 +534,7 @@ struct SelectUserView: View { } } .onChange(of: selectUserAllServersSplashscreen) { newValue in - splashScreenImageSource = makeSplashScreenImageSource( + splashScreenImageSources = makeSplashScreenImageSources( serverSelection: serverSelection, allServersSelection: newValue ) @@ -545,7 +542,7 @@ struct SelectUserView: View { .onChange(of: serverSelection) { newValue in gridItems = makeGridItems(for: newValue) - splashScreenImageSource = makeSplashScreenImageSource( + splashScreenImageSources = makeSplashScreenImageSources( serverSelection: newValue, allServersSelection: selectUserAllServersSplashscreen )