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