[iOS & tvOS] Upgrade SDK to 10.10 (#1463)
* Buildable! * Update file names. * Default sort to sort name NOT name. * SessionInfoDto vs SessionInfo * Targetting * Fix many invalid `ItemSortBy` existing. Will need to revisit later to see which can still be used! * ExtraTypes Patch. * Move from Binding to OnChange. Tested and Working. * Update README.md Update README to use 10.10.6. Bumped up from 10.8.13 * Update to Main on https://github.com/jellyfin/jellyfin-sdk-swift.git * Now using https://github.com/jellyfin/jellyfin-sdk-swift.git again! * Paths.getUserViews() userId moved to parameters * Fix ViewModels where -Dto suffixes were removed by https://github.com/jellyfin/Swiftfin/pull/1465 auto-merge. * SupportedCaseIterable * tvOS supportedCases fixes for build issue. * cleanup * update API to 0.5.1 and correct VideoRangeTypes. * Remove deviceProfile.responseProfiles = videoPlayer.responseProfiles * Second to last adjustment: Resolved: // TODO: 10.10 - Filter to only valid SortBy's for each BaseItemKind. Last outstanding item: // TODO: 10.10 - What should authenticationProviderID & passwordResetProviderID be? * Trailers itemID must precede userID * Force User Policy to exist. --------- Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
parent
0845545417
commit
0025422634
|
@ -4,7 +4,7 @@
|
|||
<h1>Swiftfin</h1>
|
||||
<img src="https://img.shields.io/badge/iOS-15+-red"/>
|
||||
<img src="https://img.shields.io/badge/tvOS-17+-red"/>
|
||||
<img src="https://img.shields.io/badge/Jellyfin-10.8.13-9962be"/>
|
||||
<img src="https://img.shields.io/badge/Jellyfin-10.10.6-9962be"/>
|
||||
|
||||
<a href="https://translate.jellyfin.org/engage/swiftfin/">
|
||||
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/>
|
||||
|
@ -54,4 +54,4 @@ Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfi
|
|||
|
||||
<a href="https://translate.jellyfin.org/engage/swiftfin/">
|
||||
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/multi-auto.svg"/>
|
||||
</a>
|
||||
</a>
|
||||
|
|
|
@ -92,7 +92,7 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeActiveDeviceDetails(box: BindingBox<SessionInfo?>) -> some View {
|
||||
func makeActiveDeviceDetails(box: BindingBox<SessionInfoDto?>) -> some View {
|
||||
ActiveSessionDetailView(box: box)
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeDeviceDetails(device: DeviceInfo) -> some View {
|
||||
func makeDeviceDetails(device: DeviceInfoDto) -> some View {
|
||||
DeviceDetailsView(device: device)
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,9 @@ extension Array {
|
|||
}
|
||||
}
|
||||
|
||||
// extension Array where Element: RawRepresentable<String> {
|
||||
//
|
||||
// var asCommaString: String {}
|
||||
// }
|
||||
extension Array where Element: Equatable {
|
||||
|
||||
mutating func removeAll(equalTo element: Element) {
|
||||
removeAll { $0 == element }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,18 +46,4 @@ extension BaseItemPerson {
|
|||
|
||||
return final
|
||||
}
|
||||
|
||||
// Only displayed person types.
|
||||
// Will ignore types like "GuestStar"
|
||||
enum DisplayedType: String {
|
||||
case actor = "Actor"
|
||||
case director = "Director"
|
||||
case writer = "Writer"
|
||||
case producer = "Producer"
|
||||
}
|
||||
|
||||
var isDisplayed: Bool {
|
||||
guard let type = type else { return false }
|
||||
return DisplayedType(rawValue: type) != nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// 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) 2025 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
extension CollectionType: SupportedCaseIterable {
|
||||
|
||||
static var supportedCases: [CollectionType] {
|
||||
[
|
||||
.boxsets,
|
||||
.folders,
|
||||
.movies,
|
||||
.tvshows,
|
||||
.livetv,
|
||||
]
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
extension DeviceInfo {
|
||||
extension DeviceInfoDto {
|
||||
|
||||
var type: DeviceType {
|
||||
DeviceType(
|
|
@ -22,7 +22,6 @@ extension DeviceProfile {
|
|||
// MARK: - Video Player Specific Logic
|
||||
|
||||
deviceProfile.codecProfiles = videoPlayer.codecProfiles
|
||||
deviceProfile.responseProfiles = videoPlayer.responseProfiles
|
||||
deviceProfile.subtitleProfiles = videoPlayer.subtitleProfiles
|
||||
|
||||
// MARK: - DirectPlay & Transcoding Profiles
|
||||
|
|
|
@ -20,13 +20,13 @@ extension MediaSourceInfo {
|
|||
|
||||
let userSession: UserSession! = Container.shared.currentUserSession()
|
||||
let playbackURL: URL
|
||||
let streamType: StreamType
|
||||
let playMethod: PlayMethod
|
||||
|
||||
if let transcodingURL {
|
||||
guard let fullTranscodeURL = userSession.client.fullURL(with: transcodingURL)
|
||||
else { throw JellyfinAPIError("Unable to make transcode URL") }
|
||||
playbackURL = fullTranscodeURL
|
||||
streamType = .transcode
|
||||
playMethod = .transcode
|
||||
} else {
|
||||
let videoStreamParameters = Paths.GetVideoStreamParameters(
|
||||
isStatic: true,
|
||||
|
@ -44,7 +44,7 @@ extension MediaSourceInfo {
|
|||
else { throw JellyfinAPIError("Unable to make stream URL") }
|
||||
|
||||
playbackURL = streamURL
|
||||
streamType = .direct
|
||||
playMethod = .directPlay
|
||||
}
|
||||
|
||||
let videoStreams = mediaStreams?.filter { $0.type == .video } ?? []
|
||||
|
@ -62,7 +62,7 @@ extension MediaSourceInfo {
|
|||
selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1,
|
||||
selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1,
|
||||
chapters: item.fullChapterInfo,
|
||||
streamType: streamType
|
||||
playMethod: playMethod
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -70,16 +70,16 @@ extension MediaSourceInfo {
|
|||
|
||||
let userSession: UserSession! = Container.shared.currentUserSession()
|
||||
let playbackURL: URL
|
||||
let streamType: StreamType
|
||||
let playMethod: PlayMethod
|
||||
|
||||
if let transcodingURL {
|
||||
guard let fullTranscodeURL = URL(string: transcodingURL, relativeTo: userSession.server.currentURL)
|
||||
else { throw JellyfinAPIError("Unable to construct transcoded url") }
|
||||
playbackURL = fullTranscodeURL
|
||||
streamType = .transcode
|
||||
playMethod = .transcode
|
||||
} else if self.isSupportsDirectPlay ?? false, let path = self.path, let playbackUrl = URL(string: path) {
|
||||
playbackURL = playbackUrl
|
||||
streamType = .direct
|
||||
playMethod = .directPlay
|
||||
} else {
|
||||
let videoStreamParameters = Paths.GetVideoStreamParameters(
|
||||
isStatic: true,
|
||||
|
@ -97,7 +97,7 @@ extension MediaSourceInfo {
|
|||
throw JellyfinAPIError("Unable to construct transcoded url")
|
||||
}
|
||||
playbackURL = fullURL
|
||||
streamType = .direct
|
||||
playMethod = .directPlay
|
||||
}
|
||||
|
||||
let videoStreams = mediaStreams?.filter { $0.type == .video } ?? []
|
||||
|
@ -115,7 +115,7 @@ extension MediaSourceInfo {
|
|||
selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1,
|
||||
selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1,
|
||||
chapters: item.fullChapterInfo,
|
||||
streamType: streamType
|
||||
playMethod: playMethod
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ extension MediaStream {
|
|||
}
|
||||
|
||||
if let value = videoRange {
|
||||
properties.append(.init(title: "Video Range", subtitle: value))
|
||||
properties.append(.init(title: "Video Range", subtitle: value.rawValue))
|
||||
}
|
||||
|
||||
if let value = isInterlaced {
|
||||
|
@ -223,7 +223,7 @@ extension [MediaStream] {
|
|||
/// For transcode stream type:
|
||||
/// Only the first internal video track and the first internal audio track are included, in that order.
|
||||
/// In both cases, external tracks are appended in their original order with indexes continuing after internal tracks.
|
||||
func adjustedTrackIndexes(for streamType: StreamType, selectedAudioStreamIndex: Int) -> [MediaStream] {
|
||||
func adjustedTrackIndexes(for playMethod: PlayMethod, selectedAudioStreamIndex: Int) -> [MediaStream] {
|
||||
let internalTracks = self.filter { !($0.isExternal ?? false) }
|
||||
let externalTracks = self.filter { $0.isExternal ?? false }
|
||||
|
||||
|
@ -234,7 +234,7 @@ extension [MediaStream] {
|
|||
// TODO: Do we need this for other media types? I think movies/shows we only care about video, audio, and subtitles.
|
||||
let otherInternal = internalTracks.filter { $0.type != .video && $0.type != .audio && $0.type != .subtitle }
|
||||
|
||||
if streamType == .transcode {
|
||||
if playMethod == .transcode {
|
||||
// Only include the first video and first audio track for transcode.
|
||||
let videoInternal = internalTracks.filter { $0.type == .video }
|
||||
let audioInternal = internalTracks.filter { $0.type == .audio }
|
||||
|
@ -288,11 +288,11 @@ extension [MediaStream] {
|
|||
}
|
||||
|
||||
var hasHDRVideo: Bool {
|
||||
contains { VideoRangeType(from: $0.videoRangeType).isHDR }
|
||||
contains { $0.videoRangeType?.isHDR == true }
|
||||
}
|
||||
|
||||
var hasDolbyVision: Bool {
|
||||
contains { VideoRangeType(from: $0.videoRangeType).isDolbyVision }
|
||||
contains { $0.videoRangeType?.isDolbyVision == true }
|
||||
}
|
||||
|
||||
var hasSubtitles: Bool {
|
||||
|
|
|
@ -9,37 +9,7 @@
|
|||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
// TODO: No longer needed in 10.9+
|
||||
public enum PersonKind: String, Codable, CaseIterable {
|
||||
case unknown = "Unknown"
|
||||
case actor = "Actor"
|
||||
case director = "Director"
|
||||
case composer = "Composer"
|
||||
case writer = "Writer"
|
||||
case guestStar = "GuestStar"
|
||||
case producer = "Producer"
|
||||
case conductor = "Conductor"
|
||||
case lyricist = "Lyricist"
|
||||
case arranger = "Arranger"
|
||||
case engineer = "Engineer"
|
||||
case mixer = "Mixer"
|
||||
case remixer = "Remixer"
|
||||
case creator = "Creator"
|
||||
case artist = "Artist"
|
||||
case albumArtist = "AlbumArtist"
|
||||
case author = "Author"
|
||||
case illustrator = "Illustrator"
|
||||
case penciller = "Penciller"
|
||||
case inker = "Inker"
|
||||
case colorist = "Colorist"
|
||||
case letterer = "Letterer"
|
||||
case coverArtist = "CoverArtist"
|
||||
case editor = "Editor"
|
||||
case translator = "Translator"
|
||||
}
|
||||
|
||||
// TODO: Still needed in 10.9+
|
||||
extension PersonKind: Displayable {
|
||||
extension PersonKind: Displayable, SupportedCaseIterable {
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
|
@ -94,4 +64,8 @@ extension PersonKind: Displayable {
|
|||
return L10n.translator
|
||||
}
|
||||
}
|
||||
|
||||
static var supportedCases: [PersonKind] {
|
||||
[.actor, .director, .writer, .producer]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ extension RemoteSearchResult: Displayable {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: fix in SDK, should already be equatable
|
||||
extension RemoteSearchResult: @retroactive Hashable, @retroactive Identifiable {
|
||||
|
||||
public var id: Int {
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import Foundation
|
||||
|
||||
// TODO: remove and have sdk use strong types instead
|
||||
|
||||
typealias ServerTicks = Int
|
||||
|
||||
extension ServerTicks {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
extension SessionInfo {
|
||||
extension SessionInfoDto {
|
||||
|
||||
var device: DeviceType {
|
||||
DeviceType(
|
|
@ -9,31 +9,34 @@
|
|||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
extension SpecialFeatureType: Displayable {
|
||||
extension ExtraType: Displayable {
|
||||
|
||||
// TODO: localize
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return L10n.unknown
|
||||
case .clip:
|
||||
return "Clip"
|
||||
return L10n.clip
|
||||
case .trailer:
|
||||
return "Trailer"
|
||||
return L10n.trailer
|
||||
case .behindTheScenes:
|
||||
return "Behind the Scenes"
|
||||
return L10n.behindTheScenes
|
||||
case .deletedScene:
|
||||
return "Deleted Scene"
|
||||
return L10n.deletedScene
|
||||
case .interview:
|
||||
return "Interview"
|
||||
return L10n.interview
|
||||
case .scene:
|
||||
return "Scene"
|
||||
return L10n.scene
|
||||
case .sample:
|
||||
return "Sample"
|
||||
return L10n.sample
|
||||
case .themeSong:
|
||||
return "Theme Song"
|
||||
return L10n.themeSong
|
||||
case .themeVideo:
|
||||
return "Theme Video"
|
||||
return L10n.themeVideo
|
||||
case .featurette:
|
||||
return L10n.featurette
|
||||
case .short:
|
||||
return L10n.short
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,16 +7,9 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
// TODO: move to SDK as patch file
|
||||
|
||||
enum TaskTriggerType: String, Codable, CaseIterable, Displayable, SystemImageable {
|
||||
|
||||
case daily = "DailyTrigger"
|
||||
case weekly = "WeeklyTrigger"
|
||||
case interval = "IntervalTrigger"
|
||||
case startup = "StartupTrigger"
|
||||
|
||||
extension TaskTriggerType: Displayable, SystemImageable {
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .daily:
|
||||
|
|
|
@ -64,6 +64,8 @@ extension TranscodeReason: Displayable, SystemImageable {
|
|||
return L10n.directPlayError
|
||||
case .videoRangeTypeNotSupported:
|
||||
return L10n.videoRangeTypeNotSupported
|
||||
case .videoCodecTagNotSupported:
|
||||
return L10n.videoCodecTagNotSupported
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,6 +95,7 @@ extension TranscodeReason: Displayable, SystemImageable {
|
|||
.interlacedVideoNotSupported,
|
||||
.videoBitrateNotSupported,
|
||||
.unknownVideoStreamInfo,
|
||||
.videoCodecTagNotSupported,
|
||||
.videoRangeTypeNotSupported:
|
||||
return "photo.tv"
|
||||
case .subtitleCodecNotSupported:
|
||||
|
|
|
@ -21,7 +21,7 @@ extension TranscodingProfile {
|
|||
isEstimateContentLength: Bool? = nil,
|
||||
maxAudioChannels: String? = nil,
|
||||
minSegments: Int? = nil,
|
||||
protocol: String? = nil,
|
||||
protocol: MediaStreamProtocol? = nil,
|
||||
segmentLength: Int? = nil,
|
||||
transcodeSeekInfo: TranscodeSeekInfo? = nil,
|
||||
type: DlnaProfileType? = nil,
|
||||
|
|
|
@ -7,39 +7,20 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
// TODO: 10.10+ Replace with extension of https://github.com/jellyfin/jellyfin-sdk-swift/blob/main/Sources/Entities/VideoRangeType.swift
|
||||
enum VideoRangeType: String, Displayable {
|
||||
/// Unknown video range type.
|
||||
case unknown = "Unknown"
|
||||
/// SDR video range type (8bit).
|
||||
case sdr = "SDR"
|
||||
/// HDR10 video range type (10bit).
|
||||
case hdr10 = "HDR10"
|
||||
/// HLG video range type (10bit).
|
||||
case hlg = "HLG"
|
||||
/// Dolby Vision video range type (10bit encoded / 12bit remapped).
|
||||
case dovi = "DOVI"
|
||||
/// Dolby Vision with HDR10 video range fallback (10bit).
|
||||
case doviWithHDR10 = "DOVIWithHDR10"
|
||||
/// Dolby Vision with HLG video range fallback (10bit).
|
||||
case doviWithHLG = "DOVIWithHLG"
|
||||
/// Dolby Vision with SDR video range fallback (8bit / 10bit).
|
||||
case doviWithSDR = "DOVIWithSDR"
|
||||
/// HDR10+ video range type (10bit to 16bit).
|
||||
case hdr10Plus = "HDR10Plus"
|
||||
|
||||
/// Initializes from an optional string, defaulting to `.unknown` if nil or invalid.
|
||||
init(from rawValue: String?) {
|
||||
self = VideoRangeType(rawValue: rawValue ?? "") ?? .unknown
|
||||
}
|
||||
|
||||
/// Returns a human-readable display title for each video range type.
|
||||
extension VideoRangeType: Displayable {
|
||||
/// Dolby Vision is a proper noun so it is not localized
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return L10n.unknown
|
||||
case .sdr:
|
||||
return "SDR"
|
||||
case .hdr10:
|
||||
return "HDR10"
|
||||
case .hlg:
|
||||
return "HLG"
|
||||
case .dovi:
|
||||
return "Dolby Vision"
|
||||
case .doviWithHDR10:
|
||||
|
@ -50,18 +31,16 @@ enum VideoRangeType: String, Displayable {
|
|||
return "Dolby Vision / SDR"
|
||||
case .hdr10Plus:
|
||||
return "HDR10+"
|
||||
default:
|
||||
return self.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the video format is HDR (including Dolby Vision).
|
||||
var isHDR: Bool {
|
||||
switch self {
|
||||
case .unknown, .sdr:
|
||||
return false
|
||||
default:
|
||||
case .hdr10, .hlg, .hdr10Plus, .dovi, .doviWithHDR10, .doviWithHLG, .doviWithSDR:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ enum ItemArrayElements: Displayable {
|
|||
name: String,
|
||||
id: String?,
|
||||
personRole: String?,
|
||||
personKind: String?
|
||||
personKind: PersonKind?
|
||||
) -> T {
|
||||
switch self {
|
||||
case .genres, .tags:
|
||||
|
|
|
@ -16,7 +16,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
|
|||
var genres: [ItemGenre] = []
|
||||
var itemTypes: [BaseItemKind] = []
|
||||
var letter: [ItemLetter] = []
|
||||
var sortBy: [ItemSortBy] = [ItemSortBy.name]
|
||||
var sortBy: [ItemSortBy] = [ItemSortBy.sortName]
|
||||
var sortOrder: [ItemSortOrder] = [ItemSortOrder.ascending]
|
||||
var tags: [ItemTag] = []
|
||||
var traits: [ItemTrait] = []
|
||||
|
@ -29,7 +29,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
|
|||
traits: [ItemTrait.isFavorite]
|
||||
)
|
||||
static let recent: ItemFilterCollection = .init(
|
||||
sortBy: [ItemSortBy.dateAdded],
|
||||
sortBy: [ItemSortBy.dateLastContentAdded],
|
||||
sortOrder: [ItemSortOrder.descending]
|
||||
)
|
||||
|
||||
|
@ -39,7 +39,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
|
|||
/// available values within the current context.
|
||||
static let all: ItemFilterCollection = .init(
|
||||
letter: ItemLetter.allCases,
|
||||
sortBy: ItemSortBy.allCases,
|
||||
sortBy: ItemSortBy.supportedCases,
|
||||
sortOrder: ItemSortOrder.allCases,
|
||||
traits: ItemTrait.supportedCases
|
||||
)
|
||||
|
|
|
@ -9,28 +9,85 @@
|
|||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
// TODO: Remove when JellyfinAPI generates 10.9.0 schema
|
||||
|
||||
enum ItemSortBy: String, CaseIterable, Displayable, Codable {
|
||||
|
||||
case premiereDate = "PremiereDate"
|
||||
case name = "SortName"
|
||||
case dateAdded = "DateCreated"
|
||||
case random = "Random"
|
||||
|
||||
// TODO: Localize
|
||||
extension ItemSortBy: Displayable, SupportedCaseIterable {
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .default:
|
||||
return L10n.default
|
||||
case .airedEpisodeOrder:
|
||||
return L10n.airedEpisodeOrder
|
||||
case .album:
|
||||
return L10n.album
|
||||
case .albumArtist:
|
||||
return L10n.albumArtist
|
||||
case .artist:
|
||||
return L10n.artist
|
||||
case .dateCreated:
|
||||
return L10n.dateCreated
|
||||
case .officialRating:
|
||||
return L10n.officialRating
|
||||
case .datePlayed:
|
||||
return L10n.datePlayed
|
||||
case .premiereDate:
|
||||
return L10n.premiereDate
|
||||
case .startDate:
|
||||
return L10n.startDate
|
||||
case .sortName:
|
||||
return L10n.sortName
|
||||
case .name:
|
||||
return L10n.name
|
||||
case .dateAdded:
|
||||
return L10n.dateAdded
|
||||
case .random:
|
||||
return L10n.random
|
||||
case .runtime:
|
||||
return L10n.runtime
|
||||
case .communityRating:
|
||||
return L10n.communityRating
|
||||
case .productionYear:
|
||||
return L10n.year
|
||||
case .playCount:
|
||||
return L10n.playCount
|
||||
case .criticRating:
|
||||
return L10n.criticRating
|
||||
case .isFolder:
|
||||
return L10n.folder
|
||||
case .isUnplayed:
|
||||
return L10n.unplayed
|
||||
case .isPlayed:
|
||||
return L10n.played
|
||||
case .seriesSortName:
|
||||
return L10n.seriesName
|
||||
case .videoBitRate:
|
||||
return L10n.videoBitRate
|
||||
case .airTime:
|
||||
return L10n.airTime
|
||||
case .studio:
|
||||
return L10n.studio
|
||||
case .isFavoriteOrLiked:
|
||||
return L10n.favorite
|
||||
case .dateLastContentAdded:
|
||||
return L10n.dateAdded
|
||||
case .seriesDatePlayed:
|
||||
return L10n.seriesDatePlayed
|
||||
case .parentIndexNumber:
|
||||
return L10n.parentIndexNumber
|
||||
case .indexNumber:
|
||||
return L10n.indexNumber
|
||||
case .similarityScore:
|
||||
return L10n.similarityScore
|
||||
case .searchScore:
|
||||
return L10n.searchScore
|
||||
}
|
||||
}
|
||||
|
||||
static var supportedCases: [ItemSortBy] {
|
||||
[
|
||||
.premiereDate,
|
||||
.name,
|
||||
.sortName,
|
||||
.dateLastContentAdded,
|
||||
.random,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemSortBy: ItemFilter {
|
||||
|
|
|
@ -33,7 +33,7 @@ extension PlaybackCompatibility {
|
|||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: StreamType.hls.rawValue,
|
||||
protocol: MediaStreamProtocol.hls,
|
||||
type: .video
|
||||
) {
|
||||
AudioCodec.aac
|
||||
|
|
|
@ -56,7 +56,7 @@ struct CustomDeviceProfile: Hashable, Storable {
|
|||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: StreamType.hls.rawValue,
|
||||
protocol: MediaStreamProtocol.hls,
|
||||
type: .video
|
||||
) {
|
||||
audio
|
||||
|
|
|
@ -1,27 +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) 2025 Jellyfin & Jellyfin Contributors
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StreamType: String, Displayable {
|
||||
|
||||
case direct
|
||||
case transcode
|
||||
case hls
|
||||
|
||||
var displayTitle: String {
|
||||
switch self {
|
||||
case .direct:
|
||||
return L10n.direct
|
||||
case .transcode:
|
||||
return L10n.transcode
|
||||
case .hls:
|
||||
return "HLS"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,3 +18,10 @@ protocol SupportedCaseIterable: CaseIterable {
|
|||
|
||||
static var supportedCases: Self.SupportedCases { get }
|
||||
}
|
||||
|
||||
extension SupportedCaseIterable where SupportedCases.Element: Equatable {
|
||||
|
||||
var isSupported: Bool {
|
||||
Self.supportedCases.contains(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,11 +31,9 @@ struct UserPermissions {
|
|||
self.canDelete = policy?.enableContentDeletion ?? false || policy?.enableContentDeletionFromFolders != []
|
||||
self.canDownload = policy?.enableContentDownloading ?? false
|
||||
self.canEditMetadata = isAdministrator
|
||||
// TODO: SDK 10.9 Enable Comments
|
||||
self.canManageSubtitles = isAdministrator // || policy?.enableSubtitleManagement ?? false
|
||||
self.canManageCollections = isAdministrator // || policy?.enableCollectionManagement ?? false
|
||||
// TODO: SDK 10.10 Enable Comments
|
||||
self.canManageLyrics = isAdministrator // || policy?.enableSubtitleManagement ?? false
|
||||
self.canManageSubtitles = isAdministrator || policy?.enableSubtitleManagement ?? false
|
||||
self.canManageCollections = isAdministrator || policy?.enableCollectionManagement ?? false
|
||||
self.canManageLyrics = isAdministrator || policy?.enableSubtitleManagement ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ extension VideoPlayerType {
|
|||
enableSubtitlesInManifest: true,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: "hls",
|
||||
protocol: MediaStreamProtocol.hls,
|
||||
type: .video
|
||||
) {
|
||||
AudioCodec.aac
|
||||
|
|
|
@ -83,15 +83,4 @@ extension VideoPlayerType {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - response profiles
|
||||
|
||||
@ArrayBuilder<ResponseProfile>
|
||||
var responseProfiles: [ResponseProfile] {
|
||||
ResponseProfile(
|
||||
container: MediaContainer.m4v.rawValue,
|
||||
mimeType: "video/mp4",
|
||||
type: .video
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ extension VideoPlayerType {
|
|||
context: .streaming,
|
||||
maxAudioChannels: "8",
|
||||
minSegments: 2,
|
||||
protocol: StreamType.hls.rawValue,
|
||||
protocol: MediaStreamProtocol.hls,
|
||||
type: .video
|
||||
) {
|
||||
AudioCodec.aac
|
||||
|
|
|
@ -70,12 +70,16 @@ internal enum L10n {
|
|||
}
|
||||
/// Aired
|
||||
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
|
||||
/// Aired episode order
|
||||
internal static let airedEpisodeOrder = L10n.tr("Localizable", "airedEpisodeOrder", fallback: "Aired episode order")
|
||||
/// Air Time
|
||||
internal static let airTime = L10n.tr("Localizable", "airTime", fallback: "Air Time")
|
||||
/// Airs %s
|
||||
internal static func airWithDate(_ p1: UnsafePointer<CChar>) -> String {
|
||||
return L10n.tr("Localizable", "airWithDate", p1, fallback: "Airs %s")
|
||||
}
|
||||
/// Album
|
||||
internal static let album = L10n.tr("Localizable", "album", fallback: "Album")
|
||||
/// Album Artist
|
||||
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
|
||||
/// All
|
||||
|
@ -170,6 +174,8 @@ internal enum L10n {
|
|||
internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons")
|
||||
/// Behavior
|
||||
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior")
|
||||
/// Behind the Scenes
|
||||
internal static let behindTheScenes = L10n.tr("Localizable", "behindTheScenes", fallback: "Behind the Scenes")
|
||||
/// Tests your server connection to assess internet speed and adjust bandwidth automatically.
|
||||
internal static let birateAutoDescription = L10n.tr("Localizable", "birateAutoDescription", fallback: "Tests your server connection to assess internet speed and adjust bandwidth automatically.")
|
||||
/// Birthday
|
||||
|
@ -268,6 +274,8 @@ internal enum L10n {
|
|||
internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background")
|
||||
/// Client
|
||||
internal static let client = L10n.tr("Localizable", "client", fallback: "Client")
|
||||
/// Clip
|
||||
internal static let clip = L10n.tr("Localizable", "clip", fallback: "Clip")
|
||||
/// Close
|
||||
internal static let close = L10n.tr("Localizable", "close", fallback: "Close")
|
||||
/// Collections
|
||||
|
@ -356,8 +364,6 @@ internal enum L10n {
|
|||
internal static let customConnectionsDescription = L10n.tr("Localizable", "customConnectionsDescription", fallback: "Manually set the maximum number of connections a user can have to the server.")
|
||||
/// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.
|
||||
internal static let customDescription = L10n.tr("Localizable", "customDescription", fallback: "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.")
|
||||
/// Custom Device Name
|
||||
internal static let customDeviceName = L10n.tr("Localizable", "customDeviceName", fallback: "Custom Device Name")
|
||||
/// Your custom device name '%1$@' has been saved.
|
||||
internal static func customDeviceNameSaved(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "customDeviceNameSaved", String(describing: p1), fallback: "Your custom device name '%1$@' has been saved.")
|
||||
|
@ -390,10 +396,14 @@ internal enum L10n {
|
|||
internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
|
||||
/// Date created
|
||||
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date created")
|
||||
/// Date added
|
||||
internal static let dateLastContentAdded = L10n.tr("Localizable", "dateLastContentAdded", fallback: "Date added")
|
||||
/// Date modified
|
||||
internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date modified")
|
||||
/// Date of death
|
||||
internal static let dateOfDeath = L10n.tr("Localizable", "dateOfDeath", fallback: "Date of death")
|
||||
/// Date played
|
||||
internal static let datePlayed = L10n.tr("Localizable", "datePlayed", fallback: "Date played")
|
||||
/// Dates
|
||||
internal static let dates = L10n.tr("Localizable", "dates", fallback: "Dates")
|
||||
/// Day of Week
|
||||
|
@ -418,6 +428,8 @@ internal enum L10n {
|
|||
}
|
||||
/// Are you sure you wish to delete this device? This session will be logged out.
|
||||
internal static let deleteDeviceWarning = L10n.tr("Localizable", "deleteDeviceWarning", fallback: "Are you sure you wish to delete this device? This session will be logged out.")
|
||||
/// Deleted Scene
|
||||
internal static let deletedScene = L10n.tr("Localizable", "deletedScene", fallback: "Deleted Scene")
|
||||
/// Delete image
|
||||
internal static let deleteImage = L10n.tr("Localizable", "deleteImage", fallback: "Delete image")
|
||||
/// Are you sure you want to delete this item?
|
||||
|
@ -602,16 +614,22 @@ internal enum L10n {
|
|||
internal static let external = L10n.tr("Localizable", "external", fallback: "External")
|
||||
/// Failed logins
|
||||
internal static let failedLogins = L10n.tr("Localizable", "failedLogins", fallback: "Failed logins")
|
||||
/// Favorite
|
||||
internal static let favorite = L10n.tr("Localizable", "favorite", fallback: "Favorite")
|
||||
/// Favorited
|
||||
internal static let favorited = L10n.tr("Localizable", "favorited", fallback: "Favorited")
|
||||
/// Favorites
|
||||
internal static let favorites = L10n.tr("Localizable", "favorites", fallback: "Favorites")
|
||||
/// Featurette
|
||||
internal static let featurette = L10n.tr("Localizable", "featurette", fallback: "Featurette")
|
||||
/// Filters
|
||||
internal static let filters = L10n.tr("Localizable", "filters", fallback: "Filters")
|
||||
/// Find Missing
|
||||
internal static let findMissing = L10n.tr("Localizable", "findMissing", fallback: "Find Missing")
|
||||
/// Find missing metadata and images.
|
||||
internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.")
|
||||
/// Folder
|
||||
internal static let folder = L10n.tr("Localizable", "folder", fallback: "Folder")
|
||||
/// Force remote media transcoding
|
||||
internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding")
|
||||
/// Format
|
||||
|
@ -670,6 +688,8 @@ internal enum L10n {
|
|||
internal static let imageSource = L10n.tr("Localizable", "imageSource", fallback: "Image source")
|
||||
/// Index
|
||||
internal static let index = L10n.tr("Localizable", "index", fallback: "Index")
|
||||
/// Index number
|
||||
internal static let indexNumber = L10n.tr("Localizable", "indexNumber", fallback: "Index number")
|
||||
/// Indicators
|
||||
internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators")
|
||||
/// Inker
|
||||
|
@ -678,6 +698,8 @@ internal enum L10n {
|
|||
internal static let interlacedVideoNotSupported = L10n.tr("Localizable", "interlacedVideoNotSupported", fallback: "Interlaced video is not supported")
|
||||
/// Interval
|
||||
internal static let interval = L10n.tr("Localizable", "interval", fallback: "Interval")
|
||||
/// Interview
|
||||
internal static let interview = L10n.tr("Localizable", "interview", fallback: "Interview")
|
||||
/// Inverted Dark
|
||||
internal static let invertedDark = L10n.tr("Localizable", "invertedDark", fallback: "Inverted Dark")
|
||||
/// Inverted Light
|
||||
|
@ -932,6 +954,8 @@ internal enum L10n {
|
|||
internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls")
|
||||
/// Parental rating
|
||||
internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental rating")
|
||||
/// Parent index
|
||||
internal static let parentIndexNumber = L10n.tr("Localizable", "parentIndexNumber", fallback: "Parent index")
|
||||
/// Password
|
||||
internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
|
||||
/// User password has been changed.
|
||||
|
@ -966,6 +990,8 @@ internal enum L10n {
|
|||
internal static let playbackQuality = L10n.tr("Localizable", "playbackQuality", fallback: "Playback Quality")
|
||||
/// Playback Speed
|
||||
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed", fallback: "Playback Speed")
|
||||
/// Play count
|
||||
internal static let playCount = L10n.tr("Localizable", "playCount", fallback: "Play count")
|
||||
/// Played
|
||||
internal static let played = L10n.tr("Localizable", "played", fallback: "Played")
|
||||
/// Play From Beginning
|
||||
|
@ -1144,10 +1170,14 @@ internal enum L10n {
|
|||
internal static let running = L10n.tr("Localizable", "running", fallback: "Running...")
|
||||
/// Runtime
|
||||
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
|
||||
/// Sample
|
||||
internal static let sample = L10n.tr("Localizable", "sample", fallback: "Sample")
|
||||
/// Save
|
||||
internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
|
||||
/// Save the user to this device without any local authentication.
|
||||
internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.")
|
||||
/// Scene
|
||||
internal static let scene = L10n.tr("Localizable", "scene", fallback: "Scene")
|
||||
/// Schedule already exists
|
||||
internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists")
|
||||
/// Score
|
||||
|
@ -1158,6 +1188,8 @@ internal enum L10n {
|
|||
internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time")
|
||||
/// Search
|
||||
internal static let search = L10n.tr("Localizable", "search", fallback: "Search")
|
||||
/// Search score
|
||||
internal static let searchScore = L10n.tr("Localizable", "searchScore", fallback: "Search score")
|
||||
/// Season
|
||||
internal static let season = L10n.tr("Localizable", "season", fallback: "Season")
|
||||
/// S%1$@:E%2$@
|
||||
|
@ -1182,6 +1214,10 @@ internal enum L10n {
|
|||
internal static let series = L10n.tr("Localizable", "series", fallback: "Series")
|
||||
/// Series Backdrop
|
||||
internal static let seriesBackdrop = L10n.tr("Localizable", "seriesBackdrop", fallback: "Series Backdrop")
|
||||
/// Series date played
|
||||
internal static let seriesDatePlayed = L10n.tr("Localizable", "seriesDatePlayed", fallback: "Series date played")
|
||||
/// Series name
|
||||
internal static let seriesName = L10n.tr("Localizable", "seriesName", fallback: "Series name")
|
||||
/// Server
|
||||
internal static let server = L10n.tr("Localizable", "server", fallback: "Server")
|
||||
/// %@ is already connected.
|
||||
|
@ -1212,6 +1248,8 @@ internal enum L10n {
|
|||
internal static let setPinHintDescription = L10n.tr("Localizable", "setPinHintDescription", fallback: "Set a hint when prompting for the pin.")
|
||||
/// Settings
|
||||
internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings")
|
||||
/// Short
|
||||
internal static let short = L10n.tr("Localizable", "short", fallback: "Short")
|
||||
/// Show Favorited
|
||||
internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited")
|
||||
/// Show Favorites
|
||||
|
@ -1248,6 +1286,8 @@ internal enum L10n {
|
|||
internal static let signoutClose = L10n.tr("Localizable", "signoutClose", fallback: "Sign out on close")
|
||||
/// Signs out the last user when Swiftfin has been force closed
|
||||
internal static let signoutCloseFooter = L10n.tr("Localizable", "signoutCloseFooter", fallback: "Signs out the last user when Swiftfin has been force closed")
|
||||
/// Similarity score
|
||||
internal static let similarityScore = L10n.tr("Localizable", "similarityScore", fallback: "Similarity score")
|
||||
/// Slider
|
||||
internal static let slider = L10n.tr("Localizable", "slider", fallback: "Slider")
|
||||
/// Slider Color
|
||||
|
@ -1274,6 +1314,8 @@ internal enum L10n {
|
|||
internal static let splashscreenFooter = L10n.tr("Localizable", "splashscreenFooter", fallback: "When All Servers is selected, use the splashscreen from a single server or a random server")
|
||||
/// Sports
|
||||
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
|
||||
/// Start date
|
||||
internal static let startDate = L10n.tr("Localizable", "startDate", fallback: "Start date")
|
||||
/// Start Time
|
||||
internal static let startTime = L10n.tr("Localizable", "startTime", fallback: "Start Time")
|
||||
/// Status
|
||||
|
@ -1284,6 +1326,8 @@ internal enum L10n {
|
|||
internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc")
|
||||
/// Streams
|
||||
internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams")
|
||||
/// Studio
|
||||
internal static let studio = L10n.tr("Localizable", "studio", fallback: "Studio")
|
||||
/// Studios
|
||||
internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios")
|
||||
/// Studio(s) involved in the creation of media.
|
||||
|
@ -1306,14 +1350,10 @@ internal enum L10n {
|
|||
internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: "Subtitle Size")
|
||||
/// Success
|
||||
internal static let success = L10n.tr("Localizable", "success", fallback: "Success")
|
||||
/// Content Uploading
|
||||
internal static let supportsContentUploading = L10n.tr("Localizable", "supportsContentUploading", fallback: "Content Uploading")
|
||||
/// Media Control
|
||||
internal static let supportsMediaControl = L10n.tr("Localizable", "supportsMediaControl", fallback: "Media Control")
|
||||
/// Persistent Identifier
|
||||
internal static let supportsPersistentIdentifier = L10n.tr("Localizable", "supportsPersistentIdentifier", fallback: "Persistent Identifier")
|
||||
/// Sync
|
||||
internal static let supportsSync = L10n.tr("Localizable", "supportsSync", fallback: "Sync")
|
||||
/// Switch User
|
||||
internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User")
|
||||
/// SyncPlay
|
||||
|
@ -1352,6 +1392,10 @@ internal enum L10n {
|
|||
internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps")
|
||||
/// Test Size
|
||||
internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size")
|
||||
/// Theme Song
|
||||
internal static let themeSong = L10n.tr("Localizable", "themeSong", fallback: "Theme Song")
|
||||
/// Theme Video
|
||||
internal static let themeVideo = L10n.tr("Localizable", "themeVideo", fallback: "Theme Video")
|
||||
/// Thumb
|
||||
internal static let thumb = L10n.tr("Localizable", "thumb", fallback: "Thumb")
|
||||
/// Time
|
||||
|
@ -1464,10 +1508,14 @@ internal enum L10n {
|
|||
internal static let video = L10n.tr("Localizable", "video", fallback: "Video")
|
||||
/// The video bit depth is not supported
|
||||
internal static let videoBitDepthNotSupported = L10n.tr("Localizable", "videoBitDepthNotSupported", fallback: "The video bit depth is not supported")
|
||||
/// Video bitrate
|
||||
internal static let videoBitRate = L10n.tr("Localizable", "videoBitRate", fallback: "Video bitrate")
|
||||
/// The video bitrate is not supported
|
||||
internal static let videoBitrateNotSupported = L10n.tr("Localizable", "videoBitrateNotSupported", fallback: "The video bitrate is not supported")
|
||||
/// The video codec is not supported
|
||||
internal static let videoCodecNotSupported = L10n.tr("Localizable", "videoCodecNotSupported", fallback: "The video codec is not supported")
|
||||
/// Video codec tag is not supported
|
||||
internal static let videoCodecTagNotSupported = L10n.tr("Localizable", "videoCodecTagNotSupported", fallback: "Video codec tag is not supported")
|
||||
/// The video framerate is not supported
|
||||
internal static let videoFramerateNotSupported = L10n.tr("Localizable", "videoFramerateNotSupported", fallback: "The video framerate is not supported")
|
||||
/// The video level is not supported
|
||||
|
|
|
@ -152,13 +152,10 @@ extension UserState {
|
|||
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
|
||||
|
||||
let parameters = Paths.GetUserImageParameters(
|
||||
userID: id,
|
||||
maxWidth: scaleWidth
|
||||
)
|
||||
let request = Paths.getUserImage(
|
||||
userID: id,
|
||||
imageType: "Primary",
|
||||
parameters: parameters
|
||||
)
|
||||
let request = Paths.getUserImage(parameters: parameters)
|
||||
|
||||
let profileImageURL = client.fullURL(with: request)
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ final class ActiveSessionsViewModel: ViewModel, Stateful {
|
|||
@Published
|
||||
var backgroundStates: Set<BackgroundState> = []
|
||||
@Published
|
||||
var sessions: OrderedDictionary<String, BindingBox<SessionInfo?>> = [:]
|
||||
var sessions: OrderedDictionary<String, BindingBox<SessionInfoDto?>> = [:]
|
||||
@Published
|
||||
var state: State = .initial
|
||||
|
||||
|
@ -119,7 +119,7 @@ final class ActiveSessionsViewModel: ViewModel, Stateful {
|
|||
return !sessions.keys.contains(id)
|
||||
}
|
||||
.map { s in
|
||||
BindingBox<SessionInfo?>(
|
||||
BindingBox<SessionInfoDto?>(
|
||||
source: .init(
|
||||
get: { s },
|
||||
set: { _ in }
|
||||
|
|
|
@ -36,7 +36,7 @@ final class DeviceDetailViewModel: ViewModel, Stateful, Eventful {
|
|||
var state: State = .initial
|
||||
|
||||
@Published
|
||||
private(set) var device: DeviceInfo
|
||||
private(set) var device: DeviceInfoDto
|
||||
|
||||
var events: AnyPublisher<Event, Never> {
|
||||
eventSubject
|
||||
|
@ -46,7 +46,7 @@ final class DeviceDetailViewModel: ViewModel, Stateful, Eventful {
|
|||
|
||||
private var eventSubject: PassthroughSubject<Event, Never> = .init()
|
||||
|
||||
init(device: DeviceInfo) {
|
||||
init(device: DeviceInfoDto) {
|
||||
self.device = device
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,10 @@ final class DeviceDetailViewModel: ViewModel, Stateful, Eventful {
|
|||
|
||||
let request = Paths.updateDeviceOptions(id: id, .init(customName: newName))
|
||||
try await userSession.client.send(request)
|
||||
|
||||
await MainActor.run {
|
||||
self.device.customName = newName
|
||||
}
|
||||
}
|
||||
|
||||
private func getDeviceInfo() async throws {
|
||||
|
|
|
@ -49,7 +49,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
|
|||
@Published
|
||||
var backgroundStates: Set<BackgroundState> = []
|
||||
@Published
|
||||
var devices: [DeviceInfo] = []
|
||||
var devices: [DeviceInfoDto] = []
|
||||
@Published
|
||||
var state: State = .initial
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ final class ChannelLibraryViewModel: PagingLibraryViewModel<ChannelProgram> {
|
|||
var parameters = Paths.GetLiveTvChannelsParameters()
|
||||
parameters.fields = .MinimumFields
|
||||
parameters.userID = userSession.user.id
|
||||
parameters.sortBy = [ItemSortBy.name.rawValue]
|
||||
parameters.sortBy = [ItemSortBy.name]
|
||||
|
||||
parameters.limit = pageSize
|
||||
parameters.startIndex = page * pageSize
|
||||
|
@ -40,7 +40,7 @@ final class ChannelLibraryViewModel: PagingLibraryViewModel<ChannelProgram> {
|
|||
parameters.userID = userSession.user.id
|
||||
parameters.maxStartDate = maxStartDate
|
||||
parameters.minEndDate = minEndDate
|
||||
parameters.sortBy = ["StartDate"]
|
||||
parameters.sortBy = [ItemSortBy.startDate]
|
||||
|
||||
let request = Paths.getLiveTvPrograms(parameters: parameters)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
|
|
@ -174,12 +174,13 @@ final class HomeViewModel: ViewModel, Stateful {
|
|||
|
||||
private func getResumeItems() async throws -> [BaseItemDto] {
|
||||
var parameters = Paths.GetResumeItemsParameters()
|
||||
parameters.userID = userSession.user.id
|
||||
parameters.enableUserData = true
|
||||
parameters.fields = .MinimumFields
|
||||
parameters.includeItemTypes = [.movie, .episode]
|
||||
parameters.limit = 20
|
||||
|
||||
let request = Paths.getResumeItems(userID: userSession.user.id, parameters: parameters)
|
||||
let request = Paths.getResumeItems(parameters: parameters)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
return response.value.items ?? []
|
||||
|
@ -187,13 +188,14 @@ final class HomeViewModel: ViewModel, Stateful {
|
|||
|
||||
private func getLibraries() async throws -> [LatestInLibraryViewModel] {
|
||||
|
||||
let userViewsPath = Paths.getUserViews(userID: userSession.user.id)
|
||||
let parameters = Paths.GetUserViewsParameters(userID: userSession.user.id)
|
||||
let userViewsPath = Paths.getUserViews(parameters: parameters)
|
||||
async let userViews = userSession.client.send(userViewsPath)
|
||||
|
||||
async let excludedLibraryIDs = getExcludedLibraries()
|
||||
|
||||
return try await (userViews.value.items ?? [])
|
||||
.intersection(["movies", "tvshows"], using: \.collectionType)
|
||||
.intersection([.movies, .tvshows], using: \.collectionType)
|
||||
.subtracting(excludedLibraryIDs, using: \.id)
|
||||
.map { LatestInLibraryViewModel(parent: $0) }
|
||||
}
|
||||
|
@ -211,13 +213,13 @@ final class HomeViewModel: ViewModel, Stateful {
|
|||
|
||||
if isPlayed {
|
||||
request = Paths.markPlayedItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
} else {
|
||||
request = Paths.markUnplayedItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -213,7 +213,10 @@ final class IdentifyItemViewModel: ViewModel, Stateful, Eventful {
|
|||
private func refreshItem() async throws {
|
||||
guard let itemID = item.id else { return }
|
||||
|
||||
let request = Paths.getItem(userID: userSession.user.id, itemID: itemID)
|
||||
let request = Paths.getItem(
|
||||
itemID: itemID,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
await MainActor.run {
|
||||
|
|
|
@ -250,7 +250,10 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
|
|||
_ = self.backgroundStates.insert(.refreshing)
|
||||
}
|
||||
|
||||
let request = Paths.getItem(userID: userSession.user.id, itemID: itemId)
|
||||
let request = Paths.getItem(
|
||||
itemID: itemId,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
await MainActor.run {
|
||||
|
|
|
@ -388,8 +388,8 @@ final class ItemImagesViewModel: ViewModel, Stateful, Eventful {
|
|||
}
|
||||
|
||||
let request = Paths.getItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: itemID
|
||||
itemID: itemID,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
|
||||
let response = try await userSession.client.send(request)
|
||||
|
|
|
@ -136,7 +136,10 @@ final class RefreshMetadataViewModel: ViewModel, Stateful, Eventful {
|
|||
|
||||
try await pollRefreshProgress()
|
||||
|
||||
let request = Paths.getItem(userID: userSession.user.id, itemID: itemId)
|
||||
let request = Paths.getItem(
|
||||
itemID: itemId,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
await MainActor.run {
|
||||
|
@ -149,7 +152,7 @@ final class RefreshMetadataViewModel: ViewModel, Stateful, Eventful {
|
|||
|
||||
// MARK: - Poll Progress
|
||||
|
||||
// TODO: Find a way to actually check refresh progress. Not currently possible on 10.10.
|
||||
// TODO: Find a way to actually check refresh progress. Not currently possible on 10.10.6 (2025-03-27)
|
||||
private func pollRefreshProgress() async throws {
|
||||
let totalDuration: Double = 5.0
|
||||
let interval: Double = 0.05
|
||||
|
|
|
@ -325,8 +325,8 @@ class ItemViewModel: ViewModel, Stateful {
|
|||
private func getSpecialFeatures() async -> [BaseItemDto] {
|
||||
|
||||
let request = Paths.getSpecialFeatures(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
let response = try? await userSession.client.send(request)
|
||||
|
||||
|
@ -338,7 +338,7 @@ class ItemViewModel: ViewModel, Stateful {
|
|||
|
||||
guard let itemID = item.id else { return [] }
|
||||
|
||||
let request = Paths.getLocalTrailers(userID: userSession.user.id, itemID: itemID)
|
||||
let request = Paths.getLocalTrailers(itemID: itemID, userID: userSession.user.id)
|
||||
let response = try? await userSession.client.send(request)
|
||||
|
||||
return response?.value ?? []
|
||||
|
@ -352,13 +352,13 @@ class ItemViewModel: ViewModel, Stateful {
|
|||
|
||||
if isPlayed {
|
||||
request = Paths.markPlayedItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
} else {
|
||||
request = Paths.markUnplayedItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -372,13 +372,13 @@ class ItemViewModel: ViewModel, Stateful {
|
|||
|
||||
if isFavorite {
|
||||
request = Paths.markFavoriteItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
} else {
|
||||
request = Paths.unmarkFavoriteItem(
|
||||
userID: userSession.user.id,
|
||||
itemID: item.id!
|
||||
itemID: item.id!,
|
||||
userID: userSession.user.id
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -87,11 +87,12 @@ final class SeriesItemViewModel: ItemViewModel {
|
|||
private func getResumeItem() async throws -> BaseItemDto? {
|
||||
|
||||
var parameters = Paths.GetResumeItemsParameters()
|
||||
parameters.userID = userSession.user.id
|
||||
parameters.fields = .MinimumFields
|
||||
parameters.limit = 1
|
||||
parameters.parentID = item.id
|
||||
|
||||
let request = Paths.getResumeItems(userID: userSession.user.id, parameters: parameters)
|
||||
let request = Paths.getResumeItems(parameters: parameters)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
return response.value.items?.first
|
||||
|
|
|
@ -30,7 +30,7 @@ final class ItemLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
|
|||
let items = (response.value.items ?? [])
|
||||
.filter { item in
|
||||
if let collectionType = item.collectionType {
|
||||
return ["movies", "tvshows", "mixed", "boxsets"].contains(collectionType)
|
||||
return CollectionType.supportedCases.contains(collectionType)
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
@ -14,7 +14,7 @@ final class LatestInLibraryViewModel: PagingLibraryViewModel<BaseItemDto>, Ident
|
|||
override func get(page: Int) async throws -> [BaseItemDto] {
|
||||
|
||||
let parameters = parameters()
|
||||
let request = Paths.getLatestMedia(userID: userSession.user.id, parameters: parameters)
|
||||
let request = Paths.getLatestMedia(parameters: parameters)
|
||||
let response = try await userSession.client.send(request)
|
||||
|
||||
return response.value
|
||||
|
@ -23,6 +23,7 @@ final class LatestInLibraryViewModel: PagingLibraryViewModel<BaseItemDto>, Ident
|
|||
private func parameters() -> Paths.GetLatestMediaParameters {
|
||||
|
||||
var parameters = Paths.GetLatestMediaParameters()
|
||||
parameters.userID = userSession.user.id
|
||||
parameters.parentID = parent?.id
|
||||
parameters.fields = .MinimumFields
|
||||
parameters.enableUserData = true
|
||||
|
|
|
@ -42,7 +42,7 @@ final class RecentlyAddedLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
|
|||
parameters.includeItemTypes = [.movie, .series]
|
||||
parameters.isRecursive = true
|
||||
parameters.limit = pageSize
|
||||
parameters.sortBy = [ItemSortBy.dateAdded.rawValue]
|
||||
parameters.sortBy = [ItemSortBy.dateLastContentAdded.rawValue]
|
||||
parameters.sortOrder = [.descending]
|
||||
parameters.startIndex = page
|
||||
|
||||
|
|
|
@ -13,9 +13,6 @@ import OrderedCollections
|
|||
|
||||
final class MediaViewModel: ViewModel, Stateful {
|
||||
|
||||
// TODO: remove once collection types become an enum
|
||||
static let supportedCollectionTypes: [String] = ["boxsets", "folders", "movies", "tvshows", "livetv"]
|
||||
|
||||
// MARK: Action
|
||||
|
||||
enum Action: Equatable {
|
||||
|
@ -75,7 +72,7 @@ final class MediaViewModel: ViewModel, Stateful {
|
|||
|
||||
let media: [MediaType] = try await getUserViews()
|
||||
.compactMap { userView in
|
||||
if userView.collectionType == "livetv" {
|
||||
if userView.collectionType == .livetv {
|
||||
return .liveTV(userView)
|
||||
}
|
||||
|
||||
|
@ -90,7 +87,8 @@ final class MediaViewModel: ViewModel, Stateful {
|
|||
|
||||
private func getUserViews() async throws -> [BaseItemDto] {
|
||||
|
||||
let userViewsPath = Paths.getUserViews(userID: userSession.user.id)
|
||||
let parameters = Paths.GetUserViewsParameters(userID: userSession.user.id)
|
||||
let userViewsPath = Paths.getUserViews(parameters: parameters)
|
||||
async let userViews = userSession.client.send(userViewsPath)
|
||||
|
||||
async let excludedLibraryIDs = getExcludedLibraries()
|
||||
|
@ -98,11 +96,11 @@ final class MediaViewModel: ViewModel, Stateful {
|
|||
// folders has `type = UserView`, but we manually
|
||||
// force it to `folders` for better view handling
|
||||
let supportedUserViews = try await (userViews.value.items ?? [])
|
||||
.intersection(Self.supportedCollectionTypes, using: \.collectionType)
|
||||
.intersection(CollectionType.supportedCases, using: \.collectionType)
|
||||
.subtracting(excludedLibraryIDs, using: \.id)
|
||||
.map { item in
|
||||
|
||||
if item.type == .userView, item.collectionType == "folders" {
|
||||
if item.type == .userView, item.collectionType == .folders {
|
||||
return item.mutating(\.type, with: .folder)
|
||||
}
|
||||
|
||||
|
|
|
@ -146,7 +146,6 @@ final class UserProfileImageViewModel: ViewModel, Eventful, Stateful {
|
|||
|
||||
var request = Paths.postUserImage(
|
||||
userID: userID,
|
||||
imageType: "Primary",
|
||||
imageData
|
||||
)
|
||||
request.headers = ["Content-Type": contentType]
|
||||
|
@ -172,10 +171,7 @@ final class UserProfileImageViewModel: ViewModel, Eventful, Stateful {
|
|||
|
||||
guard let userID = user.id else { return }
|
||||
|
||||
let request = Paths.deleteUserImage(
|
||||
userID: userID,
|
||||
imageType: "Primary"
|
||||
)
|
||||
let request = Paths.deleteUserImage(userID: userID)
|
||||
let _ = try await userSession.client.send(request)
|
||||
|
||||
sweepProfileImageCache()
|
||||
|
|
|
@ -31,7 +31,7 @@ final class DownloadVideoPlayerManager: VideoPlayerManager {
|
|||
selectedAudioStreamIndex: 1,
|
||||
selectedSubtitleStreamIndex: 1,
|
||||
chapters: downloadTask.item.fullChapterInfo,
|
||||
streamType: .direct
|
||||
playMethod: .directPlay
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -26,14 +26,14 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
let selectedAudioStreamIndex: Int
|
||||
let selectedSubtitleStreamIndex: Int
|
||||
let chapters: [ChapterInfo.FullInfo]
|
||||
let streamType: StreamType
|
||||
let playMethod: PlayMethod
|
||||
|
||||
var hlsPlaybackURL: URL {
|
||||
let parameters = Paths.GetMasterHlsVideoPlaylistParameters(
|
||||
isStatic: true,
|
||||
tag: mediaSource.eTag,
|
||||
playSessionID: playSessionID,
|
||||
segmentContainer: "mp4",
|
||||
segmentContainer: MediaContainer.mp4.rawValue,
|
||||
minSegments: 2,
|
||||
mediaSourceID: mediaSource.id!,
|
||||
deviceID: UIDevice.vendorUUIDString,
|
||||
|
@ -97,7 +97,7 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
selectedAudioStreamIndex: Int,
|
||||
selectedSubtitleStreamIndex: Int,
|
||||
chapters: [ChapterInfo.FullInfo],
|
||||
streamType: StreamType
|
||||
playMethod: PlayMethod
|
||||
) {
|
||||
self.item = item
|
||||
self.mediaSource = mediaSource
|
||||
|
@ -108,16 +108,16 @@ final class VideoPlayerViewModel: ViewModel {
|
|||
fatalError("Media source does not have any streams")
|
||||
}
|
||||
|
||||
let adjustedStreams = mediaStreams.adjustedTrackIndexes(for: streamType, selectedAudioStreamIndex: selectedAudioStreamIndex)
|
||||
let adjustedStreams = mediaStreams.adjustedTrackIndexes(for: playMethod, selectedAudioStreamIndex: selectedAudioStreamIndex)
|
||||
|
||||
self.videoStreams = adjustedStreams.filter { $0.type == .video }
|
||||
self.audioStreams = adjustedStreams.filter { $0.type == .audio }
|
||||
self.subtitleStreams = adjustedStreams.filter { $0.type == .subtitle }
|
||||
self.videoStreams = adjustedStreams.filter { $0.type == MediaStreamType.video }
|
||||
self.audioStreams = adjustedStreams.filter { $0.type == MediaStreamType.audio }
|
||||
self.subtitleStreams = adjustedStreams.filter { $0.type == MediaStreamType.subtitle }
|
||||
|
||||
self.selectedAudioStreamIndex = selectedAudioStreamIndex
|
||||
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
|
||||
self.chapters = chapters
|
||||
self.streamType = streamType
|
||||
self.playMethod = playMethod
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@ extension ItemView {
|
|||
PosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
type: .portrait,
|
||||
items: people.filter(\.isDisplayed)
|
||||
items: people.filter { person in
|
||||
person.type?.isSupported ?? false
|
||||
}
|
||||
)
|
||||
.onSelect { person in
|
||||
let viewModel = ItemLibraryViewModel(parent: person)
|
||||
|
|
|
@ -31,8 +31,8 @@
|
|||
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; };
|
||||
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
|
||||
4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
|
||||
4E17498E2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; };
|
||||
4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; };
|
||||
4E17498E2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */; };
|
||||
4E17498F2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */; };
|
||||
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; };
|
||||
4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; };
|
||||
4E1A39332D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.swift */; };
|
||||
|
@ -172,6 +172,8 @@
|
|||
4E90F7672CC72B1F00417C31 /* TriggerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75F2CC72B1F00417C31 /* TriggerRow.swift */; };
|
||||
4E90F7682CC72B1F00417C31 /* TriggersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */; };
|
||||
4E90F76A2CC72B1F00417C31 /* DetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F7592CC72B1F00417C31 /* DetailsSection.swift */; };
|
||||
4E9654482D99C553006CB024 /* CollectionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9654472D99C551006CB024 /* CollectionType.swift */; };
|
||||
4E9654492D99C553006CB024 /* CollectionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9654472D99C551006CB024 /* CollectionType.swift */; };
|
||||
4E97D1832D064748004B89AD /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1822D064748004B89AD /* ItemSection.swift */; };
|
||||
4E97D1852D064B43004B89AD /* RefreshMetadataButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */; };
|
||||
4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */; };
|
||||
|
@ -263,6 +265,10 @@
|
|||
4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
|
||||
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; };
|
||||
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; };
|
||||
4EF36F642D962A430065BB79 /* ItemSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF36F632D962A430065BB79 /* ItemSortBy.swift */; };
|
||||
4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF36F632D962A430065BB79 /* ItemSortBy.swift */; };
|
||||
4EF36F662D9649050065BB79 /* SessionInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */; };
|
||||
4EF36F672D9649050065BB79 /* SessionInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */; };
|
||||
4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; };
|
||||
4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */; };
|
||||
4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */; };
|
||||
|
@ -731,7 +737,6 @@
|
|||
E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */; };
|
||||
E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */; };
|
||||
E148128928C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */; };
|
||||
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128A28C15526003B8787 /* ItemSortBy.swift */; };
|
||||
E149CCAD2BE6ECC8008B9331 /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E149CCAC2BE6ECC8008B9331 /* Storable.swift */; };
|
||||
E149CCAE2BE6ECC8008B9331 /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E149CCAC2BE6ECC8008B9331 /* Storable.swift */; };
|
||||
E14A08CB28E6831D004FC984 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */; };
|
||||
|
@ -785,13 +790,11 @@
|
|||
E1575E58293E7685001665B1 /* Files in Frameworks */ = {isa = PBXBuildFile; productRef = E1575E57293E7685001665B1 /* Files */; };
|
||||
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
|
||||
E1575E5D293E77B5001665B1 /* ItemViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F328875037002A7A66 /* ItemViewType.swift */; };
|
||||
E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EF4C402911B783008CC695 /* StreamType.swift */; };
|
||||
E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129429728F4785200796AC6 /* CaseIterablePicker.swift */; };
|
||||
E1575E65293E77B5001665B1 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||
E1575E66293E77B5001665B1 /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; };
|
||||
E1575E67293E77B5001665B1 /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
|
||||
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133728BEADBA00930F75 /* LibraryParent.swift */; };
|
||||
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E148128A28C15526003B8787 /* ItemSortBy.swift */; };
|
||||
E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */; };
|
||||
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; };
|
||||
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129429228F2845000796AC6 /* SliderType.swift */; };
|
||||
|
@ -1240,7 +1243,6 @@
|
|||
E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED91172B95993300802036 /* TitledLibraryParent.swift */; };
|
||||
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED91172B95993300802036 /* TitledLibraryParent.swift */; };
|
||||
E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */; };
|
||||
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EF4C402911B783008CC695 /* StreamType.swift */; };
|
||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
|
||||
E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; };
|
||||
E1F5CF062CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; };
|
||||
|
@ -1302,7 +1304,7 @@
|
|||
4E16FD502C0183DB00110147 /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = "<group>"; };
|
||||
4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = "<group>"; };
|
||||
4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerOrientation.swift; sourceTree = "<group>"; };
|
||||
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = "<group>"; };
|
||||
4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfoDto.swift; sourceTree = "<group>"; };
|
||||
4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksView.swift; sourceTree = "<group>"; };
|
||||
4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = "<group>"; };
|
||||
4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewAttributes.swift; sourceTree = "<group>"; };
|
||||
|
@ -1411,6 +1413,7 @@
|
|||
4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggersSection.swift; sourceTree = "<group>"; };
|
||||
4E90F75F2CC72B1F00417C31 /* TriggerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerRow.swift; sourceTree = "<group>"; };
|
||||
4E90F7612CC72B1F00417C31 /* EditServerTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerTaskView.swift; sourceTree = "<group>"; };
|
||||
4E9654472D99C551006CB024 /* CollectionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionType.swift; sourceTree = "<group>"; };
|
||||
4E97D1822D064748004B89AD /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = "<group>"; };
|
||||
4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshMetadataButton.swift; sourceTree = "<group>"; };
|
||||
4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = "<group>"; };
|
||||
|
@ -1467,7 +1470,7 @@
|
|||
4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = "<group>"; };
|
||||
4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleView.swift; sourceTree = "<group>"; };
|
||||
4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleRow.swift; sourceTree = "<group>"; };
|
||||
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
|
||||
4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfoDto.swift; sourceTree = "<group>"; };
|
||||
4EDDB49B2D596E0700DA16E8 /* VersionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionMenu.swift; sourceTree = "<group>"; };
|
||||
4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = "<group>"; };
|
||||
4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = "<group>"; };
|
||||
|
@ -1491,6 +1494,7 @@
|
|||
4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = "<group>"; };
|
||||
4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = "<group>"; };
|
||||
4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = "<group>"; };
|
||||
4EF36F632D962A430065BB79 /* ItemSortBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSortBy.swift; sourceTree = "<group>"; };
|
||||
4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = "<group>"; };
|
||||
4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerUserAccessTagsView.swift; sourceTree = "<group>"; };
|
||||
4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessTagRow.swift; sourceTree = "<group>"; };
|
||||
|
@ -1824,7 +1828,6 @@
|
|||
E146A9DA2BE6E9BF0034DA1E /* StoredValues+User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredValues+User.swift"; sourceTree = "<group>"; };
|
||||
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SortOrder+ItemSortOrder.swift"; sourceTree = "<group>"; };
|
||||
E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemFilter+ItemTrait.swift"; sourceTree = "<group>"; };
|
||||
E148128A28C15526003B8787 /* ItemSortBy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSortBy.swift; sourceTree = "<group>"; };
|
||||
E149CCAC2BE6ECC8008B9331 /* Storable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storable.swift; sourceTree = "<group>"; };
|
||||
E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLetter.swift; sourceTree = "<group>"; };
|
||||
|
@ -2138,7 +2141,6 @@
|
|||
E1ED7FE12CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerLogsViewModel.swift; sourceTree = "<group>"; };
|
||||
E1ED91142B95897500802036 /* LatestInLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestInLibraryViewModel.swift; sourceTree = "<group>"; };
|
||||
E1ED91172B95993300802036 /* TitledLibraryParent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitledLibraryParent.swift; sourceTree = "<group>"; };
|
||||
E1EF4C402911B783008CC695 /* StreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamType.swift; sourceTree = "<group>"; };
|
||||
E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerJumpLength.swift; sourceTree = "<group>"; };
|
||||
E1F5CF042CB09EA000607465 /* CurrentDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentDate.swift; sourceTree = "<group>"; };
|
||||
E1F5CF072CB0A04500607465 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
|
||||
|
@ -3420,7 +3422,6 @@
|
|||
E129429228F2845000796AC6 /* SliderType.swift */,
|
||||
E11042742B8013DF00821020 /* Stateful.swift */,
|
||||
E149CCAC2BE6ECC8008B9331 /* Storable.swift */,
|
||||
E1EF4C402911B783008CC695 /* StreamType.swift */,
|
||||
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */,
|
||||
E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */,
|
||||
E1A1528428FD191A00600579 /* TextPair.swift */,
|
||||
|
@ -4598,7 +4599,7 @@
|
|||
E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */,
|
||||
E11BDF762B8513B40045C54A /* ItemGenre.swift */,
|
||||
E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */,
|
||||
E148128A28C15526003B8787 /* ItemSortBy.swift */,
|
||||
4EF36F632D962A430065BB79 /* ItemSortBy.swift */,
|
||||
E11BDF962B865F550045C54A /* ItemTag.swift */,
|
||||
E14EDECB2B8FB709000F00A4 /* ItemYear.swift */,
|
||||
);
|
||||
|
@ -5083,8 +5084,9 @@
|
|||
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
|
||||
E1002B632793CEE700E47059 /* ChapterInfo.swift */,
|
||||
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */,
|
||||
4E9654472D99C551006CB024 /* CollectionType.swift */,
|
||||
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */,
|
||||
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */,
|
||||
4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */,
|
||||
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
|
||||
4E12F9152CBE9615006C217E /* DeviceType.swift */,
|
||||
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
|
||||
|
@ -5109,8 +5111,7 @@
|
|||
4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */,
|
||||
4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */,
|
||||
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */,
|
||||
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */,
|
||||
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */,
|
||||
4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */,
|
||||
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
|
||||
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
|
||||
E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */,
|
||||
|
@ -5973,7 +5974,6 @@
|
|||
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
|
||||
E18E021E2887492B0022598C /* RowDivider.swift in Sources */,
|
||||
4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */,
|
||||
4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */,
|
||||
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
|
||||
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
|
||||
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
|
||||
|
@ -6011,6 +6011,7 @@
|
|||
E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
|
||||
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
|
||||
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
|
||||
4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */,
|
||||
E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */,
|
||||
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
|
||||
E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */,
|
||||
|
@ -6107,7 +6108,6 @@
|
|||
E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */,
|
||||
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */,
|
||||
E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */,
|
||||
E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */,
|
||||
4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */,
|
||||
E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */,
|
||||
E1575E93293E7B1E001665B1 /* Double.swift in Sources */,
|
||||
|
@ -6117,8 +6117,10 @@
|
|||
E17AC96B2954D00E003D2BC2 /* URLResponse.swift in Sources */,
|
||||
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */,
|
||||
E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */,
|
||||
4EF36F672D9649050065BB79 /* SessionInfoDto.swift in Sources */,
|
||||
E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */,
|
||||
4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */,
|
||||
4E9654482D99C553006CB024 /* CollectionType.swift in Sources */,
|
||||
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */,
|
||||
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
|
||||
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
|
||||
|
@ -6180,13 +6182,12 @@
|
|||
4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */,
|
||||
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */,
|
||||
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */,
|
||||
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
|
||||
E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */,
|
||||
4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */,
|
||||
E1DC9845296DECB600982F06 /* ProgressIndicator.swift in Sources */,
|
||||
E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */,
|
||||
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
|
||||
4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */,
|
||||
4E17498F2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */,
|
||||
E12CC1C928D132B800678D5D /* RecentlyAddedView.swift in Sources */,
|
||||
E19D41B32BF2BFEF0082B8B2 /* URLSessionConfiguration.swift in Sources */,
|
||||
E10B1ECE2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */,
|
||||
|
@ -6432,7 +6433,6 @@
|
|||
621338932660107500A81A2A /* String.swift in Sources */,
|
||||
E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */,
|
||||
BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */,
|
||||
4EB4ECE32CBEFC4D002FF2FC /* SessionInfo.swift in Sources */,
|
||||
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */,
|
||||
62C83B08288C6A630004ED0C /* FontPickerView.swift in Sources */,
|
||||
E122A9132788EAAD0060FA63 /* MediaStream.swift in Sources */,
|
||||
|
@ -6724,7 +6724,6 @@
|
|||
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
|
||||
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
|
||||
4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */,
|
||||
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
|
||||
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
|
||||
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
|
||||
|
@ -6740,6 +6739,7 @@
|
|||
4E8F74AC2CE03DD300CC8969 /* DeleteItemViewModel.swift in Sources */,
|
||||
4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */,
|
||||
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
|
||||
4E9654492D99C553006CB024 /* CollectionType.swift in Sources */,
|
||||
4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */,
|
||||
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
|
||||
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */,
|
||||
|
@ -6750,7 +6750,7 @@
|
|||
4EC2B19E2CC96EAB00D866BE /* ServerUsersRow.swift in Sources */,
|
||||
E1C8CE7C28FF015000DF5D7B /* TrailingTimestampType.swift in Sources */,
|
||||
C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */,
|
||||
4E17498E2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */,
|
||||
4E17498E2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */,
|
||||
4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */,
|
||||
4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */,
|
||||
4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */,
|
||||
|
@ -6775,8 +6775,8 @@
|
|||
E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */,
|
||||
E1DE84142B9531C1008CCE21 /* OrderedSectionSelectorView.swift in Sources */,
|
||||
E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */,
|
||||
4EF36F642D962A430065BB79 /* ItemSortBy.swift in Sources */,
|
||||
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
|
||||
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */,
|
||||
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
||||
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */,
|
||||
E1EA09672BED6815004CDE76 /* UserSignInSecurityView.swift in Sources */,
|
||||
|
@ -6951,6 +6951,7 @@
|
|||
4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */,
|
||||
62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */,
|
||||
E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
|
||||
4EF36F662D9649050065BB79 /* SessionInfoDto.swift in Sources */,
|
||||
E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */,
|
||||
4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */,
|
||||
E18E01E6288747230022598C /* CollectionItemView.swift in Sources */,
|
||||
|
@ -7805,7 +7806,7 @@
|
|||
repositoryURL = "https://github.com/jellyfin/jellyfin-sdk-swift.git";
|
||||
requirement = {
|
||||
kind = upToNextMinorVersion;
|
||||
minimumVersion = 0.3.0;
|
||||
minimumVersion = 0.5.1;
|
||||
};
|
||||
};
|
||||
E15210522946DF1B00375CC2 /* XCRemoteSwiftPackageReference "Pulse" */ = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "66bff9f26defe8d2dfa92b4e65d0ae348e3b586d0fbb7de49c9c937459e6b55c",
|
||||
"originHash" : "59e91adc6b66cec011d85f8b5356b72b51a6e772e85bad7fbeac490d39e91f45",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "blurhashkit",
|
||||
|
@ -87,8 +87,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kean/Get",
|
||||
"state" : {
|
||||
"revision" : "74dba201ebe42e9c15c1db6ee1cc893025bbef94",
|
||||
"version" : "2.2.0"
|
||||
"revision" : "31249885da1052872e0ac91a2943f62567c0d96d",
|
||||
"version" : "2.2.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -96,8 +96,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/jellyfin/jellyfin-sdk-swift.git",
|
||||
"state" : {
|
||||
"revision" : "eae2ab5ed7caf770d79afbcdae08aab48df27a6e",
|
||||
"version" : "0.3.4"
|
||||
"revision" : "10624671970ee1ad49ce817ee7b8ee3074f89a32",
|
||||
"version" : "0.5.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -17,12 +17,12 @@ struct ActiveSessionDetailView: View {
|
|||
private var router: AdminDashboardCoordinator.Router
|
||||
|
||||
@ObservedObject
|
||||
var box: BindingBox<SessionInfo?>
|
||||
var box: BindingBox<SessionInfoDto?>
|
||||
|
||||
// MARK: Create Idle Content View
|
||||
|
||||
@ViewBuilder
|
||||
private func idleContent(session: SessionInfo) -> some View {
|
||||
private func idleContent(session: SessionInfoDto) -> some View {
|
||||
List {
|
||||
if let userID = session.userID {
|
||||
let user = UserDto(id: userID, name: session.userName)
|
||||
|
@ -47,7 +47,7 @@ struct ActiveSessionDetailView: View {
|
|||
|
||||
@ViewBuilder
|
||||
private func sessionContent(
|
||||
session: SessionInfo,
|
||||
session: SessionInfoDto,
|
||||
nowPlayingItem: BaseItemDto,
|
||||
playState: PlayerStateInfo
|
||||
) -> some View {
|
||||
|
|
|
@ -18,15 +18,15 @@ extension ActiveSessionsView {
|
|||
private var currentDate: Date
|
||||
|
||||
@ObservedObject
|
||||
private var box: BindingBox<SessionInfo?>
|
||||
private var box: BindingBox<SessionInfoDto?>
|
||||
|
||||
private let onSelect: () -> Void
|
||||
|
||||
private var session: SessionInfo {
|
||||
private var session: SessionInfoDto {
|
||||
box.value ?? .init()
|
||||
}
|
||||
|
||||
init(box: BindingBox<SessionInfo?>, onSelect action: @escaping () -> Void) {
|
||||
init(box: BindingBox<SessionInfoDto?>, onSelect action: @escaping () -> Void) {
|
||||
self.box = box
|
||||
self.onSelect = action
|
||||
}
|
||||
|
|
|
@ -11,14 +11,10 @@ import SwiftUI
|
|||
|
||||
extension DeviceDetailsView {
|
||||
struct CapabilitiesSection: View {
|
||||
var device: DeviceInfo
|
||||
var device: DeviceInfoDto
|
||||
|
||||
var body: some View {
|
||||
Section(L10n.capabilities) {
|
||||
if let supportsContentUploading = device.capabilities?.isSupportsContentUploading {
|
||||
TextPairView(leading: L10n.supportsContentUploading, trailing: supportsContentUploading ? L10n.yes : L10n.no)
|
||||
}
|
||||
|
||||
if let supportsMediaControl = device.capabilities?.isSupportsMediaControl {
|
||||
TextPairView(leading: L10n.supportsMediaControl, trailing: supportsMediaControl ? L10n.yes : L10n.no)
|
||||
}
|
||||
|
@ -26,10 +22,6 @@ extension DeviceDetailsView {
|
|||
if let supportsPersistentIdentifier = device.capabilities?.isSupportsPersistentIdentifier {
|
||||
TextPairView(leading: L10n.supportsPersistentIdentifier, trailing: supportsPersistentIdentifier ? L10n.yes : L10n.no)
|
||||
}
|
||||
|
||||
if let supportsSync = device.capabilities?.isSupportsSync {
|
||||
TextPairView(leading: L10n.supportsSync, trailing: supportsSync ? L10n.yes : L10n.no)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ extension DeviceDetailsView {
|
|||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
Section(L10n.customDeviceName) {
|
||||
Section(L10n.name) {
|
||||
TextField(
|
||||
L10n.name,
|
||||
text: $customName
|
||||
|
|
|
@ -10,8 +10,6 @@ import Defaults
|
|||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Enable for CustomNames for Devices with SDK Changes
|
||||
|
||||
struct DeviceDetailsView: View {
|
||||
|
||||
// MARK: - Current Date
|
||||
|
@ -44,11 +42,9 @@ struct DeviceDetailsView: View {
|
|||
|
||||
// MARK: - Initializer
|
||||
|
||||
init(device: DeviceInfo) {
|
||||
init(device: DeviceInfoDto) {
|
||||
_viewModel = StateObject(wrappedValue: DeviceDetailViewModel(device: device))
|
||||
|
||||
// TODO: Enable with SDK Change
|
||||
self.temporaryCustomName = device.name ?? "" // device.customName ?? device.name
|
||||
self.temporaryCustomName = device.customName ?? device.name ?? ""
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
@ -69,8 +65,7 @@ struct DeviceDetailsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Enable with SDK Change
|
||||
// CustomDeviceNameSection(customName: $temporaryCustomName)
|
||||
CustomDeviceNameSection(customName: $temporaryCustomName)
|
||||
|
||||
AdminDashboardView.DeviceSection(
|
||||
client: viewModel.device.appName,
|
||||
|
@ -94,22 +89,15 @@ struct DeviceDetailsView: View {
|
|||
.topBarTrailing {
|
||||
if viewModel.backgroundStates.contains(.updating) {
|
||||
ProgressView()
|
||||
|
||||
// TODO: Enable with SDK Change
|
||||
/*
|
||||
Button(L10n.save) {
|
||||
UIDevice.impact(.light)
|
||||
if device.id != nil {
|
||||
viewModel.send(.setCustomName(
|
||||
id: device.id ?? "",
|
||||
newName: temporaryCustomName
|
||||
))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.toolbarPill)
|
||||
.disabled(temporaryCustomName == device.customName)
|
||||
*/
|
||||
}
|
||||
Button(L10n.save) {
|
||||
UIDevice.impact(.light)
|
||||
if viewModel.device.id != nil {
|
||||
viewModel.send(.setCustomName(temporaryCustomName))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.toolbarPill)
|
||||
.disabled(temporaryCustomName == viewModel.device.customName)
|
||||
}
|
||||
.alert(
|
||||
L10n.success.text,
|
||||
|
|
|
@ -32,14 +32,14 @@ extension DevicesView {
|
|||
|
||||
// MARK: - Properties
|
||||
|
||||
let device: DeviceInfo
|
||||
let device: DeviceInfoDto
|
||||
let onSelect: () -> Void
|
||||
let onDelete: (() -> Void)?
|
||||
|
||||
// MARK: - Initializer
|
||||
|
||||
init(
|
||||
device: DeviceInfo,
|
||||
device: DeviceInfoDto,
|
||||
onSelect: @escaping () -> Void,
|
||||
onDelete: (() -> Void)? = nil
|
||||
) {
|
||||
|
@ -83,7 +83,7 @@ extension DevicesView {
|
|||
private var rowContent: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text(device.name ?? L10n.unknown)
|
||||
Text(device.customName ?? device.name ?? L10n.unknown)
|
||||
.font(.headline)
|
||||
.lineLimit(2)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
|
|
@ -11,8 +11,6 @@ import JellyfinAPI
|
|||
import OrderedCollections
|
||||
import SwiftUI
|
||||
|
||||
// TODO: Replace with CustomName when Available
|
||||
|
||||
struct DevicesView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
|
|
|
@ -45,7 +45,7 @@ struct AddTaskTriggerView: View {
|
|||
intervalTicks: nil,
|
||||
maxRuntimeTicks: nil,
|
||||
timeOfDayTicks: nil,
|
||||
type: TaskTriggerType.startup.rawValue
|
||||
type: TaskTriggerType.startup
|
||||
)
|
||||
|
||||
_taskTriggerInfo = State(initialValue: newTrigger)
|
||||
|
@ -82,11 +82,11 @@ struct AddTaskTriggerView: View {
|
|||
TriggerTypeRow(taskTriggerInfo: $taskTriggerInfo)
|
||||
|
||||
if let taskType = taskTriggerInfo.type {
|
||||
if taskType == TaskTriggerType.daily.rawValue {
|
||||
if taskType == TaskTriggerType.daily {
|
||||
dailyView
|
||||
} else if taskType == TaskTriggerType.weekly.rawValue {
|
||||
} else if taskType == TaskTriggerType.weekly {
|
||||
weeklyView
|
||||
} else if taskType == TaskTriggerType.interval.rawValue {
|
||||
} else if taskType == TaskTriggerType.interval {
|
||||
intervalView
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,30 +19,20 @@ extension AddTaskTriggerView {
|
|||
var body: some View {
|
||||
Picker(
|
||||
L10n.type,
|
||||
selection: Binding<TaskTriggerType?>(
|
||||
get: {
|
||||
if let t = taskTriggerInfo.type {
|
||||
return TaskTriggerType(rawValue: t)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
},
|
||||
set: { newValue in
|
||||
if taskTriggerInfo.type != newValue?.rawValue {
|
||||
resetValuesForNewType(newType: newValue)
|
||||
}
|
||||
}
|
||||
)
|
||||
selection: $taskTriggerInfo.type
|
||||
) {
|
||||
ForEach(TaskTriggerType.allCases, id: \.self) { type in
|
||||
Text(type.displayTitle)
|
||||
.tag(type as TaskTriggerType?)
|
||||
}
|
||||
}
|
||||
.onChange(of: taskTriggerInfo.type) { newType in
|
||||
resetValuesForNewType(newType: newType)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetValuesForNewType(newType: TaskTriggerType?) {
|
||||
taskTriggerInfo.type = newType?.rawValue
|
||||
taskTriggerInfo.type = newType
|
||||
let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks
|
||||
|
||||
switch newType {
|
||||
|
|
|
@ -16,23 +16,13 @@ extension EditServerTaskView {
|
|||
|
||||
let taskTriggerInfo: TaskTriggerInfo
|
||||
|
||||
// TODO: remove after `TaskTriggerType` is provided by SDK
|
||||
|
||||
private var taskTriggerType: TaskTriggerType {
|
||||
if let t = taskTriggerInfo.type, let type = TaskTriggerType(rawValue: t) {
|
||||
return type
|
||||
} else {
|
||||
return .startup
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Text(triggerDisplayText)
|
||||
Text(triggerDisplayText(for: taskTriggerInfo.type))
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Group {
|
||||
|
@ -52,7 +42,7 @@ extension EditServerTaskView {
|
|||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: taskTriggerType.systemImage)
|
||||
Image(systemName: (taskTriggerInfo.type ?? .startup).systemImage)
|
||||
.backport
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.secondary)
|
||||
|
@ -61,12 +51,15 @@ extension EditServerTaskView {
|
|||
|
||||
// MARK: - Trigger Display Text
|
||||
|
||||
private var triggerDisplayText: String {
|
||||
switch taskTriggerType {
|
||||
private func triggerDisplayText(for triggerType: TaskTriggerType?) -> String {
|
||||
|
||||
guard let triggerType else { return L10n.unknown }
|
||||
|
||||
switch triggerType {
|
||||
case .daily:
|
||||
if let timeOfDayTicks = taskTriggerInfo.timeOfDayTicks {
|
||||
return L10n.itemAtItem(
|
||||
taskTriggerType.displayTitle,
|
||||
triggerType.displayTitle,
|
||||
ServerTicks(timeOfDayTicks)
|
||||
.date.formatted(date: .omitted, time: .shortened)
|
||||
)
|
||||
|
@ -89,7 +82,7 @@ extension EditServerTaskView {
|
|||
)
|
||||
}
|
||||
case .startup:
|
||||
return taskTriggerType.displayTitle
|
||||
return triggerType.displayTitle
|
||||
}
|
||||
|
||||
return L10n.unknown
|
||||
|
|
|
@ -45,8 +45,7 @@ struct AddServerUserAccessTagsView: View {
|
|||
// MARK: - Tag is Already Blocked/Allowed
|
||||
|
||||
private var tagIsDuplicate: Bool {
|
||||
viewModel.user.policy!.blockedTags!.contains(tempTag) // &&
|
||||
//! viewModel.user.policy!.allowedTags!.contains(tempTag)
|
||||
viewModel.user.policy!.blockedTags!.contains(tempTag) || viewModel.user.policy!.allowedTags!.contains(tempTag)
|
||||
}
|
||||
|
||||
// MARK: - Tag Already Exists on Jellyfin
|
||||
|
@ -84,9 +83,8 @@ struct AddServerUserAccessTagsView: View {
|
|||
} else {
|
||||
Button(L10n.save) {
|
||||
if access {
|
||||
// TODO: Enable on 10.10
|
||||
/* tempPolicy.allowedTags = tempPolicy.allowedTags
|
||||
.appendedOrInit(tempTag) */
|
||||
tempPolicy.allowedTags = tempPolicy.allowedTags
|
||||
.appendedOrInit(tempTag)
|
||||
} else {
|
||||
tempPolicy.blockedTags = tempPolicy.blockedTags
|
||||
.appendedOrInit(tempTag)
|
||||
|
|
|
@ -29,27 +29,25 @@ extension AddServerUserAccessTagsView {
|
|||
// MARK: - Body
|
||||
|
||||
var body: some View {
|
||||
// TODO: Enable on 10.10
|
||||
// Section {
|
||||
// Picker(L10n.access, selection: $access) {
|
||||
// Text(L10n.allowed).tag(true)
|
||||
// Text(L10n.blocked).tag(false)
|
||||
// }
|
||||
// .disabled(true)
|
||||
// } header: {
|
||||
// Text(L10n.access)
|
||||
// } footer: {
|
||||
// LearnMoreButton(L10n.accessTags) {
|
||||
// TextPair(
|
||||
// title: L10n.allowed,
|
||||
// subtitle: L10n.accessTagAllowDescription
|
||||
// )
|
||||
// TextPair(
|
||||
// title: L10n.blocked,
|
||||
// subtitle: L10n.accessTagBlockDescription
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
Section {
|
||||
Picker(L10n.access, selection: $access) {
|
||||
Text(L10n.allowed).tag(true)
|
||||
Text(L10n.blocked).tag(false)
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.access)
|
||||
} footer: {
|
||||
LearnMoreButton(L10n.accessTags) {
|
||||
TextPair(
|
||||
title: L10n.allowed,
|
||||
subtitle: L10n.accessTagAllowDescription
|
||||
)
|
||||
TextPair(
|
||||
title: L10n.blocked,
|
||||
subtitle: L10n.accessTagBlockDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
TextField(L10n.name, text: $tag)
|
||||
|
|
|
@ -42,18 +42,18 @@ struct EditServerUserAccessTagsView: View {
|
|||
@State
|
||||
private var error: Error?
|
||||
|
||||
private var allowedTags: [TagWithAccess] {
|
||||
viewModel.user.policy?.allowedTags?
|
||||
.sorted()
|
||||
.map { TagWithAccess(tag: $0, access: true) } ?? []
|
||||
}
|
||||
|
||||
private var blockedTags: [TagWithAccess] {
|
||||
viewModel.user.policy?.blockedTags?
|
||||
.sorted()
|
||||
.map { TagWithAccess(tag: $0, access: false) } ?? []
|
||||
}
|
||||
|
||||
// private var allowedTags: [TagWithAccess] {
|
||||
// viewModel.user.policy?.allowedTags?
|
||||
// .sorted()
|
||||
// .map { TagWithAccess(tag: $0, access: true) } ?? []
|
||||
// }
|
||||
|
||||
// MARK: - Initializera
|
||||
|
||||
init(viewModel: ServerUserAdminViewModel) {
|
||||
|
@ -173,22 +173,29 @@ struct EditServerUserAccessTagsView: View {
|
|||
UIApplication.shared.open(.jellyfinDocsManagingUsers)
|
||||
}
|
||||
|
||||
if blockedTags.isEmpty {
|
||||
if blockedTags.isEmpty, allowedTags.isEmpty {
|
||||
Button(L10n.add) {
|
||||
router.route(to: \.userAddAccessTag, viewModel)
|
||||
}
|
||||
} else {
|
||||
|
||||
// TODO: with allowed, use `DisclosureGroup` instead
|
||||
Section(L10n.blocked) {
|
||||
ForEach(
|
||||
blockedTags,
|
||||
id: \.self,
|
||||
content: makeRow
|
||||
)
|
||||
if allowedTags.isNotEmpty {
|
||||
DisclosureGroup(L10n.allowed) {
|
||||
ForEach(
|
||||
allowedTags,
|
||||
id: \.self,
|
||||
content: makeRow
|
||||
)
|
||||
}
|
||||
}
|
||||
if blockedTags.isNotEmpty {
|
||||
DisclosureGroup(L10n.blocked) {
|
||||
ForEach(
|
||||
blockedTags,
|
||||
id: \.self,
|
||||
content: makeRow
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: allowed with 10.10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -213,13 +220,17 @@ struct EditServerUserAccessTagsView: View {
|
|||
Button(L10n.cancel, role: .cancel) {}
|
||||
|
||||
Button(L10n.delete, role: .destructive) {
|
||||
var tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
guard let policy = viewModel.user.policy else {
|
||||
preconditionFailure("User policy cannot be empty.")
|
||||
}
|
||||
|
||||
var tempPolicy = policy
|
||||
|
||||
for tag in selectedTags {
|
||||
if tag.access {
|
||||
// tempPolicy.allowedTags?.removeAll { $0 == tag.tag }
|
||||
tempPolicy.allowedTags?.removeAll(equalTo: tag.tag)
|
||||
} else {
|
||||
tempPolicy.blockedTags?.removeAll { $0 == tag.tag }
|
||||
tempPolicy.blockedTags?.removeAll(equalTo: tag.tag)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,12 @@ struct ServerUserMediaAccessView: View {
|
|||
|
||||
init(viewModel: ServerUserAdminViewModel) {
|
||||
self.viewModel = viewModel
|
||||
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
|
||||
guard let policy = viewModel.user.policy else {
|
||||
preconditionFailure("User policy cannot be empty.")
|
||||
}
|
||||
|
||||
self.tempPolicy = policy
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
@ -123,7 +128,7 @@ struct ServerUserMediaAccessView: View {
|
|||
if tempPolicy.enableContentDeletion == false {
|
||||
Section {
|
||||
ForEach(
|
||||
viewModel.libraries.filter { $0.collectionType != "boxsets" },
|
||||
viewModel.libraries.filter { $0.collectionType != .boxsets },
|
||||
id: \.id
|
||||
) { library in
|
||||
Toggle(
|
||||
|
|
|
@ -41,7 +41,12 @@ struct ServerUserDeviceAccessView: View {
|
|||
|
||||
init(viewModel: ServerUserAdminViewModel) {
|
||||
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
|
||||
guard let policy = viewModel.user.policy else {
|
||||
preconditionFailure("User policy cannot be empty.")
|
||||
}
|
||||
|
||||
self.tempPolicy = policy
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
|
|
@ -39,7 +39,12 @@ struct ServerUserLiveTVAccessView: View {
|
|||
|
||||
init(viewModel: ServerUserAdminViewModel) {
|
||||
self.viewModel = viewModel
|
||||
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
|
||||
guard let policy = viewModel.user.policy else {
|
||||
preconditionFailure("User policy cannot be empty.")
|
||||
}
|
||||
|
||||
self.tempPolicy = policy
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
|
|
@ -36,7 +36,12 @@ struct ServerUserParentalRatingView: View {
|
|||
|
||||
init(viewModel: ServerUserAdminViewModel) {
|
||||
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
|
||||
guard let policy = viewModel.user.policy else {
|
||||
preconditionFailure("User policy cannot be empty.")
|
||||
}
|
||||
|
||||
self.tempPolicy = policy
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
|
|
@ -24,22 +24,20 @@ extension ServerUserPermissionsView {
|
|||
isOn: $policy.isAdministrator.coalesce(false)
|
||||
)
|
||||
|
||||
// TODO: Enable for 10.9
|
||||
/* Toggle(L10n.collections, isOn: Binding(
|
||||
get: { policy.enableCollectionManagement ?? false },
|
||||
set: { policy.enableCollectionManagement = $0 }
|
||||
))
|
||||
Toggle(
|
||||
L10n.collections,
|
||||
isOn: $policy.enableCollectionManagement
|
||||
)
|
||||
|
||||
Toggle(L10n.subtitles, isOn: Binding(
|
||||
get: { policy.enableSubtitleManagement ?? false },
|
||||
set: { policy.enableSubtitleManagement = $0 }
|
||||
)) */
|
||||
Toggle(
|
||||
L10n.subtitles,
|
||||
isOn: $policy.enableSubtitleManagement
|
||||
)
|
||||
|
||||
// TODO: Enable for 10.10
|
||||
/* Toggle(L10n.lyrics, isOn: Binding(
|
||||
get: { policy.enableLyricManagement ?? false },
|
||||
set: { policy.enableLyricManagement = $0 }
|
||||
)) */
|
||||
Toggle(
|
||||
L10n.lyrics,
|
||||
isOn: $policy.enableLyricManagement
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,12 @@ struct ServerUserPermissionsView: View {
|
|||
|
||||
init(viewModel: ServerUserAdminViewModel) {
|
||||
self._viewModel = ObservedObject(wrappedValue: viewModel)
|
||||
self.tempPolicy = viewModel.user.policy ?? UserPolicy()
|
||||
|
||||
guard let policy = viewModel.user.policy else {
|
||||
preconditionFailure("User policy cannot be empty.")
|
||||
}
|
||||
|
||||
self.tempPolicy = policy
|
||||
}
|
||||
|
||||
// MARK: - Body
|
||||
|
|
|
@ -87,7 +87,7 @@ struct AddItemElementView<Element: Hashable>: View {
|
|||
name: name,
|
||||
id: id,
|
||||
personRole: personRole.isEmpty ? (personKind == .unknown ? nil : personKind.rawValue) : personRole,
|
||||
personKind: personKind.rawValue
|
||||
personKind: personKind
|
||||
)]))
|
||||
}
|
||||
.buttonStyle(.toolbarPill)
|
||||
|
|
|
@ -24,8 +24,6 @@ extension AddItemElementView {
|
|||
|
||||
let type: ItemArrayElements
|
||||
let population: [Element]
|
||||
|
||||
// TODO: Why doesn't environment(\.isSearching) work?
|
||||
let isSearching: Bool
|
||||
|
||||
// MARK: - Body
|
||||
|
|
|
@ -66,7 +66,7 @@ extension EditItemElementView {
|
|||
let person = (item as! BaseItemPerson)
|
||||
|
||||
TextPairView(
|
||||
leading: person.type ?? .emptyDash,
|
||||
leading: person.type?.displayTitle ?? .emptyDash,
|
||||
trailing: person.role ?? .emptyDash
|
||||
)
|
||||
.foregroundStyle(
|
||||
|
|
|
@ -17,7 +17,6 @@ extension EditMetadataView {
|
|||
@Binding
|
||||
var item: BaseItemDto
|
||||
|
||||
// TODO: Animation when lockAllFields is selected
|
||||
var body: some View {
|
||||
Section(L10n.lockedFields) {
|
||||
Toggle(
|
||||
|
|
|
@ -118,7 +118,7 @@ struct EditMetadataView: View {
|
|||
|
||||
ParentalRatingSection(item: $tempItem)
|
||||
|
||||
if [BaseItemKind.movie, .episode].contains(itemType) {
|
||||
if [.movie, .episode].contains(itemType) {
|
||||
MediaFormatSection(item: $tempItem)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@ extension ItemView {
|
|||
PosterHStack(
|
||||
title: L10n.castAndCrew,
|
||||
type: .portrait,
|
||||
items: people.filter(\.isDisplayed)
|
||||
items: people.filter { person in
|
||||
person.type?.isSupported ?? false
|
||||
}
|
||||
)
|
||||
.trailing {
|
||||
SeeAllButton()
|
||||
|
|
|
@ -85,12 +85,18 @@
|
|||
/// Aired
|
||||
"aired" = "Aired";
|
||||
|
||||
/// Aired episode order
|
||||
"airedEpisodeOrder" = "Aired episode order";
|
||||
|
||||
/// Air Time
|
||||
"airTime" = "Air Time";
|
||||
|
||||
/// Airs %s
|
||||
"airWithDate" = "Airs %s";
|
||||
|
||||
/// Album
|
||||
"album" = "Album";
|
||||
|
||||
/// Album Artist
|
||||
"albumArtist" = "Album Artist";
|
||||
|
||||
|
@ -232,6 +238,9 @@
|
|||
/// Behavior
|
||||
"behavior" = "Behavior";
|
||||
|
||||
/// Behind the Scenes
|
||||
"behindTheScenes" = "Behind the Scenes";
|
||||
|
||||
/// Tests your server connection to assess internet speed and adjust bandwidth automatically.
|
||||
"birateAutoDescription" = "Tests your server connection to assess internet speed and adjust bandwidth automatically.";
|
||||
|
||||
|
@ -376,6 +385,9 @@
|
|||
/// Client
|
||||
"client" = "Client";
|
||||
|
||||
/// Clip
|
||||
"clip" = "Clip";
|
||||
|
||||
/// Close
|
||||
"close" = "Close";
|
||||
|
||||
|
@ -505,9 +517,6 @@
|
|||
/// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.
|
||||
"customDescription" = "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.";
|
||||
|
||||
/// Custom Device Name
|
||||
"customDeviceName" = "Custom Device Name";
|
||||
|
||||
/// Your custom device name '%1$@' has been saved.
|
||||
"customDeviceNameSaved" = "Your custom device name '%1$@' has been saved.";
|
||||
|
||||
|
@ -553,12 +562,18 @@
|
|||
/// Date created
|
||||
"dateCreated" = "Date created";
|
||||
|
||||
/// Date added
|
||||
"dateLastContentAdded" = "Date added";
|
||||
|
||||
/// Date modified
|
||||
"dateModified" = "Date modified";
|
||||
|
||||
/// Date of death
|
||||
"dateOfDeath" = "Date of death";
|
||||
|
||||
/// Date played
|
||||
"datePlayed" = "Date played";
|
||||
|
||||
/// Dates
|
||||
"dates" = "Dates";
|
||||
|
||||
|
@ -592,6 +607,9 @@
|
|||
/// Are you sure you wish to delete this device? This session will be logged out.
|
||||
"deleteDeviceWarning" = "Are you sure you wish to delete this device? This session will be logged out.";
|
||||
|
||||
/// Deleted Scene
|
||||
"deletedScene" = "Deleted Scene";
|
||||
|
||||
/// Delete image
|
||||
"deleteImage" = "Delete image";
|
||||
|
||||
|
@ -847,12 +865,18 @@
|
|||
/// Failed logins
|
||||
"failedLogins" = "Failed logins";
|
||||
|
||||
/// Favorite
|
||||
"favorite" = "Favorite";
|
||||
|
||||
/// Favorited
|
||||
"favorited" = "Favorited";
|
||||
|
||||
/// Favorites
|
||||
"favorites" = "Favorites";
|
||||
|
||||
/// Featurette
|
||||
"featurette" = "Featurette";
|
||||
|
||||
/// Filters
|
||||
"filters" = "Filters";
|
||||
|
||||
|
@ -862,6 +886,9 @@
|
|||
/// Find missing metadata and images.
|
||||
"findMissingDescription" = "Find missing metadata and images.";
|
||||
|
||||
/// Folder
|
||||
"folder" = "Folder";
|
||||
|
||||
/// Force remote media transcoding
|
||||
"forceRemoteTranscoding" = "Force remote media transcoding";
|
||||
|
||||
|
@ -949,6 +976,9 @@
|
|||
/// Index
|
||||
"index" = "Index";
|
||||
|
||||
/// Index number
|
||||
"indexNumber" = "Index number";
|
||||
|
||||
/// Indicators
|
||||
"indicators" = "Indicators";
|
||||
|
||||
|
@ -961,6 +991,9 @@
|
|||
/// Interval
|
||||
"interval" = "Interval";
|
||||
|
||||
/// Interview
|
||||
"interview" = "Interview";
|
||||
|
||||
/// Inverted Dark
|
||||
"invertedDark" = "Inverted Dark";
|
||||
|
||||
|
@ -1330,6 +1363,9 @@
|
|||
/// Parental rating
|
||||
"parentalRating" = "Parental rating";
|
||||
|
||||
/// Parent index
|
||||
"parentIndexNumber" = "Parent index";
|
||||
|
||||
/// Password
|
||||
"password" = "Password";
|
||||
|
||||
|
@ -1381,6 +1417,9 @@
|
|||
/// Playback Speed
|
||||
"playbackSpeed" = "Playback Speed";
|
||||
|
||||
/// Play count
|
||||
"playCount" = "Play count";
|
||||
|
||||
/// Played
|
||||
"played" = "Played";
|
||||
|
||||
|
@ -1642,12 +1681,18 @@
|
|||
/// Runtime
|
||||
"runtime" = "Runtime";
|
||||
|
||||
/// Sample
|
||||
"sample" = "Sample";
|
||||
|
||||
/// Save
|
||||
"save" = "Save";
|
||||
|
||||
/// Save the user to this device without any local authentication.
|
||||
"saveUserWithoutAuthDescription" = "Save the user to this device without any local authentication.";
|
||||
|
||||
/// Scene
|
||||
"scene" = "Scene";
|
||||
|
||||
/// Schedule already exists
|
||||
"scheduleAlreadyExists" = "Schedule already exists";
|
||||
|
||||
|
@ -1663,6 +1708,9 @@
|
|||
/// Search
|
||||
"search" = "Search";
|
||||
|
||||
/// Search score
|
||||
"searchScore" = "Search score";
|
||||
|
||||
/// Season
|
||||
"season" = "Season";
|
||||
|
||||
|
@ -1696,6 +1744,12 @@
|
|||
/// Series Backdrop
|
||||
"seriesBackdrop" = "Series Backdrop";
|
||||
|
||||
/// Series date played
|
||||
"seriesDatePlayed" = "Series date played";
|
||||
|
||||
/// Series name
|
||||
"seriesName" = "Series name";
|
||||
|
||||
/// Server
|
||||
"server" = "Server";
|
||||
|
||||
|
@ -1735,6 +1789,9 @@
|
|||
/// Settings
|
||||
"settings" = "Settings";
|
||||
|
||||
/// Short
|
||||
"short" = "Short";
|
||||
|
||||
/// Show Favorited
|
||||
"showFavorited" = "Show Favorited";
|
||||
|
||||
|
@ -1786,6 +1843,9 @@
|
|||
/// Signs out the last user when Swiftfin has been force closed
|
||||
"signoutCloseFooter" = "Signs out the last user when Swiftfin has been force closed";
|
||||
|
||||
/// Similarity score
|
||||
"similarityScore" = "Similarity score";
|
||||
|
||||
/// Slider
|
||||
"slider" = "Slider";
|
||||
|
||||
|
@ -1825,6 +1885,9 @@
|
|||
/// Sports
|
||||
"sports" = "Sports";
|
||||
|
||||
/// Start date
|
||||
"startDate" = "Start date";
|
||||
|
||||
/// Start Time
|
||||
"startTime" = "Start Time";
|
||||
|
||||
|
@ -1840,6 +1903,9 @@
|
|||
/// Streams
|
||||
"streams" = "Streams";
|
||||
|
||||
/// Studio
|
||||
"studio" = "Studio";
|
||||
|
||||
/// Studios
|
||||
"studios" = "Studios";
|
||||
|
||||
|
@ -1873,18 +1939,12 @@
|
|||
/// Success
|
||||
"success" = "Success";
|
||||
|
||||
/// Content Uploading
|
||||
"supportsContentUploading" = "Content Uploading";
|
||||
|
||||
/// Media Control
|
||||
"supportsMediaControl" = "Media Control";
|
||||
|
||||
/// Persistent Identifier
|
||||
"supportsPersistentIdentifier" = "Persistent Identifier";
|
||||
|
||||
/// Sync
|
||||
"supportsSync" = "Sync";
|
||||
|
||||
/// Switch User
|
||||
"switchUser" = "Switch User";
|
||||
|
||||
|
@ -1942,6 +2002,12 @@
|
|||
/// Test Size
|
||||
"testSize" = "Test Size";
|
||||
|
||||
/// Theme Song
|
||||
"themeSong" = "Theme Song";
|
||||
|
||||
/// Theme Video
|
||||
"themeVideo" = "Theme Video";
|
||||
|
||||
/// Thumb
|
||||
"thumb" = "Thumb";
|
||||
|
||||
|
@ -2104,12 +2170,18 @@
|
|||
/// The video bit depth is not supported
|
||||
"videoBitDepthNotSupported" = "The video bit depth is not supported";
|
||||
|
||||
/// Video bitrate
|
||||
"videoBitRate" = "Video bitrate";
|
||||
|
||||
/// The video bitrate is not supported
|
||||
"videoBitrateNotSupported" = "The video bitrate is not supported";
|
||||
|
||||
/// The video codec is not supported
|
||||
"videoCodecNotSupported" = "The video codec is not supported";
|
||||
|
||||
/// Video codec tag is not supported
|
||||
"videoCodecTagNotSupported" = "Video codec tag is not supported";
|
||||
|
||||
/// The video framerate is not supported
|
||||
"videoFramerateNotSupported" = "The video framerate is not supported";
|
||||
|
||||
|
|
Loading…
Reference in New Issue