From 5c4ee44575888604187ba8224254c977ffab82f8 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 16 Jul 2021 00:50:46 +0900 Subject: [PATCH] Implement search by ItemType Search Logic Improvements --- .../Components/PortraitItemView.swift | 2 +- JellyfinPlayer/LibrarySearchView.swift | 61 +++++++++++---- Shared/Typings/Typings.swift | 6 ++ .../ViewModels/LibrarySearchViewModel.swift | 77 ++++++++++++++++--- 4 files changed, 120 insertions(+), 26 deletions(-) diff --git a/JellyfinPlayer/Components/PortraitItemView.swift b/JellyfinPlayer/Components/PortraitItemView.swift index fcc261a2..b81ab3a9 100644 --- a/JellyfinPlayer/Components/PortraitItemView.swift +++ b/JellyfinPlayer/Components/PortraitItemView.swift @@ -74,7 +74,7 @@ struct PortraitItemView: View { .font(.caption) .fontWeight(.medium) } else { - Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))") + Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")") .foregroundColor(.secondary) .font(.caption) .fontWeight(.medium) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 2d82099c..99a446f8 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -27,7 +27,7 @@ struct LibrarySearchView: View { if searchQuery.isEmpty { suggestionsListView } else { - itemsGridView + resultView } } if viewModel.isLoading { @@ -61,21 +61,56 @@ struct LibrarySearchView: View { } } - var itemsGridView: some View { - ScrollView { - if !viewModel.items.isEmpty { - LazyVGrid(columns: tracks) { - ForEach(viewModel.items, id: \.id) { item in - PortraitItemView(item: item) + var resultView: some View { + VStack(alignment: .leading, spacing: 16) { + Picker("ItemType", selection: $viewModel.selectedItemType) { + ForEach(viewModel.supportedItemTypeList, id: \.self) { + Text($0.localized) + .tag($0) + } + } + .pickerStyle(SegmentedPickerStyle()) + let items = items(for: viewModel.selectedItemType) + ScrollView { + LazyVStack(alignment: .leading, spacing: 16) { + if !items.isEmpty { + LazyVGrid(columns: tracks) { + ForEach(items, id: \.id) { item in + PortraitItemView(item: item) + } + } + .padding(.bottom, 16) } } - .padding(.bottom, 16) - .onRotate { _ in - recalcTracks() - } - } else { - Text("Query returned 0 results.") } } + .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 + } + } +} + +private extension ItemType { + + var localized: String { + switch self { + case .episode: + return "Episodes" + case .movie: + return "Movies" + case .series: + return "Shows" + } } } diff --git a/Shared/Typings/Typings.swift b/Shared/Typings/Typings.swift index 99f9374c..45e4ac5b 100644 --- a/Shared/Typings/Typings.swift +++ b/Shared/Typings/Typings.swift @@ -67,3 +67,9 @@ extension APISortOrder { } } } + +enum ItemType: String { + case episode = "Episode" + case movie = "Movie" + case series = "Series" +} diff --git a/Shared/ViewModels/LibrarySearchViewModel.swift b/Shared/ViewModels/LibrarySearchViewModel.swift index b7dca966..3ee5f7de 100644 --- a/Shared/ViewModels/LibrarySearchViewModel.swift +++ b/Shared/ViewModels/LibrarySearchViewModel.swift @@ -12,32 +12,63 @@ import Foundation import JellyfinAPI final class LibrarySearchViewModel: ViewModel { - @Published var items = [BaseItemDto]() - + + @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(with:)) + .sink(receiveValue: search) .store(in: &cancellables) + let supportedItemTypeListPublishers = Publishers.CombineLatest3($movieItems, $showItems, $episodeItems) + .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 + } + + supportedItemTypeListPublishers + .assign(to: \.supportedItemTypeList, on: self) + .store(in: &cancellables) + supportedItemTypeListPublishers + .compactMap(\.first) + .assign(to: \.selectedItemType, on: self) + .store(in: &cancellables) + + requestSuggestions() } - + func requestSuggestions() { ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 20, recursive: true, parentId: parentID, - includeItemTypes: ["Movie", "Series", "MusicArtist"], + includeItemTypes: ["Movie", "Series"], sortBy: ["IsFavoriteOrLiked", "Random"], imageTypeLimit: 0, enableTotalRecordCount: false, @@ -48,17 +79,39 @@ final class LibrarySearchViewModel: ViewModel { } .store(in: &cancellables) } - + func search(with query: String) { - ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 60, recursive: true, searchTerm: query, + ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: parentID, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], - includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true) + includeItemTypes: [ItemType.movie.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) .trackActivity(loading) .sink(receiveCompletion: { [weak self] completion in self?.handleAPIRequestCompletion(completion: completion) }, receiveValue: { [weak self] response in - self?.items = response.items ?? [] + self?.movieItems = response.items ?? [] + }) + .store(in: &cancellables) + ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + sortOrder: [.ascending], parentId: parentID, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + includeItemTypes: [ItemType.series.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestCompletion(completion: completion) + }, receiveValue: { [weak self] response in + self?.showItems = response.items ?? [] + }) + .store(in: &cancellables) + ItemsAPI.getItemsByUserId(userId: SessionManager.current.user.user_id!, limit: 50, recursive: true, searchTerm: query, + sortOrder: [.ascending], parentId: parentID, + fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], + includeItemTypes: [ItemType.episode.rawValue], sortBy: ["SortName"], enableUserData: true, enableImages: true) + .trackActivity(loading) + .sink(receiveCompletion: { [weak self] completion in + self?.handleAPIRequestCompletion(completion: completion) + }, receiveValue: { [weak self] response in + self?.episodeItems = response.items ?? [] }) .store(in: &cancellables) }