diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index adcaff91..e098a7ff 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -19,8 +19,6 @@ final class LibraryCoordinator: NavigationCoordinatable { @Root var start = makeStart - @Route(.push) - var search = makeSearch @Route(.modal) var filter = makeFilter @@ -45,10 +43,6 @@ final class LibraryCoordinator: NavigationCoordinatable { LibraryView(viewModel: self.viewModel) } - func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { - SearchCoordinator(viewModel: viewModel) - } - func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator { NavigationViewCoordinator(FilterCoordinator( filters: params.filters, diff --git a/Shared/Coordinators/LibraryListCoordinator.swift b/Shared/Coordinators/LibraryListCoordinator.swift index 701f6c7a..dceec45f 100644 --- a/Shared/Coordinators/LibraryListCoordinator.swift +++ b/Shared/Coordinators/LibraryListCoordinator.swift @@ -17,8 +17,6 @@ final class LibraryListCoordinator: NavigationCoordinatable { @Root var start = makeStart @Route(.push) - var search = makeSearch - @Route(.push) var library = makeLibrary #if os(iOS) @Route(.push) @@ -35,10 +33,6 @@ final class LibraryListCoordinator: NavigationCoordinatable { LibraryCoordinator(viewModel: params.viewModel, title: params.title) } - func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator { - SearchCoordinator(viewModel: viewModel) - } - #if os(iOS) func makeLiveTV() -> LiveTVCoordinator { LiveTVCoordinator() diff --git a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index 4c547c03..d6d8eb88 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -13,11 +13,14 @@ import SwiftUI final class MainTabCoordinator: TabCoordinatable { var child = TabChild(startingItems: [ \MainTabCoordinator.home, + \MainTabCoordinator.search, \MainTabCoordinator.allMedia, ]) @Route(tabItem: makeHomeTab, onTapped: onHomeTapped) var home = makeHome + @Route(tabItem: makeSearchTab, onTapped: onSearchTapped) + var search = makeSearch @Route(tabItem: makeAllMediaTab, onTapped: onMediaTapped) var allMedia = makeAllMedia @@ -37,6 +40,22 @@ final class MainTabCoordinator: TabCoordinatable { L10n.home.text } + func makeSearch() -> NavigationViewCoordinator { + NavigationViewCoordinator(SearchCoordinator()) + } + + func onSearchTapped(isRepeat: Bool, coordinator: NavigationViewCoordinator) { + if isRepeat { + coordinator.child.popToRoot() + } + } + + @ViewBuilder + func makeSearchTab(isActive: Bool) -> some View { + Image(systemName: "magnifyingglass") + L10n.search.text + } + func makeAllMedia() -> NavigationViewCoordinator { NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) } diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 8a5f0c5c..47f879c4 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -15,6 +15,7 @@ final class MainTabCoordinator: TabCoordinatable { \MainTabCoordinator.home, \MainTabCoordinator.tv, \MainTabCoordinator.movies, + \MainTabCoordinator.search, \MainTabCoordinator.other, \MainTabCoordinator.settings, ]) @@ -25,6 +26,8 @@ final class MainTabCoordinator: TabCoordinatable { var tv = makeTv @Route(tabItem: makeMoviesTab) var movies = makeMovies + @Route(tabItem: makeSearchTab) + var search = makeSearch @Route(tabItem: makeOtherTab) var other = makeOther @Route(tabItem: makeSettingsTab) @@ -66,6 +69,18 @@ final class MainTabCoordinator: TabCoordinatable { } } + func makeSearch() -> NavigationViewCoordinator { + NavigationViewCoordinator(SearchCoordinator()) + } + + @ViewBuilder + func makeSearchTab(isActive: Bool) -> some View { + HStack { + Image(systemName: "magnifyingglass") + L10n.search.text + } + } + func makeOther() -> NavigationViewCoordinator { NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel())) } diff --git a/Shared/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift index 6e264f7b..d9c1dca6 100644 --- a/Shared/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -17,21 +17,26 @@ final class SearchCoordinator: NavigationCoordinatable { @Root var start = makeStart - @Route(.push) - var item = makeItem + #if os(tvOS) + @Route(.modal) + var item = makeItem + #else + @Route(.push) + var item = makeItem + #endif - let viewModel: LibrarySearchViewModel - - init(viewModel: LibrarySearchViewModel) { - self.viewModel = viewModel - } - - func makeItem(item: BaseItemDto) -> ItemCoordinator { - ItemCoordinator(item: item) - } + #if os(tvOS) + func makeItem(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemCoordinator(item: item)) + } + #else + func makeItem(item: BaseItemDto) -> ItemCoordinator { + ItemCoordinator(item: item) + } + #endif @ViewBuilder func makeStart() -> some View { - LibrarySearchView(viewModel: self.viewModel) + SearchView(viewModel: .init()) } } diff --git a/Shared/Extensions/StringExtensions.swift b/Shared/Extensions/StringExtensions.swift index d71d04d4..800cb713 100644 --- a/Shared/Extensions/StringExtensions.swift +++ b/Shared/Extensions/StringExtensions.swift @@ -42,4 +42,16 @@ extension String { let initials = self.split(separator: " ").compactMap(\.first) return String(initials) } + + func heightOfString(usingFont font: UIFont) -> CGFloat { + let fontAttributes = [NSAttributedString.Key.font: font] + let textSize = self.size(withAttributes: fontAttributes) + return textSize.height + } + + func widthOfString(usingFont font: UIFont) -> CGFloat { + let fontAttributes = [NSAttributedString.Key.font: font] + let textSize = self.size(withAttributes: fontAttributes) + return textSize.width + } } diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index b85a8fb5..3c3782b8 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -36,6 +36,7 @@ extension Defaults.Keys { static let recentlyAddedPosterType = Key("recentlyAddedPosterType", default: .portrait, suite: .generalSuite) static let latestInLibraryPosterType = Key("latestInLibraryPosterType", default: .portrait, suite: .generalSuite) static let recommendedPosterType = Key("recommendedPosterType", default: .portrait, suite: .generalSuite) + static let searchPosterType = Key("searchPosterType", default: .portrait, suite: .generalSuite) static let libraryPosterType = Key("libraryPosterType", default: .portrait, suite: .generalSuite) enum Episodes { diff --git a/Shared/ViewModels/LibrarySearchViewModel.swift b/Shared/ViewModels/LibrarySearchViewModel.swift deleted file mode 100644 index 94a999cc..00000000 --- a/Shared/ViewModels/LibrarySearchViewModel.swift +++ /dev/null @@ -1,179 +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 CombineExt -import Foundation -import JellyfinAPI -import SwiftUI - -final class LibrarySearchViewModel: ViewModel { - - @Published - var supportedItemTypeList = [ItemType]() - - @Published - var selectedItemType: ItemType = .movie - - @Published - var movieItems = [BaseItemDto]() - @Published - var showItems = [BaseItemDto]() - @Published - var episodeItems = [BaseItemDto]() - - @Published - var suggestions = [BaseItemDto]() - - var searchQuerySubject = CurrentValueSubject("") - var parentID: String? - - init(parentID: String?) { - self.parentID = parentID - super.init() - - searchQuerySubject - .filter { !$0.isEmpty } - .debounce(for: 0.25, scheduler: DispatchQueue.main) - .sink(receiveValue: search) - .store(in: &cancellables) - setupPublishersForSupportedItemType() - - requestSuggestions() - } - - func setupPublishersForSupportedItemType() { - Publishers.CombineLatest3($movieItems, $showItems, $episodeItems) - .debounce(for: 0.25, scheduler: DispatchQueue.main) - .map { arg -> [ItemType] in - var typeList = [ItemType]() - if !arg.0.isEmpty { - typeList.append(.movie) - } - if !arg.1.isEmpty { - typeList.append(.series) - } - if !arg.2.isEmpty { - typeList.append(.episode) - } - return typeList - } - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] typeList in - withAnimation { - self?.supportedItemTypeList = typeList - } - }) - .store(in: &cancellables) - - $supportedItemTypeList - .receive(on: DispatchQueue.main) - .withLatestFrom($selectedItemType) - .compactMap { selectedItemType in - if self.supportedItemTypeList.contains(selectedItemType) { - return selectedItemType - } else { - return self.supportedItemTypeList.first - } - } - .sink(receiveValue: { [weak self] itemType in - withAnimation { - self?.selectedItemType = itemType - } - }) - .store(in: &cancellables) - } - - func requestSuggestions() { - ItemsAPI.getItemsByUserId( - userId: SessionManager.main.currentLogin.user.id, - limit: 20, - recursive: true, - parentId: parentID, - includeItemTypes: [.movie, .series], - sortBy: ["IsFavoriteOrLiked", "Random"], - imageTypeLimit: 0, - enableTotalRecordCount: false, - enableImages: false - ) - .trackActivity(loading) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] response in - self?.suggestions = response.items ?? [] - }) - .store(in: &cancellables) - } - - func search(with query: String) { - ItemsAPI.getItemsByUserId( - userId: SessionManager.main.currentLogin.user.id, - limit: 50, - recursive: true, - searchTerm: query, - sortOrder: [.ascending], - parentId: parentID, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - includeItemTypes: [.movie], - sortBy: ["SortName"], - enableUserData: true, - enableImages: true - ) - .trackActivity(loading) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] response in - self?.movieItems = response.items ?? [] - }) - .store(in: &cancellables) - ItemsAPI.getItemsByUserId( - userId: SessionManager.main.currentLogin.user.id, - limit: 50, - recursive: true, - searchTerm: query, - sortOrder: [.ascending], - parentId: parentID, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - includeItemTypes: [.series], - sortBy: ["SortName"], - enableUserData: true, - enableImages: true - ) - .trackActivity(loading) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] response in - self?.showItems = response.items ?? [] - }) - .store(in: &cancellables) - ItemsAPI.getItemsByUserId( - userId: SessionManager.main.currentLogin.user.id, - limit: 50, - recursive: true, - searchTerm: query, - sortOrder: [.ascending], - parentId: parentID, - fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - includeItemTypes: [.episode], - sortBy: ["SortName"], - enableUserData: true, - enableImages: true - ) - .trackActivity(loading) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - self?.handleAPIRequestError(completion: completion) - }, receiveValue: { [weak self] response in - self?.episodeItems = response.items ?? [] - }) - .store(in: &cancellables) - } -} diff --git a/Shared/ViewModels/SearchViewModel.swift b/Shared/ViewModels/SearchViewModel.swift new file mode 100644 index 00000000..2dff86ef --- /dev/null +++ b/Shared/ViewModels/SearchViewModel.swift @@ -0,0 +1,129 @@ +// +// 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 +import SwiftUI + +final class SearchViewModel: ViewModel { + + private var searchCancellables = Set() + + @Published + var movies: [BaseItemDto] = [] + @Published + var collections: [BaseItemDto] = [] + @Published + var series: [BaseItemDto] = [] + @Published + var episodes: [BaseItemDto] = [] + @Published + var people: [BaseItemDto] = [] + @Published + var suggestions: [BaseItemDto] = [] + + var noResults: Bool { + movies.isEmpty && + collections.isEmpty && + series.isEmpty && + episodes.isEmpty && + people.isEmpty + } + + private var searchTextSubject = CurrentValueSubject("") + + override init() { + super.init() + + getSuggestions() + + searchTextSubject + .handleEvents(receiveOutput: { _ in self.cancelPreviousSearch() }) + .filter { !$0.isEmpty } + .debounce(for: 0.25, scheduler: DispatchQueue.main) + .sink(receiveValue: _search) + .store(in: &cancellables) + } + + private func cancelPreviousSearch() { + searchCancellables.forEach { $0.cancel() } + print(searchCancellables.count) + } + + func search(with query: String) { + 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 getItems( + with query: String, + for itemType: BaseItemKind, + keyPath: ReferenceWritableKeyPath + ) { + ItemsAPI.getItemsByUserId( + userId: SessionManager.main.currentLogin.user.id, + limit: 20, + recursive: true, + searchTerm: query, + sortOrder: [.ascending], + fields: ItemFields.allCases, + includeItemTypes: [itemType], + sortBy: ["SortName"], + enableUserData: true, + enableImages: true + ) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + self?[keyPath: keyPath] = response.items ?? [] + }) + .store(in: &searchCancellables) + } + + private func getPeople(with query: String) { + PersonsAPI.getPersons( + limit: 20, + searchTerm: query + ) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + self?.people = response.items ?? [] + }) + .store(in: &searchCancellables) + } + + private func getSuggestions() { + ItemsAPI.getItemsByUserId( + userId: SessionManager.main.currentLogin.user.id, + limit: 10, + recursive: true, + includeItemTypes: [.movie, .series], + sortBy: ["IsFavoriteOrLiked", "Random"], + imageTypeLimit: 0, + enableTotalRecordCount: false, + enableImages: false + ) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestError(completion: completion) + }, receiveValue: { [weak self] response in + self?.suggestions = response.items ?? [] + }) + .store(in: &cancellables) + } +} diff --git a/Shared/Views/AttributeFillView.swift b/Shared/Views/AttributeFillView.swift index 21328ce5..a363fd4c 100644 --- a/Shared/Views/AttributeFillView.swift +++ b/Shared/Views/AttributeFillView.swift @@ -8,6 +8,7 @@ import SwiftUI +// TODO: Replace with `attributeStyle` struct AttributeFillView: View { let text: String diff --git a/Shared/Views/AttributeOutlineView.swift b/Shared/Views/AttributeOutlineView.swift index d982b294..21b6f158 100644 --- a/Shared/Views/AttributeOutlineView.swift +++ b/Shared/Views/AttributeOutlineView.swift @@ -8,6 +8,7 @@ import SwiftUI +// TODO: Replace with `attributeStyle` struct AttributeOutlineView: View { let text: String diff --git a/Shared/Views/SearchBarView.swift b/Shared/Views/SearchBarView.swift deleted file mode 100644 index 66dc899a..00000000 --- a/Shared/Views/SearchBarView.swift +++ /dev/null @@ -1,38 +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 - -struct SearchBar: View { - @Binding - var text: String - - @State - private var isEditing = false - - var body: some View { - HStack(spacing: 8) { - TextField(L10n.searchDots, text: $text) - .padding(8) - .padding(.horizontal, 16) - #if os(iOS) - .background(Color(.systemGray6)) - #endif - .cornerRadius(8) - if !text.isEmpty { - Button(action: { - self.text = "" - }) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.secondary) - } - } - } - .padding(.horizontal, 16) - } -} diff --git a/Shared/Views/SearchablePickerView.swift b/Shared/Views/SearchablePickerView.swift deleted file mode 100644 index 93ea9e8d..00000000 --- a/Shared/Views/SearchablePickerView.swift +++ /dev/null @@ -1,77 +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 Foundation -import SwiftUI - -private struct SearchablePickerView: View { - @Environment(\.presentationMode) - var presentationMode - - let options: [Selectable] - let optionToString: (Selectable) -> String - let label: String - - @State - var text = "" - @Binding - var selected: Selectable - - var body: some View { - VStack { - SearchBar(text: $text) - List(options.filter { - guard !text.isEmpty else { return true } - return optionToString($0).lowercased().contains(text.lowercased()) - }, id: \.self) { selectable in - Button(action: { - selected = selectable - presentationMode.wrappedValue.dismiss() - }) { - HStack { - Text(optionToString(selectable)).foregroundColor(Color.primary) - Spacer() - if selected == selectable { - Image(systemName: "checkmark").foregroundColor(.accentColor) - } - } - } - }.listStyle(GroupedListStyle()) - } - } -} - -struct SearchablePicker: View { - let label: String - let options: [Selectable] - let optionToString: (Selectable) -> String - - @Binding - var selected: Selectable - - var body: some View { - NavigationLink(destination: searchablePickerView()) { - HStack { - Text(label) - Spacer() - Text(optionToString(selected)) - .foregroundColor(.gray) - .multilineTextAlignment(.trailing) - } - } - } - - private func searchablePickerView() -> some View { - SearchablePickerView( - options: options, - optionToString: optionToString, - label: label, - selected: $selected - ) - } -} diff --git a/Shared/Views/TruncatedTextView.swift b/Shared/Views/TruncatedTextView.swift index b590db09..23b26b45 100644 --- a/Shared/Views/TruncatedTextView.swift +++ b/Shared/Views/TruncatedTextView.swift @@ -8,40 +8,6 @@ import SwiftUI -extension TruncatedTextView { - func font(_ font: Font) -> TruncatedTextView { - var result = self - result.font = font - return result - } - - func lineLimit(_ lineLimit: Int) -> TruncatedTextView { - var result = self - result.lineLimit = lineLimit - return result - } - - func foregroundColor(_ color: Color) -> TruncatedTextView { - var result = self - result.foregroundColor = color - return result - } -} - -extension String { - func heightOfString(usingFont font: UIFont) -> CGFloat { - let fontAttributes = [NSAttributedString.Key.font: font] - let textSize = self.size(withAttributes: fontAttributes) - return textSize.height - } - - func widthOfString(usingFont font: UIFont) -> CGFloat { - let fontAttributes = [NSAttributedString.Key.font: font] - let textSize = self.size(withAttributes: fontAttributes) - return textSize.width - } -} - struct TruncatedTextView: View { @State @@ -144,3 +110,23 @@ struct TruncatedTextView: View { } } } + +extension TruncatedTextView { + func font(_ font: Font) -> Self { + var result = self + result.font = font + return result + } + + func lineLimit(_ lineLimit: Int) -> Self { + var result = self + result.lineLimit = lineLimit + return result + } + + func foregroundColor(_ color: Color) -> Self { + var result = self + result.foregroundColor = color + return result + } +} diff --git a/Swiftfin tvOS/Components/PortraitItemElement.swift b/Swiftfin tvOS/Components/PortraitItemElement.swift index b3d280df..db3b1d66 100644 --- a/Swiftfin tvOS/Components/PortraitItemElement.swift +++ b/Swiftfin tvOS/Components/PortraitItemElement.swift @@ -9,7 +9,7 @@ import JellyfinAPI import SwiftUI -// TODO: Transition to `PortraitButton` +// TODO: Transition to PosterButton` struct PortraitItemElement: View { @Environment(\.isFocused) diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index 46d3c04c..00ccf5cc 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -29,6 +29,8 @@ struct ItemView: View { SeriesItemView(viewModel: .init(item: item)) case .boxSet: CollectionItemView(viewModel: .init(item: item)) + case .person: + LibraryView(viewModel: .init(person: .init(id: item.id))) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin tvOS/Views/LibrarySearchView.swift b/Swiftfin tvOS/Views/LibrarySearchView.swift deleted file mode 100644 index 64aa6b88..00000000 --- a/Swiftfin tvOS/Views/LibrarySearchView.swift +++ /dev/null @@ -1,107 +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 JellyfinAPI -import SwiftUI - -struct LibrarySearchView: View { - @StateObject - var viewModel: LibrarySearchViewModel - @State - var searchQuery = "" - - @State - private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) - - func recalcTracks() { - tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) - } - - var body: some View { - ZStack { - VStack { - SearchBar(text: $searchQuery) - .padding(.top, 16) - .padding(.bottom, 8) - if searchQuery.isEmpty { - suggestionsListView - } else { - resultView - } - } - if viewModel.isLoading { - ProgressView() - } - } - .onChange(of: searchQuery) { query in - viewModel.searchQuerySubject.send(query) - } - .navigationBarTitle(L10n.search) - } - - var suggestionsListView: some View { - ScrollView { - LazyVStack(spacing: 8) { - L10n.suggestions.text - .font(.headline) - .fontWeight(.bold) - .foregroundColor(.primary) - .padding(.bottom, 8) - ForEach(viewModel.suggestions, id: \.id) { item in - Button { - searchQuery = item.name ?? "" - } label: { - Text(item.name ?? "") - .font(.body) - } - } - } - .padding(.horizontal, 16) - } - } - - var resultView: some View { - let items = items(for: viewModel.selectedItemType) - return VStack(alignment: .leading, spacing: 16) { - Picker("ItemType", selection: $viewModel.selectedItemType) { - ForEach(viewModel.supportedItemTypeList, id: \.self) { - Text($0.localized) - .tag($0) - } - } - .pickerStyle(SegmentedPickerStyle()) - .padding(.horizontal, 16) - ScrollView { - LazyVStack(alignment: .leading, spacing: 16) { - if !items.isEmpty { - LazyVGrid(columns: tracks) { - ForEach(items, id: \.id) { item in - ItemView(item: item) - } - } - .padding(.bottom, 16) - } - } - } - } - } - - func items(for type: ItemType) -> [BaseItemDto] { - switch type { - case .episode: - return viewModel.episodeItems - case .movie: - return viewModel.movieItems - case .series: - return viewModel.showItems - default: - return [] - } - } -} diff --git a/Swiftfin tvOS/Views/MovieLibrariesView.swift b/Swiftfin tvOS/Views/MovieLibrariesView.swift index f989c222..109117e0 100644 --- a/Swiftfin tvOS/Views/MovieLibrariesView.swift +++ b/Swiftfin tvOS/Views/MovieLibrariesView.swift @@ -11,6 +11,7 @@ import SwiftUI import SwiftUICollection struct MovieLibrariesView: View { + @EnvironmentObject private var movieLibrariesRouter: MovieLibrariesCoordinator.Router @StateObject diff --git a/Swiftfin tvOS/Views/SearchView.swift b/Swiftfin tvOS/Views/SearchView.swift new file mode 100644 index 00000000..05de6c46 --- /dev/null +++ b/Swiftfin tvOS/Views/SearchView.swift @@ -0,0 +1,96 @@ +// +// 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 SearchView: View { + + @EnvironmentObject + private var router: SearchCoordinator.Router + @ObservedObject + var viewModel: SearchViewModel + + @State + private var searchText = "" + + @ViewBuilder + private var suggestionsView: some View { + VStack(spacing: 20) { + ForEach(viewModel.suggestions, id: \.id) { item in + Button { + searchText = item.displayName + } label: { + Text(item.displayName) + .font(.body) + } + } + } + } + + @ViewBuilder + private var resultsView: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 20) { + if !viewModel.movies.isEmpty { + itemsSection(title: L10n.movies, keyPath: \.movies) + } + + if !viewModel.collections.isEmpty { + // TODO: Localize after organization + itemsSection(title: "Collections", keyPath: \.collections) + } + + if !viewModel.series.isEmpty { + itemsSection(title: L10n.tvShows, keyPath: \.series) + } + + if !viewModel.episodes.isEmpty { + itemsSection(title: L10n.episodes, keyPath: \.episodes) + } + + if !viewModel.people.isEmpty { + // TODO: Localize after organization + itemsSection(title: "People", keyPath: \.people) + } + } + } + .ignoresSafeArea() + } + + @ViewBuilder + private func itemsSection( + title: String, + keyPath: ReferenceWritableKeyPath + ) -> some View { + PosterHStack( + title: title, + type: .portrait, + items: viewModel[keyPath: keyPath] + ) + .onSelect { item in + router.route(to: \.item, item) + } + } + + var body: some View { + Group { + if searchText.isEmpty { + EmptyView() + } else if !viewModel.isLoading && viewModel.noResults { + L10n.noResults.text + } else { + resultsView + } + } + .onChange(of: searchText) { newText in + viewModel.search(with: newText) + } + .searchable(text: $searchText, prompt: L10n.search) + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 95e9b104..0829d7b5 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -83,10 +83,8 @@ 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53ABFDEA2679753200886593 /* ConnectToServerView.swift */; }; 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 53ABFDEC26799D7700886593 /* ActivityIndicator */; }; 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CD2A3F268A49C2002ABD4E /* ItemView.swift */; }; - 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; }; - 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; - 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; + 53EE24E6265060780068F029 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* SearchView.swift */; }; 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; }; @@ -106,7 +104,6 @@ 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; }; 62400C4B287ED19600F6AD3D /* UDPBroadcast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; }; 62400C4C287ED19600F6AD3D /* UDPBroadcast.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; }; 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; }; 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; }; @@ -160,8 +157,8 @@ 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; - 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; - 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; + 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* SearchViewModel.swift */; }; + 62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* SearchViewModel.swift */; }; 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; }; 62E632E1267D30CA0063E547 /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */; }; @@ -224,7 +221,6 @@ C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; }; C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; }; C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; - C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; C4E5598928124C10003DECA5 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */; }; E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */; }; E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; @@ -269,7 +265,6 @@ E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; }; E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; }; - E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9D271A2CD600EA0737 /* CombineExt */; }; E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; }; E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; }; E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; }; @@ -372,14 +367,11 @@ E18E0207288749200022598C /* AttributeOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0202288749200022598C /* AttributeOutlineView.swift */; }; E18E0208288749200022598C /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0203288749200022598C /* BlurView.swift */; }; E18E021A2887492B0022598C /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0200288749200022598C /* AppIcon.swift */; }; - E18E021B2887492B0022598C /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; }; E18E021C2887492B0022598C /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0203288749200022598C /* BlurView.swift */; }; E18E021D2887492B0022598C /* AttributeOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0202288749200022598C /* AttributeOutlineView.swift */; }; E18E021E2887492B0022598C /* Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E01FF288749200022598C /* Divider.swift */; }; E18E021F2887492B0022598C /* InitialFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1047E2227E5880000CB0D4A /* InitialFailureView.swift */; }; E18E02202887492B0022598C /* AttributeFillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0201288749200022598C /* AttributeFillView.swift */; }; - E18E02212887492B0022598C /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; }; - E18E02222887492B0022598C /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; E18E02232887492B0022598C /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; E18E02252887492B0022598C /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; }; E18E023A288749540022598C /* UIScrollViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18E0239288749540022598C /* UIScrollViewExtensions.swift */; }; @@ -432,7 +424,6 @@ E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; }; E1B2AB9928808E150072B3B9 /* GoogleCast.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1B2AB9628808CDF0072B3B9 /* GoogleCast.xcframework */; }; E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; }; - E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; }; E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE9271A23880015B715 /* SwiftyJSON */; }; E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */; }; E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; @@ -484,6 +475,9 @@ E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; }; 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 */; }; E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; }; E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */; }; E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */; }; @@ -618,11 +612,9 @@ 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 = ""; }; - 53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = ""; }; 53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = ""; }; - 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = ""; }; - 53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.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 = ""; }; 5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = ""; }; @@ -636,7 +628,6 @@ 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = ""; }; 6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSVideoPlayerCoordinator.swift; sourceTree = ""; }; 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = ""; }; - 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; 625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = ""; }; 625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = ""; }; 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = ""; }; @@ -681,7 +672,7 @@ 62C83B07288C6A630004ED0C /* FontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPicker.swift; sourceTree = ""; }; 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; - 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; + 62E632DB267D2E130063E547 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; 62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemViewModel.swift; sourceTree = ""; }; 62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemViewModel.swift; sourceTree = ""; }; @@ -725,7 +716,6 @@ C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = ""; }; C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = ""; }; C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; - C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = ""; }; @@ -899,6 +889,8 @@ E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsCoordinator.swift; sourceTree = ""; }; 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 = ""; }; E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = ""; }; E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailsView.swift; sourceTree = ""; }; E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = ""; }; @@ -926,7 +918,6 @@ C409CE9C284EA6EA00CABC12 /* SwiftUICollection in Frameworks */, 62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */, 62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */, - E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */, 62666E2A27E5020A00EC0ECD /* OpenGLES.framework in Frameworks */, E1002B6B2793E36600E47059 /* Algorithms in Frameworks */, 62666E1D27E501DB00EC0ECD /* CoreMedia.framework in Frameworks */, @@ -995,7 +986,6 @@ 62666E3F27E5040300EC0ECD /* SystemConfiguration.framework in Frameworks */, 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */, 62666E3927E502CE00EC0ECD /* SwizzleSwift in Frameworks */, - E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */, E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */, E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */, ); @@ -1039,11 +1029,12 @@ 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, 625CB5742678C33500530A6E /* LibraryListViewModel.swift */, - 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */, + 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */, + 62E632DB267D2E130063E547 /* SearchViewModel.swift */, E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */, E13DD3E027176BD3009D4DAF /* ServerListViewModel.swift */, 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */, @@ -1053,7 +1044,6 @@ 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, E126F73F278A655300A522BF /* VideoPlayerViewModel */, 625CB57B2678CE1000530A6E /* ViewModel.swift */, - 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -1623,16 +1613,16 @@ E1B59FD62786AE2C00A5287E /* ContinueWatchingView */, 531690E6267ABD79005D8AB9 /* HomeView.swift */, E193D54E271942C000900D82 /* ItemView */, - E193D54C2719426600900D82 /* LibraryFilterView.swift */, E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, + E193D54C2719426600900D82 /* LibraryFilterView.swift */, C4E508172703E8190045C9AB /* LibraryListView.swift */, - C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */, - C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, + C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */, C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */, C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */, + E1E1643928BAC2EF00323B0A /* SearchView.swift */, E193D54F2719430400900D82 /* ServerDetailView.swift */, E193D54A271941D300900D82 /* ServerListView.swift */, E1E5D54D2783E66600692DFE /* SettingsView */, @@ -1684,7 +1674,6 @@ E14F7D0A26DB3714007C3AE6 /* ItemView */, 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, 6213388F265F83A900A81A2A /* LibraryListView.swift */, - 53EE24E5265060780068F029 /* LibrarySearchView.swift */, 53DF641D263D9C0600A7CD1A /* LibraryView.swift */, C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */, C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */, @@ -1692,6 +1681,7 @@ C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */, C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */, E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */, + 53EE24E5265060780068F029 /* SearchView.swift */, E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */, E13DD3E427177D15009D4DAF /* ServerListView.swift */, E1E5D54A2783E26100692DFE /* SettingsView */, @@ -1994,10 +1984,8 @@ E18E01FF288749200022598C /* Divider.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */, E1047E2227E5880000CB0D4A /* InitialFailureView.swift */, - 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */, + E1E1643D28BB074000323B0A /* MultiSelectorView.swift */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, - 624C21742685CF60007F1390 /* SearchablePickerView.swift */, - 53DE4BD1267098F300739748 /* SearchBarView.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */, ); path = Views; @@ -2145,7 +2133,6 @@ 6220D0C826D63F3700B8E046 /* Stinsen */, E13DD3CC27164CA7009D4DAF /* CoreStore */, E12186DD2718F1C50010884C /* Defaults */, - E1218C9D271A2CD600EA0737 /* CombineExt */, E178857C278037FD0094FBCF /* JellyfinAPI */, E1002B6A2793E36600E47059 /* Algorithms */, E1347DB5279E3CA500BC6161 /* Puppy */, @@ -2183,7 +2170,6 @@ 62C29E9B26D0FE4200C1D2E7 /* Stinsen */, E13DD3C52716499E009D4DAF /* CoreStore */, E13DD3D227168E65009D4DAF /* Defaults */, - E1B6DCE7271A23780015B715 /* CombineExt */, E1B6DCE9271A23880015B715 /* SwiftyJSON */, E10EAA44277BB646000269ED /* JellyfinAPI */, E10EAA4C277BB716000269ED /* Sliders */, @@ -2250,7 +2236,6 @@ 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */, E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, - E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */, E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */, E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */, @@ -2384,17 +2369,16 @@ C4534985279A40C60045F1E2 /* LiveTVVideoPlayerView.swift in Sources */, E1A2C15A279A7D76005EC829 /* BundleExtensions.swift in Sources */, C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */, - C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */, E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, + E1E1643E28BB074000323B0A /* MultiSelectorView.swift in Sources */, E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, - E18E02212887492B0022598C /* MultiSelectorView.swift in Sources */, C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, E1FCD09726C47118007C8DCF /* ErrorMessage.swift in Sources */, @@ -2475,13 +2459,11 @@ E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, - E18E02222887492B0022598C /* SearchablePickerView.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */, - 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, + 62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */, - E18E021B2887492B0022598C /* SearchBarView.swift in Sources */, E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, @@ -2514,6 +2496,7 @@ E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */, E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */, E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */, + E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */, E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */, E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */, E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */, @@ -2581,7 +2564,7 @@ 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */, - 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, + 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */, @@ -2589,6 +2572,7 @@ E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */, E18E0208288749200022598C /* BlurView.swift in Sources */, E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */, + E1E1643F28BB075C00323B0A /* MultiSelectorView.swift in Sources */, E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */, E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */, @@ -2685,12 +2669,10 @@ E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, - 53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */, E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */, E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */, E18E01DC288747230022598C /* iPadOSCinematicScrollView.swift in Sources */, - 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, E18E01E2288747230022598C /* EpisodeItemView.swift in Sources */, 6220D0C626D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, @@ -2720,7 +2702,6 @@ E184C160288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */, E1C925F72887504B002A7A66 /* PanDirectionGestureRecognizer.swift in Sources */, E18E01E9288747230022598C /* SeriesItemView.swift in Sources */, - 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, E18E023A288749540022598C /* UIScrollViewExtensions.swift in Sources */, E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, @@ -2764,7 +2745,7 @@ 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */, E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */, - 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, + 53EE24E6265060780068F029 /* SearchView.swift in Sources */, 625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3256,14 +3237,6 @@ minimumVersion = 0.5.0; }; }; - E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/CombineCommunity/CombineExt"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.0.0; - }; - }; E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/LePips/Puppy"; @@ -3398,11 +3371,6 @@ package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; - E1218C9D271A2CD600EA0737 /* CombineExt */ = { - isa = XCSwiftPackageProductDependency; - package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; - productName = CombineExt; - }; E1347DB1279E3C6200BC6161 /* Puppy */ = { isa = XCSwiftPackageProductDependency; package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */; @@ -3478,11 +3446,6 @@ package = E19E6E0828A0BEFF005C10C8 /* XCRemoteSwiftPackageReference "BlurHashKit" */; productName = BlurHashKit; }; - E1B6DCE7271A23780015B715 /* CombineExt */ = { - isa = XCSwiftPackageProductDependency; - package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; - productName = CombineExt; - }; E1B6DCE9271A23880015B715 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fa79f0ff..251f8fb6 100644 --- a/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -36,15 +36,6 @@ "revision" : "1dbf31d860626f8debdbb08201517a4684d226c6" } }, - { - "identity" : "combineext", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CombineCommunity/CombineExt", - "state" : { - "revision" : "581849239060948b626d1173eb0e5926818e7f8c", - "version" : "1.7.0" - } - }, { "identity" : "corestore", "kind" : "remoteSourceControl", diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 9eb30e57..e24d47b3 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -42,6 +42,8 @@ struct ItemView: View { } else { CollectionItemView(viewModel: .init(item: item)) } + case .person: + LibraryView(viewModel: .init(person: .init(id: item.id))) default: Text(L10n.notImplementedYetWithType(item.type ?? "--")) } diff --git a/Swiftfin/Views/LibraryListView.swift b/Swiftfin/Views/LibraryListView.swift index 31b9e12b..f44a2335 100644 --- a/Swiftfin/Views/LibraryListView.swift +++ b/Swiftfin/Views/LibraryListView.swift @@ -89,14 +89,5 @@ struct LibraryListView: View { .padding(.top, 8) } .navigationTitle(L10n.allMedia) - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button { - libraryListRouter.route(to: \.search, LibrarySearchViewModel(parentID: nil)) - } label: { - Image(systemName: "magnifyingglass") - } - } - } } } diff --git a/Swiftfin/Views/LibrarySearchView.swift b/Swiftfin/Views/LibrarySearchView.swift deleted file mode 100644 index 95d112d7..00000000 --- a/Swiftfin/Views/LibrarySearchView.swift +++ /dev/null @@ -1,114 +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 JellyfinAPI -import Stinsen -import SwiftUI - -struct LibrarySearchView: View { - @EnvironmentObject - private var searchRouter: SearchCoordinator.Router - @StateObject - var viewModel: LibrarySearchViewModel - @State - private var searchQuery = "" - - @State - private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) - - func recalcTracks() { - tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) - } - - var body: some View { - ZStack { - VStack { - SearchBar(text: $searchQuery) - .padding(.top, 16) - .padding(.bottom, 8) - if searchQuery.isEmpty { - suggestionsListView - } else { - resultView - } - } - if viewModel.isLoading { - ProgressView() - } - } - .onChange(of: searchQuery) { query in - viewModel.searchQuerySubject.send(query) - } - .navigationBarTitle(L10n.search, displayMode: .inline) - } - - var suggestionsListView: some View { - ScrollView { - LazyVStack(spacing: 8) { - L10n.suggestions.text - .font(.headline) - .fontWeight(.bold) - .foregroundColor(.primary) - .padding(.bottom, 8) - ForEach(viewModel.suggestions, id: \.id) { item in - Button { - searchQuery = item.name ?? "" - } label: { - Text(item.name ?? "") - .font(.body) - } - } - } - .padding(.horizontal, 16) - } - } - - var resultView: some View { - let items = items(for: viewModel.selectedItemType) - return VStack(alignment: .leading, spacing: 16) { - Picker("ItemType", selection: $viewModel.selectedItemType) { - ForEach(viewModel.supportedItemTypeList, id: \.self) { - Text($0.localized) - .tag($0) - } - } - .pickerStyle(SegmentedPickerStyle()) - ScrollView { - LazyVStack(alignment: .leading, spacing: 16) { - if !items.isEmpty { - LazyVGrid(columns: tracks) { - ForEach(items, id: \.id) { item in - PosterButton(item: item, type: .portrait) - .onSelect { item in - searchRouter.route(to: \.item, item) - } - } - } - } - } - } - } - .onRotate { _ in - recalcTracks() - } - } - - func items(for type: ItemType) -> [BaseItemDto] { - switch type { - case .episode: - return viewModel.episodeItems - case .movie: - return viewModel.movieItems - case .series: - return viewModel.showItems - default: - return [] - } - } -} diff --git a/Swiftfin/Views/LibraryView.swift b/Swiftfin/Views/LibraryView.swift index 77bdb3bf..ee6432e7 100644 --- a/Swiftfin/Views/LibraryView.swift +++ b/Swiftfin/Views/LibraryView.swift @@ -18,7 +18,7 @@ struct LibraryView: View { var viewModel: LibraryViewModel @Default(.Customization.libraryPosterType) - var libraryPosterType + private var libraryPosterType @ViewBuilder private var loadingView: some View { @@ -89,12 +89,6 @@ struct LibraryView: View { Image(systemName: "line.horizontal.3.decrease.circle") } .foregroundColor(viewModel.filters == .default ? .accentColor : Color(UIColor.systemOrange)) - - Button { - libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID)) - } label: { - Image(systemName: "magnifyingglass") - } } } } diff --git a/Swiftfin/Views/SearchView.swift b/Swiftfin/Views/SearchView.swift new file mode 100644 index 00000000..230ab5b1 --- /dev/null +++ b/Swiftfin/Views/SearchView.swift @@ -0,0 +1,102 @@ +// +// 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 CollectionView +import Defaults +import JellyfinAPI +import SwiftUI + +struct SearchView: View { + + @EnvironmentObject + private var router: SearchCoordinator.Router + @ObservedObject + var viewModel: SearchViewModel + + @Default(.Customization.searchPosterType) + private var searchPosterType + @State + private var searchText = "" + + @ViewBuilder + private var suggestionsView: some View { + VStack(spacing: 20) { + ForEach(viewModel.suggestions, id: \.id) { item in + Button { + searchText = item.displayName + } label: { + Text(item.displayName) + .font(.body) + } + } + } + } + + @ViewBuilder + private var resultsView: some View { + ScrollView(showsIndicators: false) { + VStack(spacing: 20) { + if !viewModel.movies.isEmpty { + itemsSection(title: L10n.movies, keyPath: \.movies, posterType: searchPosterType) + } + + if !viewModel.collections.isEmpty { + // TODO: Localize after organization + itemsSection(title: "Collections", keyPath: \.collections, posterType: searchPosterType) + } + + if !viewModel.series.isEmpty { + itemsSection(title: L10n.tvShows, keyPath: \.series, posterType: searchPosterType) + } + + if !viewModel.episodes.isEmpty { + itemsSection(title: L10n.episodes, keyPath: \.episodes, posterType: searchPosterType) + } + + if !viewModel.people.isEmpty { + // TODO: Localize after organization + itemsSection(title: "People", keyPath: \.people, posterType: .portrait) + } + } + } + } + + @ViewBuilder + private func itemsSection( + title: String, + keyPath: ReferenceWritableKeyPath, + posterType: PosterType + ) -> some View { + PosterHStack( + title: title, + type: posterType, + items: viewModel[keyPath: keyPath] + ) + .onSelect { item in + router.route(to: \.item, item) + } + } + + var body: some View { + Group { + if searchText.isEmpty { + suggestionsView + } else if !viewModel.isLoading && viewModel.noResults { + L10n.noResults.text + } else { + resultsView + } + } + .onChange(of: searchText) { newText in + viewModel.search(with: newText) + } + .searchable(text: $searchText, placement: .navigationBarDrawer, prompt: L10n.search) + .navigationTitle(L10n.search) + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift index c635dad4..4cf99f14 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift @@ -31,6 +31,8 @@ struct CustomizeViewsSettings: View { var latestInLibraryPosterType @Default(.Customization.recommendedPosterType) var recommendedPosterType + @Default(.Customization.searchPosterType) + var searchPosterType @Default(.Customization.libraryPosterType) var libraryPosterType @@ -89,6 +91,12 @@ struct CustomizeViewsSettings: View { // } // } + Picker(L10n.search, selection: $searchPosterType) { + ForEach(PosterType.allCases, id: \.self) { type in + Text(type.localizedName).tag(type.rawValue) + } + } + Picker(L10n.library, selection: $libraryPosterType) { ForEach(PosterType.allCases, id: \.self) { type in Text(type.localizedName).tag(type.rawValue)