Refactor Search and implement in tvOS (#539)
This commit is contained in:
parent
5d0f933a2c
commit
98a5507b52
|
@ -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<FilterCoordinator> {
|
||||
NavigationViewCoordinator(FilterCoordinator(
|
||||
filters: params.filters,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<SearchCoordinator> {
|
||||
NavigationViewCoordinator(SearchCoordinator())
|
||||
}
|
||||
|
||||
func onSearchTapped(isRepeat: Bool, coordinator: NavigationViewCoordinator<SearchCoordinator>) {
|
||||
if isRepeat {
|
||||
coordinator.child.popToRoot()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeSearchTab(isActive: Bool) -> some View {
|
||||
Image(systemName: "magnifyingglass")
|
||||
L10n.search.text
|
||||
}
|
||||
|
||||
func makeAllMedia() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
|
||||
}
|
||||
|
|
|
@ -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<SearchCoordinator> {
|
||||
NavigationViewCoordinator(SearchCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeSearchTab(isActive: Bool) -> some View {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
L10n.search.text
|
||||
}
|
||||
}
|
||||
|
||||
func makeOther() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
NavigationViewCoordinator(LibraryListCoordinator(viewModel: LibraryListViewModel()))
|
||||
}
|
||||
|
|
|
@ -17,21 +17,26 @@ final class SearchCoordinator: NavigationCoordinatable {
|
|||
|
||||
@Root
|
||||
var start = makeStart
|
||||
#if os(tvOS)
|
||||
@Route(.modal)
|
||||
var item = makeItem
|
||||
#else
|
||||
@Route(.push)
|
||||
var item = makeItem
|
||||
#endif
|
||||
|
||||
let viewModel: LibrarySearchViewModel
|
||||
|
||||
init(viewModel: LibrarySearchViewModel) {
|
||||
self.viewModel = viewModel
|
||||
#if os(tvOS)
|
||||
func makeItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ extension Defaults.Keys {
|
|||
static let recentlyAddedPosterType = Key<PosterType>("recentlyAddedPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let latestInLibraryPosterType = Key<PosterType>("latestInLibraryPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let recommendedPosterType = Key<PosterType>("recommendedPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let searchPosterType = Key<PosterType>("searchPosterType", default: .portrait, suite: .generalSuite)
|
||||
static let libraryPosterType = Key<PosterType>("libraryPosterType", default: .portrait, suite: .generalSuite)
|
||||
|
||||
enum Episodes {
|
||||
|
|
|
@ -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<String, Never>("")
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
@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<String, Never>("")
|
||||
|
||||
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<SearchViewModel, [BaseItemDto]>
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Replace with `attributeStyle`
|
||||
struct AttributeFillView: View {
|
||||
|
||||
let text: String
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Replace with `attributeStyle`
|
||||
struct AttributeOutlineView: View {
|
||||
|
||||
let text: String
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Selectable: Hashable>: 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<Selectable: Hashable>: 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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Transition to `PortraitButton`
|
||||
// TODO: Transition to PosterButton`
|
||||
struct PortraitItemElement: View {
|
||||
|
||||
@Environment(\.isFocused)
|
||||
|
|
|
@ -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 ?? "--"))
|
||||
}
|
||||
|
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import SwiftUI
|
|||
import SwiftUICollection
|
||||
|
||||
struct MovieLibrariesView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var movieLibrariesRouter: MovieLibrariesCoordinator.Router
|
||||
@StateObject
|
||||
|
|
|
@ -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<SearchViewModel, [BaseItemDto]>
|
||||
) -> 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)
|
||||
}
|
||||
}
|
|
@ -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 = "<group>"; };
|
||||
53CD2A3F268A49C2002ABD4E /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
|
||||
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
|
||||
53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = "<group>"; };
|
||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
||||
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = "<group>"; };
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
53EE24E5265060780068F029 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = "<group>"; };
|
||||
5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VLCPlayer+subtitles.swift"; sourceTree = "<group>"; };
|
||||
5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = "<group>"; };
|
||||
|
@ -636,7 +628,6 @@
|
|||
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = "<group>"; };
|
||||
6220D0C526D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||
6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.swift; sourceTree = "<group>"; };
|
||||
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
|
||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -681,7 +672,7 @@
|
|||
62C83B07288C6A630004ED0C /* FontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPicker.swift; sourceTree = "<group>"; };
|
||||
62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = "<group>"; };
|
||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
||||
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
||||
62E632DB267D2E130063E547 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
|
||||
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
|
||||
62E632E2267D3BA60063E547 /* MovieItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemViewModel.swift; sourceTree = "<group>"; };
|
||||
62E632E5267D3F5B0063E547 /* EpisodeItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeItemViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -725,7 +716,6 @@
|
|||
C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelsView.swift; sourceTree = "<group>"; };
|
||||
C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVHomeView.swift; sourceTree = "<group>"; };
|
||||
C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||
C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = "<group>"; };
|
||||
E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = "<group>"; };
|
||||
|
@ -899,6 +889,8 @@
|
|||
E1D4BF892719D3D000A11E64 /* BasicAppSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||
E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1E00A34278628A40022235B /* DoubleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleExtensions.swift; sourceTree = "<group>"; };
|
||||
E1E1643928BAC2EF00323B0A /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
E1E1643D28BB074000323B0A /* MultiSelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = "<group>"; };
|
||||
E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = "<group>"; };
|
||||
E1E5D5432783BB5100692DFE /* ItemDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailsView.swift; sourceTree = "<group>"; };
|
||||
E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlaySettingsView.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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" */;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 ?? "--"))
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SearchViewModel, [BaseItemDto]>,
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue