iOS/iPadOS - Refactor Filter Selection (#548)

This commit is contained in:
Ethan Pippin 2022-09-01 23:29:52 -06:00 committed by GitHub
parent 109c0328b6
commit f92edb83fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 1358 additions and 997 deletions

View File

@ -17,6 +17,7 @@
--varattributes prev-line --varattributes prev-line
--trailingclosures --trailingclosures
--shortoptionals "always" --shortoptionals "always"
--ifdef no-indent
--enable isEmpty, \ --enable isEmpty, \
leadingDelimiters, \ leadingDelimiters, \

View File

@ -7,31 +7,41 @@
// //
import Foundation import Foundation
import JellyfinAPI
import Stinsen import Stinsen
import SwiftUI import SwiftUI
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
final class FilterCoordinator: NavigationCoordinatable { 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) let stack = NavigationStack(initial: \FilterCoordinator.start)
@Root @Root
var start = makeStart var start = makeStart
@Binding private let parameters: Parameters
var filters: LibraryFilters
var enabledFilterType: [FilterType]
var parentId: String = ""
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) { init(parameters: Parameters) {
_filters = filters self.parameters = parameters
self.enabledFilterType = enabledFilterType
self.parentId = parentId
} }
@ViewBuilder @ViewBuilder
func makeStart() -> some View { 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
} }
} }

View File

@ -21,36 +21,38 @@ final class HomeCoordinator: NavigationCoordinatable {
var settings = makeSettings var settings = makeSettings
#if os(tvOS) #if os(tvOS)
@Route(.modal) @Route(.modal)
var item = makeModalItem var item = makeItem
@Route(.modal) @Route(.modal)
var library = makeModalLibrary var library = makeLibrary
#else #else
@Route(.push) @Route(.push)
var item = makeItem var item = makeItem
@Route(.push) @Route(.push)
var library = makeLibrary var library = makeLibrary
#endif #endif
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> { func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
NavigationViewCoordinator(SettingsCoordinator()) NavigationViewCoordinator(SettingsCoordinator())
} }
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { #if os(tvOS)
LibraryCoordinator(viewModel: params.viewModel, title: params.title) 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 { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator {
NavigationViewCoordinator(ItemCoordinator(item: item)) LibraryCoordinator(parameters: parameters)
}
func makeModalLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> {
NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title))
} }
#endif
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {

View File

@ -32,8 +32,8 @@ final class ItemCoordinator: NavigationCoordinatable {
self.itemDto = item self.itemDto = item
} }
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title) LibraryCoordinator(parameters: parameters)
} }
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {

View File

@ -26,9 +26,9 @@ final class ItemOverviewCoordinator: NavigationCoordinatable {
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
#if os(tvOS) #if os(tvOS)
EmptyView() EmptyView()
#else #else
ItemOverviewView(item: item) ItemOverviewView(item: item)
#endif #endif
} }
} }

View File

@ -11,51 +11,71 @@ import JellyfinAPI
import Stinsen import Stinsen
import SwiftUI import SwiftUI
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
final class LibraryCoordinator: NavigationCoordinatable { 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) let stack = NavigationStack(initial: \LibraryCoordinator.start)
@Root @Root
var start = makeStart var start = makeStart
@Route(.modal)
var filter = makeFilter
#if os(tvOS) #if os(tvOS)
@Route(.modal) @Route(.modal)
var item = makeModalItem var item = makeItem
#else #else
@Route(.push) @Route(.push)
var item = makeItem var item = makeItem
@Route(.modal)
var filter = makeFilter
#endif #endif
let viewModel: LibraryViewModel private let parameters: Parameters
let title: String
init(viewModel: LibraryViewModel, title: String) { init(parameters: Parameters) {
self.viewModel = viewModel self.parameters = parameters
self.title = title
} }
@ViewBuilder @ViewBuilder
func makeStart() -> some View { 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> { #if os(tvOS)
NavigationViewCoordinator(FilterCoordinator( func makeItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
filters: params.filters, NavigationViewCoordinator(ItemCoordinator(item: item))
enabledFilterType: params.enabledFilterType,
parentId: params.parentId
))
} }
#else
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
func makeModalItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(FilterCoordinator(parameters: parameters))
} }
#endif
} }

View File

@ -17,28 +17,28 @@ final class MediaCoordinator: NavigationCoordinatable {
@Root @Root
var start = makeStart var start = makeStart
#if os(tvOS) #if os(tvOS)
@Route(.modal) @Route(.modal)
var library = makeLibrary var library = makeLibrary
#else #else
@Route(.push) @Route(.push)
var library = makeLibrary var library = makeLibrary
@Route(.push) @Route(.push)
var liveTV = makeLiveTV var liveTV = makeLiveTV
#endif #endif
#if os(tvOS) #if os(tvOS)
func makeLibrary(params: LibraryCoordinatorParams) -> NavigationViewCoordinator<LibraryCoordinator> { func makeLibrary(parameters: LibraryCoordinator.Parameters) -> NavigationViewCoordinator<LibraryCoordinator> {
NavigationViewCoordinator(LibraryCoordinator(viewModel: params.viewModel, title: params.title)) NavigationViewCoordinator(LibraryCoordinator(parameters: parameters))
} }
#else #else
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator { func makeLibrary(parameters: LibraryCoordinator.Parameters) -> LibraryCoordinator {
LibraryCoordinator(viewModel: params.viewModel, title: params.title) LibraryCoordinator(parameters: parameters)
} }
func makeLiveTV() -> LiveTVCoordinator { func makeLiveTV() -> LiveTVCoordinator {
LiveTVCoordinator() LiveTVCoordinator()
} }
#endif #endif
@ViewBuilder @ViewBuilder

View File

@ -36,10 +36,10 @@ final class MovieLibrariesCoordinator: NavigationCoordinatable {
} }
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { 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 { func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init()))
} }
} }

View File

@ -18,21 +18,27 @@ final class SearchCoordinator: NavigationCoordinatable {
@Root @Root
var start = makeStart var start = makeStart
#if os(tvOS) #if os(tvOS)
@Route(.modal) @Route(.modal)
var item = makeItem var item = makeItem
#else #else
@Route(.push) @Route(.push)
var item = makeItem var item = makeItem
@Route(.modal)
var filter = makeFilter
#endif #endif
#if os(tvOS) #if os(tvOS)
func makeItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> { func makeItem(item: BaseItemDto) -> NavigationViewCoordinator<ItemCoordinator> {
NavigationViewCoordinator(ItemCoordinator(item: item)) NavigationViewCoordinator(ItemCoordinator(item: item))
} }
#else #else
func makeItem(item: BaseItemDto) -> ItemCoordinator { func makeItem(item: BaseItemDto) -> ItemCoordinator {
ItemCoordinator(item: item) ItemCoordinator(item: item)
} }
func makeFilter(parameters: FilterCoordinator.Parameters) -> NavigationViewCoordinator<FilterCoordinator> {
NavigationViewCoordinator(FilterCoordinator(parameters: parameters))
}
#endif #endif
@ViewBuilder @ViewBuilder

View File

@ -27,10 +27,10 @@ final class SettingsCoordinator: NavigationCoordinatable {
var about = makeAbout var about = makeAbout
#if !os(tvOS) #if !os(tvOS)
@Route(.push) @Route(.push)
var quickConnect = makeQuickConnectSettings var quickConnect = makeQuickConnectSettings
@Route(.push) @Route(.push)
var fontPicker = makeFontPicker var fontPicker = makeFontPicker
#endif #endif
@ViewBuilder @ViewBuilder
@ -59,17 +59,17 @@ final class SettingsCoordinator: NavigationCoordinatable {
} }
#if !os(tvOS) #if !os(tvOS)
@ViewBuilder @ViewBuilder
func makeQuickConnectSettings() -> some View { func makeQuickConnectSettings() -> some View {
let viewModel = QuickConnectSettingsViewModel() let viewModel = QuickConnectSettingsViewModel()
QuickConnectSettingsView(viewModel: viewModel) QuickConnectSettingsView(viewModel: viewModel)
} }
@ViewBuilder @ViewBuilder
func makeFontPicker() -> some View { func makeFontPicker() -> some View {
FontPickerView() FontPickerView()
.navigationTitle(L10n.subtitleFont) .navigationTitle(L10n.subtitleFont)
} }
#endif #endif
@ViewBuilder @ViewBuilder

View File

@ -36,10 +36,10 @@ final class TVLibrariesCoordinator: NavigationCoordinatable {
} }
func makeLibrary(library: BaseItemDto) -> LibraryCoordinator { 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 { func makeRootLibrary(library: BaseItemDto) -> LibraryCoordinator {
LibraryCoordinator(viewModel: LibraryViewModel(library: library), title: library.title) LibraryCoordinator(parameters: .init(parent: library, type: .library, filters: .init()))
} }
} }

View File

@ -17,8 +17,8 @@ final class UserSignInCoordinator: NavigationCoordinatable {
@Root @Root
var start = makeStart var start = makeStart
#if !os(tvOS) #if !os(tvOS)
@Route(.modal) @Route(.modal)
var quickConnect = makeQuickConnect var quickConnect = makeQuickConnect
#endif #endif
let viewModel: UserSignInViewModel let viewModel: UserSignInViewModel
@ -28,9 +28,9 @@ final class UserSignInCoordinator: NavigationCoordinatable {
} }
#if !os(tvOS) #if !os(tvOS)
func makeQuickConnect() -> NavigationViewCoordinator<QuickConnectCoordinator> { func makeQuickConnect() -> NavigationViewCoordinator<QuickConnectCoordinator> {
NavigationViewCoordinator(QuickConnectCoordinator(viewModel: viewModel)) NavigationViewCoordinator(QuickConnectCoordinator(viewModel: viewModel))
} }
#endif #endif
@ViewBuilder @ViewBuilder

View File

@ -13,15 +13,15 @@ public extension Color {
internal static let jellyfinPurple = Color(uiColor: .jellyfinPurple) internal static let jellyfinPurple = Color(uiColor: .jellyfinPurple)
#if os(tvOS) // tvOS doesn't have these #if os(tvOS) // tvOS doesn't have these
static let systemFill = Color(UIColor.white) static let systemFill = Color(UIColor.white)
static let secondarySystemFill = Color(UIColor.gray) static let secondarySystemFill = Color(UIColor.gray)
static let tertiarySystemFill = Color(UIColor.black) static let tertiarySystemFill = Color(UIColor.black)
static let lightGray = Color(UIColor.lightGray) static let lightGray = Color(UIColor.lightGray)
#else #else
static let systemFill = Color(UIColor.systemFill) static let systemFill = Color(UIColor.systemFill)
static let systemBackground = Color(UIColor.systemBackground) static let systemBackground = Color(UIColor.systemBackground)
static let secondarySystemFill = Color(UIColor.secondarySystemFill) static let secondarySystemFill = Color(UIColor.secondarySystemFill)
static let tertiarySystemFill = Color(UIColor.tertiarySystemFill) static let tertiarySystemFill = Color(UIColor.tertiarySystemFill)
#endif #endif
} }

View File

@ -12,8 +12,8 @@ extension Font {
func toUIFont() -> UIFont { func toUIFont() -> UIFont {
switch self { switch self {
#if !os(tvOS) #if !os(tvOS)
case .largeTitle: case .largeTitle:
return UIFont.preferredFont(forTextStyle: .largeTitle) return UIFont.preferredFont(forTextStyle: .largeTitle)
#endif #endif
case .title: case .title:
return UIFont.preferredFont(forTextStyle: .title1) return UIFont.preferredFont(forTextStyle: .title1)

View File

@ -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)
}
}

View File

@ -86,7 +86,7 @@ extension BaseItemDto {
} }
var displayName: String { var displayName: String {
name ?? "--" name ?? .emptyDash
} }
// MARK: ItemDetail // MARK: ItemDetail
@ -247,3 +247,5 @@ extension BaseItemDtoImageBlurHashes {
} }
} }
} }
extension BaseItemDto: LibraryParent {}

View File

@ -14,10 +14,6 @@ import UIKit
extension BaseItemPerson: Poster { extension BaseItemPerson: Poster {
var title: String {
self.name ?? "--"
}
var subtitle: String? { var subtitle: String? {
self.firstRole self.firstRole
} }

View File

@ -50,3 +50,11 @@ extension BaseItemPerson {
return DisplayedType(rawValue: type) != nil return DisplayedType(rawValue: type) != nil
} }
} }
extension BaseItemPerson: Displayable {
var displayName: String {
self.name ?? .emptyDash
}
}
extension BaseItemPerson: LibraryParent {}

View File

@ -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)
}
}

View File

@ -9,8 +9,16 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
extension NameGuidPair: PillStackable { extension NameGuidPair {
var title: String { var filter: ItemFilters.Filter {
self.name ?? "" .init(displayName: displayName, id: id, filterName: displayName)
} }
} }
extension NameGuidPair: Displayable {
var displayName: String {
self.name ?? .emptyDash
}
}
extension NameGuidPair: LibraryParent {}

View File

@ -54,6 +54,12 @@ extension String {
let textSize = self.size(withAttributes: fontAttributes) let textSize = self.size(withAttributes: fontAttributes)
return textSize.width return textSize.width
} }
var filter: ItemFilters.Filter {
.init(displayName: self, id: self, filterName: self)
}
static var emptyDash = "--"
} }
public extension CharacterSet { public extension CharacterSet {

View File

@ -22,22 +22,22 @@ extension UIDevice {
} }
#if os(iOS) #if os(iOS)
static var isPortrait: Bool { static var isPortrait: Bool {
UIDevice.current.orientation.isPortrait UIDevice.current.orientation.isPortrait
} }
static var isLandscape: Bool { static var isLandscape: Bool {
isIPad || UIDevice.current.orientation.isLandscape isIPad || UIDevice.current.orientation.isLandscape
} }
static func feedback(_ type: UINotificationFeedbackGenerator.FeedbackType) { static func feedback(_ type: UINotificationFeedbackGenerator.FeedbackType) {
let generator = UINotificationFeedbackGenerator() let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(type) generator.notificationOccurred(type)
} }
static func impact(_ type: UIImpactFeedbackGenerator.FeedbackStyle) { static func impact(_ type: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: type) let generator = UIImpactFeedbackGenerator(style: type)
generator.impactOccurred() generator.impactOccurred()
} }
#endif #endif
} }

View File

@ -8,9 +8,9 @@
import UIKit import UIKit
#if os(tvOS) #if os(tvOS)
import TVVLCKit import TVVLCKit
#else #else
import MobileVLCKit import MobileVLCKit
#endif #endif
extension VLCMediaPlayer { extension VLCMediaPlayer {

View File

@ -234,16 +234,16 @@ class DeviceProfileBuilder {
private func CPUinfo() -> CPUModel { private func CPUinfo() -> CPUModel {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]! let identifier = ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"]!
#else #else
var systemInfo = utsname() var systemInfo = utsname()
uname(&systemInfo) uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine) let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier } guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value))) return identifier + String(UnicodeScalar(UInt8(value)))
} }
#endif #endif
switch identifier { switch identifier {

View File

@ -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))
}
}

View File

@ -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 }
}

View File

@ -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
}
}

View File

@ -15,7 +15,7 @@ struct LibraryItem: Equatable, Poster {
var library: BaseItemDto var library: BaseItemDto
var viewModel: MediaViewModel var viewModel: MediaViewModel
var title: String = "" var displayName: String = ""
var subtitle: String? var subtitle: String?
var showTitle: Bool = false var showTitle: Bool = false

View File

@ -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
}

View File

@ -10,8 +10,7 @@ import Defaults
import Foundation import Foundation
import SwiftUI import SwiftUI
protocol Poster: Hashable { protocol Poster: Displayable, Hashable {
var title: String { get }
var subtitle: String? { get } var subtitle: String? { get }
var showTitle: Bool { get } var showTitle: Bool { get }
@ -21,7 +20,7 @@ protocol Poster: Hashable {
extension Poster { extension Poster {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(title) hasher.combine(displayName)
hasher.combine(subtitle) hasher.combine(subtitle)
} }
} }

View File

@ -22,6 +22,7 @@ enum PosterType: String, CaseIterable, Defaults.Serializable {
} }
} }
// TODO: localize
var localizedName: String { var localizedName: String {
switch self { switch self {
case .portrait: case .portrait:
@ -33,15 +34,15 @@ enum PosterType: String, CaseIterable, Defaults.Serializable {
enum Width { enum Width {
#if os(tvOS) #if os(tvOS)
static let portrait = 250.0 static let portrait = 250.0
static let landscape = 490.0 static let landscape = 490.0
#else #else
@ScaledMetric(relativeTo: .largeTitle) @ScaledMetric(relativeTo: .largeTitle)
static var portrait = 100.0 static var portrait = 100.0
@ScaledMetric(relativeTo: .largeTitle) @ScaledMetric(relativeTo: .largeTitle)
static var landscape = 200.0 static var landscape = 200.0
#endif #endif
} }
} }

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
protocol PillStackable { enum SelectorType {
var title: String { get } case single
case multi
} }

View File

@ -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)
}
}

View File

@ -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 ""
}
}
}

View File

@ -302,9 +302,9 @@ final class SessionManager {
let platform: String let platform: String
#if os(tvOS) #if os(tvOS)
platform = "tvOS" platform = "tvOS"
#else #else
platform = "iOS" platform = "iOS"
#endif #endif
var header = "MediaBrowser " var header = "MediaBrowser "

View File

@ -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)
}
}

View File

@ -24,9 +24,6 @@ final class HomeViewModel: ViewModel {
@Published @Published
var libraries: [BaseItemDto] = [] var libraries: [BaseItemDto] = []
// temp
static let recentFilterSet = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])
override init() { override init() {
super.init() super.init()
refresh() refresh()

View File

@ -69,7 +69,7 @@ final class EpisodeItemViewModel: ItemViewModel {
.joined(separator: ", ") .joined(separator: ", ")
let currentMediaItems: [BaseItemDto.ItemDetail] = [ let currentMediaItems: [BaseItemDto.ItemDetail] = [
.init(title: "File", content: viewModel.filename ?? "--"), .init(title: "File", content: viewModel.filename ?? .emptyDash),
.init(title: "Audio", content: audioStreams), .init(title: "Audio", content: audioStreams),
.init(title: "Subtitles", content: subtitleStreams), .init(title: "Subtitles", content: subtitleStreams),
] ]

View File

@ -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)
}
}

View File

@ -9,63 +9,61 @@
import Combine import Combine
import Defaults import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI
import UIKit import UIKit
// TODO: Look at refactoring // TODO: Look at refactoring
final class LibraryViewModel: ViewModel { 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) @Default(.Customization.Library.gridPosterType)
private var libraryGridPosterType private var libraryGridPosterType
let library: BaseItemDto? @Published
let person: BaseItemPerson? var items: [BaseItemDto] = []
let genre: NameGuidPair?
let studio: NameGuidPair? 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 { private var pageItemSize: Int {
let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77 let height = libraryGridPosterType == .portrait ? libraryGridPosterType.width * 1.5 : libraryGridPosterType.width / 1.77
return UIScreen.itemsFillableOnScreen(width: libraryGridPosterType.width, height: height) return UIScreen.itemsFillableOnScreen(width: libraryGridPosterType.width, height: height)
} }
var enabledFilterType: [FilterType] { func requestItemsAsync(with filters: ItemFilters, replaceCurrentItems: Bool = false) {
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) {
if replaceCurrentItems { if replaceCurrentItems {
self.items = [] self.items = []
@ -73,23 +71,26 @@ final class LibraryViewModel: ViewModel {
self.hasNextPage = true self.hasNextPage = true
} }
let personIDs: [String] = [person].compactMap(\.?.id) var libraryID: String?
let studioIDs: [String] = [studio].compactMap(\.?.id) var personIDs: [String]?
let genreIDs: [String] var studioIDs: [String]?
if filters.withGenres.isEmpty { if let parent = parent {
genreIDs = [genre].compactMap(\.?.id) switch type {
} else { case .library, .folders:
genreIDs = filters.withGenres.compactMap(\.id) libraryID = parent.id
case .person:
personIDs = [parent].compactMap(\.id)
case .studio:
studioIDs = [parent].compactMap(\.id)
}
} }
let sortBy = filters.sortBy.map(\.rawValue)
let includeItemTypes: [BaseItemKind] let includeItemTypes: [BaseItemKind]
if filters.filters.contains(.isFavorite) { if filters.filters.contains(ItemFilter.isFavorite.filter) {
includeItemTypes = [.movie, .boxSet, .series, .season, .episode] includeItemTypes = [.movie, .boxSet, .series, .season, .episode]
} else if library?.collectionType == "folders" { } else if type == .folders {
includeItemTypes = [.collectionFolder] includeItemTypes = [.collectionFolder]
} else { } else {
includeItemTypes = [.movie, .series, .boxSet] includeItemTypes = [.movie, .series, .boxSet]
@ -97,26 +98,31 @@ final class LibraryViewModel: ViewModel {
let excludedIDs: [String]? let excludedIDs: [String]?
if filters.sortBy == [.random] { if filters.sortBy.first == SortBy.random.filter {
excludedIDs = items.compactMap(\.id) excludedIDs = items.compactMap(\.id)
} else { } else {
excludedIDs = nil 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( ItemsAPI.getItemsByUserId(
userId: SessionManager.main.currentLogin.user.id, userId: SessionManager.main.currentLogin.user.id,
excludeItemIds: excludedIDs, excludeItemIds: excludedIDs,
startIndex: currentPage * pageItemSize, startIndex: currentPage * pageItemSize,
limit: pageItemSize, limit: pageItemSize,
recursive: true, recursive: true,
searchTerm: nil, sortOrder: sortOrder,
sortOrder: filters.sortOrder.compactMap { SortOrder(rawValue: $0.rawValue) }, parentId: libraryID,
parentId: library?.id,
fields: ItemFields.allCases, fields: ItemFields.allCases,
includeItemTypes: includeItemTypes, includeItemTypes: includeItemTypes,
filters: filters.filters, filters: itemFilters,
sortBy: sortBy, sortBy: sortBy,
tags: filters.tags, tags: tags,
enableUserData: true, enableUserData: true,
personIds: personIDs, personIds: personIDs,
studioIds: studioIDs, studioIds: studioIDs,
@ -139,7 +145,7 @@ final class LibraryViewModel: ViewModel {
// excluded ids. This causes shorter item additions when using "Random" over // 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. // consecutive calls. Investigation needs to be done to find the root of the problem.
// Only filter for "Random" as an optimization. // 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) } ?? [] items = response.items?.filter { !(self?.items.contains($0) ?? true) } ?? []
} else { } else {
items = response.items ?? [] items = response.items ?? []
@ -153,7 +159,7 @@ final class LibraryViewModel: ViewModel {
func requestNextPageAsync() { func requestNextPageAsync() {
guard hasNextPage else { return } guard hasNextPage else { return }
currentPage += 1 currentPage += 1
requestItemsAsync(with: filters) requestItemsAsync(with: filterViewModel.currentFilters)
} }
} }

View File

@ -13,8 +13,6 @@ import SwiftUI
final class SearchViewModel: ViewModel { final class SearchViewModel: ViewModel {
private var searchCancellables = Set<AnyCancellable>()
@Published @Published
var movies: [BaseItemDto] = [] var movies: [BaseItemDto] = []
@Published @Published
@ -28,6 +26,10 @@ final class SearchViewModel: ViewModel {
@Published @Published
var suggestions: [BaseItemDto] = [] var suggestions: [BaseItemDto] = []
let filterViewModel: FilterViewModel
private var searchTextSubject = CurrentValueSubject<String, Never>("")
private var searchCancellables = Set<AnyCancellable>()
var noResults: Bool { var noResults: Bool {
movies.isEmpty && movies.isEmpty &&
collections.isEmpty && collections.isEmpty &&
@ -36,9 +38,8 @@ final class SearchViewModel: ViewModel {
people.isEmpty people.isEmpty
} }
private var searchTextSubject = CurrentValueSubject<String, Never>("")
override init() { override init() {
self.filterViewModel = .init(parent: nil, currentFilters: .init())
super.init() super.init()
getSuggestions() getSuggestions()
@ -47,7 +48,15 @@ final class SearchViewModel: ViewModel {
.handleEvents(receiveOutput: { _ in self.cancelPreviousSearch() }) .handleEvents(receiveOutput: { _ in self.cancelPreviousSearch() })
.filter { !$0.isEmpty } .filter { !$0.isEmpty }
.debounce(for: 0.25, scheduler: DispatchQueue.main) .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) .store(in: &cancellables)
} }
@ -59,29 +68,39 @@ final class SearchViewModel: ViewModel {
searchTextSubject.send(query) searchTextSubject.send(query)
} }
private func _search(with query: String) { private func _search(with query: String, filters: ItemFilters) {
getItems(with: query, for: .movie, keyPath: \.movies) getItems(for: query, with: filters, type: .movie, keyPath: \.movies)
getItems(with: query, for: .boxSet, keyPath: \.collections) getItems(for: query, with: filters, type: .boxSet, keyPath: \.collections)
getItems(with: query, for: .series, keyPath: \.series) getItems(for: query, with: filters, type: .series, keyPath: \.series)
getItems(with: query, for: .episode, keyPath: \.episodes) getItems(for: query, with: filters, type: .episode, keyPath: \.episodes)
getPeople(with: query) getPeople(for: query, with: filters)
} }
private func getItems( private func getItems(
with query: String, for query: String,
for itemType: BaseItemKind, with filters: ItemFilters,
type itemType: BaseItemKind,
keyPath: ReferenceWritableKeyPath<SearchViewModel, [BaseItemDto]> 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( ItemsAPI.getItemsByUserId(
userId: SessionManager.main.currentLogin.user.id, userId: SessionManager.main.currentLogin.user.id,
limit: 20, limit: 20,
recursive: true, recursive: true,
searchTerm: query, searchTerm: query,
sortOrder: [.ascending], sortOrder: sortOrder,
fields: ItemFields.allCases, fields: ItemFields.allCases,
includeItemTypes: [itemType], includeItemTypes: [itemType],
sortBy: ["SortName"], filters: itemFilters,
sortBy: sortBy,
tags: tags,
enableUserData: true, enableUserData: true,
genreIds: genreIDs,
enableImages: true enableImages: true
) )
.trackActivity(loading) .trackActivity(loading)
@ -93,7 +112,12 @@ final class SearchViewModel: ViewModel {
.store(in: &searchCancellables) .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( PersonsAPI.getPersons(
limit: 20, limit: 20,
searchTerm: query searchTerm: query

View File

@ -99,7 +99,7 @@ final class UserSignInViewModel: ViewModel {
self.quickConnectSecret = response.secret self.quickConnectSecret = response.secret
self.quickConnectCode = response.code 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.quickConnectTimer = RepeatingTimer(interval: 5) {
self.checkAuthStatus(onSuccess) self.checkAuthStatus(onSuccess)

View File

@ -14,9 +14,9 @@ import JellyfinAPI
import UIKit import UIKit
#if os(tvOS) #if os(tvOS)
import TVVLCKit import TVVLCKit
#else #else
import MobileVLCKit import MobileVLCKit
#endif #endif
final class VideoPlayerViewModel: ViewModel { final class VideoPlayerViewModel: ViewModel {

View File

@ -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
)
}
}

View File

@ -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)
}
}
}

View File

@ -59,17 +59,17 @@ struct TruncatedTextView: View {
if truncated { if truncated {
#if os(tvOS) #if os(tvOS)
Text(seeMoreText)
.font(font)
.foregroundColor(.purple)
#else
Button {
seeMoreAction()
} label: {
Text(seeMoreText) Text(seeMoreText)
.font(font) .font(font)
.foregroundColor(.purple) .foregroundColor(.purple)
#else }
Button {
seeMoreAction()
} label: {
Text(seeMoreText)
.font(font)
.foregroundColor(.purple)
}
#endif #endif
} }
} }

View File

@ -209,7 +209,7 @@ struct PosterButtonDefaultContentView<Item: Poster>: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if item.showTitle { if item.showTitle {
Text(item.title) Text(item.displayName)
.font(.footnote) .font(.footnote)
.fontWeight(.regular) .fontWeight(.regular)
.foregroundColor(.primary) .foregroundColor(.primary)

View File

@ -30,7 +30,7 @@ struct BasicAppSettingsView: View {
HStack { HStack {
L10n.version.text L10n.version.text
Spacer() Spacer()
Text("\(UIApplication.appVersion ?? "--") (\(UIApplication.bundleVersion ?? "--"))") Text("\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }

View File

@ -70,7 +70,7 @@ struct ContinueWatchingCard: View {
.frame(width: 500, alignment: .leading) .frame(width: 500, alignment: .leading)
if item.type == .episode { if item.type == .episode {
Text(item.episodeLocator ?? "--") Text(item.episodeLocator ?? .emptyDash)
.font(.callout) .font(.callout)
.fontWeight(.medium) .fontWeight(.medium)
.foregroundColor(.secondary) .foregroundColor(.secondary)

View File

@ -30,7 +30,7 @@ struct ItemView: View {
case .boxSet: case .boxSet:
CollectionItemView(viewModel: .init(item: item)) CollectionItemView(viewModel: .init(item: item))
case .person: case .person:
LibraryView(viewModel: .init(person: .init(id: item.id))) LibraryView(viewModel: .init(parent: item, type: .person, filters: .init()))
default: default:
Text(L10n.notImplementedYetWithType(item.type ?? "--")) Text(L10n.notImplementedYetWithType(item.type ?? "--"))
} }

View File

@ -20,17 +20,7 @@ struct LatestInLibraryView: View {
PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: .portrait, items: viewModel.items) PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: .portrait, items: viewModel.items)
.trailing { .trailing {
Button { Button {
router.route(to: \.library, ( router.route(to: \.library, .init(parent: viewModel.library, type: .library, filters: .recent))
viewModel: .init(
library: viewModel.library,
filters: LibraryFilters(
filters: [],
sortOrder: [.descending],
sortBy: [.dateAdded]
)
),
title: viewModel.library.displayName
))
} label: { } label: {
ZStack { ZStack {
Color(UIColor.darkGray) Color(UIColor.darkGray)

View File

@ -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
}
}
}
}
}

View File

@ -21,7 +21,7 @@ struct LibraryView: View {
private var scrollViewOffset: CGPoint = .zero private var scrollViewOffset: CGPoint = .zero
@Default(.Customization.Library.gridPosterType) @Default(.Customization.Library.gridPosterType)
var libraryPosterType private var libraryPosterType
@ViewBuilder @ViewBuilder
private var loadingView: some View { private var loadingView: some View {

View File

@ -27,11 +27,13 @@ struct MediaView: View {
.onSelect { _ in .onSelect { _ in
switch item.library.collectionType { switch item.library.collectionType {
case "favorites": 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": case "liveTV":
tabRouter.root(\.liveTV) tabRouter.root(\.liveTV)
default: 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 .imageOverlay { _ in

View File

@ -142,7 +142,7 @@ struct SettingsView: View {
HStack { HStack {
L10n.version.text L10n.version.text
Spacer() Spacer()
Text("\(UIApplication.appVersion ?? "--") (\(UIApplication.bundleVersion ?? "--"))") Text("\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }

View File

@ -476,11 +476,11 @@ extension LiveTVPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { 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] { } 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 { } 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)")
} }
} }

View File

@ -476,11 +476,11 @@ extension VLCPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { 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] { } 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 { } 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)")
} }
} }

View File

@ -18,8 +18,7 @@
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; };
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; }; 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531AC8BE26750DE20091C7EB /* ImageView.swift */; };
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; }; 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5321753A2671BCFC005491E6 /* SettingsViewModel.swift */; };
5321753E2671DE9C005491E6 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 5321753E2671DE9C005491E6 /* ItemFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* ItemFilters.swift */; };
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; };
53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; };
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; };
534D4FF026A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEE26A7D7CC000A7A48 /* Localizable.strings */; }; 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 */; }; 5358707E2669D64F00D05A09 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; }; 535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; };
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; 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 */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; };
53649AB2269D019100A2D8B7 /* 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 */; }; 62E632EA267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632E8267D3FF50063E547 /* SeasonItemViewModel.swift */; };
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; }; 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */; };
62E632ED267D410B0063E547 /* 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 */; }; 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632F2267D54030063E547 /* ItemViewModel.swift */; };
62E632F4267D54030063E547 /* 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 */; }; 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; };
62EC3530267666A5000E9F2D /* 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 */; }; 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */; }; 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175A287DDFB9000603CE /* QuickConnectSettingsView.swift */; };
6334175D287DE0D0000603CE /* QuickConnectSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175C287DE0D0000603CE /* QuickConnectSettingsViewModel.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 */; }; 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 */; }; E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; }; 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 */; }; E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */; };
E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E118959C289312020042947B /* BaseItemPerson+Poster.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 */; }; 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 */; }; E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; };
E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; };
E126F742278A656C00A522BF /* 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 */; }; E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB1279E3C6200BC6161 /* Puppy */; };
E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB5279E3CA500BC6161 /* Puppy */; }; E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB5279E3CA500BC6161 /* Puppy */; };
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; 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 */; }; E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */; };
E13F05F228BC9016003499D2 /* 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 */; }; 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 */; }; E1546777289AF46E00087E35 /* CollectionItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546776289AF46E00087E35 /* CollectionItemView.swift */; };
E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; }; E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1546779289AF48200087E35 /* CollectionItemContentView.swift */; };
E168BD10289A4162001A6922 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E168BD08289A4162001A6922 /* HomeView.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 */; }; E178859E2780F53B0094FBCF /* SliderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859D2780F53B0094FBCF /* SliderView.swift */; };
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; }; E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */; };
E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17885A3278105170094FBCF /* SFSymbolButton.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 */; }; E184C160288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */; };
E184C161288C5C08000B25BA /* 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 */; }; 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 */; }; E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A3D288F0D3D00CB80AA /* UIScreenExtensions.swift */; };
E1937A61288F32DB00CB80AA /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; }; E1937A61288F32DB00CB80AA /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; };
E1937A62288F32DB00CB80AA /* 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 */; }; E193D53227193F7B00900D82 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; };
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; }; E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; };
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.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 */; }; E193D547271941C500900D82 /* UserListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D546271941C500900D82 /* UserListView.swift */; };
E193D549271941CC00900D82 /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D548271941CC00900D82 /* UserSignInView.swift */; }; E193D549271941CC00900D82 /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D548271941CC00900D82 /* UserSignInView.swift */; };
E193D54B271941D300900D82 /* ServerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54A271941D300900D82 /* ServerListView.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 */; }; E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; };
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; };
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.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 */; }; E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
E1E00A36278628A40022235B /* 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 */; }; E1E1643A28BAC2EF00323B0A /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643928BAC2EF00323B0A /* SearchView.swift */; };
E1E1643E28BB074000323B0A /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* MultiSelectorView.swift */; }; E1E1643E28BB074000323B0A /* SelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* SelectorView.swift */; };
E1E1643F28BB075C00323B0A /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* MultiSelectorView.swift */; }; E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1643D28BB074000323B0A /* SelectorView.swift */; };
E1E1644128BB301900323B0A /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644028BB301900323B0A /* ArrayExtensions.swift */; }; E1E1644128BB301900323B0A /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E1644028BB301900323B0A /* ArrayExtensions.swift */; };
E1E1644228BB301900323B0A /* 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshHelper.swift; sourceTree = "<group>"; };
@ -1038,10 +1066,10 @@
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */, E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */,
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */, 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
E10D87E127852FD000BD264C /* EpisodesRowManager.swift */, E10D87E127852FD000BD264C /* EpisodesRowManager.swift */,
E113133928BEB71D00930F75 /* FilterViewModel.swift */,
625CB5722678C32A00530A6E /* HomeViewModel.swift */, 625CB5722678C32A00530A6E /* HomeViewModel.swift */,
E107BB9127880A4000354E07 /* ItemViewModel */, E107BB9127880A4000354E07 /* ItemViewModel */,
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */, 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */,
62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */,
62E632DF267D30CA0063E547 /* LibraryViewModel.swift */, 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */,
C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */, C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */,
C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */, C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */,
@ -1155,21 +1183,23 @@
E1D4BF802719D22800A11E64 /* AppAppearance.swift */, E1D4BF802719D22800A11E64 /* AppAppearance.swift */,
E1D4BF862719D27100A11E64 /* Bitrates.swift */, E1D4BF862719D27100A11E64 /* Bitrates.swift */,
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, E17FB55128C119D400311DFE /* Displayable.swift */,
E19169CD272514760085832A /* HTTPScheme.swift */, E19169CD272514760085832A /* HTTPScheme.swift */,
535870AC2669D8DD00D05A09 /* ItemFilters.swift */,
E1C925F328875037002A7A66 /* ItemViewType.swift */, E1C925F328875037002A7A66 /* ItemViewType.swift */,
E1E1644328BC60C600323B0A /* LibraryItem.swift */, E1E1644328BC60C600323B0A /* LibraryItem.swift */,
E113133728BEADBA00930F75 /* LibraryParent.swift */,
E13F05EB28BC9000003499D2 /* LibraryViewType.swift */, E13F05EB28BC9000003499D2 /* LibraryViewType.swift */,
E1AA331E2782639D00F6439C /* OverlayType.swift */, E1AA331E2782639D00F6439C /* OverlayType.swift */,
E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */,
E193D4DA27193CCA00900D82 /* PillStackable.swift */,
E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */,
E1937A60288F32DB00CB80AA /* Poster.swift */, E1937A60288F32DB00CB80AA /* Poster.swift */,
E1CCF12D28ABF989006CAC9E /* PosterType.swift */, E1CCF12D28ABF989006CAC9E /* PosterType.swift */,
E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */, E18CE0B328A22EDA0092E7F1 /* RepeatingTimer.swift */,
E17FB54E28C1197700311DFE /* SelectorType.swift */,
E148128A28C15526003B8787 /* SortBy.swift */,
5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */, 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */,
E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */,
535870AC2669D8DD00D05A09 /* Typings.swift */,
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */, E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */,
); );
path = Objects; path = Objects;
@ -1558,6 +1588,15 @@
path = ItemViewModel; path = ItemViewModel;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E113133028BDB6D600930F75 /* NavBarDrawerButtons */ = {
isa = PBXGroup;
children = (
E113132E28BDB66A00930F75 /* NavBarDrawerModifier.swift */,
E113132A28BDB4B500930F75 /* NavBarDrawerView.swift */,
);
path = NavBarDrawerButtons;
sourceTree = "<group>";
};
E1171A1A28A2215800FA1AF5 /* UserSignInView */ = { E1171A1A28A2215800FA1AF5 /* UserSignInView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1578,13 +1617,13 @@
path = ViewExtensions; path = ViewExtensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E11895B12893842D0042947B /* NavBarOffsetModifier */ = { E11895B12893842D0042947B /* NavBarOffset */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */, E11895AB289383EE0042947B /* NavBarOffsetModifier.swift */,
E11895AE2893840F0042947B /* NavBarOffsetView.swift */, E11895AE2893840F0042947B /* NavBarOffsetView.swift */,
); );
path = NavBarOffsetModifier; path = NavBarOffset;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E11CEB85289984F5003E74C7 /* Extensions */ = { E11CEB85289984F5003E74C7 /* Extensions */ = {
@ -1598,8 +1637,9 @@
E11CEB8828998522003E74C7 /* iOSViewExtensions */ = { E11CEB8828998522003E74C7 /* iOSViewExtensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E11895B12893842D0042947B /* NavBarOffsetModifier */,
E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */, E11CEB8A28998552003E74C7 /* iOSViewExtensions.swift */,
E113133028BDB6D600930F75 /* NavBarDrawerButtons */,
E11895B12893842D0042947B /* NavBarOffset */,
); );
path = iOSViewExtensions; path = iOSViewExtensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1631,7 +1671,6 @@
531690E6267ABD79005D8AB9 /* HomeView.swift */, 531690E6267ABD79005D8AB9 /* HomeView.swift */,
E193D54E271942C000900D82 /* ItemView */, E193D54E271942C000900D82 /* ItemView */,
E1C925F828875647002A7A66 /* LatestInLibraryView.swift */, E1C925F828875647002A7A66 /* LatestInLibraryView.swift */,
E193D54C2719426600900D82 /* LibraryFilterView.swift */,
53A83C32268A309300DF3D92 /* LibraryView.swift */, 53A83C32268A309300DF3D92 /* LibraryView.swift */,
C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */, C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */,
C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */, C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */,
@ -1689,7 +1728,7 @@
E168BD07289A4162001A6922 /* HomeView */, E168BD07289A4162001A6922 /* HomeView */,
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */, E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
E14F7D0A26DB3714007C3AE6 /* ItemView */, E14F7D0A26DB3714007C3AE6 /* ItemView */,
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */, E113133128BDC72000930F75 /* FilterView.swift */,
E13F05EE28BC9016003499D2 /* LibraryView */, E13F05EE28BC9016003499D2 /* LibraryView */,
C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */, C4E5598828124C10003DECA5 /* LiveTVChannelItemElement.swift */,
C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */, C400DB6C27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift */,
@ -1848,8 +1887,8 @@
E18E01BD288747230022598C /* MovieItemView */ = { E18E01BD288747230022598C /* MovieItemView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E18E01BE288747230022598C /* iPadOSMovieItemView.swift */,
E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */, E18E01BF288747230022598C /* iPadOSMovieItemContentView.swift */,
E18E01BE288747230022598C /* iPadOSMovieItemView.swift */,
); );
path = MovieItemView; path = MovieItemView;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1888,8 +1927,8 @@
E18E01C8288747230022598C /* CollectionItemView */ = { E18E01C8288747230022598C /* CollectionItemView */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E18E01C9288747230022598C /* CollectionItemView.swift */,
E18E01CA288747230022598C /* CollectionItemContentView.swift */, E18E01CA288747230022598C /* CollectionItemContentView.swift */,
E18E01C9288747230022598C /* CollectionItemView.swift */,
); );
path = CollectionItemView; path = CollectionItemView;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1915,12 +1954,16 @@
E18E01D4288747230022598C /* Components */ = { E18E01D4288747230022598C /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E176DE6E278E3522001EFD8D /* EpisodesRowView */,
E18E01D5288747230022598C /* AboutView.swift */, E18E01D5288747230022598C /* AboutView.swift */,
E18E01D6288747230022598C /* ListDetailsView.swift */,
E18E01D7288747230022598C /* AttributeHStack.swift */,
E18E01D8288747230022598C /* PlayButton.swift */,
E18E01D9288747230022598C /* ActionButtonHStack.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; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1984,6 +2027,7 @@
E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = { E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E148128428C15472003B8787 /* APISortOrderExtensions.swift */,
E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */, E1937A3A288E54AD00CB80AA /* BaseItemDto+Images.swift */,
E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */, E18845F426DD631E00B0C5B7 /* BaseItemDto+Poster.swift */,
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */, E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */,
@ -1994,6 +2038,7 @@
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */,
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */, E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */,
E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */,
E148128728C154BF003B8787 /* ItemFilterExtensions.swift */,
E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */, E184C15F288C5C08000B25BA /* RequestBuilderExtensions.swift */,
E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */, E18CE0B128A229E70092E7F1 /* UserDtoExtensions.swift */,
); );
@ -2010,8 +2055,8 @@
E18E01FF288749200022598C /* Divider.swift */, E18E01FF288749200022598C /* Divider.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */, 531AC8BE26750DE20091C7EB /* ImageView.swift */,
E1047E2227E5880000CB0D4A /* InitialFailureView.swift */, E1047E2227E5880000CB0D4A /* InitialFailureView.swift */,
E1E1643D28BB074000323B0A /* MultiSelectorView.swift */,
531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */,
E1E1643D28BB074000323B0A /* SelectorView.swift */,
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */,
); );
path = Views; path = Views;
@ -2029,6 +2074,8 @@
E1C55AB228BD051700A9AD88 /* Components */ = { E1C55AB228BD051700A9AD88 /* Components */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E113133528BE98AA00930F75 /* FilterDrawerButton.swift */,
E113133328BE988200930F75 /* FilterDrawerHStack.swift */,
E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */, E13F05EF28BC9016003499D2 /* LibraryItemRow.swift */,
); );
path = Components; path = Components;
@ -2394,7 +2441,6 @@
E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */, E193D53627193F8500900D82 /* LibraryCoordinator.swift in Sources */,
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */, C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */,
E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E18845F626DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
E193D4DC27193CCA00900D82 /* PillStackable.swift in Sources */,
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */, E103A6A9278AB6FF00820EC7 /* CinematicNextUpCardView.swift in Sources */,
C4B9B91427E1921B0063535C /* LiveTVNativeVideoPlayerView.swift in Sources */, C4B9B91427E1921B0063535C /* LiveTVNativeVideoPlayerView.swift in Sources */,
@ -2408,7 +2454,8 @@
E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */, E1A16CA1288A7CFD00EA4679 /* AboutViewCard.swift in Sources */,
E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, E1937A3F288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */,
531690E7267ABD79005D8AB9 /* HomeView.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 */, E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */, 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */,
E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
@ -2432,6 +2479,7 @@
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */,
E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */,
E12B835F28C07D8500878399 /* LibraryParent.swift in Sources */,
E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */,
E18E021A2887492B0022598C /* AppIcon.swift in Sources */, E18E021A2887492B0022598C /* AppIcon.swift in Sources */,
E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
@ -2458,7 +2506,6 @@
53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */, 53A83C33268A309300DF3D92 /* LibraryView.swift in Sources */,
62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
5398514526B64DA100101B49 /* SettingsView.swift in Sources */, 5398514526B64DA100101B49 /* SettingsView.swift in Sources */,
62E632F0267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
E193D54B271941D300900D82 /* ServerListView.swift in Sources */, E193D54B271941D300900D82 /* ServerListView.swift in Sources */,
53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */,
C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */, C400DB6B27FE8C97007B65FE /* LiveTVChannelItemElement.swift in Sources */,
@ -2474,13 +2521,14 @@
E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */, E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */,
E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */, E11CEB9428999D9E003E74C7 /* EpisodeItemContentView.swift in Sources */,
E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */, E17885A02780F55C0094FBCF /* tvOSVLCOverlay.swift in Sources */,
E148128328C1443D003B8787 /* NameGUIDPairExtensions.swift in Sources */,
E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */, E1BDE359278E9ED2004E4022 /* MissingItemsSettingsView.swift in Sources */,
E193D54D2719426600900D82 /* LibraryFilterView.swift in Sources */,
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */, C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */,
E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */,
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */, C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */, E1D4BF852719D25A00A11E64 /* TrackLanguage.swift in Sources */,
E1C926142887565C002A7A66 /* FocusGuide.swift in Sources */, E1C926142887565C002A7A66 /* FocusGuide.swift in Sources */,
E148128928C154BF003B8787 /* ItemFilterExtensions.swift in Sources */,
E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */,
E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */,
E1399475289B1EA900401ABC /* Defaults+Workaround.swift in Sources */, E1399475289B1EA900401ABC /* Defaults+Workaround.swift in Sources */,
@ -2489,6 +2537,7 @@
E18E02202887492B0022598C /* AttributeFillView.swift in Sources */, E18E02202887492B0022598C /* AttributeFillView.swift in Sources */,
E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */, E154677A289AF48200087E35 /* CollectionItemContentView.swift in Sources */,
E148128C28C15526003B8787 /* SortBy.swift in Sources */,
E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */, E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */,
E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */, E1A2C156279A7D5A005EC829 /* UIApplicationExtensions.swift in Sources */,
E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */, E193D53D27193F9700900D82 /* UserSignInCoordinator.swift in Sources */,
@ -2518,6 +2567,7 @@
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */, E184C161288C5C08000B25BA /* RequestBuilderExtensions.swift in Sources */,
E19169CF272514760085832A /* HTTPScheme.swift in Sources */, E19169CF272514760085832A /* HTTPScheme.swift in Sources */,
E148128628C15475003B8787 /* APISortOrderExtensions.swift in Sources */,
E1E1644528BC60C600323B0A /* LibraryItem.swift in Sources */, E1E1644528BC60C600323B0A /* LibraryItem.swift in Sources */,
E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */, E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */,
E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */, E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */,
@ -2542,7 +2592,7 @@
E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */, E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */,
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, E11895B42893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */,
5321753E2671DE9C005491E6 /* Typings.swift in Sources */, 5321753E2671DE9C005491E6 /* ItemFilters.swift in Sources */,
E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */, E1C812CA277AE40900918266 /* PlayerOverlayDelegate.swift in Sources */,
E1AA33202782639D00F6439C /* OverlayType.swift in Sources */, E1AA33202782639D00F6439C /* OverlayType.swift in Sources */,
C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */, C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */,
@ -2554,6 +2604,7 @@
E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */, E193D5512719432400900D82 /* ServerDetailViewModel.swift in Sources */,
C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */, C4E5081B2703F82A0045C9AB /* MediaView.swift in Sources */,
E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */, E193D53B27193F9200900D82 /* SettingsCoordinator.swift in Sources */,
E113133B28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
E13F05ED28BC9000003499D2 /* LibraryViewType.swift in Sources */, E13F05ED28BC9000003499D2 /* LibraryViewType.swift in Sources */,
E18E021C2887492B0022598C /* BlurView.swift in Sources */, E18E021C2887492B0022598C /* BlurView.swift in Sources */,
E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */, E1E5D5442783BB5100692DFE /* ItemDetailsView.swift in Sources */,
@ -2576,6 +2627,7 @@
E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */, E11CEB8E28999B4A003E74C7 /* FontExtensions.swift in Sources */,
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */, C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */, E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */,
E17FB55028C1197700311DFE /* SelectorType.swift in Sources */,
09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */,
E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */,
E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */, E1C9261B288756BD002A7A66 /* DotHStack.swift in Sources */,
@ -2586,6 +2638,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E17FB55528C1250B00311DFE /* SimilarItemsHStack.swift in Sources */,
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */, E18845F526DD631E00B0C5B7 /* BaseItemDto+Poster.swift in Sources */,
E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */, E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */,
@ -2596,6 +2649,7 @@
621338932660107500A81A2A /* StringExtensions.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */,
62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */, 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */,
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E17FB55928C125E900311DFE /* StudiosHStack.swift in Sources */,
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */, E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */, E1EBCB44278BD1CE009FE6E9 /* ItemOverviewCoordinator.swift in Sources */,
E1C925F428875037002A7A66 /* ItemViewType.swift in Sources */, E1C925F428875037002A7A66 /* ItemViewType.swift in Sources */,
@ -2603,6 +2657,7 @@
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */, 62C29EA826D103D500C1D2E7 /* MediaCoordinator.swift in Sources */,
62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */,
E148128828C154BF003B8787 /* ItemFilterExtensions.swift in Sources */,
C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */, C400DB6D27FE8E65007B65FE /* LiveTVChannelItemWideElement.swift in Sources */,
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */, E11895AF2893840F0042947B /* NavBarOffsetView.swift in Sources */,
@ -2610,7 +2665,7 @@
E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */, E18E01AA288746AF0022598C /* RefreshableScrollView.swift in Sources */,
E18E0208288749200022598C /* BlurView.swift in Sources */, E18E0208288749200022598C /* BlurView.swift in Sources */,
E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */, E18E01E7288747230022598C /* CollectionItemContentView.swift in Sources */,
E1E1643F28BB075C00323B0A /* MultiSelectorView.swift in Sources */, E1E1643F28BB075C00323B0A /* SelectorView.swift in Sources */,
E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */, E18E01DF288747230022598C /* iPadOSMovieItemView.swift in Sources */,
E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */, E168BD13289A4162001A6922 /* ContinueWatchingView.swift in Sources */,
C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */, C45942CD27F6994A00C54FE7 /* LiveTVPlayerView.swift in Sources */,
@ -2626,6 +2681,7 @@
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */, E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */,
E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */, E18E01EB288747230022598C /* MovieItemContentView.swift in Sources */,
E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */,
E18E01FA288747580022598C /* AboutAppView.swift in Sources */, E18E01FA288747580022598C /* AboutAppView.swift in Sources */,
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */, 6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */,
@ -2639,8 +2695,10 @@
E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */, E1A2C158279A7D76005EC829 /* BundleExtensions.swift in Sources */,
C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */, C45942C627F695FB00C54FE7 /* LiveTVProgramsCoordinator.swift in Sources */,
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
E17FB55228C119D400311DFE /* Displayable.swift in Sources */,
E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */, E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */,
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
E113132B28BDB4B500930F75 /* NavBarDrawerView.swift in Sources */,
C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */, C45942CB27F6984100C54FE7 /* LiveTVPlayerViewController.swift in Sources */,
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
E19169CE272514760085832A /* HTTPScheme.swift in Sources */, E19169CE272514760085832A /* HTTPScheme.swift in Sources */,
@ -2670,9 +2728,9 @@
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */, C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
E113133428BE988200930F75 /* FilterDrawerHStack.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */, E1171A1928A2212600FA1AF5 /* QuickConnectView.swift in Sources */,
E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */, E11CEB8D28999B4A003E74C7 /* FontExtensions.swift in Sources */,
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
@ -2681,6 +2739,7 @@
E18CE0B228A229E70092E7F1 /* UserDtoExtensions.swift in Sources */, E18CE0B228A229E70092E7F1 /* UserDtoExtensions.swift in Sources */,
E18E01F0288747230022598C /* AttributeHStack.swift in Sources */, E18E01F0288747230022598C /* AttributeHStack.swift in Sources */,
6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */, 6334175B287DDFB9000603CE /* QuickConnectSettingsView.swift in Sources */,
E17FB54F28C1197700311DFE /* SelectorType.swift in Sources */,
E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */, E13F05F128BC9016003499D2 /* LibraryItemRow.swift in Sources */,
E18E0205288749200022598C /* AppIcon.swift in Sources */, E18E0205288749200022598C /* AppIcon.swift in Sources */,
E168BD10289A4162001A6922 /* HomeView.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */,
@ -2693,6 +2752,7 @@
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */, E168BD15289A4162001A6922 /* HomeErrorView.swift in Sources */,
E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */,
E148128B28C15526003B8787 /* SortBy.swift in Sources */,
5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */,
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */, E1FA891E289A305D00176FEB /* iPadOSCollectionItemContentView.swift in Sources */,
@ -2703,8 +2763,10 @@
E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */,
E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E1937A3B288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */, 62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
E113133228BDC72000930F75 /* FilterView.swift in Sources */,
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */, 62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */, E11895AC289383EE0042947B /* NavBarOffsetModifier.swift in Sources */,
E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */,
E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* UserListView.swift in Sources */,
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
@ -2727,12 +2789,13 @@
E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */, E13DD3E927177ED6009D4DAF /* ServerListCoordinator.swift in Sources */,
E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */, E1CCF12E28ABF989006CAC9E /* PosterType.swift in Sources */,
E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */, E1C812BD277A8E5D00918266 /* PlayerOverlayDelegate.swift in Sources */,
E113132F28BDB66A00930F75 /* NavBarDrawerModifier.swift in Sources */,
C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */, C45942C527F67DA400C54FE7 /* LiveTVCoordinator.swift in Sources */,
E113133A28BEB71D00930F75 /* FilterViewModel.swift in Sources */,
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */, E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */,
E18E01EE288747230022598C /* AboutView.swift in Sources */, E18E01EE288747230022598C /* AboutView.swift in Sources */,
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */, E11CEB8B28998552003E74C7 /* iOSViewExtensions.swift in Sources */,
E193D4DB27193CCA00900D82 /* PillStackable.swift in Sources */,
E1E1644428BC60C600323B0A /* LibraryItem.swift in Sources */, E1E1644428BC60C600323B0A /* LibraryItem.swift in Sources */,
E18E0206288749200022598C /* AttributeFillView.swift in Sources */, E18E0206288749200022598C /* AttributeFillView.swift in Sources */,
E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */,
@ -2746,13 +2809,14 @@
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */, E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */,
E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */, E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */,
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */,
62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */,
E1937A3E288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */, E1937A3E288F0D3D00CB80AA /* UIScreenExtensions.swift in Sources */,
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */, C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */,
E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */, E1EBCB46278BD595009FE6E9 /* ItemOverviewView.swift in Sources */,
E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */, E18CE0B428A22EDA0092E7F1 /* RepeatingTimer.swift in Sources */,
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */, 5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */, C45942C927F697CA00C54FE7 /* iOSLiveTVVideoPlayerCoordinator.swift in Sources */,
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */, E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
@ -2761,7 +2825,7 @@
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */, E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */, 535870AD2669D8DD00D05A09 /* ItemFilters.swift in Sources */,
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */, E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
E18E01F1288747230022598C /* PlayButton.swift in Sources */, E18E01F1288747230022598C /* PlayButton.swift in Sources */,
E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,
@ -2771,7 +2835,6 @@
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */, 6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */,
E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */, E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */,
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E1D4BF8A2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */,
E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */, E13DD3F92717E961009D4DAF /* UserListViewModel.swift in Sources */,
E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */, E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */,
@ -2781,6 +2844,7 @@
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */, E1D4BF872719D27100A11E64 /* Bitrates.swift in Sources */,
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
E148128528C15472003B8787 /* APISortOrderExtensions.swift in Sources */,
E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */, E13F05F328BC9016003499D2 /* LibraryView.swift in Sources */,
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryViewType.swift in Sources */,

View File

@ -8,11 +8,11 @@
import SwiftUI import SwiftUI
struct PillHStack<Item: PillStackable>: View { struct PillHStack<Item: Displayable>: View {
let title: String private var title: String
let items: [Item] private var items: [Item]
let onSelect: (Item) -> Void private var onSelect: (Item) -> Void
private init( private init(
title: String, title: String,
@ -37,11 +37,11 @@ struct PillHStack<Item: PillStackable>: View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack { HStack {
ForEach(items, id: \.title) { item in ForEach(items, id: \.displayName) { item in
Button { Button {
onSelect(item) onSelect(item)
} label: { } label: {
Text(item.title) Text(item.displayName)
.font(.caption) .font(.caption)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(.primary)
@ -68,12 +68,9 @@ extension PillHStack {
self.init(title: title, items: items, onSelect: { _ in }) self.init(title: title, items: items, onSelect: { _ in })
} }
@ViewBuilder func onSelect(_ onSelect: @escaping (Item) -> Void) -> Self {
func onSelect(_ onSelect: @escaping (Item) -> Void) -> PillHStack { var copy = self
PillHStack( copy.onSelect = onSelect
title: title, return copy
items: items,
onSelect: onSelect
)
} }
} }

View File

@ -196,7 +196,7 @@ struct PosterButtonDefaultContentView<Item: Poster>: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if item.showTitle { if item.showTitle {
Text(item.title) Text(item.displayName)
.font(.footnote) .font(.footnote)
.fontWeight(.regular) .fontWeight(.regular)
.foregroundColor(.primary) .foregroundColor(.primary)

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -12,4 +12,8 @@ extension View {
func navBarOffset(_ scrollViewOffset: Binding<CGFloat>, start: CGFloat, end: CGFloat) -> some View { func navBarOffset(_ scrollViewOffset: Binding<CGFloat>, start: CGFloat, end: CGFloat) -> some View {
self.modifier(NavBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end)) self.modifier(NavBarOffsetModifier(scrollViewOffset: scrollViewOffset, start: start, end: end))
} }
func navBarDrawer<Drawer: View>(@ViewBuilder _ drawer: @escaping () -> Drawer) -> some View {
self.modifier(NavBarDrawerModifier(drawer: drawer))
}
} }

View File

@ -37,7 +37,7 @@ struct AboutAppView: View {
HStack { HStack {
L10n.about.text L10n.about.text
Spacer() Spacer()
Text("\(UIApplication.appVersion ?? "--") (\(UIApplication.bundleVersion ?? "--"))") Text("\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }

View File

@ -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")
}
}
}
}
}

View File

@ -24,8 +24,7 @@ struct LatestInLibraryView: View {
PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: latestInLibraryPosterType, items: viewModel.items) PosterHStack(title: L10n.latestWithString(viewModel.library.displayName), type: latestInLibraryPosterType, items: viewModel.items)
.trailing { .trailing {
Button { Button {
let libraryViewModel = LibraryViewModel(library: viewModel.library, filters: HomeViewModel.recentFilterSet) homeRouter.route(to: \.library, .init(parent: viewModel.library, type: .library, filters: .recent))
homeRouter.route(to: \.library, (viewModel: libraryViewModel, title: viewModel.library.displayName))
} label: { } label: {
HStack { HStack {
L10n.seeAll.text L10n.seeAll.text

View File

@ -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()))
}
}
}
}

View File

@ -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])))
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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()))
}
}
}
}

View File

@ -43,9 +43,9 @@ struct ItemView: View {
CollectionItemView(viewModel: .init(item: item)) CollectionItemView(viewModel: .init(item: item))
} }
case .person: case .person:
LibraryView(viewModel: .init(person: .init(id: item.id))) LibraryView(viewModel: .init(parent: item, type: .person))
case .collectionFolder: case .collectionFolder:
LibraryView(viewModel: .init(library: item)) LibraryView(viewModel: .init(parent: item, type: .folders))
default: default:
Text(L10n.notImplementedYetWithType(item.type ?? "--")) Text(L10n.notImplementedYetWithType(item.type ?? "--"))
} }

View File

@ -23,25 +23,15 @@ extension CollectionItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
).onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }

View File

@ -45,34 +45,25 @@ extension EpisodeItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
).onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
// MARK: Studios
if let studios = viewModel.item.studios, !studios.isEmpty { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }
// MARK: Cast and Crew
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
!castAndCrew.isEmpty !castAndCrew.isEmpty
{ {
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
.onSelect { person in
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
}
Divider() Divider()
} }
@ -99,7 +90,7 @@ extension EpisodeItemView.ContentView {
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 10) { VStack(alignment: .center, spacing: 10) {
Text(viewModel.item.seriesName ?? "--") Text(viewModel.item.seriesName ?? .emptyDash)
.font(.headline) .font(.headline)
.fontWeight(.semibold) .fontWeight(.semibold)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@ -25,12 +25,7 @@ extension MovieItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
).onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
@ -38,12 +33,7 @@ extension MovieItemView {
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, !studios.isEmpty { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }
@ -53,10 +43,7 @@ extension MovieItemView {
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
!castAndCrew.isEmpty !castAndCrew.isEmpty
{ {
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
.onSelect { person in
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
}
Divider() Divider()
} }
@ -64,10 +51,7 @@ extension MovieItemView {
// MARK: Similar // MARK: Similar
if !viewModel.similarItems.isEmpty { if !viewModel.similarItems.isEmpty {
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems) ItemView.SimilarItemsHStack(items: viewModel.similarItems)
.onSelect { item in
itemRouter.route(to: \.item, item)
}
Divider() Divider()
} }

View File

@ -29,9 +29,7 @@ extension SeriesItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack(title: L10n.genres, items: genres).onSelect { genre in ItemView.GenresHStack(genres: genres)
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
@ -39,23 +37,17 @@ extension SeriesItemView {
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, !studios.isEmpty { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }
// MARK: Cast and Crew // MARK: Cast and Crew
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), !castAndCrew.isEmpty { if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) !castAndCrew.isEmpty
.onSelect { person in {
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title)) ItemView.CastAndCrewHStack(people: castAndCrew)
}
Divider() Divider()
} }
@ -63,10 +55,7 @@ extension SeriesItemView {
// MARK: Similar // MARK: Similar
if !viewModel.similarItems.isEmpty { if !viewModel.similarItems.isEmpty {
PosterHStack(title: L10n.recommended, type: .portrait, items: viewModel.similarItems) ItemView.SimilarItemsHStack(items: viewModel.similarItems)
.onSelect { item in
itemRouter.route(to: \.item, item)
}
Divider() Divider()
} }

View File

@ -23,25 +23,15 @@ extension iPadOSCollectionItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
).onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }

View File

@ -24,34 +24,25 @@ extension iPadOSEpisodeItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
).onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
// MARK: Studios
if let studios = viewModel.item.studios, !studios.isEmpty { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }
// MARK: Cast and Crew
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
!castAndCrew.isEmpty !castAndCrew.isEmpty
{ {
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
.onSelect { person in
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
}
Divider() Divider()
} }

View File

@ -24,13 +24,7 @@ extension iPadOSMovieItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
)
.onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
@ -38,12 +32,7 @@ extension iPadOSMovieItemView {
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, !studios.isEmpty { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }
@ -53,10 +42,7 @@ extension iPadOSMovieItemView {
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
!castAndCrew.isEmpty !castAndCrew.isEmpty
{ {
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
.onSelect { person in
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
}
Divider() Divider()
} }

View File

@ -28,12 +28,7 @@ extension iPadOSSeriesItemView {
// MARK: Genres // MARK: Genres
if let genres = viewModel.item.genreItems, !genres.isEmpty { if let genres = viewModel.item.genreItems, !genres.isEmpty {
PillHStack( ItemView.GenresHStack(genres: genres)
title: L10n.genres,
items: genres
).onSelect { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}
Divider() Divider()
} }
@ -41,12 +36,7 @@ extension iPadOSSeriesItemView {
// MARK: Studios // MARK: Studios
if let studios = viewModel.item.studios, !studios.isEmpty { if let studios = viewModel.item.studios, !studios.isEmpty {
PillHStack( ItemView.StudiosHStack(studios: studios)
title: L10n.studios,
items: studios
).onSelect { studio in
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
}
Divider() Divider()
} }
@ -56,10 +46,7 @@ extension iPadOSSeriesItemView {
if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed), if let castAndCrew = viewModel.item.people?.filter(\.isDisplayed),
!castAndCrew.isEmpty !castAndCrew.isEmpty
{ {
PosterHStack(title: L10n.castAndCrew, type: .portrait, items: castAndCrew) ItemView.CastAndCrewHStack(people: castAndCrew)
.onSelect { person in
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
}
Divider() Divider()
} }

View File

@ -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
}
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -102,7 +102,18 @@ struct LibraryView: View {
} }
} }
} }
.navigationTitle(viewModel.parent?.displayName ?? "")
.navigationBarTitleDisplayMode(.inline) .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 { .toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) { ToolbarItemGroup(placement: .navigationBarTrailing) {
Button { Button {
@ -120,18 +131,6 @@ struct LibraryView: View {
Image(systemName: "square.grid.2x2") 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))
} }
} }
} }

View File

@ -33,11 +33,13 @@ struct MediaView: View {
.onSelect { _ in .onSelect { _ in
switch item.library.collectionType { switch item.library.collectionType {
case "favorites": 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": case "liveTV":
router.route(to: \.liveTV) router.route(to: \.liveTV)
default: 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 .imageOverlay { _ in

View File

@ -95,8 +95,18 @@ struct SearchView: View {
.onChange(of: searchText) { newText in .onChange(of: searchText) { newText in
viewModel.search(with: newText) viewModel.search(with: newText)
} }
.searchable(text: $searchText, placement: .navigationBarDrawer, prompt: L10n.search)
.navigationTitle(L10n.search) .navigationTitle(L10n.search)
.navigationBarTitleDisplayMode(.inline) .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)
} }
} }

View File

@ -25,7 +25,7 @@ extension UserSignInView {
DisclosureGroup { DisclosureGroup {
SecureField(L10n.password, text: $enteredPassword) SecureField(L10n.password, text: $enteredPassword)
Button { Button {
viewModel.signIn(username: publicUser.name ?? "--", password: enteredPassword) viewModel.signIn(username: publicUser.name ?? .emptyDash, password: enteredPassword)
} label: { } label: {
L10n.signIn.text L10n.signIn.text
} }
@ -39,7 +39,7 @@ extension UserSignInView {
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
.clipShape(Circle()) .clipShape(Circle())
Text(publicUser.name ?? "--") Text(publicUser.name ?? .emptyDash)
Spacer() Spacer()
} }
} }

View File

@ -532,11 +532,11 @@ extension LiveTVPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { 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] { } 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 { } 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)")
} }
} }

View File

@ -351,7 +351,7 @@ struct VLCPlayerOverlayView: View {
viewModel.playerOverlayDelegate?.didSelectChapters() viewModel.playerOverlayDelegate?.didSelectChapters()
} label: { } label: {
HStack { HStack {
Text(currentChapter.name ?? "--") Text(currentChapter.name ?? .emptyDash)
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
.font(.system(size: 16, weight: .semibold, design: .default)) .font(.system(size: 16, weight: .semibold, design: .default))

View File

@ -615,11 +615,11 @@ extension VLCPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { 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] { } 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 { } 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)")
} }
} }