mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2025-02-25 01:31:32 +00:00
Merge pull request #298 from LePips/ios-new-library-views
iOS - LibraryView Improvements and Tidbits
This commit is contained in:
commit
6a7ecebac5
@ -23,6 +23,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
@Route(.push)
|
||||
var experimentalSettings = makeExperimentalSettings
|
||||
@Route(.push)
|
||||
var customizeViewsSettings = makeCustomizeViewsSettings
|
||||
@Route(.push)
|
||||
var missingSettings = makeMissingSettings
|
||||
|
||||
@ViewBuilder
|
||||
@ -41,6 +43,11 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
||||
ExperimentalSettingsView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeCustomizeViewsSettings() -> some View {
|
||||
CustomizeViewsSettings()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeMissingSettings() -> some View {
|
||||
MissingItemsSettingsView()
|
||||
|
@ -56,7 +56,7 @@ extension BaseItemDto: PortraitImageStackable {
|
||||
|
||||
public var showTitle: Bool {
|
||||
switch self.itemType {
|
||||
case .episode, .series, .movie:
|
||||
case .episode, .series, .movie, .boxset:
|
||||
return Defaults[.showPosterLabels]
|
||||
default:
|
||||
return true
|
||||
|
@ -206,6 +206,7 @@ public extension BaseItemDto {
|
||||
case episode = "Episode"
|
||||
case series = "Series"
|
||||
case boxset = "BoxSet"
|
||||
case collectionFolder = "CollectionFolder"
|
||||
|
||||
case unknown
|
||||
|
||||
@ -228,7 +229,7 @@ public extension BaseItemDto {
|
||||
|
||||
func portraitHeaderViewURL(maxWidth: Int) -> URL {
|
||||
switch itemType {
|
||||
case .movie, .season, .series, .boxset:
|
||||
case .movie, .season, .series, .boxset, .collectionFolder:
|
||||
return getPrimaryImage(maxWidth: maxWidth)
|
||||
case .episode:
|
||||
return getSeriesPrimaryImage(maxWidth: maxWidth)
|
||||
|
@ -76,6 +76,8 @@ internal enum L10n {
|
||||
internal static let `continue` = L10n.tr("Localizable", "continue")
|
||||
/// Continue Watching
|
||||
internal static let continueWatching = L10n.tr("Localizable", "continueWatching")
|
||||
/// Customize
|
||||
internal static let customize = L10n.tr("Localizable", "customize")
|
||||
/// Dark
|
||||
internal static let dark = L10n.tr("Localizable", "dark")
|
||||
/// Default Scheme
|
||||
|
@ -67,7 +67,8 @@ final class LibraryFilterViewModel: ViewModel {
|
||||
}
|
||||
|
||||
func requestQueryFilters() {
|
||||
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id, parentId: self.parentId)
|
||||
FilterAPI.getQueryFilters(userId: SessionManager.main.currentLogin.user.id,
|
||||
parentId: self.parentId)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
|
@ -10,6 +10,7 @@ import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import SwiftUICollection
|
||||
import UIKit
|
||||
|
||||
typealias LibraryRow = CollectionRow<Int, LibraryRowCell>
|
||||
|
||||
@ -20,15 +21,11 @@ struct LibraryRowCell: Hashable {
|
||||
}
|
||||
|
||||
final class LibraryViewModel: ViewModel {
|
||||
var parentID: String?
|
||||
var person: BaseItemPerson?
|
||||
var genre: NameGuidPair?
|
||||
var studio: NameGuidPair?
|
||||
|
||||
@Published
|
||||
var items = [BaseItemDto]()
|
||||
var items: [BaseItemDto] = []
|
||||
@Published
|
||||
var rows = [LibraryRow]()
|
||||
var rows: [LibraryRow] = []
|
||||
|
||||
@Published
|
||||
var totalPages = 0
|
||||
@ -36,15 +33,17 @@ final class LibraryViewModel: ViewModel {
|
||||
var currentPage = 0
|
||||
@Published
|
||||
var hasNextPage = false
|
||||
@Published
|
||||
var hasPreviousPage = false
|
||||
|
||||
// temp
|
||||
@Published
|
||||
var filters: LibraryFilters
|
||||
|
||||
var parentID: String?
|
||||
var person: BaseItemPerson?
|
||||
var genre: NameGuidPair?
|
||||
var studio: NameGuidPair?
|
||||
private let columns: Int
|
||||
private var libraries = [BaseItemDto]()
|
||||
private let pageItemSize: Int
|
||||
|
||||
var enabledFilterType: [FilterType] {
|
||||
if genre == nil {
|
||||
@ -67,14 +66,25 @@ final class LibraryViewModel: ViewModel {
|
||||
self.studio = studio
|
||||
self.filters = filters
|
||||
self.columns = columns
|
||||
|
||||
// Size is typical size of portrait items
|
||||
self.pageItemSize = UIScreen.itemsFillableOnScreen(width: 130, height: 185)
|
||||
|
||||
super.init()
|
||||
|
||||
$filters
|
||||
.sink(receiveValue: requestItems(with:))
|
||||
.sink(receiveValue: { newFilters in
|
||||
self.requestItemsAsync(with: newFilters, replaceCurrentItems: true)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func requestItems(with filters: LibraryFilters) {
|
||||
func requestItemsAsync(with filters: LibraryFilters, replaceCurrentItems: Bool = false) {
|
||||
|
||||
if replaceCurrentItems {
|
||||
self.items = []
|
||||
}
|
||||
|
||||
let personIDs: [String] = [person].compactMap(\.?.id)
|
||||
let studioIDs: [String] = [studio].compactMap(\.?.id)
|
||||
let genreIDs: [String]
|
||||
@ -85,9 +95,8 @@ final class LibraryViewModel: ViewModel {
|
||||
}
|
||||
let sortBy = filters.sortBy.map(\.rawValue)
|
||||
|
||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id,
|
||||
startIndex: currentPage * 100,
|
||||
limit: 100,
|
||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * pageItemSize,
|
||||
limit: pageItemSize,
|
||||
recursive: true,
|
||||
searchTerm: nil,
|
||||
sortOrder: filters.sortOrder,
|
||||
@ -102,7 +111,8 @@ final class LibraryViewModel: ViewModel {
|
||||
.chapters,
|
||||
],
|
||||
includeItemTypes: filters.filters
|
||||
.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series", "BoxSet"],
|
||||
.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode", "BoxSet"] :
|
||||
["Movie", "Series", "BoxSet"],
|
||||
filters: filters.filters,
|
||||
sortBy: sortBy,
|
||||
tags: filters.tags,
|
||||
@ -115,61 +125,11 @@ final class LibraryViewModel: ViewModel {
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
}, receiveValue: { [weak self] response in
|
||||
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) items in library \(self?.parentID ?? "nil")")
|
||||
guard let self = self else { return }
|
||||
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0)
|
||||
self.totalPages = Int(totalPages)
|
||||
self.hasPreviousPage = self.currentPage > 0
|
||||
self.hasNextPage = self.currentPage < self.totalPages - 1
|
||||
self.items = response.items ?? []
|
||||
self.rows = self.calculateRows(for: self.items)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func requestItemsAsync(with filters: LibraryFilters) {
|
||||
let personIDs: [String] = [person].compactMap(\.?.id)
|
||||
let studioIDs: [String] = [studio].compactMap(\.?.id)
|
||||
let genreIDs: [String]
|
||||
if filters.withGenres.isEmpty {
|
||||
genreIDs = [genre].compactMap(\.?.id)
|
||||
} else {
|
||||
genreIDs = filters.withGenres.compactMap(\.id)
|
||||
}
|
||||
let sortBy = filters.sortBy.map(\.rawValue)
|
||||
|
||||
ItemsAPI.getItemsByUserId(userId: SessionManager.main.currentLogin.user.id, startIndex: currentPage * 100,
|
||||
limit: 100,
|
||||
recursive: true,
|
||||
searchTerm: nil,
|
||||
sortOrder: filters.sortOrder,
|
||||
parentId: parentID,
|
||||
fields: [
|
||||
.primaryImageAspectRatio,
|
||||
.seriesPrimaryImage,
|
||||
.seasonUserData,
|
||||
.overview,
|
||||
.genres,
|
||||
.people,
|
||||
.chapters,
|
||||
],
|
||||
includeItemTypes: filters.filters
|
||||
.contains(.isFavorite) ? ["Movie", "Series", "Season", "Episode"] : ["Movie", "Series"],
|
||||
filters: filters.filters,
|
||||
sortBy: sortBy,
|
||||
tags: filters.tags,
|
||||
enableUserData: true,
|
||||
personIds: personIDs,
|
||||
studioIds: studioIDs,
|
||||
genreIds: genreIDs,
|
||||
enableImages: true)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
self?.handleAPIRequestError(completion: completion)
|
||||
}, receiveValue: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / 100.0)
|
||||
let totalPages = ceil(Double(response.totalRecordCount ?? 0) / Double(self.pageItemSize))
|
||||
|
||||
self.totalPages = Int(totalPages)
|
||||
self.hasPreviousPage = self.currentPage > 0
|
||||
self.hasNextPage = self.currentPage < self.totalPages - 1
|
||||
self.items.append(contentsOf: response.items ?? [])
|
||||
self.rows = self.calculateRows(for: self.items)
|
||||
@ -177,21 +137,12 @@ final class LibraryViewModel: ViewModel {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func requestNextPage() {
|
||||
currentPage += 1
|
||||
requestItems(with: filters)
|
||||
}
|
||||
|
||||
func requestNextPageAsync() {
|
||||
currentPage += 1
|
||||
requestItemsAsync(with: filters)
|
||||
}
|
||||
|
||||
func requestPreviousPage() {
|
||||
currentPage -= 1
|
||||
requestItems(with: filters)
|
||||
}
|
||||
|
||||
// tvOS calculations for collection view
|
||||
private func calculateRows(for itemList: [BaseItemDto]) -> [LibraryRow] {
|
||||
guard !itemList.isEmpty else { return [] }
|
||||
let rowCount = itemList.count / columns
|
||||
@ -220,3 +171,14 @@ final class LibraryViewModel: ViewModel {
|
||||
return calculatedRows
|
||||
}
|
||||
}
|
||||
|
||||
extension UIScreen {
|
||||
|
||||
static func itemsFillableOnScreen(width: CGFloat, height: CGFloat) -> Int {
|
||||
|
||||
let screenSize = UIScreen.main.bounds.height * UIScreen.main.bounds.width
|
||||
let itemSize = width * height
|
||||
|
||||
return Int(screenSize / itemSize)
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,3 @@ struct LibraryView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stream BM^S by nicki!
|
||||
//
|
||||
|
@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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 Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct CustomizeViewsSettings: View {
|
||||
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.showCastAndCrew)
|
||||
var showCastAndCrew
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
|
||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||
|
||||
// TODO: Uncomment when cast and crew implemented in item views
|
||||
// Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
||||
|
||||
} header: {
|
||||
L10n.customize.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -131,7 +131,16 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||
Button {
|
||||
settingsRouter.route(to: \.customizeViewsSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.customize.text
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.missingSettings)
|
||||
|
@ -75,7 +75,6 @@
|
||||
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
|
||||
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; };
|
||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
|
||||
53913BEF26D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
|
||||
53913BF026D323FE00EB3286 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53913BC926D323FE00EB3286 /* Localizable.strings */; };
|
||||
@ -136,7 +135,7 @@
|
||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
|
||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */; };
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemButton.swift */; };
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
||||
5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; };
|
||||
@ -215,36 +214,26 @@
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
|
||||
9EA03141D129DC2763660E29 /* Pods_Swiftfin_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D9B2BE5F9AD4A3CDE842523 /* Pods_Swiftfin_tvOS.framework */; };
|
||||
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
|
||||
C40CD922271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; };
|
||||
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD921271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift */; };
|
||||
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; };
|
||||
C40CD926271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* MovieLibrariesViewModel.swift */; };
|
||||
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
||||
C40CD929271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD927271F8DAB000FB198 /* MovieLibrariesView.swift */; };
|
||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||
C4AE2C3027498D2300AE13CF /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */; };
|
||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */; };
|
||||
C4AE2C3327498DBE00AE13CF /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; };
|
||||
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; };
|
||||
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0762271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift */; };
|
||||
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; };
|
||||
C4BE0767271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0765271FC109003F4AD1 /* TVLibrariesViewModel.swift */; };
|
||||
C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; };
|
||||
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE0768271FC164003F4AD1 /* TVLibrariesView.swift */; };
|
||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */; };
|
||||
C4BE076F2720FEFF003F4AD1 /* PlainNavigationLinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */; };
|
||||
C4BE07712725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
||||
C4BE07722725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07702725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift */; };
|
||||
C4BE07742725EB66003F4AD1 /* LiveTVProgramsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07732725EB66003F4AD1 /* LiveTVProgramsView.swift */; };
|
||||
C4BE07762725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; };
|
||||
C4BE07772725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07752725EBEA003F4AD1 /* LiveTVProgramsViewModel.swift */; };
|
||||
C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; };
|
||||
C4BE077A2726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07782726EE82003F4AD1 /* LiveTVTabCoordinator.swift */; };
|
||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; };
|
||||
C4BE07862728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07842728446F003F4AD1 /* LiveTVChannelsViewModel.swift */; };
|
||||
C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||
C4BE07892728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE07872728448B003F4AD1 /* LiveTVChannelsCoordinator.swift */; };
|
||||
C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
||||
C4BE078C272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078A272844AF003F4AD1 /* LiveTVChannelsView.swift */; };
|
||||
C4BE078E27298818003F4AD1 /* LiveTVHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BE078D27298817003F4AD1 /* LiveTVHomeView.swift */; };
|
||||
C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; };
|
||||
@ -276,6 +265,7 @@
|
||||
E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.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 */; };
|
||||
E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */; };
|
||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||
E11B1B6E2718CDBA006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
|
||||
@ -405,6 +395,8 @@
|
||||
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */; };
|
||||
E1C812D1277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */; };
|
||||
E1C812D2277AE50A00918266 /* URLComponentsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */; };
|
||||
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */; };
|
||||
E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */; };
|
||||
E1D4BF7C2719D05000A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */; };
|
||||
E1D4BF7E2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */; };
|
||||
E1D4BF7F2719D1DD00A11E64 /* BasicAppSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */; };
|
||||
@ -549,7 +541,6 @@
|
||||
5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
5377CC02263B596B003A4E83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
|
||||
5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = "<group>"; };
|
||||
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||
53913BCA26D323FE00EB3286 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
53913BCD26D323FE00EB3286 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
@ -577,7 +568,7 @@
|
||||
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
|
||||
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = "<group>"; };
|
||||
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemView.swift; sourceTree = "<group>"; };
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemButton.swift; sourceTree = "<group>"; };
|
||||
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.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>"; };
|
||||
@ -672,6 +663,7 @@
|
||||
E10D87E127852FD000BD264C /* EpisodesRowManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodesRowManager.swift; sourceTree = "<group>"; };
|
||||
E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = "<group>"; };
|
||||
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
|
||||
E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectBottomScrollView.swift; sourceTree = "<group>"; };
|
||||
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
|
||||
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
|
||||
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = "<group>"; };
|
||||
@ -742,6 +734,8 @@
|
||||
E1C812C8277AE40900918266 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
E1C812C9277AE40900918266 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
E1C812D0277AE4E300918266 /* tvOSVideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSVideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||
E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
|
||||
E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsView.swift; sourceTree = "<group>"; };
|
||||
E1D4BF7D2719D1DC00A11E64 /* BasicAppSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicAppSettingsViewModel.swift; sourceTree = "<group>"; };
|
||||
E1D4BF802719D22800A11E64 /* AppAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearance.swift; sourceTree = "<group>"; };
|
||||
@ -1214,11 +1208,12 @@
|
||||
53F866422687A45400DCD1D7 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */,
|
||||
E176DE6E278E3522001EFD8D /* EpisodesRowView */,
|
||||
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
||||
C4BE076D2720FEA8003F4AD1 /* PortraitItemElement.swift */,
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemButton.swift */,
|
||||
E1AA331C2782541500F6439C /* PrimaryButtonView.swift */,
|
||||
E1EBCB41278BD174009FE6E9 /* TruncatedTextView.swift */,
|
||||
);
|
||||
@ -1454,7 +1449,6 @@
|
||||
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||
C4AE2C2F27498D2300AE13CF /* LiveTVHomeView.swift */,
|
||||
C4AE2C3127498D6A00AE13CF /* LiveTVProgramsView.swift */,
|
||||
5389276F263C25230035E14B /* NextUpView.swift */,
|
||||
E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */,
|
||||
E13DD3E427177D15009D4DAF /* ServerListView.swift */,
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */,
|
||||
@ -1658,10 +1652,11 @@
|
||||
E1E5D54A2783E26100692DFE /* SettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1CEFBF427914C7700F60429 /* CustomizeViewsSettings.swift */,
|
||||
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
|
||||
E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */,
|
||||
E1E5D5472783CCF900692DFE /* OverlaySettingsView.swift */,
|
||||
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
|
||||
E176DE6F278E369F001EFD8D /* MissingItemsSettingsView.swift */,
|
||||
);
|
||||
path = SettingsView;
|
||||
sourceTree = "<group>";
|
||||
@ -1669,6 +1664,7 @@
|
||||
E1E5D54D2783E66600692DFE /* SettingsView */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */,
|
||||
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */,
|
||||
E1BDE358278E9ED2004E4022 /* MissingItemsSettingsView.swift */,
|
||||
E1E5D54E2783E67100692DFE /* OverlaySettingsView.swift */,
|
||||
@ -2232,6 +2228,7 @@
|
||||
E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */,
|
||||
E1FA2F7427818A8800B4C270 /* SmallMenuOverlay.swift in Sources */,
|
||||
E193D53C27193F9500900D82 /* UserListCoordinator.swift in Sources */,
|
||||
E1CEFBF727914E6400F60429 /* CustomizeViewsSettings.swift in Sources */,
|
||||
E13DD3C927164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
|
||||
E193D53A27193F9000900D82 /* ServerListCoordinator.swift in Sources */,
|
||||
@ -2294,7 +2291,7 @@
|
||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */,
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemButton.swift in Sources */,
|
||||
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||
62C29EA126D102A500C1D2E7 /* iOSMainTabCoordinator.swift in Sources */,
|
||||
C4BE076E2720FEA8003F4AD1 /* PortraitItemElement.swift in Sources */,
|
||||
@ -2303,15 +2300,14 @@
|
||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||
E10EAA4F277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
|
||||
E10D87E227852FD000BD264C /* EpisodesRowManager.swift in Sources */,
|
||||
C40CD925271F8D1E000FB198 /* MovieLibrariesViewModel.swift in Sources */,
|
||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */,
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||
C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */,
|
||||
E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */,
|
||||
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
|
||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||
C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */,
|
||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
|
||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||
@ -2325,13 +2321,12 @@
|
||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||
E1CEFBF527914C7700F60429 /* CustomizeViewsSettings.swift in Sources */,
|
||||
E1C812BC277A8E5D00918266 /* PlaybackSpeed.swift in Sources */,
|
||||
E1E5D5492783CDD700692DFE /* OverlaySettingsView.swift in Sources */,
|
||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */,
|
||||
6264E88C273850380081A12A /* Strings.swift in Sources */,
|
||||
C4BE0766271FC109003F4AD1 /* TVLibrariesViewModel.swift in Sources */,
|
||||
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
@ -2340,7 +2335,6 @@
|
||||
E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */,
|
||||
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
|
||||
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
|
||||
C4BE0763271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
|
||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||
E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */,
|
||||
E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */,
|
||||
@ -2362,14 +2356,12 @@
|
||||
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */,
|
||||
E1AA33222782648000F6439C /* OverlaySliderColor.swift in Sources */,
|
||||
E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */,
|
||||
C4BE07792726EE82003F4AD1 /* LiveTVTabCoordinator.swift in Sources */,
|
||||
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
||||
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||
6220D0C626D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift in Sources */,
|
||||
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */,
|
||||
C4BE07712725EB06003F4AD1 /* LiveTVProgramsCoordinator.swift in Sources */,
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||
E10D87DC2784EC5200BD264C /* EpisodesRowView.swift in Sources */,
|
||||
@ -2397,17 +2389,14 @@
|
||||
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
|
||||
62E632EF267D43320063E547 /* LibraryFilterViewModel.swift in Sources */,
|
||||
5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
C40CD922271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */,
|
||||
E13DD3C827164B1E009D4DAF /* UIDeviceExtensions.swift in Sources */,
|
||||
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */,
|
||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||
E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */,
|
||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||
C4BE07882728448B003F4AD1 /* LiveTVChannelsCoordinator.swift in Sources */,
|
||||
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
|
||||
E13DD3D5271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,
|
||||
62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */,
|
||||
C4BE0769271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
|
||||
E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */,
|
||||
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
|
||||
E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */,
|
||||
@ -2425,7 +2414,6 @@
|
||||
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
|
||||
E13DD3EF27178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||
C40CD928271F8DAB000FB198 /* MovieLibrariesView.swift in Sources */,
|
||||
E10D87DE278510E400BD264C /* PosterSize.swift in Sources */,
|
||||
E13DD4022717EE79009D4DAF /* UserListCoordinator.swift in Sources */,
|
||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
|
@ -12,6 +12,18 @@
|
||||
},
|
||||
"idiom" : "iphone"
|
||||
},
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.765",
|
||||
"green" : "0.361",
|
||||
"red" : "0.667"
|
||||
}
|
||||
},
|
||||
"idiom" : "ipad"
|
||||
},
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
|
96
Swiftfin/Components/DetectBottomScrollView.swift
Normal file
96
Swiftfin/Components/DetectBottomScrollView.swift
Normal file
@ -0,0 +1,96 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// https://stackoverflow.com/questions/56573373/swiftui-get-size-of-child
|
||||
|
||||
struct ChildSizeReader<Content: View>: View {
|
||||
@Binding
|
||||
var size: CGSize
|
||||
let content: () -> Content
|
||||
var body: some View {
|
||||
ZStack {
|
||||
content()
|
||||
.background(GeometryReader { proxy in
|
||||
Color.clear
|
||||
.preference(key: SizePreferenceKey.self, value: proxy.size)
|
||||
})
|
||||
}
|
||||
.onPreferenceChange(SizePreferenceKey.self) { preferences in
|
||||
self.size = preferences
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SizePreferenceKey: PreferenceKey {
|
||||
typealias Value = CGSize
|
||||
static var defaultValue: Value = .zero
|
||||
|
||||
static func reduce(value _: inout Value, nextValue: () -> Value) {
|
||||
_ = nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
struct ViewOffsetKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue = CGFloat.zero
|
||||
static func reduce(value: inout Value, nextValue: () -> Value) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
struct DetectBottomScrollView<Content: View>: View {
|
||||
private let spaceName = "scroll"
|
||||
|
||||
@State
|
||||
private var wholeSize: CGSize = .zero
|
||||
@State
|
||||
private var scrollViewSize: CGSize = .zero
|
||||
@State
|
||||
private var previousDidReachBottom = false
|
||||
let content: () -> Content
|
||||
let didReachBottom: (Bool) -> Void
|
||||
|
||||
init(content: @escaping () -> Content,
|
||||
didReachBottom: @escaping (Bool) -> Void)
|
||||
{
|
||||
self.content = content
|
||||
self.didReachBottom = didReachBottom
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ChildSizeReader(size: $wholeSize) {
|
||||
ScrollView {
|
||||
ChildSizeReader(size: $scrollViewSize) {
|
||||
content()
|
||||
.background(GeometryReader { proxy in
|
||||
Color.clear.preference(key: ViewOffsetKey.self,
|
||||
value: -1 * proxy.frame(in: .named(spaceName)).origin.y)
|
||||
})
|
||||
.onPreferenceChange(ViewOffsetKey.self,
|
||||
perform: { value in
|
||||
|
||||
if value >= scrollViewSize.height - wholeSize.height {
|
||||
if !previousDidReachBottom {
|
||||
previousDidReachBottom = true
|
||||
didReachBottom(true)
|
||||
}
|
||||
} else {
|
||||
if previousDidReachBottom {
|
||||
previousDidReachBottom = false
|
||||
didReachBottom(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.coordinateSpace(name: spaceName)
|
||||
}
|
||||
}
|
||||
}
|
69
Swiftfin/Components/PortraitItemButton.swift
Normal file
69
Swiftfin/Components/PortraitItemButton.swift
Normal file
@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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 PortraitItemButton<ItemType: PortraitImageStackable>: View {
|
||||
|
||||
let item: ItemType
|
||||
let maxWidth: CGFloat
|
||||
let horizontalAlignment: HorizontalAlignment
|
||||
let textAlignment: TextAlignment
|
||||
let selectedAction: (ItemType) -> Void
|
||||
|
||||
init(item: ItemType,
|
||||
maxWidth: CGFloat = 110,
|
||||
horizontalAlignment: HorizontalAlignment = .leading,
|
||||
textAlignment: TextAlignment = .leading,
|
||||
selectedAction: @escaping (ItemType) -> Void)
|
||||
{
|
||||
self.item = item
|
||||
self.maxWidth = maxWidth
|
||||
self.horizontalAlignment = horizontalAlignment
|
||||
self.textAlignment = textAlignment
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
VStack(alignment: horizontalAlignment) {
|
||||
ImageView(src: item.imageURLContsructor(maxWidth: Int(maxWidth)),
|
||||
bh: item.blurHash,
|
||||
failureInitials: item.failureInitials)
|
||||
.portraitPoster(width: maxWidth)
|
||||
.shadow(radius: 4, y: 2)
|
||||
|
||||
if item.showTitle {
|
||||
Text(item.title)
|
||||
.font(.footnote)
|
||||
.fontWeight(.regular)
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(textAlignment)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
}
|
||||
|
||||
if let description = item.subtitle {
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(textAlignment)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
.frame(width: maxWidth)
|
||||
}
|
||||
.frame(alignment: .top)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
@ -1,83 +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 SwiftUI
|
||||
|
||||
struct PortraitItemView: View {
|
||||
var item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100),
|
||||
bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.overlay(Rectangle()
|
||||
.fill(Color.jellyfinPurple)
|
||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if item.userData?.isFavorite ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.6)
|
||||
Image(systemName: "heart.fill")
|
||||
.foregroundColor(Color(.systemRed))
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
}
|
||||
.padding(.leading, 2)
|
||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if item.userData?.played ?? false {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.background(Color(.white))
|
||||
.clipShape(Circle().scale(0.8))
|
||||
} else {
|
||||
if item.userData?.unplayedItemCount != nil {
|
||||
Capsule()
|
||||
.fill(Color.accentColor)
|
||||
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
|
||||
Text(String(item.userData!.unplayedItemCount ?? 0))
|
||||
.foregroundColor(.white)
|
||||
.font(.caption2)
|
||||
.padding(2)
|
||||
}
|
||||
}
|
||||
}.padding(2)
|
||||
.fixedSize()
|
||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||
Text(item.seriesName ?? item.name ?? "")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
if item.type == "Movie" || item.type == "Series" {
|
||||
Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else if item.type == "Season" {
|
||||
Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
} else {
|
||||
Text(L10n.seasonAndEpisode(String(item.parentIndexNumber ?? 0), String(item.indexNumber ?? 0)))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
}.frame(width: 100)
|
||||
}
|
||||
}
|
@ -88,7 +88,7 @@ struct LibrarySearchView: View {
|
||||
Button {
|
||||
searchRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
PortraitItemElement(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
var libraryRouter: LibraryCoordinator.Router
|
||||
@StateObject
|
||||
@ -21,87 +22,75 @@ struct LibraryView: View {
|
||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||
|
||||
@State
|
||||
private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||
private var tracks: [GridItem] = Array(repeating: .init(.flexible(), alignment: .top),
|
||||
count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||
|
||||
func recalcTracks() {
|
||||
tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||
tracks = Array(repeating: .init(.flexible(), alignment: .top), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var loadingView: some View {
|
||||
ProgressView()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var noResultsView: some View {
|
||||
L10n.noResults.text
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var libraryItemsView: some View {
|
||||
DetectBottomScrollView {
|
||||
VStack {
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
if item.type != "Folder" {
|
||||
PortraitItemButton(item: item) { item in
|
||||
libraryRouter.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.listRowSeparator(.hidden)
|
||||
.onRotate { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
.frame(height: 30)
|
||||
}
|
||||
} didReachBottom: { newValue in
|
||||
if newValue && viewModel.hasNextPage {
|
||||
viewModel.requestNextPageAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if viewModel.isLoading == true {
|
||||
if viewModel.isLoading && viewModel.items.isEmpty {
|
||||
ProgressView()
|
||||
} else if !viewModel.items.isEmpty {
|
||||
VStack {
|
||||
ScrollView(.vertical) {
|
||||
Spacer().frame(height: 16)
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
if item.type != "Folder" {
|
||||
Button {
|
||||
libraryRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onRotate { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
if viewModel.hasNextPage || viewModel.hasPreviousPage {
|
||||
HStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Button {
|
||||
viewModel.requestPreviousPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.system(size: 25))
|
||||
}.disabled(!viewModel.hasPreviousPage)
|
||||
Text(L10n.pageOfWithNumbers(String(viewModel.currentPage + 1), String(viewModel.totalPages)))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Button {
|
||||
viewModel.requestNextPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 25))
|
||||
}.disabled(!viewModel.hasNextPage)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
Spacer().frame(height: 16)
|
||||
}
|
||||
}
|
||||
libraryItemsView
|
||||
} else {
|
||||
L10n.noResults.text
|
||||
noResultsView
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(title, displayMode: .inline)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
if viewModel.hasPreviousPage {
|
||||
Button {
|
||||
viewModel.requestPreviousPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.left")
|
||||
}.disabled(viewModel.isLoading)
|
||||
|
||||
Button {
|
||||
libraryRouter
|
||||
.route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
|
||||
parentId: viewModel.parentID ?? ""))
|
||||
} label: {
|
||||
Image(systemName: "line.horizontal.3.decrease.circle")
|
||||
}
|
||||
if viewModel.hasNextPage {
|
||||
Button {
|
||||
viewModel.requestNextPage()
|
||||
} label: {
|
||||
Image(systemName: "chevron.right")
|
||||
}.disabled(viewModel.isLoading)
|
||||
}
|
||||
Label("Icon One", systemImage: "line.horizontal.3.decrease.circle")
|
||||
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
||||
.onTapGesture {
|
||||
libraryRouter
|
||||
.route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
|
||||
parentId: viewModel.parentID ?? ""))
|
||||
}
|
||||
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
||||
|
||||
Button {
|
||||
libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
|
||||
} label: {
|
||||
@ -111,6 +100,3 @@ struct LibraryView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stream BM^S by nicki!
|
||||
//
|
||||
|
@ -1,41 +0,0 @@
|
||||
//
|
||||
// Swiftfin is subject to the terms of the Mozilla Public
|
||||
// License, v2.0. If a copy of the MPL was not distributed with this
|
||||
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct NextUpView: View {
|
||||
@EnvironmentObject
|
||||
var homeRouter: HomeCoordinator.Router
|
||||
|
||||
var items: [BaseItemDto]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
L10n.nextUp.text
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding(.leading, 16)
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
.frame(height: 200)
|
||||
}
|
||||
}
|
||||
}
|
31
Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift
Normal file
31
Swiftfin/Views/SettingsView/CustomizeViewsSettings.swift
Normal file
@ -0,0 +1,31 @@
|
||||
//
|
||||
// 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 Defaults
|
||||
import SwiftUI
|
||||
|
||||
struct CustomizeViewsSettings: View {
|
||||
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.showCastAndCrew)
|
||||
var showCastAndCrew
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
|
||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||
Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
||||
|
||||
} header: {
|
||||
L10n.customize.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -38,10 +38,6 @@ struct SettingsView: View {
|
||||
var jumpBackwardLength
|
||||
@Default(.jumpGesturesEnabled)
|
||||
var jumpGesturesEnabled
|
||||
@Default(.showPosterLabels)
|
||||
var showPosterLabels
|
||||
@Default(.showCastAndCrew)
|
||||
var showCastAndCrew
|
||||
@Default(.resumeOffset)
|
||||
var resumeOffset
|
||||
@Default(.subtitleSize)
|
||||
@ -138,8 +134,17 @@ struct SettingsView: View {
|
||||
}
|
||||
|
||||
Section(header: L10n.accessibility.text) {
|
||||
Toggle(L10n.showPosterLabels, isOn: $showPosterLabels)
|
||||
Toggle(L10n.showCastAndCrew, isOn: $showCastAndCrew)
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.customizeViewsSettings)
|
||||
} label: {
|
||||
HStack {
|
||||
L10n.customize.text
|
||||
.foregroundColor(.primary)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
settingsRouter.route(to: \.missingSettings)
|
||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user