From 25b30b543674632203043dad810362afb5f3f807 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 26 May 2024 15:07:13 -0600 Subject: [PATCH] Create Library Alpha Picker (#980) --- .../Components/LetterPickerOrientation.swift | 25 ++++++++ Shared/Services/SwiftfinDefaults.swift | 4 ++ Shared/Strings/Strings.swift | 8 +++ Swiftfin.xcodeproj/project.pbxproj | 34 +++++++++++ .../Components/LetterPickerButton.swift | 55 ++++++++++++++++++ .../Components/LetterPickerOverflow.swift | 50 ++++++++++++++++ .../LetterPickerBar/LetterPickerBar.swift | 37 ++++++++++++ .../PagingLibraryView/PagingLibraryView.swift | 44 +++++++++++++- .../SettingsView/CustomizeViewsSettings.swift | 13 +++++ Translations/en.lproj/Localizable.strings | Bin 19662 -> 19866 bytes 10 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 Shared/Components/LetterPickerOrientation.swift create mode 100644 Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift create mode 100644 Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift create mode 100644 Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift diff --git a/Shared/Components/LetterPickerOrientation.swift b/Shared/Components/LetterPickerOrientation.swift new file mode 100644 index 000000000..60cbdc9c7 --- /dev/null +++ b/Shared/Components/LetterPickerOrientation.swift @@ -0,0 +1,25 @@ +// +// 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 Defaults +import SwiftUI + +enum LetterPickerOrientation: String, CaseIterable, Defaults.Serializable, Displayable { + + case leading + case trailing + + var displayTitle: String { + switch self { + case .leading: + return L10n.left + case .trailing: + return L10n.right + } + } +} diff --git a/Shared/Services/SwiftfinDefaults.swift b/Shared/Services/SwiftfinDefaults.swift index 9bab59dbe..e64317782 100644 --- a/Shared/Services/SwiftfinDefaults.swift +++ b/Shared/Services/SwiftfinDefaults.swift @@ -145,6 +145,10 @@ extension Defaults.Keys { "libraryEnabledDrawerFilters", default: ItemFilterType.allCases ) + static let letterPickerEnabled: Key = UserKey("letterPickerEnabled", default: false) + static let letterPickerOrientation: Key = .init( + "letterPickerOrientation", default: .trailing + ) static let displayType: Key = UserKey("libraryViewType", default: .grid) static let posterType: Key = UserKey("libraryPosterType", default: .portrait) static let listColumnCount: Key = UserKey("listColumnCount", default: 1) diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index a34116813..b44745735 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -220,6 +220,10 @@ internal enum L10n { internal static func latestWithString(_ p1: Any) -> String { return L10n.tr("Localizable", "latestWithString", String(describing: p1), fallback: "Latest %@") } + /// Left + internal static let `left` = L10n.tr("Localizable", "left", fallback: "Left") + /// Letter Picker + internal static let letterPicker = L10n.tr("Localizable", "letterPicker", fallback: "Letter Picker") /// Library internal static let library = L10n.tr("Localizable", "library", fallback: "Library") /// Light @@ -310,6 +314,8 @@ internal enum L10n { internal static let orange = L10n.tr("Localizable", "orange", fallback: "Orange") /// Order internal static let order = L10n.tr("Localizable", "order", fallback: "Order") + /// Orientation + internal static let orientation = L10n.tr("Localizable", "orientation", fallback: "Orientation") /// Other internal static let other = L10n.tr("Localizable", "other", fallback: "Other") /// Other User @@ -442,6 +448,8 @@ internal enum L10n { internal static let retrievingMediaInformation = L10n.tr("Localizable", "retrievingMediaInformation", fallback: "Retrieving media information") /// Retry internal static let retry = L10n.tr("Localizable", "retry", fallback: "Retry") + /// Right + internal static let `right` = L10n.tr("Localizable", "right", fallback: "Right") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Scrub Current Time diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 4a8cda5f5..ae09875ea 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -9,9 +9,14 @@ /* Begin PBXBuildFile section */ 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091B5A872683142E00D78B61 /* ServerDiscovery.swift */; }; + 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */; }; + 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; }; + 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; }; + 4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; }; 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 */; }; @@ -917,8 +922,12 @@ /* Begin PBXFileReference section */ 091B5A872683142E00D78B61 /* ServerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDiscovery.swift; sourceTree = ""; }; + 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = ""; }; + 4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = ""; }; + 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 = ""; }; @@ -1634,6 +1643,24 @@ path = ServerDiscovery; sourceTree = ""; }; + 4E16FD4E2C0183B500110147 /* LetterPickerBar */ = { + isa = PBXGroup; + children = ( + 4E16FD4F2C0183C500110147 /* Components */, + 4E16FD522C01840C00110147 /* LetterPickerBar.swift */, + ); + path = LetterPickerBar; + sourceTree = ""; + }; + 4E16FD4F2C0183C500110147 /* Components */ = { + isa = PBXGroup; + children = ( + 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */, + 4EF7A3E12C031FEB00CC58A2 /* LetterPickerOverflow.swift */, + ); + path = Components; + sourceTree = ""; + }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { isa = PBXGroup; children = ( @@ -2045,6 +2072,7 @@ E1921B7528E63306003A5238 /* GestureView.swift */, E178B0752BE435D70023651B /* HourMinutePicker.swift */, E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */, + 4E16FD4E2C0183B500110147 /* LetterPickerBar */, E1AEFA362BE317E200CFAFD8 /* ListRowButton.swift */, E1FE69AF28C2DA4A0021BC93 /* NavigationBarFilterDrawer */, E1DE84132B9531C1008CCE21 /* OrderedSectionSelectorView.swift */, @@ -3169,6 +3197,7 @@ E145EB212BDCCA43003BF6F3 /* BulletedList.swift */, E1153DCB2BBB633B00424D36 /* FastSVGView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, + 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */, E1D37F472B9C648E00343D2B /* MaxHeightText.swift */, E1DC983F296DEBA500982F06 /* PosterIndicators */, E1FE69A628C29B720021BC93 /* ProgressBar.swift */, @@ -4114,6 +4143,7 @@ C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */, E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, E113133B28BEB71D00930F75 /* FilterViewModel.swift in Sources */, + 4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */, E1575E70293E77B5001665B1 /* TextPair.swift in Sources */, E18E021C2887492B0022598C /* BlurView.swift in Sources */, E187F7682B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, @@ -4223,6 +4253,7 @@ 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 */, @@ -4345,6 +4376,7 @@ 6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */, E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */, + 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */, E1BE1CEA2BDB5AFE008176A9 /* UserGridButton.swift in Sources */, E1401CB129386C9200E8B599 /* UIColor.swift in Sources */, E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */, @@ -4479,6 +4511,7 @@ E18E01E9288747230022598C /* SeriesItemView.swift in Sources */, E15756342936851D00976E1F /* NativeVideoPlayerSettingsView.swift in Sources */, E1D4BF7C2719D05000A11E64 /* AppSettingsView.swift in Sources */, + 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */, E19D41AE2BF288320082B8B2 /* ServerCheckViewModel.swift in Sources */, E1BDF2F329524C3B00CC0294 /* ChaptersActionButton.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, @@ -4571,6 +4604,7 @@ E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */, E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */, + 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */, E1356E0329A730B200382563 /* SeparatorHStack.swift in Sources */, 5377CBF5263B596A003A4E83 /* SwiftfinApp.swift in Sources */, E13DD4022717EE79009D4DAF /* SelectUserCoordinator.swift in Sources */, diff --git a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift new file mode 100644 index 000000000..5114ce4a7 --- /dev/null +++ b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift @@ -0,0 +1,55 @@ +// +// 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 Defaults +import SwiftUI + +extension LetterPickerBar { + + struct LetterPickerButton: View { + + @Default(.accentColor) + private var accentColor + + @Environment(\.isSelected) + private var isSelected + + private let filterLetter: ItemLetter + private let viewModel: FilterViewModel + + init(filterLetter: ItemLetter, viewModel: FilterViewModel) { + self.filterLetter = filterLetter + self.viewModel = viewModel + } + + var body: some View { + Button { + if !viewModel.currentFilters.letter.contains(filterLetter) { + viewModel.currentFilters.letter = [ItemLetter(stringLiteral: filterLetter.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 { + RoundedRectangle(cornerRadius: 5) + .frame(width: 20, height: 20) + .foregroundStyle(isSelected ? accentColor.opacity(0.5) : Color.clear) + } + } + } + } +} diff --git a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift new file mode 100644 index 000000000..844687c7f --- /dev/null +++ b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerOverflow.swift @@ -0,0 +1,50 @@ +// +// 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 new file mode 100644 index 000000000..e8390f688 --- /dev/null +++ b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift @@ -0,0 +1,37 @@ +// +// 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 Defaults +import SwiftUI + +struct LetterPickerBar: View { + @ObservedObject + private var viewModel: FilterViewModel + + init(viewModel: FilterViewModel) { + self.viewModel = viewModel + } + + @ViewBuilder + var body: some View { + VStack(spacing: 0) { + Spacer() + ForEach(ItemLetter.allCases, id: \.hashValue) { filterLetter in + LetterPickerButton( + filterLetter: filterLetter, + viewModel: viewModel + ) + .environment(\.isSelected, viewModel.currentFilters.letter.contains(filterLetter)) + .frame(maxWidth: .infinity) + } + Spacer() + } + .scrollOnOverflow() + .frame(width: 30, alignment: .center) + } +} diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index 4774d8d60..eb915bb26 100644 --- a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift @@ -50,11 +50,18 @@ struct PagingLibraryView: View { @Default private var defaultPosterType: PosterDisplayType + @Default(.Customization.Library.letterPickerEnabled) + private var letterPickerEnabled + @Default(.Customization.Library.letterPickerOrientation) + private var letterPickerOrientation + @EnvironmentObject private var router: LibraryCoordinator.Router @State private var layout: CollectionVGridLayout + @State + private var safeArea: EdgeInsets = .zero @StoredValue private var displayType: LibraryDisplayType @@ -251,6 +258,34 @@ struct PagingLibraryView: View { .proxy(collectionVGridProxy) } + @ViewBuilder + private func contentLetterBarView(content: some View) -> some View { + if letterPickerEnabled, let filterViewModel = viewModel.filterViewModel { + switch letterPickerOrientation { + case .trailing: + HStack(spacing: 0) { + content + .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) + } + } + } else { + content + } + } + // MARK: body // TODO: becoming too large for typechecker during development, should break up somehow @@ -260,18 +295,21 @@ struct PagingLibraryView: View { switch viewModel.state { case .content: if viewModel.elements.isEmpty { - L10n.noResults.text + contentLetterBarView(content: L10n.noResults.text) } else { - contentView + contentLetterBarView(content: contentView) } case let .error(error): errorView(with: error) case .initial, .refreshing: - DelayedProgressView() + contentLetterBarView(content: DelayedProgressView()) } } .animation(.linear(duration: 0.1), value: viewModel.state) .ignoresSafeArea() + .onSizeChanged { _, safeArea in + self.safeArea = safeArea + } .navigationTitle(viewModel.parent?.displayTitle ?? "") .navigationBarTitleDisplayMode(.inline) .ifLet(viewModel.filterViewModel) { view, filterViewModel in diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift index ea40feba7..903f3d881 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift @@ -23,6 +23,10 @@ struct CustomizeViewsSettings: View { @Default(.Customization.shouldShowMissingEpisodes) private var shouldShowMissingEpisodes + @Default(.Customization.Library.letterPickerEnabled) + var letterPickerEnabled + @Default(.Customization.Library.letterPickerOrientation) + var letterPickerOrientation @Default(.Customization.Library.enabledDrawerFilters) private var libraryEnabledDrawerFilters @Default(.Customization.Search.enabledDrawerFilters) @@ -91,6 +95,15 @@ struct CustomizeViewsSettings: View { Section { + Toggle(L10n.letterPicker, isOn: $letterPickerEnabled) + + if letterPickerEnabled { + CaseIterablePicker( + L10n.orientation, + selection: $letterPickerOrientation + ) + } + ChevronButton(L10n.library) .onSelect { router.route(to: \.itemFilterDrawerSelector, $libraryEnabledDrawerFilters) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 09a1403ac96e2eb09611e7dc83060304df420219..243dab8a87fe6af2201aa127c156324ae8ea6170 100644 GIT binary patch delta 199 zcmX>%lX2E;#tr9u>bV${7>XD&8PXXt7)pR_1qNFnR$>T360>FismftUWk^F&;{z8# zs3-xdNClc00Mwt%kPT)dOa