From f92edb83fbef05f33cad113e55a089568199dc8c Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 1 Sep 2022 23:29:52 -0600 Subject: [PATCH] iOS/iPadOS - Refactor Filter Selection (#548) --- .swiftformat | 1 + Shared/Coordinators/FilterCoordinator.swift | 32 ++-- Shared/Coordinators/HomeCoordinator.swift | 34 ++-- Shared/Coordinators/ItemCoordinator.swift | 4 +- .../ItemOverviewCoordinator.swift | 4 +- Shared/Coordinators/LibraryCoordinator.swift | 66 +++++--- Shared/Coordinators/MediaCoordinator.swift | 30 ++-- .../MoviesLibrariesCoordinator.swift | 4 +- Shared/Coordinators/SearchCoordinator.swift | 26 +-- Shared/Coordinators/SettingsCoordinator.swift | 28 ++-- .../Coordinators/TVLibrariesCoordinator.swift | 4 +- .../Coordinators/UserSignInCoordinator.swift | 10 +- Shared/Extensions/ColorExtension.swift | 16 +- Shared/Extensions/FontExtensions.swift | 4 +- .../APISortOrderExtensions.swift | 26 +++ .../BaseItemDtoExtensions.swift | 4 +- .../BaseItemPerson+Poster.swift | 4 - .../BaseItemPersonExtensions.swift | 8 + .../ItemFilterExtensions.swift | 36 ++++ .../NameGUIDPairExtensions.swift | 14 +- Shared/Extensions/StringExtensions.swift | 6 + Shared/Extensions/UIDeviceExtensions.swift | 28 ++-- Shared/Extensions/VLCPlayer+subtitles.swift | 4 +- Shared/Objects/DeviceProfileBuilder.swift | 16 +- .../Objects/DeviceRotationViewModifier.swift | 32 ---- Shared/Objects/Displayable.swift | 13 ++ Shared/Objects/ItemFilters.swift | 39 +++++ Shared/Objects/LibraryItem.swift | 2 +- Shared/Objects/LibraryParent.swift | 21 +++ Shared/Objects/Poster.swift | 5 +- Shared/Objects/PosterType.swift | 13 +- ...PillStackable.swift => SelectorType.swift} | 5 +- Shared/Objects/SortBy.swift | 35 ++++ Shared/Objects/Typings.swift | 101 ------------ Shared/Singleton/SessionManager.swift | 4 +- Shared/ViewModels/FilterViewModel.swift | 46 ++++++ Shared/ViewModels/HomeViewModel.swift | 3 - .../ItemViewModel/EpisodeItemViewModel.swift | 2 +- .../ViewModels/LibraryFilterViewModel.swift | 86 ---------- Shared/ViewModels/LibraryViewModel.swift | 130 ++++++++------- Shared/ViewModels/SearchViewModel.swift | 56 +++++-- Shared/ViewModels/UserSignInViewModel.swift | 2 +- .../VideoPlayerViewModel.swift | 4 +- Shared/Views/MultiSelectorView.swift | 75 --------- Shared/Views/SelectorView.swift | 64 ++++++++ Shared/Views/TruncatedTextView.swift | 16 +- Swiftfin tvOS/Components/PosterButton.swift | 2 +- .../Views/BasicAppSettingsView.swift | 2 +- .../ContinueWatchingCard.swift | 2 +- Swiftfin tvOS/Views/ItemView/ItemView.swift | 2 +- Swiftfin tvOS/Views/LatestInLibraryView.swift | 12 +- Swiftfin tvOS/Views/LibraryFilterView.swift | 104 ------------ Swiftfin tvOS/Views/LibraryView.swift | 2 +- Swiftfin tvOS/Views/MediaView.swift | 6 +- .../Views/SettingsView/SettingsView.swift | 2 +- .../LiveTVPlayerViewController.swift | 6 +- .../VideoPlayer/VLCPlayerViewController.swift | 6 +- Swiftfin.xcodeproj/project.pbxproj | 154 +++++++++++++----- Swiftfin/Components/PillHStack.swift | 23 ++- Swiftfin/Components/PosterButton.swift | 2 +- .../NavBarDrawerModifier.swift | 28 ++++ .../NavBarDrawerView.swift | 127 +++++++++++++++ .../NavBarOffsetModifier.swift | 0 .../NavBarOffsetView.swift | 0 .../iOSViewExtensions/iOSViewExtensions.swift | 4 + Swiftfin/Views/AboutAppView.swift | 2 +- Swiftfin/Views/FilterView.swift | 64 ++++++++ .../Components/LatestInLibraryView.swift | 3 +- .../Components/CastAndCrewHStack.swift | 30 ++++ .../ItemView/Components/GenresHStack.swift | 28 ++++ .../Components/SimilarItemsHStack.swift | 30 ++++ .../ItemView/Components/StudiosHStack.swift | 28 ++++ Swiftfin/Views/ItemView/ItemView.swift | 4 +- .../CollectionItemContentView.swift | 16 +- .../EpisodeItemContentView.swift | 25 +-- .../MovieItemView/MovieItemContentView.swift | 24 +-- .../SeriesItemContentView.swift | 25 +-- .../iPadOSCollectionItemContentView.swift | 16 +- .../iPadOSEpisodeContentView.swift | 23 +-- .../iPadOSMovieItemContentView.swift | 20 +-- .../iPadOSSeriesItemContentView.swift | 19 +-- Swiftfin/Views/LibraryFilterView.swift | 105 ------------ .../Components/FilterDrawerButton.swift | 89 ++++++++++ .../Components/FilterDrawerHStack.swift | 98 +++++++++++ Swiftfin/Views/LibraryView/LibraryView.swift | 23 ++- Swiftfin/Views/MediaView.swift | 6 +- Swiftfin/Views/SearchView.swift | 12 +- .../Components/PublicUserSignInView.swift | 4 +- .../LiveTVPlayerViewController.swift | 6 +- .../Overlays/VLCPlayerOverlayView.swift | 2 +- .../VideoPlayer/VLCPlayerViewController.swift | 6 +- 91 files changed, 1358 insertions(+), 997 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPIExtensions/APISortOrderExtensions.swift create mode 100644 Shared/Extensions/JellyfinAPIExtensions/ItemFilterExtensions.swift delete mode 100644 Shared/Objects/DeviceRotationViewModifier.swift create mode 100644 Shared/Objects/Displayable.swift create mode 100644 Shared/Objects/ItemFilters.swift create mode 100644 Shared/Objects/LibraryParent.swift rename Shared/Objects/{PillStackable.swift => SelectorType.swift} (83%) create mode 100644 Shared/Objects/SortBy.swift delete mode 100644 Shared/Objects/Typings.swift create mode 100644 Shared/ViewModels/FilterViewModel.swift delete mode 100644 Shared/ViewModels/LibraryFilterViewModel.swift delete mode 100644 Shared/Views/MultiSelectorView.swift create mode 100644 Shared/Views/SelectorView.swift delete mode 100644 Swiftfin tvOS/Views/LibraryFilterView.swift create mode 100644 Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerModifier.swift create mode 100644 Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerView.swift rename Swiftfin/Extensions/iOSViewExtensions/{NavBarOffsetModifier => NavBarOffset}/NavBarOffsetModifier.swift (100%) rename Swiftfin/Extensions/iOSViewExtensions/{NavBarOffsetModifier => NavBarOffset}/NavBarOffsetView.swift (100%) create mode 100644 Swiftfin/Views/FilterView.swift create mode 100644 Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift create mode 100644 Swiftfin/Views/ItemView/Components/GenresHStack.swift create mode 100644 Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift create mode 100644 Swiftfin/Views/ItemView/Components/StudiosHStack.swift delete mode 100644 Swiftfin/Views/LibraryFilterView.swift create mode 100644 Swiftfin/Views/LibraryView/Components/FilterDrawerButton.swift create mode 100644 Swiftfin/Views/LibraryView/Components/FilterDrawerHStack.swift diff --git a/.swiftformat b/.swiftformat index 6b951e8f..9418f78c 100644 --- a/.swiftformat +++ b/.swiftformat @@ -17,6 +17,7 @@ --varattributes prev-line --trailingclosures --shortoptionals "always" +--ifdef no-indent --enable isEmpty, \ leadingDelimiters, \ diff --git a/Shared/Coordinators/FilterCoordinator.swift b/Shared/Coordinators/FilterCoordinator.swift index 4d1193d7..48565be9 100644 --- a/Shared/Coordinators/FilterCoordinator.swift +++ b/Shared/Coordinators/FilterCoordinator.swift @@ -7,31 +7,41 @@ // import Foundation +import JellyfinAPI import Stinsen import SwiftUI -typealias FilterCoordinatorParams = (filters: Binding, enabledFilterType: [FilterType], parentId: String) - final class FilterCoordinator: NavigationCoordinatable { + struct Parameters { + let title: String + let viewModel: FilterViewModel + let filter: WritableKeyPath + let selectorType: SelectorType + } + let stack = NavigationStack(initial: \FilterCoordinator.start) @Root var start = makeStart - @Binding - var filters: LibraryFilters - var enabledFilterType: [FilterType] - var parentId: String = "" + private let parameters: Parameters - init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { - _filters = filters - self.enabledFilterType = enabledFilterType - self.parentId = parentId + init(parameters: Parameters) { + self.parameters = parameters } @ViewBuilder func makeStart() -> some View { - LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId) + #if os(tvOS) + Text(verbatim: .emptyDash) + #else + FilterView( + title: parameters.title, + viewModel: parameters.viewModel, + filter: parameters.filter, + selectorType: parameters.selectorType + ) + #endif } } diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index 0e77d30e..087499d4 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -21,36 +21,38 @@ final class HomeCoordinator: NavigationCoordinatable { var settings = makeSettings #if os(tvOS) - @Route(.modal) - var item = makeModalItem - @Route(.modal) - var library = makeModalLibrary + @Route(.modal) + var item = makeItem + @Route(.modal) + var library = makeLibrary #else - @Route(.push) - var item = makeItem - @Route(.push) - var library = makeLibrary + @Route(.push) + var item = makeItem + @Route(.push) + var library = makeLibrary #endif func makeSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(SettingsCoordinator()) } - func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { - LibraryCoordinator(viewModel: params.viewModel, title: params.title) + #if os(tvOS) + func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemCoordinator(item: item)) } + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(LibraryCoordinator(parameters: parameters)) + } + #else func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } - func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator(ItemCoordinator(item: item)) - } - - func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator { - NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { + LibraryCoordinator(parameters: parameters) } + #endif @ViewBuilder func makeStart() -> some View { diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index 147f9c05..2780a43a 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -32,8 +32,8 @@ final class ItemCoordinator: NavigationCoordinatable { self.itemDto = item } - func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { - LibraryCoordinator(viewModel: params.viewModel, title: params.title) + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { + LibraryCoordinator(parameters: parameters) } func makeItem(item: BaseItemDto) -> ItemCoordinator { diff --git a/Shared/Coordinators/ItemOverviewCoordinator.swift b/Shared/Coordinators/ItemOverviewCoordinator.swift index cb01e9bb..ed840184 100644 --- a/Shared/Coordinators/ItemOverviewCoordinator.swift +++ b/Shared/Coordinators/ItemOverviewCoordinator.swift @@ -26,9 +26,9 @@ final class ItemOverviewCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { #if os(tvOS) - EmptyView() + EmptyView() #else - ItemOverviewView(item: item) + ItemOverviewView(item: item) #endif } } diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index e098a7ff..a19a72d3 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -11,51 +11,71 @@ import JellyfinAPI import Stinsen import SwiftUI -typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String) - final class LibraryCoordinator: NavigationCoordinatable { + struct Parameters { + let parent: LibraryParent? + let type: LibraryParentType + let filters: ItemFilters + + init( + parent: LibraryParent, + type: LibraryParentType, + filters: ItemFilters + ) { + self.parent = parent + self.type = type + self.filters = filters + } + + init(filters: ItemFilters) { + self.parent = nil + self.type = .library + self.filters = filters + } + } + let stack = NavigationStack(initial: \LibraryCoordinator.start) @Root var start = makeStart - @Route(.modal) - var filter = makeFilter #if os(tvOS) - @Route(.modal) - var item = makeModalItem + @Route(.modal) + var item = makeItem #else - @Route(.push) - var item = makeItem + @Route(.push) + var item = makeItem + @Route(.modal) + var filter = makeFilter #endif - let viewModel: LibraryViewModel - let title: String + private let parameters: Parameters - init(viewModel: LibraryViewModel, title: String) { - self.viewModel = viewModel - self.title = title + init(parameters: Parameters) { + self.parameters = parameters } @ViewBuilder func makeStart() -> some View { - LibraryView(viewModel: self.viewModel) + if let parent = parameters.parent { + LibraryView(viewModel: .init(parent: parent, type: parameters.type, filters: parameters.filters)) + } else { + LibraryView(viewModel: .init(filters: parameters.filters)) + } } - func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator { - NavigationViewCoordinator(FilterCoordinator( - filters: params.filters, - enabledFilterType: params.enabledFilterType, - parentId: params.parentId - )) + #if os(tvOS) + func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemCoordinator(item: item)) } - + #else func makeItem(item: BaseItemDto) -> ItemCoordinator { ItemCoordinator(item: item) } - func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator(ItemCoordinator(item: item)) + func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(FilterCoordinator(parameters: parameters)) } + #endif } diff --git a/Shared/Coordinators/MediaCoordinator.swift b/Shared/Coordinators/MediaCoordinator.swift index 17ff3c53..958e68ad 100644 --- a/Shared/Coordinators/MediaCoordinator.swift +++ b/Shared/Coordinators/MediaCoordinator.swift @@ -17,28 +17,28 @@ final class MediaCoordinator: NavigationCoordinatable { @Root var start = makeStart #if os(tvOS) - @Route(.modal) - var library = makeLibrary + @Route(.modal) + var library = makeLibrary #else - @Route(.push) - var library = makeLibrary - @Route(.push) - var liveTV = makeLiveTV + @Route(.push) + var library = makeLibrary + @Route(.push) + var liveTV = makeLiveTV #endif #if os(tvOS) - func makeLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator { - NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) - } + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(LibraryCoordinator(parameters: parameters)) + } #else - func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { - LibraryCoordinator(viewModel: params.viewModel, title: params.title) - } + func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator { + LibraryCoordinator(parameters: parameters) + } - func makeLiveTV() -> LiveTVCoordinator { - LiveTVCoordinator() - } + func makeLiveTV() -> LiveTVCoordinator { + LiveTVCoordinator() + } #endif @ViewBuilder diff --git a/Shared/Coordinators/MoviesLibrariesCoordinator.swift b/Shared/Coordinators/MoviesLibrariesCoordinator.swift index 66d37b3f..d6c4479c 100644 --- a/Shared/Coordinators/MoviesLibrariesCoordinator.swift +++ b/Shared/Coordinators/MoviesLibrariesCoordinator.swift @@ -36,10 +36,10 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable { } func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) + LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) } func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) + LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) } } diff --git a/Shared/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift index d9c1dca6..73071ac1 100644 --- a/Shared/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -18,21 +18,27 @@ final class SearchCoordinator: NavigationCoordinatable { @Root var start = makeStart #if os(tvOS) - @Route(.modal) - var item = makeItem + @Route(.modal) + var item = makeItem #else - @Route(.push) - var item = makeItem + @Route(.push) + var item = makeItem + @Route(.modal) + var filter = makeFilter #endif #if os(tvOS) - func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator(ItemCoordinator(item: item)) - } + func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemCoordinator(item: item)) + } #else - func makeItem(item: BaseItemDto) -> ItemCoordinator { - ItemCoordinator(item: item) - } + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) + } + + func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator { + NavigationViewCoordinator(FilterCoordinator(parameters: parameters)) + } #endif @ViewBuilder diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index 2dad306f..e9daaa53 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -27,10 +27,10 @@ final class SettingsCoordinator: NavigationCoordinatable { var about = makeAbout #if !os(tvOS) - @Route(.push) - var quickConnect = makeQuickConnectSettings - @Route(.push) - var fontPicker = makeFontPicker + @Route(.push) + var quickConnect = makeQuickConnectSettings + @Route(.push) + var fontPicker = makeFontPicker #endif @ViewBuilder @@ -59,17 +59,17 @@ final class SettingsCoordinator: NavigationCoordinatable { } #if !os(tvOS) - @ViewBuilder - func makeQuickConnectSettings() -> some View { - let viewModel = QuickConnectSettingsViewModel() - QuickConnectSettingsView(viewModel: viewModel) - } + @ViewBuilder + func makeQuickConnectSettings() -> some View { + let viewModel = QuickConnectSettingsViewModel() + QuickConnectSettingsView(viewModel: viewModel) + } - @ViewBuilder - func makeFontPicker() -> some View { - FontPickerView() - .navigationTitle(L10n.subtitleFont) - } + @ViewBuilder + func makeFontPicker() -> some View { + FontPickerView() + .navigationTitle(L10n.subtitleFont) + } #endif @ViewBuilder diff --git a/Shared/Coordinators/TVLibrariesCoordinator.swift b/Shared/Coordinators/TVLibrariesCoordinator.swift index 51bd4117..35cb2b11 100644 --- a/Shared/Coordinators/TVLibrariesCoordinator.swift +++ b/Shared/Coordinators/TVLibrariesCoordinator.swift @@ -36,10 +36,10 @@ final class TVLibrariesCoordinator: NavigationCoordinatable { } func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) + LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) } func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator { - LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) + LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init())) } } diff --git a/Shared/Coordinators/UserSignInCoordinator.swift b/Shared/Coordinators/UserSignInCoordinator.swift index 28fe2c01..1d6e0c87 100644 --- a/Shared/Coordinators/UserSignInCoordinator.swift +++ b/Shared/Coordinators/UserSignInCoordinator.swift @@ -17,8 +17,8 @@ final class UserSignInCoordinator: NavigationCoordinatable { @Root var start = makeStart #if !os(tvOS) - @Route(.modal) - var quickConnect = makeQuickConnect + @Route(.modal) + var quickConnect = makeQuickConnect #endif let viewModel: UserSignInViewModel @@ -28,9 +28,9 @@ final class UserSignInCoordinator: NavigationCoordinatable { } #if !os(tvOS) - func makeQuickConnect() -> NavigationViewCoordinator { - NavigationViewCoordinator(QuickConnectCoordinator(viewModel: viewModel)) - } + func makeQuickConnect() -> NavigationViewCoordinator { + NavigationViewCoordinator(QuickConnectCoordinator(viewModel: viewModel)) + } #endif @ViewBuilder diff --git a/Shared/Extensions/ColorExtension.swift b/Shared/Extensions/ColorExtension.swift index 6bbab868..031a7554 100644 --- a/Shared/Extensions/ColorExtension.swift +++ b/Shared/Extensions/ColorExtension.swift @@ -13,15 +13,15 @@ public extension Color { internal static let jellyfinPurple = Color(uiColor: .jellyfinPurple) #if os(tvOS) // tvOS doesn't have these - static let systemFill = Color(UIColor.white) - static let secondarySystemFill = Color(UIColor.gray) - static let tertiarySystemFill = Color(UIColor.black) - static let lightGray = Color(UIColor.lightGray) + static let systemFill = Color(UIColor.white) + static let secondarySystemFill = Color(UIColor.gray) + static let tertiarySystemFill = Color(UIColor.black) + static let lightGray = Color(UIColor.lightGray) #else - static let systemFill = Color(UIColor.systemFill) - static let systemBackground = Color(UIColor.systemBackground) - static let secondarySystemFill = Color(UIColor.secondarySystemFill) - static let tertiarySystemFill = Color(UIColor.tertiarySystemFill) + static let systemFill = Color(UIColor.systemFill) + static let systemBackground = Color(UIColor.systemBackground) + static let secondarySystemFill = Color(UIColor.secondarySystemFill) + static let tertiarySystemFill = Color(UIColor.tertiarySystemFill) #endif } diff --git a/Shared/Extensions/FontExtensions.swift b/Shared/Extensions/FontExtensions.swift index d8c2670c..03947512 100644 --- a/Shared/Extensions/FontExtensions.swift +++ b/Shared/Extensions/FontExtensions.swift @@ -12,8 +12,8 @@ extension Font { func toUIFont() -> UIFont { switch self { #if !os(tvOS) - case .largeTitle: - return UIFont.preferredFont(forTextStyle: .largeTitle) + case .largeTitle: + return UIFont.preferredFont(forTextStyle: .largeTitle) #endif case .title: return UIFont.preferredFont(forTextStyle: .title1) diff --git a/Shared/Extensions/JellyfinAPIExtensions/APISortOrderExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/APISortOrderExtensions.swift new file mode 100644 index 00000000..211c2610 --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/APISortOrderExtensions.swift @@ -0,0 +1,26 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension APISortOrder { + // TODO: Localize + var localized: String { + switch self { + case .ascending: + return "Ascending" + case .descending: + return "Descending" + } + } + + var filter: ItemFilters.Filter { + .init(displayName: localized, filterName: rawValue) + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index c3fe1018..a7bef3d4 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -86,7 +86,7 @@ extension BaseItemDto { } var displayName: String { - name ?? "--" + name ?? .emptyDash } // MARK: ItemDetail @@ -247,3 +247,5 @@ extension BaseItemDtoImageBlurHashes { } } } + +extension BaseItemDto: LibraryParent {} diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift index 48eb796c..cb143f56 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPerson+Poster.swift @@ -14,10 +14,6 @@ import UIKit extension BaseItemPerson: Poster { - var title: String { - self.name ?? "--" - } - var subtitle: String? { self.firstRole } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift index 68395287..f9e5d33d 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemPersonExtensions.swift @@ -50,3 +50,11 @@ extension BaseItemPerson { return DisplayedType(rawValue: type) != nil } } + +extension BaseItemPerson: Displayable { + var displayName: String { + self.name ?? .emptyDash + } +} + +extension BaseItemPerson: LibraryParent {} diff --git a/Shared/Extensions/JellyfinAPIExtensions/ItemFilterExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/ItemFilterExtensions.swift new file mode 100644 index 00000000..a36ec855 --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/ItemFilterExtensions.swift @@ -0,0 +1,36 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension ItemFilter { + static var supportedCases: [ItemFilter] { + [.isUnplayed, .isPlayed, .isFavorite, .likes] + } + + // TODO: Localize + var localized: String { + switch self { + case .isUnplayed: + return "Unplayed" + case .isPlayed: + return "Played" + case .isFavorite: + return "Favorites" + case .likes: + return "Liked Items" + default: + return "" + } + } + + var filter: ItemFilters.Filter { + .init(displayName: localized, filterName: rawValue) + } +} diff --git a/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift index 69e5e2a2..0bcd865f 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/NameGUIDPairExtensions.swift @@ -9,8 +9,16 @@ import Foundation import JellyfinAPI -extension NameGuidPair: PillStackable { - var title: String { - self.name ?? "" +extension NameGuidPair { + var filter: ItemFilters.Filter { + .init(displayName: displayName, id: id, filterName: displayName) } } + +extension NameGuidPair: Displayable { + var displayName: String { + self.name ?? .emptyDash + } +} + +extension NameGuidPair: LibraryParent {} diff --git a/Shared/Extensions/StringExtensions.swift b/Shared/Extensions/StringExtensions.swift index 20acad28..745356a3 100644 --- a/Shared/Extensions/StringExtensions.swift +++ b/Shared/Extensions/StringExtensions.swift @@ -54,6 +54,12 @@ extension String { let textSize = self.size(withAttributes: fontAttributes) return textSize.width } + + var filter: ItemFilters.Filter { + .init(displayName: self, id: self, filterName: self) + } + + static var emptyDash = "--" } public extension CharacterSet { diff --git a/Shared/Extensions/UIDeviceExtensions.swift b/Shared/Extensions/UIDeviceExtensions.swift index 8ccea68a..e78aafcd 100644 --- a/Shared/Extensions/UIDeviceExtensions.swift +++ b/Shared/Extensions/UIDeviceExtensions.swift @@ -22,22 +22,22 @@ extension UIDevice { } #if os(iOS) - static var isPortrait: Bool { - UIDevice.current.orientation.isPortrait - } + static var isPortrait: Bool { + UIDevice.current.orientation.isPortrait + } - static var isLandscape: Bool { - isIPad || UIDevice.current.orientation.isLandscape - } + static var isLandscape: Bool { + isIPad || UIDevice.current.orientation.isLandscape + } - static func feedback(_ type: UINotificationFeedbackGenerator.FeedbackType) { - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(type) - } + static func feedback(_ type: UINotificationFeedbackGenerator.FeedbackType) { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(type) + } - static func impact(_ type: UIImpactFeedbackGenerator.FeedbackStyle) { - let generator = UIImpactFeedbackGenerator(style: type) - generator.impactOccurred() - } + static func impact(_ type: UIImpactFeedbackGenerator.FeedbackStyle) { + let generator = UIImpactFeedbackGenerator(style: type) + generator.impactOccurred() + } #endif } diff --git a/Shared/Extensions/VLCPlayer+subtitles.swift b/Shared/Extensions/VLCPlayer+subtitles.swift index b3da03ee..6485386a 100644 --- a/Shared/Extensions/VLCPlayer+subtitles.swift +++ b/Shared/Extensions/VLCPlayer+subtitles.swift @@ -8,9 +8,9 @@ import UIKit #if os(tvOS) - import TVVLCKit +import TVVLCKit #else - import MobileVLCKit +import MobileVLCKit #endif extension VLCMediaPlayer { diff --git a/Shared/Objects/DeviceProfileBuilder.swift b/Shared/Objects/DeviceProfileBuilder.swift index f67a33dc..8946ea14 100644 --- a/Shared/Objects/DeviceProfileBuilder.swift +++ b/Shared/Objects/DeviceProfileBuilder.swift @@ -234,16 +234,16 @@ class DeviceProfileBuilder { private func CPUinfo() -> CPUModel { #if targetEnvironment(simulator) - let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! + let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! #else - var systemInfo = utsname() - uname(&systemInfo) - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) - } + var systemInfo = utsname() + uname(&systemInfo) + let machineMirror = Mirror(reflecting: systemInfo.machine) + let identifier = machineMirror.children.reduce("") { identifier, element in + guard let value = element.value as? Int8, value != 0 else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) + } #endif switch identifier { diff --git a/Shared/Objects/DeviceRotationViewModifier.swift b/Shared/Objects/DeviceRotationViewModifier.swift deleted file mode 100644 index e9ac5bb6..00000000 --- a/Shared/Objects/DeviceRotationViewModifier.swift +++ /dev/null @@ -1,32 +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) 2022 Jellyfin & Jellyfin Contributors -// - -// https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation -import Foundation -import SwiftUI - -// Our custom view modifier to track rotation and -// call our action -struct DeviceRotationViewModifier: ViewModifier { - let action: (UIDeviceOrientation) -> Void - - func body(content: Content) -> some View { - content - .onAppear() - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - action(UIDevice.current.orientation) - } - } -} - -// A View wrapper to make the modifier easier to use -extension View { - func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View { - self.modifier(DeviceRotationViewModifier(action: action)) - } -} diff --git a/Shared/Objects/Displayable.swift b/Shared/Objects/Displayable.swift new file mode 100644 index 00000000..af7b7d7c --- /dev/null +++ b/Shared/Objects/Displayable.swift @@ -0,0 +1,13 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +protocol Displayable { + var displayName: String { get } +} diff --git a/Shared/Objects/ItemFilters.swift b/Shared/Objects/ItemFilters.swift new file mode 100644 index 00000000..a5f77e90 --- /dev/null +++ b/Shared/Objects/ItemFilters.swift @@ -0,0 +1,39 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +struct ItemFilters: Hashable { + + var genres: [Filter] = [] + var tags: [Filter] = [] + var filters: [Filter] = [] + var sortOrder: [Filter] = [APISortOrder.ascending.filter] + var sortBy: [Filter] = [SortBy.name.filter] + + static let favorites: ItemFilters = .init(filters: [ItemFilter.isFavorite.filter]) + static let recent: ItemFilters = .init(sortOrder: [APISortOrder.descending.filter], sortBy: [SortBy.dateAdded.filter]) + static let all: ItemFilters = .init( + filters: ItemFilter.supportedCases.map(\.filter), + sortOrder: APISortOrder.allCases.map(\.filter), + sortBy: SortBy.allCases.map(\.filter) + ) + + var hasFilters: Bool { + self != .init() + } + + // Type-erased object for use with WritableKeyPath + struct Filter: Displayable, Hashable, Identifiable { + var displayName: String + var id: String? + var filterName: String + } +} diff --git a/Shared/Objects/LibraryItem.swift b/Shared/Objects/LibraryItem.swift index 9afbfeef..fc796db5 100644 --- a/Shared/Objects/LibraryItem.swift +++ b/Shared/Objects/LibraryItem.swift @@ -15,7 +15,7 @@ struct LibraryItem: Equatable, Poster { var library: BaseItemDto var viewModel: MediaViewModel - var title: String = "" + var displayName: String = "" var subtitle: String? var showTitle: Bool = false diff --git a/Shared/Objects/LibraryParent.swift b/Shared/Objects/LibraryParent.swift new file mode 100644 index 00000000..0eee8498 --- /dev/null +++ b/Shared/Objects/LibraryParent.swift @@ -0,0 +1,21 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation + +protocol LibraryParent: Displayable { + var id: String? { get } +} + +// TODO: Remove so multiple people/studios can be used +enum LibraryParentType { + case library + case folders + case person + case studio +} diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift index 6940f181..bbd3deeb 100644 --- a/Shared/Objects/Poster.swift +++ b/Shared/Objects/Poster.swift @@ -10,8 +10,7 @@ import Defaults import Foundation import SwiftUI -protocol Poster: Hashable { - var title: String { get } +protocol Poster: Displayable, Hashable { var subtitle: String? { get } var showTitle: Bool { get } @@ -21,7 +20,7 @@ protocol Poster: Hashable { extension Poster { func hash(into hasher: inout Hasher) { - hasher.combine(title) + hasher.combine(displayName) hasher.combine(subtitle) } } diff --git a/Shared/Objects/PosterType.swift b/Shared/Objects/PosterType.swift index 34327359..84350fbb 100644 --- a/Shared/Objects/PosterType.swift +++ b/Shared/Objects/PosterType.swift @@ -22,6 +22,7 @@ enum PosterType: String, CaseIterable, Defaults.Serializable { } } + // TODO: localize var localizedName: String { switch self { case .portrait: @@ -33,15 +34,15 @@ enum PosterType: String, CaseIterable, Defaults.Serializable { enum Width { #if os(tvOS) - static let portrait = 250.0 + static let portrait = 250.0 - static let landscape = 490.0 + static let landscape = 490.0 #else - @ScaledMetric(relativeTo: .largeTitle) - static var portrait = 100.0 + @ScaledMetric(relativeTo: .largeTitle) + static var portrait = 100.0 - @ScaledMetric(relativeTo: .largeTitle) - static var landscape = 200.0 + @ScaledMetric(relativeTo: .largeTitle) + static var landscape = 200.0 #endif } } diff --git a/Shared/Objects/PillStackable.swift b/Shared/Objects/SelectorType.swift similarity index 83% rename from Shared/Objects/PillStackable.swift rename to Shared/Objects/SelectorType.swift index 071adbb9..a3aadcf1 100644 --- a/Shared/Objects/PillStackable.swift +++ b/Shared/Objects/SelectorType.swift @@ -8,6 +8,7 @@ import Foundation -protocol PillStackable { - var title: String { get } +enum SelectorType { + case single + case multi } diff --git a/Shared/Objects/SortBy.swift b/Shared/Objects/SortBy.swift new file mode 100644 index 00000000..eb0c1bbd --- /dev/null +++ b/Shared/Objects/SortBy.swift @@ -0,0 +1,35 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +public enum SortBy: String, Codable, CaseIterable { + case premiereDate = "PremiereDate" + case name = "SortName" + case dateAdded = "DateCreated" + case random = "Random" + + // TODO: Localize + var localized: String { + switch self { + case .premiereDate: + return "Premiere date" + case .name: + return "Name" + case .dateAdded: + return "Date added" + case .random: + return "Random" + } + } + + var filter: ItemFilters.Filter { + .init(displayName: localized, filterName: rawValue) + } +} diff --git a/Shared/Objects/Typings.swift b/Shared/Objects/Typings.swift deleted file mode 100644 index 7d7e6f83..00000000 --- a/Shared/Objects/Typings.swift +++ /dev/null @@ -1,101 +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) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -// TODO: Look at refactoring everything in this file, probably move to JellyfinAPI -struct LibraryFilters: Codable, Hashable { - var filters: [ItemFilter] = [] - var sortOrder: [APISortOrder] = [.ascending] - var withGenres: [NameGuidPair] = [] - var tags: [String] = [] - var sortBy: [SortBy] = [.name] - - static let `default` = LibraryFilters() - static let favorites: LibraryFilters = .init(filters: [.isFavorite], sortOrder: [.ascending], sortBy: [.name]) -} - -public enum SortBy: String, Codable, CaseIterable { - case premiereDate = "PremiereDate" - case name = "SortName" - case dateAdded = "DateCreated" - case random = "Random" -} - -extension SortBy { - // TODO: Localize - var localized: String { - switch self { - case .premiereDate: - return "Premiere date" - case .name: - return "Name" - case .dateAdded: - return "Date added" - case .random: - return "Random" - } - } -} - -extension ItemFilter { - static var supportedTypes: [ItemFilter] { - [.isUnplayed, isPlayed, .isFavorite, .likes] - } - - // TODO: Localize - var localized: String { - switch self { - case .isUnplayed: - return "Unplayed" - case .isPlayed: - return "Played" - case .isFavorite: - return "Favorites" - case .likes: - return "Liked Items" - default: - return "" - } - } -} - -extension APISortOrder { - // TODO: Localize - var localized: String { - switch self { - case .ascending: - return "Ascending" - case .descending: - return "Descending" - } - } -} - -// TODO: Remove -enum ItemType: String { - case episode = "Episode" - case movie = "Movie" - case series = "Series" - case season = "Season" - - var localized: String { - switch self { - case .episode: - return L10n.episodes - case .movie: - return "Movies" - case .series: - return "Shows" - default: - return "" - } - } -} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 7ff479fd..4091f64a 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -302,9 +302,9 @@ final class SessionManager { let platform: String #if os(tvOS) - platform = "tvOS" + platform = "tvOS" #else - platform = "iOS" + platform = "iOS" #endif var header = "MediaBrowser " diff --git a/Shared/ViewModels/FilterViewModel.swift b/Shared/ViewModels/FilterViewModel.swift new file mode 100644 index 00000000..97b72d73 --- /dev/null +++ b/Shared/ViewModels/FilterViewModel.swift @@ -0,0 +1,46 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import SwiftUI + +final class FilterViewModel: ViewModel { + + @Published + var allFilters: ItemFilters = .all + @Published + var currentFilters: ItemFilters + + let parent: LibraryParent? + + init( + parent: LibraryParent?, + currentFilters: ItemFilters + ) { + self.parent = parent + self.currentFilters = currentFilters + super.init() + + getQueryFilters() + } + + private func getQueryFilters() { + FilterAPI.getQueryFilters( + userId: SessionManager.main.currentLogin.user.id, + parentId: parent?.id + ) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] queryFilters in + self?.allFilters.genres = queryFilters.genres?.map(\.filter) ?? [] + self?.allFilters.tags = queryFilters.tags?.map(\.filter) ?? [] + }) + .store(in: &cancellables) + } +} diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index cc5a2f33..2772c34c 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -24,9 +24,6 @@ final class HomeViewModel: ViewModel { @Published var libraries: [BaseItemDto] = [] - // temp - static let recentFilterSet = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded]) - override init() { super.init() refresh() diff --git a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift index a40fa7c2..911f9ae1 100644 --- a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift @@ -69,7 +69,7 @@ final class EpisodeItemViewModel: ItemViewModel { .joined(separator: ", ") let currentMediaItems: [BaseItemDto.ItemDetail] = [ - .init(title: "File", content: viewModel.filename ?? "--"), + .init(title: "File", content: viewModel.filename ?? .emptyDash), .init(title: "Audio", content: audioStreams), .init(title: "Subtitles", content: subtitleStreams), ] diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift deleted file mode 100644 index 621ba81d..00000000 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ /dev/null @@ -1,86 +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) 2022 Jellyfin & Jellyfin Contributors -// - -import Combine -import Foundation -import JellyfinAPI - -enum FilterType { - case tag - case genre - case sortOrder - case sortBy - case filter -} - -final class LibraryFilterViewModel: ViewModel { - - @Published - var modifiedFilters = LibraryFilters() - - @Published - var possibleGenres = [NameGuidPair]() - @Published - var possibleTags = [String]() - @Published - var possibleSortOrders = APISortOrder.allCases - @Published - var possibleSortBys = SortBy.allCases - @Published - var possibleItemFilters = ItemFilter.supportedTypes - @Published - var enabledFilterType: [FilterType] - @Published - var selectedSortOrder: APISortOrder = .descending - @Published - var selectedSortBy: SortBy = .name - - var parentId: String = "" - - func updateModifiedFilter() { - modifiedFilters.sortOrder = [selectedSortOrder] - modifiedFilters.sortBy = [selectedSortBy] - } - - func resetFilters() { - modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) - } - - init( - filters: LibraryFilters? = nil, - enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter], - parentId: String - ) { - self.enabledFilterType = enabledFilterType - self.selectedSortBy = filters?.sortBy.first ?? .name - self.selectedSortOrder = filters?.sortOrder.first ?? .descending - self.parentId = parentId - - super.init() - if let filters = filters { - self.modifiedFilters = filters - } - requestQueryFilters() - } - - func requestQueryFilters() { - FilterAPI.getQueryFilters( - userId: SessionManager.main.currentLogin.user.id, - parentId: self.parentId - ) - .trackActivity(loading) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] queryFilters in - guard let self = self else { return } - self.possibleGenres = queryFilters.genres ?? [] - self.possibleTags = queryFilters.tags ?? [] - }) - .store(in: &cancellables) - } -} diff --git a/Shared/ViewModels/LibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel.swift index 36e6ff1f..a772e0d8 100644 --- a/Shared/ViewModels/LibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel.swift @@ -9,63 +9,61 @@ import Combine import Defaults import JellyfinAPI +import SwiftUI import UIKit // TODO: Look at refactoring final class LibraryViewModel: ViewModel { - @Published - var items: [BaseItemDto] = [] - @Published - private var currentPage = 0 - private var hasNextPage = true - @Published - var filters: LibraryFilters - @Default(.Customization.Library.gridPosterType) private var libraryGridPosterType - let library: BaseItemDto? - let person: BaseItemPerson? - let genre: NameGuidPair? - let studio: NameGuidPair? + @Published + var items: [BaseItemDto] = [] + + let filterViewModel: FilterViewModel + private var currentPage = 0 + private var hasNextPage = true + + let parent: LibraryParent? + let type: LibraryParentType + + init(filters: ItemFilters) { + self.parent = nil + self.type = .library + self.filterViewModel = .init(parent: nil, currentFilters: filters) + super.init() + + filterViewModel.$currentFilters + .sink { newFilters in + self.requestItemsAsync(with: newFilters, replaceCurrentItems: true) + } + .store(in: &cancellables) + } + + init( + parent: LibraryParent, + type: LibraryParentType, + filters: ItemFilters = .init() + ) { + self.parent = parent + self.type = type + self.filterViewModel = .init(parent: parent, currentFilters: filters) + super.init() + + filterViewModel.$currentFilters + .sink { newFilters in + self.requestItemsAsync(with: newFilters, replaceCurrentItems: true) + } + .store(in: &cancellables) + } private var pageItemSize: Int { let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77 return UIScreen.itemsFillableOnScreen(width: libraryGridPosterType.width, height: height) } - var enabledFilterType: [FilterType] { - if genre == nil { - return [.tag, .genre, .sortBy, .sortOrder, .filter] - } else { - return [.tag, .sortBy, .sortOrder, .filter] - } - } - - init( - library: BaseItemDto? = nil, - person: BaseItemPerson? = nil, - genre: NameGuidPair? = nil, - studio: NameGuidPair? = nil, - filters: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], sortBy: [.name]) - ) { - self.library = library - self.person = person - self.genre = genre - self.studio = studio - self.filters = filters - - super.init() - - $filters - .sink(receiveValue: { newFilters in - self.requestItemsAsync(with: newFilters, replaceCurrentItems: true) - }) - .store(in: &cancellables) - } - - func requestItemsAsync(with filters: LibraryFilters, replaceCurrentItems: Bool = false) { + func requestItemsAsync(with filters: ItemFilters, replaceCurrentItems: Bool = false) { if replaceCurrentItems { self.items = [] @@ -73,23 +71,26 @@ final class LibraryViewModel: ViewModel { self.hasNextPage = true } - let personIDs: [String] = [person].compactMap(\.?.id) - let studioIDs: [String] = [studio].compactMap(\.?.id) - let genreIDs: [String] + var libraryID: String? + var personIDs: [String]? + var studioIDs: [String]? - if filters.withGenres.isEmpty { - genreIDs = [genre].compactMap(\.?.id) - } else { - genreIDs = filters.withGenres.compactMap(\.id) + if let parent = parent { + switch type { + case .library, .folders: + libraryID = parent.id + case .person: + personIDs = [parent].compactMap(\.id) + case .studio: + studioIDs = [parent].compactMap(\.id) + } } - let sortBy = filters.sortBy.map(\.rawValue) - let includeItemTypes: [BaseItemKind] - if filters.filters.contains(.isFavorite) { + if filters.filters.contains(ItemFilter.isFavorite.filter) { includeItemTypes = [.movie, .boxSet, .series, .season, .episode] - } else if library?.collectionType == "folders" { + } else if type == .folders { includeItemTypes = [.collectionFolder] } else { includeItemTypes = [.movie, .series, .boxSet] @@ -97,26 +98,31 @@ final class LibraryViewModel: ViewModel { let excludedIDs: [String]? - if filters.sortBy == [.random] { + if filters.sortBy.first == SortBy.random.filter { excludedIDs = items.compactMap(\.id) } else { excludedIDs = nil } + let genreIDs = filters.genres.compactMap(\.id) + let sortBy: [String] = filters.sortBy.map(\.filterName) + let sortOrder = filters.sortOrder.map { SortOrder(rawValue: $0.filterName) ?? .ascending } + let itemFilters: [ItemFilter] = filters.filters.compactMap { .init(rawValue: $0.filterName) } + let tags: [String] = filters.tags.map(\.filterName) + ItemsAPI.getItemsByUserId( userId: SessionManager.main.currentLogin.user.id, excludeItemIds: excludedIDs, startIndex: currentPage * pageItemSize, limit: pageItemSize, recursive: true, - searchTerm: nil, - sortOrder: filters.sortOrder.compactMap { SortOrder(rawValue: $0.rawValue) }, - parentId: library?.id, + sortOrder: sortOrder, + parentId: libraryID, fields: ItemFields.allCases, includeItemTypes: includeItemTypes, - filters: filters.filters, + filters: itemFilters, sortBy: sortBy, - tags: filters.tags, + tags: tags, enableUserData: true, personIds: personIDs, studioIds: studioIDs, @@ -139,7 +145,7 @@ final class LibraryViewModel: ViewModel { // excluded ids. This causes shorter item additions when using "Random" over // consecutive calls. Investigation needs to be done to find the root of the problem. // Only filter for "Random" as an optimization. - if filters.sortBy == [.random] { + if filters.sortBy.first == SortBy.random.filter { items = response.items?.filter { !(self?.items.contains($0) ?? true) } ?? [] } else { items = response.items ?? [] @@ -153,7 +159,7 @@ final class LibraryViewModel: ViewModel { func requestNextPageAsync() { guard hasNextPage else { return } currentPage += 1 - requestItemsAsync(with: filters) + requestItemsAsync(with: filterViewModel.currentFilters) } } diff --git a/Shared/ViewModels/SearchViewModel.swift b/Shared/ViewModels/SearchViewModel.swift index 2a72177e..0fe46d72 100644 --- a/Shared/ViewModels/SearchViewModel.swift +++ b/Shared/ViewModels/SearchViewModel.swift @@ -13,8 +13,6 @@ import SwiftUI final class SearchViewModel: ViewModel { - private var searchCancellables = Set() - @Published var movies: [BaseItemDto] = [] @Published @@ -28,6 +26,10 @@ final class SearchViewModel: ViewModel { @Published var suggestions: [BaseItemDto] = [] + let filterViewModel: FilterViewModel + private var searchTextSubject = CurrentValueSubject("") + private var searchCancellables = Set() + var noResults: Bool { movies.isEmpty && collections.isEmpty && @@ -36,9 +38,8 @@ final class SearchViewModel: ViewModel { people.isEmpty } - private var searchTextSubject = CurrentValueSubject("") - override init() { + self.filterViewModel = .init(parent: nil, currentFilters: .init()) super.init() getSuggestions() @@ -47,7 +48,15 @@ final class SearchViewModel: ViewModel { .handleEvents(receiveOutput: { _ in self.cancelPreviousSearch() }) .filter { !$0.isEmpty } .debounce(for: 0.25, scheduler: DispatchQueue.main) - .sink(receiveValue: _search) + .sink { newSearch in + self._search(with: newSearch, filters: self.filterViewModel.currentFilters) + } + .store(in: &cancellables) + + filterViewModel.$currentFilters + .sink { newFilters in + self._search(with: self.searchTextSubject.value, filters: newFilters) + } .store(in: &cancellables) } @@ -59,29 +68,39 @@ final class SearchViewModel: ViewModel { searchTextSubject.send(query) } - private func _search(with query: String) { - getItems(with: query, for: .movie, keyPath: \.movies) - getItems(with: query, for: .boxSet, keyPath: \.collections) - getItems(with: query, for: .series, keyPath: \.series) - getItems(with: query, for: .episode, keyPath: \.episodes) - getPeople(with: query) + private func _search(with query: String, filters: ItemFilters) { + getItems(for: query, with: filters, type: .movie, keyPath: \.movies) + getItems(for: query, with: filters, type: .boxSet, keyPath: \.collections) + getItems(for: query, with: filters, type: .series, keyPath: \.series) + getItems(for: query, with: filters, type: .episode, keyPath: \.episodes) + getPeople(for: query, with: filters) } private func getItems( - with query: String, - for itemType: BaseItemKind, + for query: String, + with filters: ItemFilters, + type itemType: BaseItemKind, keyPath: ReferenceWritableKeyPath ) { + let genreIDs = filters.genres.compactMap(\.id) + let sortBy: [String] = filters.sortBy.map(\.filterName) + let sortOrder = filters.sortOrder.map { SortOrder(rawValue: $0.filterName) ?? .ascending } + let itemFilters: [ItemFilter] = filters.filters.compactMap { .init(rawValue: $0.filterName) } + let tags: [String] = filters.tags.map(\.filterName) + ItemsAPI.getItemsByUserId( userId: SessionManager.main.currentLogin.user.id, limit: 20, recursive: true, searchTerm: query, - sortOrder: [.ascending], + sortOrder: sortOrder, fields: ItemFields.allCases, includeItemTypes: [itemType], - sortBy: ["SortName"], + filters: itemFilters, + sortBy: sortBy, + tags: tags, enableUserData: true, + genreIds: genreIDs, enableImages: true ) .trackActivity(loading) @@ -93,7 +112,12 @@ final class SearchViewModel: ViewModel { .store(in: &searchCancellables) } - private func getPeople(with query: String) { + private func getPeople(for query: String?, with filters: ItemFilters) { + guard !filters.hasFilters else { + self.people = [] + return + } + PersonsAPI.getPersons( limit: 20, searchTerm: query diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 0591e040..7c464246 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -99,7 +99,7 @@ final class UserSignInViewModel: ViewModel { self.quickConnectSecret = response.secret self.quickConnectCode = response.code - LogManager.log.debug("QuickConnect code: \(response.code ?? "--")") + LogManager.log.debug("QuickConnect code: \(response.code ?? .emptyDash)") self.quickConnectTimer = RepeatingTimer(interval: 5) { self.checkAuthStatus(onSuccess) diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 58121e79..27b34c98 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -14,9 +14,9 @@ import JellyfinAPI import UIKit #if os(tvOS) - import TVVLCKit +import TVVLCKit #else - import MobileVLCKit +import MobileVLCKit #endif final class VideoPlayerViewModel: ViewModel { diff --git a/Shared/Views/MultiSelectorView.swift b/Shared/Views/MultiSelectorView.swift deleted file mode 100644 index 75da9898..00000000 --- a/Shared/Views/MultiSelectorView.swift +++ /dev/null @@ -1,75 +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) 2022 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -private struct MultiSelectionView: View { - let options: [Selectable] - let optionToString: (Selectable) -> String - let label: String - - @Binding - var selected: [Selectable] - - var body: some View { - List { - ForEach(options, id: \.self) { selectable in - Button(action: { toggleSelection(selectable: selectable) }) { - HStack { - Text(optionToString(selectable)).foregroundColor(Color.primary) - Spacer() - if selected.contains { $0 == selectable } { - Image(systemName: "checkmark").foregroundColor(.accentColor) - } - } - }.tag(selectable) - } - }.listStyle(GroupedListStyle()) - } - - private func toggleSelection(selectable: Selectable) { - if let existingIndex = selected.firstIndex(where: { $0 == selectable }) { - selected.remove(at: existingIndex) - } else { - selected.append(selectable) - } - } -} - -struct MultiSelector: View { - let label: String - let options: [Selectable] - let optionToString: (Selectable) -> String - - var selected: Binding<[Selectable]> - - private var formattedSelectedListString: String { - ListFormatter.localizedString(byJoining: selected.wrappedValue.map { optionToString($0) }) - } - - var body: some View { - NavigationLink(destination: multiSelectionView()) { - HStack { - Text(label) - Spacer() - Text(formattedSelectedListString) - .foregroundColor(.gray) - .multilineTextAlignment(.trailing) - } - } - } - - private func multiSelectionView() -> some View { - MultiSelectionView( - options: options, - optionToString: optionToString, - label: self.label, - selected: selected - ) - } -} diff --git a/Shared/Views/SelectorView.swift b/Shared/Views/SelectorView.swift new file mode 100644 index 00000000..a0cb0714 --- /dev/null +++ b/Shared/Views/SelectorView.swift @@ -0,0 +1,64 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +// TODO: Implement different behavior types, where selected/unselected +// items appear in different sections +struct SelectorView: View { + + private let allItems: [Item] + @Binding + private var selectedItems: [Item] + private let type: SelectorType + + init(type: SelectorType, allItems: [Item], selectedItems: Binding<[Item]>) { + self.type = type + self.allItems = allItems + self._selectedItems = selectedItems + } + + var body: some View { + List { + ForEach(allItems, id: \.displayName) { item in + Button { + switch type { + case .single: + handleSingleSelect(with: item) + case .multi: + handleMultiSelect(with: item) + } + } label: { + HStack { + Text(item.displayName) + .foregroundColor(.primary) + + Spacer() + + if selectedItems.contains { $0.displayName == item.displayName } { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.jellyfinPurple) + } + } + } + } + } + } + + private func handleSingleSelect(with item: Item) { + selectedItems = [item] + } + + private func handleMultiSelect(with item: Item) { + if selectedItems.contains(where: { $0.displayName == item.displayName }) { + selectedItems.removeAll(where: { $0.displayName == item.displayName }) + } else { + selectedItems.append(item) + } + } +} diff --git a/Shared/Views/TruncatedTextView.swift b/Shared/Views/TruncatedTextView.swift index 23b26b45..1f227949 100644 --- a/Shared/Views/TruncatedTextView.swift +++ b/Shared/Views/TruncatedTextView.swift @@ -59,17 +59,17 @@ struct TruncatedTextView: View { if truncated { #if os(tvOS) + Text(seeMoreText) + .font(font) + .foregroundColor(.purple) + #else + Button { + seeMoreAction() + } label: { Text(seeMoreText) .font(font) .foregroundColor(.purple) - #else - Button { - seeMoreAction() - } label: { - Text(seeMoreText) - .font(font) - .foregroundColor(.purple) - } + } #endif } } diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index 577d52eb..bf3290d6 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -209,7 +209,7 @@ struct PosterButtonDefaultContentView: View { var body: some View { VStack(alignment: .leading) { if item.showTitle { - Text(item.title) + Text(item.displayName) .font(.footnote) .fontWeight(.regular) .foregroundColor(.primary) diff --git a/Swiftfin tvOS/Views/BasicAppSettingsView.swift b/Swiftfin tvOS/Views/BasicAppSettingsView.swift index e1174ea3..c693a361 100644 --- a/Swiftfin tvOS/Views/BasicAppSettingsView.swift +++ b/Swiftfin tvOS/Views/BasicAppSettingsView.swift @@ -30,7 +30,7 @@ struct BasicAppSettingsView: View { HStack { L10n.version.text Spacer() - Text("\(UIApplication.appVersion ?? "--") (\(UIApplication.bundleVersion ?? "--"))") + Text("\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))") .foregroundColor(.secondary) } } diff --git a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift index f7d35a9f..15c95d8b 100644 --- a/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift +++ b/Swiftfin tvOS/Views/ContinueWatchingView/ContinueWatchingCard.swift @@ -70,7 +70,7 @@ struct ContinueWatchingCard: View { .frame(width: 500, alignment: .leading) if item.type == .episode { - Text(item.episodeLocator ?? "--") + Text(item.episodeLocator ?? .emptyDash) .font(.callout) .fontWeight(.medium) .foregroundColor(.secondary) diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index 00ccf5cc..c2a443f3 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -30,7 +30,7 @@ struct ItemView: View { case .boxSet: CollectionItemView(viewModel: .init(item: item)) case .person: - LibraryView(viewModel: .init(person: .init(id: item.id))) + LibraryView(viewModel: .init(parent: item, type: .person, filters: .init())) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin tvOS/Views/LatestInLibraryView.swift b/Swiftfin tvOS/Views/LatestInLibraryView.swift index aa6981d3..46dfe417 100644 --- a/Swiftfin tvOS/Views/LatestInLibraryView.swift +++ b/Swiftfin tvOS/Views/LatestInLibraryView.swift @@ -20,17 +20,7 @@ struct LatestInLibraryView: View { PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: .portrait, items: viewModel.items) .trailing { Button { - router.route(to: \.library, ( - viewModel: .init( - library: viewModel.library, - filters: LibraryFilters( - filters: [], - sortOrder: [.descending], - sortBy: [.dateAdded] - ) - ), - title: viewModel.library.displayName - )) + router.route(to: \.library, .init(parent: viewModel.library, type: .library, filters: .recent)) } label: { ZStack { Color(UIColor.darkGray) diff --git a/Swiftfin tvOS/Views/LibraryFilterView.swift b/Swiftfin tvOS/Views/LibraryFilterView.swift deleted file mode 100644 index 76b28f1e..00000000 --- a/Swiftfin tvOS/Views/LibraryFilterView.swift +++ /dev/null @@ -1,104 +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) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import Stinsen -import SwiftUI - -struct LibraryFilterView: View { - - @EnvironmentObject - private var filterRouter: FilterCoordinator.Router - @Binding - var filters: LibraryFilters - var parentId: String = "" - - @StateObject - var viewModel: LibraryFilterViewModel - - init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { - _filters = filters - self.parentId = parentId - _viewModel = - StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId)) - } - - var body: some View { - VStack { - if viewModel.isLoading { - ProgressView() - } else { - Form { - if viewModel.enabledFilterType.contains(.genre) { - MultiSelector( - label: L10n.genres, - options: viewModel.possibleGenres, - optionToString: { $0.name ?? "" }, - selected: $viewModel.modifiedFilters.withGenres - ) - } - if viewModel.enabledFilterType.contains(.filter) { - MultiSelector( - label: L10n.filters, - options: viewModel.possibleItemFilters, - optionToString: { $0.localized }, - selected: $viewModel.modifiedFilters.filters - ) - } - if viewModel.enabledFilterType.contains(.tag) { - MultiSelector( - label: L10n.tags, - options: viewModel.possibleTags, - optionToString: { $0 }, - selected: $viewModel.modifiedFilters.tags - ) - } - if viewModel.enabledFilterType.contains(.sortBy) { - Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) { - ForEach(viewModel.possibleSortBys, id: \.self) { so in - Text(so.localized).tag(so) - } - } - } - if viewModel.enabledFilterType.contains(.sortOrder) { - Picker(selection: $viewModel.selectedSortOrder, label: L10n.displayOrder.text) { - ForEach(viewModel.possibleSortOrders, id: \.self) { so in - Text(so.rawValue).tag(so) - } - } - } - } - Button { - viewModel.resetFilters() - self.filters = viewModel.modifiedFilters - filterRouter.dismissCoordinator() - } label: { - L10n.reset.text - } - } - } - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - filterRouter.dismissCoordinator() - } label: { - Image(systemName: "xmark") - } - } - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - viewModel.updateModifiedFilter() - self.filters = viewModel.modifiedFilters - filterRouter.dismissCoordinator() - } label: { - L10n.apply.text - } - } - } - } -} diff --git a/Swiftfin tvOS/Views/LibraryView.swift b/Swiftfin tvOS/Views/LibraryView.swift index 4e953045..e871f3ba 100644 --- a/Swiftfin tvOS/Views/LibraryView.swift +++ b/Swiftfin tvOS/Views/LibraryView.swift @@ -21,7 +21,7 @@ struct LibraryView: View { private var scrollViewOffset: CGPoint = .zero @Default(.Customization.Library.gridPosterType) - var libraryPosterType + private var libraryPosterType @ViewBuilder private var loadingView: some View { diff --git a/Swiftfin tvOS/Views/MediaView.swift b/Swiftfin tvOS/Views/MediaView.swift index f55f9ca2..a96cd12d 100644 --- a/Swiftfin tvOS/Views/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView.swift @@ -27,11 +27,13 @@ struct MediaView: View { .onSelect { _ in switch item.library.collectionType { case "favorites": - router.route(to: \.library, (viewModel: .init(filters: .favorites), title: "")) + router.route(to: \.library, .init(parent: item.library, type: .library, filters: .favorites)) + case "folders": + router.route(to: \.library, .init(parent: item.library, type: .folders, filters: .init())) case "liveTV": tabRouter.root(\.liveTV) default: - router.route(to: \.library, (viewModel: .init(library: item.library), title: "")) + router.route(to: \.library, .init(parent: item.library, type: .library, filters: .init())) } } .imageOverlay { _ in diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index 7333b44c..480d4eec 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -142,7 +142,7 @@ struct SettingsView: View { HStack { L10n.version.text Spacer() - Text("\(UIApplication.appVersion ?? "--") (\(UIApplication.bundleVersion ?? "--"))") + Text("\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))") .foregroundColor(.secondary) } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveTVPlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveTVPlayerViewController.swift index 9ccdcba9..500090db 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveTVPlayerViewController.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveTVPlayerViewController.swift @@ -476,11 +476,11 @@ extension LiveTVPlayerViewController { viewModel = newViewModel if viewModel.streamType == .direct { - LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? .emptyDash)") } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { - LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? .emptyDash)") } else { - LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? .emptyDash)") } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift index 32ad9085..98bae2df 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -476,11 +476,11 @@ extension VLCPlayerViewController { viewModel = newViewModel if viewModel.streamType == .direct { - LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? .emptyDash)") } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { - LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? .emptyDash)") } else { - LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? .emptyDash)") } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 9e2887e6..d89ed37b 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -18,8 +18,7 @@ 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; - 5321753E2671DE9C005491E6 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; - 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; }; + 5321753E2671DE9C005491E6 /* ItemFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* ItemFilters.swift */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; 534D4FF026A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; @@ -34,7 +33,7 @@ 5358707E2669D64F00D05A09 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; 535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; }; 535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; - 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; + 535870AD2669D8DD00D05A09 /* ItemFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* ItemFilters.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; @@ -169,13 +168,10 @@ 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */; }; 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; }; 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; }; - 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; - 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */; }; 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62E632F4267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; 62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; }; - 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */; }; 6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */; }; @@ -243,6 +239,14 @@ E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; }; + E113132B28BDB4B500930F75 /* NavBarDrawerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */; }; + E113132F28BDB66A00930F75 /* NavBarDrawerModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */; }; + E113133228BDC72000930F75 /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133128BDC72000930F75 /* FilterView.swift */; }; + E113133428BE988200930F75 /* FilterDrawerHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133328BE988200930F75 /* FilterDrawerHStack.swift */; }; + E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133528BE98AA00930F75 /* FilterDrawerButton.swift */; }; + E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133728BEADBA00930F75 /* LibraryParent.swift */; }; + E113133A28BEB71D00930F75 /* FilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133928BEB71D00930F75 /* FilterViewModel.swift */; }; + E113133B28BEB71D00930F75 /* FilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133928BEB71D00930F75 /* FilterViewModel.swift */; }; E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */; }; E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.swift */; }; @@ -268,6 +272,7 @@ E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; }; E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; + E12B835F28C07D8500878399 /* LibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133728BEADBA00930F75 /* LibraryParent.swift */; }; E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB1279E3C6200BC6161 /* Puppy */; }; E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB5279E3CA500BC6161 /* Puppy */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; @@ -309,6 +314,13 @@ E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; }; E13F05F228BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; }; E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05F028BC9016003499D2 /* LibraryView.swift */; }; + E148128328C1443D003B8787 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; + E148128528C15472003B8787 /* APISortOrderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128428C15472003B8787 /* APISortOrderExtensions.swift */; }; + E148128628C15475003B8787 /* APISortOrderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128428C15472003B8787 /* APISortOrderExtensions.swift */; }; + E148128828C154BF003B8787 /* ItemFilterExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128728C154BF003B8787 /* ItemFilterExtensions.swift */; }; + E148128928C154BF003B8787 /* ItemFilterExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128728C154BF003B8787 /* ItemFilterExtensions.swift */; }; + E148128B28C15526003B8787 /* SortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128A28C15526003B8787 /* SortBy.swift */; }; + E148128C28C15526003B8787 /* SortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128A28C15526003B8787 /* SortBy.swift */; }; E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546776289AF46E00087E35 /* CollectionItemView.swift */; }; E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; }; E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.swift */; }; @@ -328,6 +340,14 @@ E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; }; E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; }; E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.swift */; }; + E17FB54F28C1197700311DFE /* SelectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB54E28C1197700311DFE /* SelectorType.swift */; }; + E17FB55028C1197700311DFE /* SelectorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB54E28C1197700311DFE /* SelectorType.swift */; }; + E17FB55228C119D400311DFE /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; }; + E17FB55328C119D400311DFE /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; }; + E17FB55528C1250B00311DFE /* SimilarItemsHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55428C1250B00311DFE /* SimilarItemsHStack.swift */; }; + E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */; }; + E17FB55928C125E900311DFE /* StudiosHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55828C125E900311DFE /* StudiosHStack.swift */; }; + E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55A28C1266400311DFE /* GenresHStack.swift */; }; E184C160288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */; }; E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */; }; E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */; }; @@ -387,8 +407,6 @@ E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */; }; E1937A61288F32DB00CB80AA /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; }; E1937A62288F32DB00CB80AA /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; }; - E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; }; - E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D4DA27193CCA00900D82 /* PillStackable.swift */; }; E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; @@ -404,7 +422,6 @@ E193D547271941C500900D82 /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D546271941C500900D82 /* UserListView.swift */; }; E193D549271941CC00900D82 /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D548271941CC00900D82 /* UserSignInView.swift */; }; E193D54B271941D300900D82 /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54A271941D300900D82 /* ServerListView.swift */; }; - E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54C2719426600900D82 /* LibraryFilterView.swift */; }; E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; }; E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; }; @@ -479,8 +496,8 @@ E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; }; E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; }; E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643928BAC2EF00323B0A /* SearchView.swift */; }; - E1E1643E28BB074000323B0A /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* MultiSelectorView.swift */; }; - E1E1643F28BB075C00323B0A /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* MultiSelectorView.swift */; }; + E1E1643E28BB074000323B0A /* SelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* SelectorView.swift */; }; + E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* SelectorView.swift */; }; E1E1644128BB301900323B0A /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644028BB301900323B0A /* ArrayExtensions.swift */; }; E1E1644228BB301900323B0A /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644028BB301900323B0A /* ArrayExtensions.swift */; }; E1E1644428BC60C600323B0A /* LibraryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644328BC60C600323B0A /* LibraryItem.swift */; }; @@ -575,7 +592,7 @@ 535870662669D21700D05A09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 535870692669D21700D05A09 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 535870702669D21700D05A09 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = ""; }; + 535870AC2669D8DD00D05A09 /* ItemFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemFilters.swift; sourceTree = ""; }; 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; 5362E4A7267D4067000E2F71 /* GoogleCast.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GoogleCast.framework; path = "../../Downloads/GoogleCastSDK-ios-4.6.0_dynamic/GoogleCast.framework"; sourceTree = ""; }; 5362E4AA267D40AD000E2F71 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; @@ -622,7 +639,6 @@ 53ABFDEA2679753200886593 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 53CD2A3F268A49C2002ABD4E /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; - 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; 53EE24E5265060780068F029 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = ""; }; 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VLCPlayer+subtitles.swift"; sourceTree = ""; }; @@ -687,10 +703,8 @@ 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemViewModel.swift; sourceTree = ""; }; 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemViewModel.swift; sourceTree = ""; }; 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeriesItemViewModel.swift; sourceTree = ""; }; - 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterViewModel.swift; sourceTree = ""; }; 62E632F2267D54030063E547 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; 62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; - 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectSettingsView.swift; sourceTree = ""; }; 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectSettingsViewModel.swift; sourceTree = ""; }; @@ -740,6 +754,13 @@ E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = ""; }; E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = ""; }; E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = ""; }; + E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarDrawerView.swift; sourceTree = ""; }; + E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBarDrawerModifier.swift; sourceTree = ""; }; + E113133128BDC72000930F75 /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = ""; }; + E113133328BE988200930F75 /* FilterDrawerHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterDrawerHStack.swift; sourceTree = ""; }; + E113133528BE98AA00930F75 /* FilterDrawerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterDrawerButton.swift; sourceTree = ""; }; + E113133728BEADBA00930F75 /* LibraryParent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryParent.swift; sourceTree = ""; }; + E113133928BEB71D00930F75 /* FilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterViewModel.swift; sourceTree = ""; }; E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectView.swift; sourceTree = ""; }; E118959C289312020042947B /* BaseItemPerson+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemPerson+Poster.swift"; sourceTree = ""; }; E11895A8289383BC0042947B /* ScrollViewOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewOffsetModifier.swift; sourceTree = ""; }; @@ -777,6 +798,9 @@ E13F05EB28BC9000003499D2 /* LibraryViewType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryViewType.swift; sourceTree = ""; }; E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryItemRow.swift; sourceTree = ""; }; E13F05F028BC9016003499D2 /* LibraryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; + E148128428C15472003B8787 /* APISortOrderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APISortOrderExtensions.swift; sourceTree = ""; }; + E148128728C154BF003B8787 /* ItemFilterExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemFilterExtensions.swift; sourceTree = ""; }; + E148128A28C15526003B8787 /* SortBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBy.swift; sourceTree = ""; }; E1546776289AF46E00087E35 /* CollectionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemView.swift; sourceTree = ""; }; E1546779289AF48200087E35 /* CollectionItemContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemContentView.swift; sourceTree = ""; }; E168BD08289A4162001A6922 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; @@ -793,6 +817,12 @@ E178859D2780F53B0094FBCF /* SliderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderView.swift; sourceTree = ""; }; E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVLCOverlay.swift; sourceTree = ""; }; E17885A3278105170094FBCF /* SFSymbolButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolButton.swift; sourceTree = ""; }; + E17FB54E28C1197700311DFE /* SelectorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorType.swift; sourceTree = ""; }; + E17FB55128C119D400311DFE /* Displayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Displayable.swift; sourceTree = ""; }; + E17FB55428C1250B00311DFE /* SimilarItemsHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarItemsHStack.swift; sourceTree = ""; }; + E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewHStack.swift; sourceTree = ""; }; + E17FB55828C125E900311DFE /* StudiosHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudiosHStack.swift; sourceTree = ""; }; + E17FB55A28C1266400311DFE /* GenresHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenresHStack.swift; sourceTree = ""; }; E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilderExtensions.swift; sourceTree = ""; }; E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Poster.swift"; sourceTree = ""; }; E18CE0AE28A222240092E7F1 /* PublicUserSignInView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicUserSignInView.swift; sourceTree = ""; }; @@ -841,12 +871,10 @@ E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScreenExtensions.swift; sourceTree = ""; }; E1937A60288F32DB00CB80AA /* Poster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poster.swift; sourceTree = ""; }; E1937A63288F683300CB80AA /* ContinueWatchingCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingCard.swift; sourceTree = ""; }; - E193D4DA27193CCA00900D82 /* PillStackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillStackable.swift; sourceTree = ""; }; E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainCoordinator.swift; sourceTree = ""; }; E193D546271941C500900D82 /* UserListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserListView.swift; sourceTree = ""; }; E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = ""; }; E193D54A271941D300900D82 /* ServerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerListView.swift; sourceTree = ""; }; - E193D54C2719426600900D82 /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = ""; }; E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomEdgeGradientModifier.swift; sourceTree = ""; }; @@ -902,7 +930,7 @@ E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = ""; }; E1E00A34278628A40022235B /* DoubleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleExtensions.swift; sourceTree = ""; }; E1E1643928BAC2EF00323B0A /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - E1E1643D28BB074000323B0A /* MultiSelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = ""; }; + E1E1643D28BB074000323B0A /* SelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorView.swift; sourceTree = ""; }; E1E1644028BB301900323B0A /* ArrayExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; E1E1644328BC60C600323B0A /* LibraryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryItem.swift; sourceTree = ""; }; E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = ""; }; @@ -1038,10 +1066,10 @@ E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */, 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, E10D87E127852FD000BD264C /* EpisodesRowManager.swift */, + E113133928BEB71D00930F75 /* FilterViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */, E107BB9127880A4000354E07 /* ItemViewModel */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, - 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, @@ -1155,21 +1183,23 @@ E1D4BF802719D22800A11E64 /* AppAppearance.swift */, E1D4BF862719D27100A11E64 /* Bitrates.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, - 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + E17FB55128C119D400311DFE /* Displayable.swift */, E19169CD272514760085832A /* HTTPScheme.swift */, + 535870AC2669D8DD00D05A09 /* ItemFilters.swift */, E1C925F328875037002A7A66 /* ItemViewType.swift */, E1E1644328BC60C600323B0A /* LibraryItem.swift */, + E113133728BEADBA00930F75 /* LibraryParent.swift */, E13F05EB28BC9000003499D2 /* LibraryViewType.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, - E193D4DA27193CCA00900D82 /* PillStackable.swift */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E1937A60288F32DB00CB80AA /* Poster.swift */, E1CCF12D28ABF989006CAC9E /* PosterType.swift */, E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */, + E17FB54E28C1197700311DFE /* SelectorType.swift */, + E148128A28C15526003B8787 /* SortBy.swift */, 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */, E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, - 535870AC2669D8DD00D05A09 /* Typings.swift */, E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */, ); path = Objects; @@ -1558,6 +1588,15 @@ path = ItemViewModel; sourceTree = ""; }; + E113133028BDB6D600930F75 /* NavBarDrawerButtons */ = { + isa = PBXGroup; + children = ( + E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */, + E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */, + ); + path = NavBarDrawerButtons; + sourceTree = ""; + }; E1171A1A28A2215800FA1AF5 /* UserSignInView */ = { isa = PBXGroup; children = ( @@ -1578,13 +1617,13 @@ path = ViewExtensions; sourceTree = ""; }; - E11895B12893842D0042947B /* NavBarOffsetModifier */ = { + E11895B12893842D0042947B /* NavBarOffset */ = { isa = PBXGroup; children = ( E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */, E11895AE2893840F0042947B /* NavBarOffsetView.swift */, ); - path = NavBarOffsetModifier; + path = NavBarOffset; sourceTree = ""; }; E11CEB85289984F5003E74C7 /* Extensions */ = { @@ -1598,8 +1637,9 @@ E11CEB8828998522003E74C7 /* iOSViewExtensions */ = { isa = PBXGroup; children = ( - E11895B12893842D0042947B /* NavBarOffsetModifier */, E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */, + E113133028BDB6D600930F75 /* NavBarDrawerButtons */, + E11895B12893842D0042947B /* NavBarOffset */, ); path = iOSViewExtensions; sourceTree = ""; @@ -1631,7 +1671,6 @@ 531690E6267ABD79005D8AB9 /* HomeView.swift */, E193D54E271942C000900D82 /* ItemView */, E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, - E193D54C2719426600900D82 /* LibraryFilterView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, @@ -1689,7 +1728,7 @@ E168BD07289A4162001A6922 /* HomeView */, E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */, E14F7D0A26DB3714007C3AE6 /* ItemView */, - 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, + E113133128BDC72000930F75 /* FilterView.swift */, E13F05EE28BC9016003499D2 /* LibraryView */, C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */, C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */, @@ -1848,8 +1887,8 @@ E18E01BD288747230022598C /* MovieItemView */ = { isa = PBXGroup; children = ( - E18E01BE288747230022598C /* iPadOSMovieItemView.swift */, E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */, + E18E01BE288747230022598C /* iPadOSMovieItemView.swift */, ); path = MovieItemView; sourceTree = ""; @@ -1888,8 +1927,8 @@ E18E01C8288747230022598C /* CollectionItemView */ = { isa = PBXGroup; children = ( - E18E01C9288747230022598C /* CollectionItemView.swift */, E18E01CA288747230022598C /* CollectionItemContentView.swift */, + E18E01C9288747230022598C /* CollectionItemView.swift */, ); path = CollectionItemView; sourceTree = ""; @@ -1915,12 +1954,16 @@ E18E01D4288747230022598C /* Components */ = { isa = PBXGroup; children = ( - E176DE6E278E3522001EFD8D /* EpisodesRowView */, E18E01D5288747230022598C /* AboutView.swift */, - E18E01D6288747230022598C /* ListDetailsView.swift */, - E18E01D7288747230022598C /* AttributeHStack.swift */, - E18E01D8288747230022598C /* PlayButton.swift */, E18E01D9288747230022598C /* ActionButtonHStack.swift */, + E18E01D7288747230022598C /* AttributeHStack.swift */, + E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */, + E176DE6E278E3522001EFD8D /* EpisodesRowView */, + E17FB55A28C1266400311DFE /* GenresHStack.swift */, + E18E01D6288747230022598C /* ListDetailsView.swift */, + E18E01D8288747230022598C /* PlayButton.swift */, + E17FB55428C1250B00311DFE /* SimilarItemsHStack.swift */, + E17FB55828C125E900311DFE /* StudiosHStack.swift */, ); path = Components; sourceTree = ""; @@ -1984,6 +2027,7 @@ E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { isa = PBXGroup; children = ( + E148128428C15472003B8787 /* APISortOrderExtensions.swift */, E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */, E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */, E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */, @@ -1994,6 +2038,7 @@ E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, + E148128728C154BF003B8787 /* ItemFilterExtensions.swift */, E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */, E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */, ); @@ -2010,8 +2055,8 @@ E18E01FF288749200022598C /* Divider.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, E1047E2227E5880000CB0D4A /* InitialFailureView.swift */, - E1E1643D28BB074000323B0A /* MultiSelectorView.swift */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, + E1E1643D28BB074000323B0A /* SelectorView.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */, ); path = Views; @@ -2029,6 +2074,8 @@ E1C55AB228BD051700A9AD88 /* Components */ = { isa = PBXGroup; children = ( + E113133528BE98AA00930F75 /* FilterDrawerButton.swift */, + E113133328BE988200930F75 /* FilterDrawerHStack.swift */, E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */, ); path = Components; @@ -2394,7 +2441,6 @@ E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */, C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */, E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, - E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */, C4B9B91427E1921B0063535C /* LiveTVNativeVideoPlayerView.swift in Sources */, @@ -2408,7 +2454,8 @@ E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */, E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, - E1E1643E28BB074000323B0A /* MultiSelectorView.swift in Sources */, + E17FB55328C119D400311DFE /* Displayable.swift in Sources */, + E1E1643E28BB074000323B0A /* SelectorView.swift in Sources */, E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, @@ -2432,6 +2479,7 @@ E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, + E12B835F28C07D8500878399 /* LibraryParent.swift in Sources */, E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, E18E021A2887492B0022598C /* AppIcon.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, @@ -2458,7 +2506,6 @@ 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */, 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, - 62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, E193D54B271941D300900D82 /* ServerListView.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */, @@ -2474,13 +2521,14 @@ E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */, E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */, E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */, + E148128328C1443D003B8787 /* NameGUIDPairExtensions.swift in Sources */, E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */, - E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */, C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */, E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, E1C926142887565C002A7A66 /* FocusGuide.swift in Sources */, + E148128928C154BF003B8787 /* ItemFilterExtensions.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, E1399475289B1EA900401ABC /* Defaults+Workaround.swift in Sources */, @@ -2489,6 +2537,7 @@ E18E02202887492B0022598C /* AttributeFillView.swift in Sources */, E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, + E148128C28C15526003B8787 /* SortBy.swift in Sources */, E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */, E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, @@ -2518,6 +2567,7 @@ 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */, E19169CF272514760085832A /* HTTPScheme.swift in Sources */, + E148128628C15475003B8787 /* APISortOrderExtensions.swift in Sources */, E1E1644528BC60C600323B0A /* LibraryItem.swift in Sources */, E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */, E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */, @@ -2542,7 +2592,7 @@ E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, - 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, + 5321753E2671DE9C005491E6 /* ItemFilters.swift in Sources */, E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */, E1AA33202782639D00F6439C /* OverlayType.swift in Sources */, C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */, @@ -2554,6 +2604,7 @@ E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */, C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */, E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, + E113133B28BEB71D00930F75 /* FilterViewModel.swift in Sources */, E13F05ED28BC9000003499D2 /* LibraryViewType.swift in Sources */, E18E021C2887492B0022598C /* BlurView.swift in Sources */, E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */, @@ -2576,6 +2627,7 @@ E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */, C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */, + E17FB55028C1197700311DFE /* SelectorType.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */, @@ -2586,6 +2638,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E17FB55528C1250B00311DFE /* SimilarItemsHStack.swift in Sources */, 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */, @@ -2596,6 +2649,7 @@ 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */, E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, + E17FB55928C125E900311DFE /* StudiosHStack.swift in Sources */, E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */, E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */, E1C925F428875037002A7A66 /* ItemViewType.swift in Sources */, @@ -2603,6 +2657,7 @@ 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */, 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, + E148128828C154BF003B8787 /* ItemFilterExtensions.swift in Sources */, C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */, @@ -2610,7 +2665,7 @@ E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */, E18E0208288749200022598C /* BlurView.swift in Sources */, E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */, - E1E1643F28BB075C00323B0A /* MultiSelectorView.swift in Sources */, + E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */, E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */, E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */, @@ -2626,6 +2681,7 @@ E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */, E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */, + E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */, E18E01FA288747580022598C /* AboutAppView.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, @@ -2639,8 +2695,10 @@ E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */, C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E17FB55228C119D400311DFE /* Displayable.swift in Sources */, E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, + E113132B28BDB4B500930F75 /* NavBarDrawerView.swift in Sources */, C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, E19169CE272514760085832A /* HTTPScheme.swift in Sources */, @@ -2670,9 +2728,9 @@ C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + E113133428BE988200930F75 /* FilterDrawerHStack.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, - 532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */, E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */, E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */, E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, @@ -2681,6 +2739,7 @@ E18CE0B228A229E70092E7F1 /* UserDtoExtensions.swift in Sources */, E18E01F0288747230022598C /* AttributeHStack.swift in Sources */, 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */, + E17FB54F28C1197700311DFE /* SelectorType.swift in Sources */, E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */, E18E0205288749200022598C /* AppIcon.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */, @@ -2693,6 +2752,7 @@ 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, + E148128B28C15526003B8787 /* SortBy.swift in Sources */, 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, @@ -2703,8 +2763,10 @@ E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, + E113133228BDC72000930F75 /* FilterView.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */, + E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, @@ -2727,12 +2789,13 @@ E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */, E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */, + E113132F28BDB66A00930F75 /* NavBarDrawerModifier.swift in Sources */, C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */, + E113133A28BEB71D00930F75 /* FilterViewModel.swift in Sources */, E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */, E18E01EE288747230022598C /* AboutView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */, - E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */, E1E1644428BC60C600323B0A /* LibraryItem.swift in Sources */, E18E0206288749200022598C /* AttributeFillView.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, @@ -2746,13 +2809,14 @@ E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, + E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, + E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */, E1937A3E288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */, E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */, E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, - 62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */, 5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */, C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */, E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */, @@ -2761,7 +2825,7 @@ E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, - 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, + 535870AD2669D8DD00D05A09 /* ItemFilters.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, E18E01F1288747230022598C /* PlayButton.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, @@ -2771,7 +2835,6 @@ 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */, - 62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */, E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */, E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */, @@ -2781,6 +2844,7 @@ 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, + E148128528C15472003B8787 /* APISortOrderExtensions.swift in Sources */, E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */, diff --git a/Swiftfin/Components/PillHStack.swift b/Swiftfin/Components/PillHStack.swift index 032c9eb5..525299a5 100644 --- a/Swiftfin/Components/PillHStack.swift +++ b/Swiftfin/Components/PillHStack.swift @@ -8,11 +8,11 @@ import SwiftUI -struct PillHStack: View { +struct PillHStack: View { - let title: String - let items: [Item] - let onSelect: (Item) -> Void + private var title: String + private var items: [Item] + private var onSelect: (Item) -> Void private init( title: String, @@ -37,11 +37,11 @@ struct PillHStack: View { ScrollView(.horizontal, showsIndicators: false) { HStack { - ForEach(items, id: \.title) { item in + ForEach(items, id: \.displayName) { item in Button { onSelect(item) } label: { - Text(item.title) + Text(item.displayName) .font(.caption) .fontWeight(.semibold) .foregroundColor(.primary) @@ -68,12 +68,9 @@ extension PillHStack { self.init(title: title, items: items, onSelect: { _ in }) } - @ViewBuilder - func onSelect(_ onSelect: @escaping (Item) -> Void) -> PillHStack { - PillHStack( - title: title, - items: items, - onSelect: onSelect - ) + func onSelect(_ onSelect: @escaping (Item) -> Void) -> Self { + var copy = self + copy.onSelect = onSelect + return copy } } diff --git a/Swiftfin/Components/PosterButton.swift b/Swiftfin/Components/PosterButton.swift index 17fedb75..86d4f700 100644 --- a/Swiftfin/Components/PosterButton.swift +++ b/Swiftfin/Components/PosterButton.swift @@ -196,7 +196,7 @@ struct PosterButtonDefaultContentView: View { var body: some View { VStack(alignment: .leading) { if item.showTitle { - Text(item.title) + Text(item.displayName) .font(.footnote) .fontWeight(.regular) .foregroundColor(.primary) diff --git a/Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerModifier.swift b/Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerModifier.swift new file mode 100644 index 00000000..25199200 --- /dev/null +++ b/Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerModifier.swift @@ -0,0 +1,28 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct NavBarDrawerModifier: ViewModifier { + + let drawer: () -> Drawer + + init(@ViewBuilder drawer: @escaping () -> Drawer) { + self.drawer = drawer + } + + func body(content: Content) -> some View { + NavBarDrawerView { + drawer() + .ignoresSafeArea() + } content: { + content + } + .ignoresSafeArea() + } +} diff --git a/Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerView.swift b/Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerView.swift new file mode 100644 index 00000000..09ba9876 --- /dev/null +++ b/Swiftfin/Extensions/iOSViewExtensions/NavBarDrawerButtons/NavBarDrawerView.swift @@ -0,0 +1,127 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +private let drawerHeight: CGFloat = 36 + +struct NavBarDrawerView: UIViewControllerRepresentable { + + private let buttons: () -> Buttons + private let content: () -> Content + + init( + @ViewBuilder buttons: @escaping () -> Buttons, + @ViewBuilder content: @escaping () -> Content + ) { + self.buttons = buttons + self.content = content + } + + func makeUIViewController(context: Context) -> UINavBarDrawerHostingController { + UINavBarDrawerHostingController(buttons: buttons, content: content) + } + + func updateUIViewController(_ uiViewController: UINavBarDrawerHostingController, context: Context) {} +} + +class UINavBarDrawerHostingController: UIViewController { + + private let buttons: () -> Buttons + private let content: () -> Content + + private lazy var navBarBlurView: UIVisualEffectView = { + let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial)) + blurView.translatesAutoresizingMaskIntoConstraints = false + return blurView + }() + + private lazy var contentView: UIHostingController = { + let contentView = UIHostingController(rootView: content()) + contentView.view.translatesAutoresizingMaskIntoConstraints = false + contentView.view.backgroundColor = nil + return contentView + }() + + private lazy var drawerButtonsView: UIHostingController = { + let drawerButtonsView = UIHostingController(rootView: buttons()) + drawerButtonsView.view.translatesAutoresizingMaskIntoConstraints = false + drawerButtonsView.view.backgroundColor = nil + return drawerButtonsView + }() + + init( + buttons: @escaping () -> Buttons, + content: @escaping () -> Content + ) { + self.buttons = buttons + self.content = content + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = nil + + addChild(contentView) + view.addSubview(contentView.view) + contentView.didMove(toParent: self) + + view.addSubview(navBarBlurView) + + addChild(drawerButtonsView) + view.addSubview(drawerButtonsView.view) + drawerButtonsView.didMove(toParent: self) + + NSLayoutConstraint.activate([ + drawerButtonsView.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: -drawerHeight), + drawerButtonsView.view.heightAnchor.constraint(equalToConstant: drawerHeight), + 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), + ]) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + self.navigationController?.navigationBar.shadowImage = UIImage() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.navigationController?.navigationBar.setBackgroundImage(nil, for: .default) + self.navigationController?.navigationBar.shadowImage = nil + } + + override var additionalSafeAreaInsets: UIEdgeInsets { + get { + .init(top: drawerHeight, left: 0, bottom: 0, right: 0) + } + set { + super.additionalSafeAreaInsets = .init(top: drawerHeight, left: 0, bottom: 0, right: 0) + } + } +} diff --git a/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetModifier.swift b/Swiftfin/Extensions/iOSViewExtensions/NavBarOffset/NavBarOffsetModifier.swift similarity index 100% rename from Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetModifier.swift rename to Swiftfin/Extensions/iOSViewExtensions/NavBarOffset/NavBarOffsetModifier.swift diff --git a/Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetView.swift b/Swiftfin/Extensions/iOSViewExtensions/NavBarOffset/NavBarOffsetView.swift similarity index 100% rename from Swiftfin/Extensions/iOSViewExtensions/NavBarOffsetModifier/NavBarOffsetView.swift rename to Swiftfin/Extensions/iOSViewExtensions/NavBarOffset/NavBarOffsetView.swift diff --git a/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift b/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift index 3ebdf829..caa46caf 100644 --- a/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift +++ b/Swiftfin/Extensions/iOSViewExtensions/iOSViewExtensions.swift @@ -12,4 +12,8 @@ extension View { func navBarOffset(_ scrollViewOffset: Binding, start: CGFloat, end: CGFloat) -> some View { self.modifier(NavBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end)) } + + func navBarDrawer(@ViewBuilder _ drawer: @escaping () -> Drawer) -> some View { + self.modifier(NavBarDrawerModifier(drawer: drawer)) + } } diff --git a/Swiftfin/Views/AboutAppView.swift b/Swiftfin/Views/AboutAppView.swift index 1ed8d379..2c1781f9 100644 --- a/Swiftfin/Views/AboutAppView.swift +++ b/Swiftfin/Views/AboutAppView.swift @@ -37,7 +37,7 @@ struct AboutAppView: View { HStack { L10n.about.text Spacer() - Text("\(UIApplication.appVersion ?? "--") (\(UIApplication.bundleVersion ?? "--"))") + Text("\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))") .foregroundColor(.secondary) } diff --git a/Swiftfin/Views/FilterView.swift b/Swiftfin/Views/FilterView.swift new file mode 100644 index 00000000..d293ffb4 --- /dev/null +++ b/Swiftfin/Views/FilterView.swift @@ -0,0 +1,64 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct FilterView: View { + + @EnvironmentObject + private var router: FilterCoordinator.Router + + @ObservedObject + private var viewModel: FilterViewModel + + private let title: String + private let filter: WritableKeyPath + private let selectedFiltersBinding: Binding<[ItemFilters.Filter]> + private let selectorType: SelectorType + + init( + title: String, + viewModel: FilterViewModel, + filter: WritableKeyPath, + selectorType: SelectorType + ) { + self.title = title + self.viewModel = viewModel + self.filter = filter + self.selectorType = selectorType + + self.selectedFiltersBinding = Binding(get: { + viewModel.currentFilters[keyPath: filter] + }, set: { newValue, _ in + viewModel.currentFilters[keyPath: filter] = newValue + }) + } + + var body: some View { + + VStack { + SelectorView( + type: selectorType, + allItems: viewModel.allFilters[keyPath: filter], + selectedItems: selectedFiltersBinding + ) + } + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + router.dismissCoordinator() + } label: { + Image(systemName: "xmark.circle.fill") + } + } + } + } +} diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index 2dd9b4b2..fe221cbb 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -24,8 +24,7 @@ struct LatestInLibraryView: View { PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: latestInLibraryPosterType, items: viewModel.items) .trailing { Button { - let libraryViewModel = LibraryViewModel(library: viewModel.library, filters: HomeViewModel.recentFilterSet) - homeRouter.route(to: \.library, (viewModel: libraryViewModel, title: viewModel.library.displayName)) + homeRouter.route(to: \.library, .init(parent: viewModel.library, type: .library, filters: .recent)) } label: { HStack { L10n.seeAll.text diff --git a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift new file mode 100644 index 00000000..ad105c33 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift @@ -0,0 +1,30 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemView { + struct CastAndCrewHStack: View { + + @EnvironmentObject + private var router: ItemCoordinator.Router + let people: [BaseItemPerson] + + var body: some View { + PosterHStack( + title: L10n.castAndCrew, + type: .portrait, + items: people + ) + .onSelect { person in + router.route(to: \.library, .init(parent: person, type: .person, filters: .init())) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/GenresHStack.swift b/Swiftfin/Views/ItemView/Components/GenresHStack.swift new file mode 100644 index 00000000..303193a4 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/GenresHStack.swift @@ -0,0 +1,28 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemView { + struct GenresHStack: View { + + @EnvironmentObject + private var router: ItemCoordinator.Router + let genres: [NameGuidPair] + + var body: some View { + PillHStack( + title: L10n.genres, + items: genres + ).onSelect { genre in + router.route(to: \.library, .init(filters: .init(genres: [genre.filter]))) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift new file mode 100644 index 00000000..06d78253 --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift @@ -0,0 +1,30 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemView { + struct SimilarItemsHStack: View { + + @EnvironmentObject + private var router: ItemCoordinator.Router + let items: [BaseItemDto] + + var body: some View { + PosterHStack( + title: L10n.recommended, + type: .portrait, + items: items + ) + .onSelect { item in + router.route(to: \.item, item) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift new file mode 100644 index 00000000..3fe318db --- /dev/null +++ b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift @@ -0,0 +1,28 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemView { + struct StudiosHStack: View { + + @EnvironmentObject + private var router: ItemCoordinator.Router + let studios: [NameGuidPair] + + var body: some View { + PillHStack( + title: L10n.studios, + items: studios + ).onSelect { studio in + router.route(to: \.library, .init(parent: studio, type: .studio, filters: .init())) + } + } + } +} diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 1b8b5461..35ad436c 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -43,9 +43,9 @@ struct ItemView: View { CollectionItemView(viewModel: .init(item: item)) } case .person: - LibraryView(viewModel: .init(person: .init(id: item.id))) + LibraryView(viewModel: .init(parent: item, type: .person)) case .collectionFolder: - LibraryView(viewModel: .init(library: item)) + LibraryView(viewModel: .init(parent: item, type: .folders)) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift index 59949b61..8e8d9ccc 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift @@ -23,25 +23,15 @@ extension CollectionItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } // MARK: Studios - if let studios = viewModel.item.studios { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + if let studios = viewModel.item.studios, !studios.isEmpty { + ItemView.StudiosHStack(studios: studios) Divider() } diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift index 6c9a12c1..2856c6ea 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift @@ -45,34 +45,25 @@ extension EpisodeItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } + // MARK: Studios + if let studios = viewModel.item.studios, !studios.isEmpty { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + ItemView.StudiosHStack(studios: studios) Divider() } + // MARK: Cast and Crew + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { - PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) - .onSelect { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } + ItemView.CastAndCrewHStack(people: castAndCrew) Divider() } @@ -99,7 +90,7 @@ extension EpisodeItemView.ContentView { var body: some View { VStack(alignment: .center, spacing: 10) { - Text(viewModel.item.seriesName ?? "--") + Text(viewModel.item.seriesName ?? .emptyDash) .font(.headline) .fontWeight(.semibold) .multilineTextAlignment(.center) diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift index 05b18f71..f34ae937 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift @@ -25,12 +25,7 @@ extension MovieItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } @@ -38,12 +33,7 @@ extension MovieItemView { // MARK: Studios if let studios = viewModel.item.studios, !studios.isEmpty { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + ItemView.StudiosHStack(studios: studios) Divider() } @@ -53,10 +43,7 @@ extension MovieItemView { if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { - PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) - .onSelect { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } + ItemView.CastAndCrewHStack(people: castAndCrew) Divider() } @@ -64,10 +51,7 @@ extension MovieItemView { // MARK: Similar if !viewModel.similarItems.isEmpty { - PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems) - .onSelect { item in - itemRouter.route(to: \.item, item) - } + ItemView.SimilarItemsHStack(items: viewModel.similarItems) Divider() } diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift index e0991e57..ab7db080 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift @@ -29,9 +29,7 @@ extension SeriesItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack(title: L10n.genres, items: genres).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } @@ -39,23 +37,17 @@ extension SeriesItemView { // MARK: Studios if let studios = viewModel.item.studios, !studios.isEmpty { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + ItemView.StudiosHStack(studios: studios) Divider() } // MARK: Cast and Crew - if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { - PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) - .onSelect { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), + !castAndCrew.isEmpty + { + ItemView.CastAndCrewHStack(people: castAndCrew) Divider() } @@ -63,10 +55,7 @@ extension SeriesItemView { // MARK: Similar if !viewModel.similarItems.isEmpty { - PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems) - .onSelect { item in - itemRouter.route(to: \.item, item) - } + ItemView.SimilarItemsHStack(items: viewModel.similarItems) Divider() } diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift index 673d497e..0c462f4d 100644 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift @@ -23,25 +23,15 @@ extension iPadOSCollectionItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } // MARK: Studios - if let studios = viewModel.item.studios { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + if let studios = viewModel.item.studios, !studios.isEmpty { + ItemView.StudiosHStack(studios: studios) Divider() } diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift index 91b02cd6..fc096aec 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift @@ -24,34 +24,25 @@ extension iPadOSEpisodeItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } + // MARK: Studios + if let studios = viewModel.item.studios, !studios.isEmpty { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + ItemView.StudiosHStack(studios: studios) Divider() } + // MARK: Cast and Crew + if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { - PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) - .onSelect { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } + ItemView.CastAndCrewHStack(people: castAndCrew) Divider() } diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift index 82dad318..947cba2c 100644 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift @@ -24,13 +24,7 @@ extension iPadOSMovieItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ) - .onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } @@ -38,12 +32,7 @@ extension iPadOSMovieItemView { // MARK: Studios if let studios = viewModel.item.studios, !studios.isEmpty { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + ItemView.StudiosHStack(studios: studios) Divider() } @@ -53,10 +42,7 @@ extension iPadOSMovieItemView { if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { - PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) - .onSelect { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } + ItemView.CastAndCrewHStack(people: castAndCrew) Divider() } diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift index 469637da..1d478043 100644 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift @@ -28,12 +28,7 @@ extension iPadOSSeriesItemView { // MARK: Genres if let genres = viewModel.item.genreItems, !genres.isEmpty { - PillHStack( - title: L10n.genres, - items: genres - ).onSelect { genre in - itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) - } + ItemView.GenresHStack(genres: genres) Divider() } @@ -41,12 +36,7 @@ extension iPadOSSeriesItemView { // MARK: Studios if let studios = viewModel.item.studios, !studios.isEmpty { - PillHStack( - title: L10n.studios, - items: studios - ).onSelect { studio in - itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? "")) - } + ItemView.StudiosHStack(studios: studios) Divider() } @@ -56,10 +46,7 @@ extension iPadOSSeriesItemView { if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { - PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) - .onSelect { person in - itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) - } + ItemView.CastAndCrewHStack(people: castAndCrew) Divider() } diff --git a/Swiftfin/Views/LibraryFilterView.swift b/Swiftfin/Views/LibraryFilterView.swift deleted file mode 100644 index 06bcb4ec..00000000 --- a/Swiftfin/Views/LibraryFilterView.swift +++ /dev/null @@ -1,105 +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) 2022 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import Stinsen -import SwiftUI - -struct LibraryFilterView: View { - - @EnvironmentObject - private var filterRouter: FilterCoordinator.Router - @Binding - var filters: LibraryFilters - var parentId: String = "" - - @StateObject - var viewModel: LibraryFilterViewModel - - init(filters: Binding, enabledFilterType: [FilterType], parentId: String) { - _filters = filters - self.parentId = parentId - _viewModel = - StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId)) - } - - var body: some View { - VStack { - if viewModel.isLoading { - ProgressView() - } else { - Form { - if viewModel.enabledFilterType.contains(.genre) { - MultiSelector( - label: L10n.genres, - options: viewModel.possibleGenres, - optionToString: { $0.name ?? "" }, - selected: $viewModel.modifiedFilters.withGenres - ) - } - if viewModel.enabledFilterType.contains(.filter) { - MultiSelector( - label: L10n.filters, - options: viewModel.possibleItemFilters, - optionToString: { $0.localized }, - selected: $viewModel.modifiedFilters.filters - ) - } - if viewModel.enabledFilterType.contains(.tag) { - MultiSelector( - label: L10n.tags, - options: viewModel.possibleTags, - optionToString: { $0 }, - selected: $viewModel.modifiedFilters.tags - ) - } - if viewModel.enabledFilterType.contains(.sortBy) { - Picker(selection: $viewModel.selectedSortBy, label: L10n.sortBy.text) { - ForEach(viewModel.possibleSortBys, id: \.self) { so in - Text(so.localized).tag(so) - } - } - } - if viewModel.enabledFilterType.contains(.sortOrder) { - Picker(selection: $viewModel.selectedSortOrder, label: L10n.displayOrder.text) { - ForEach(viewModel.possibleSortOrders, id: \.self) { so in - Text(so.rawValue).tag(so) - } - } - } - } - Button { - viewModel.resetFilters() - self.filters = viewModel.modifiedFilters - filterRouter.dismissCoordinator() - } label: { - L10n.reset.text - } - } - } - .navigationBarTitle(L10n.filterResults, displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - filterRouter.dismissCoordinator() - } label: { - Image(systemName: "xmark") - } - } - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - viewModel.updateModifiedFilter() - self.filters = viewModel.modifiedFilters - filterRouter.dismissCoordinator() - } label: { - L10n.apply.text - } - } - } - } -} diff --git a/Swiftfin/Views/LibraryView/Components/FilterDrawerButton.swift b/Swiftfin/Views/LibraryView/Components/FilterDrawerButton.swift new file mode 100644 index 00000000..bb66d794 --- /dev/null +++ b/Swiftfin/Views/LibraryView/Components/FilterDrawerButton.swift @@ -0,0 +1,89 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension FilterDrawerHStack { + struct FilterDrawerButton: View { + + private let systemName: String? + private let title: String + private let activated: Bool + private var onSelect: () -> Void + + private init( + systemName: String?, + title: String, + activated: Bool, + onSelect: @escaping () -> Void + ) { + self.systemName = systemName + self.title = title + self.activated = activated + self.onSelect = onSelect + } + + var body: some View { + Button { + onSelect() + } label: { + HStack(spacing: 2) { + Group { + if let systemName = systemName { + Image(systemName: systemName) + } else { + Text(title) + } + } + .font(.footnote.weight(.semibold)) + + Image(systemName: "chevron.down") + .font(.caption) + } + .foregroundColor(.primary) + .padding(.horizontal, 10) + .padding(.vertical, 5) + .background { + Capsule() + .foregroundColor(activated ? .jellyfinPurple : Color(UIColor.secondarySystemFill)) + .opacity(0.5) + } + .overlay( + Capsule() + .stroke(activated ? .purple : Color(UIColor.secondarySystemFill), lineWidth: 1) + ) + } + } + } +} + +extension FilterDrawerHStack.FilterDrawerButton { + init(title: String, activated: Bool) { + self.init( + systemName: nil, + title: title, + activated: activated, + onSelect: {} + ) + } + + init(systemName: String, activated: Bool) { + self.init( + systemName: systemName, + title: "", + activated: activated, + onSelect: {} + ) + } + + func onSelect(_ action: @escaping () -> Void) -> Self { + var copy = self + copy.onSelect = action + return copy + } +} diff --git a/Swiftfin/Views/LibraryView/Components/FilterDrawerHStack.swift b/Swiftfin/Views/LibraryView/Components/FilterDrawerHStack.swift new file mode 100644 index 00000000..5cbdf7c1 --- /dev/null +++ b/Swiftfin/Views/LibraryView/Components/FilterDrawerHStack.swift @@ -0,0 +1,98 @@ +// +// 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) 2022 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct FilterDrawerHStack: View { + + @ObservedObject + var viewModel: FilterViewModel + private var onSelect: (FilterCoordinator.Parameters) -> Void + + var body: some View { + HStack { + if viewModel.currentFilters.hasFilters { + Menu { + Button(role: .destructive) { + viewModel.currentFilters = .init() + } label: { + L10n.reset.text + } + } label: { + FilterDrawerButton(systemName: "line.3.horizontal.decrease.circle.fill", activated: true) + } + } + + FilterDrawerButton(title: L10n.genres, activated: viewModel.currentFilters.genres != []) + .onSelect { + onSelect(.init( + title: L10n.genres, + viewModel: viewModel, + filter: \.genres, + selectorType: .multi + )) + } + + FilterDrawerButton(title: L10n.tags, activated: viewModel.currentFilters.tags != []) + .onSelect { + onSelect(.init( + title: L10n.tags, + viewModel: viewModel, + filter: \.tags, + selectorType: .multi + )) + } + + FilterDrawerButton(title: L10n.filters, activated: viewModel.currentFilters.filters != []) + .onSelect { + onSelect(.init( + title: L10n.filters, + viewModel: viewModel, + filter: \.filters, + selectorType: .multi + )) + } + + // TODO: Localize + FilterDrawerButton(title: "Order", activated: viewModel.currentFilters.sortOrder != [APISortOrder.ascending.filter]) + .onSelect { + onSelect(.init( + title: "Order", + viewModel: viewModel, + filter: \.sortOrder, + selectorType: .single + )) + } + + // TODO: Localize + FilterDrawerButton(title: "Sort", activated: viewModel.currentFilters.sortBy != [SortBy.name.filter]) + .onSelect { + onSelect(.init( + title: "Sort", + viewModel: viewModel, + filter: \.sortBy, + selectorType: .single + )) + } + } + } +} + +extension FilterDrawerHStack { + init(viewModel: FilterViewModel) { + self.viewModel = viewModel + self.onSelect = { _ in } + } + + func onSelect(_ onSelect: @escaping (FilterCoordinator.Parameters) -> Void) -> Self { + var copy = self + copy.onSelect = onSelect + return copy + } +} diff --git a/Swiftfin/Views/LibraryView/LibraryView.swift b/Swiftfin/Views/LibraryView/LibraryView.swift index 4abd5814..758e034b 100644 --- a/Swiftfin/Views/LibraryView/LibraryView.swift +++ b/Swiftfin/Views/LibraryView/LibraryView.swift @@ -102,7 +102,18 @@ struct LibraryView: View { } } } + .navigationTitle(viewModel.parent?.displayName ?? "") .navigationBarTitleDisplayMode(.inline) + .navBarDrawer { + ScrollView(.horizontal, showsIndicators: false) { + FilterDrawerHStack(viewModel: viewModel.filterViewModel) + .onSelect { filterCoordinatorParameters in + router.route(to: \.filter, filterCoordinatorParameters) + } + .padding(.horizontal) + .padding(.vertical, 1) + } + } .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { @@ -120,18 +131,6 @@ struct LibraryView: View { Image(systemName: "square.grid.2x2") } } - - Button { - router - .route(to: \.filter, ( - filters: $viewModel.filters, - enabledFilterType: viewModel.enabledFilterType, - parentId: viewModel.library?.id ?? "" - )) - } label: { - Image(systemName: "line.horizontal.3.decrease.circle") - } - .foregroundColor(viewModel.filters == .default ? .accentColor : Color(UIColor.systemOrange)) } } } diff --git a/Swiftfin/Views/MediaView.swift b/Swiftfin/Views/MediaView.swift index 80657a8f..530c993f 100644 --- a/Swiftfin/Views/MediaView.swift +++ b/Swiftfin/Views/MediaView.swift @@ -33,11 +33,13 @@ struct MediaView: View { .onSelect { _ in switch item.library.collectionType { case "favorites": - router.route(to: \.library, (viewModel: .init(filters: .favorites), title: "")) + router.route(to: \.library, .init(parent: item.library, type: .library, filters: .favorites)) + case "folders": + router.route(to: \.library, .init(parent: item.library, type: .folders, filters: .init())) case "liveTV": router.route(to: \.liveTV) default: - router.route(to: \.library, (viewModel: .init(library: item.library), title: "")) + router.route(to: \.library, .init(parent: item.library, type: .library, filters: .init())) } } .imageOverlay { _ in diff --git a/Swiftfin/Views/SearchView.swift b/Swiftfin/Views/SearchView.swift index 230ab5b1..3c1c0f11 100644 --- a/Swiftfin/Views/SearchView.swift +++ b/Swiftfin/Views/SearchView.swift @@ -95,8 +95,18 @@ struct SearchView: View { .onChange(of: searchText) { newText in viewModel.search(with: newText) } - .searchable(text: $searchText, placement: .navigationBarDrawer, prompt: L10n.search) .navigationTitle(L10n.search) .navigationBarTitleDisplayMode(.inline) + .navBarDrawer { + ScrollView(.horizontal, showsIndicators: false) { + FilterDrawerHStack(viewModel: viewModel.filterViewModel) + .onSelect { filterCoordinatorParameters in + router.route(to: \.filter, filterCoordinatorParameters) + } + .padding(.horizontal) + .padding(.vertical, 1) + } + } + .searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: L10n.search) } } diff --git a/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift b/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift index 9014bfab..1c390262 100644 --- a/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/Components/PublicUserSignInView.swift @@ -25,7 +25,7 @@ extension UserSignInView { DisclosureGroup { SecureField(L10n.password, text: $enteredPassword) Button { - viewModel.signIn(username: publicUser.name ?? "--", password: enteredPassword) + viewModel.signIn(username: publicUser.name ?? .emptyDash, password: enteredPassword) } label: { L10n.signIn.text } @@ -39,7 +39,7 @@ extension UserSignInView { .frame(width: 50, height: 50) .clipShape(Circle()) - Text(publicUser.name ?? "--") + Text(publicUser.name ?? .emptyDash) Spacer() } } diff --git a/Swiftfin/Views/VideoPlayer/LiveTVPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/LiveTVPlayerViewController.swift index f8f2a1ff..e4b36067 100644 --- a/Swiftfin/Views/VideoPlayer/LiveTVPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/LiveTVPlayerViewController.swift @@ -532,11 +532,11 @@ extension LiveTVPlayerViewController { viewModel = newViewModel if viewModel.streamType == .direct { - LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? .emptyDash)") } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { - LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? .emptyDash)") } else { - LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? .emptyDash)") } } diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index ac7cc6f6..ea29459b 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -351,7 +351,7 @@ struct VLCPlayerOverlayView: View { viewModel.playerOverlayDelegate?.didSelectChapters() } label: { HStack { - Text(currentChapter.name ?? "--") + Text(currentChapter.name ?? .emptyDash) Image(systemName: "chevron.right") } .font(.system(size: 16, weight: .semibold, design: .default)) diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 07659dfb..92e58782 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -615,11 +615,11 @@ extension VLCPlayerViewController { viewModel = newViewModel if viewModel.streamType == .direct { - LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? .emptyDash)") } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { - LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? .emptyDash)") } else { - LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") + LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? .emptyDash)") } }