[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:
Joe Kribs 2025-04-06 21:42:47 -06:00 committed by GitHub
parent 0845545417
commit 0025422634
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 551 additions and 446 deletions

View File

@ -4,7 +4,7 @@
<h1>Swiftfin</h1> <h1>Swiftfin</h1>
<img src="https://img.shields.io/badge/iOS-15+-red"/> <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/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/"> <a href="https://translate.jellyfin.org/engage/swiftfin/">
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/> <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/"> <a href="https://translate.jellyfin.org/engage/swiftfin/">
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/multi-auto.svg"/> <img src="https://translate.jellyfin.org/widgets/swiftfin/-/multi-auto.svg"/>
</a> </a>

View File

@ -92,7 +92,7 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
} }
@ViewBuilder @ViewBuilder
func makeActiveDeviceDetails(box: BindingBox<SessionInfo?>) -> some View { func makeActiveDeviceDetails(box: BindingBox<SessionInfoDto?>) -> some View {
ActiveSessionDetailView(box: box) ActiveSessionDetailView(box: box)
} }
@ -122,7 +122,7 @@ final class AdminDashboardCoordinator: NavigationCoordinatable {
} }
@ViewBuilder @ViewBuilder
func makeDeviceDetails(device: DeviceInfo) -> some View { func makeDeviceDetails(device: DeviceInfoDto) -> some View {
DeviceDetailsView(device: device) DeviceDetailsView(device: device)
} }

View File

@ -51,7 +51,9 @@ extension Array {
} }
} }
// extension Array where Element: RawRepresentable<String> { extension Array where Element: Equatable {
//
// var asCommaString: String {} mutating func removeAll(equalTo element: Element) {
// } removeAll { $0 == element }
}
}

View File

@ -46,18 +46,4 @@ extension BaseItemPerson {
return final 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
}
} }

View File

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

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
extension DeviceInfo { extension DeviceInfoDto {
var type: DeviceType { var type: DeviceType {
DeviceType( DeviceType(

View File

@ -22,7 +22,6 @@ extension DeviceProfile {
// MARK: - Video Player Specific Logic // MARK: - Video Player Specific Logic
deviceProfile.codecProfiles = videoPlayer.codecProfiles deviceProfile.codecProfiles = videoPlayer.codecProfiles
deviceProfile.responseProfiles = videoPlayer.responseProfiles
deviceProfile.subtitleProfiles = videoPlayer.subtitleProfiles deviceProfile.subtitleProfiles = videoPlayer.subtitleProfiles
// MARK: - DirectPlay & Transcoding Profiles // MARK: - DirectPlay & Transcoding Profiles

View File

@ -20,13 +20,13 @@ extension MediaSourceInfo {
let userSession: UserSession! = Container.shared.currentUserSession() let userSession: UserSession! = Container.shared.currentUserSession()
let playbackURL: URL let playbackURL: URL
let streamType: StreamType let playMethod: PlayMethod
if let transcodingURL { if let transcodingURL {
guard let fullTranscodeURL = userSession.client.fullURL(with: transcodingURL) guard let fullTranscodeURL = userSession.client.fullURL(with: transcodingURL)
else { throw JellyfinAPIError("Unable to make transcode URL") } else { throw JellyfinAPIError("Unable to make transcode URL") }
playbackURL = fullTranscodeURL playbackURL = fullTranscodeURL
streamType = .transcode playMethod = .transcode
} else { } else {
let videoStreamParameters = Paths.GetVideoStreamParameters( let videoStreamParameters = Paths.GetVideoStreamParameters(
isStatic: true, isStatic: true,
@ -44,7 +44,7 @@ extension MediaSourceInfo {
else { throw JellyfinAPIError("Unable to make stream URL") } else { throw JellyfinAPIError("Unable to make stream URL") }
playbackURL = streamURL playbackURL = streamURL
streamType = .direct playMethod = .directPlay
} }
let videoStreams = mediaStreams?.filter { $0.type == .video } ?? [] let videoStreams = mediaStreams?.filter { $0.type == .video } ?? []
@ -62,7 +62,7 @@ extension MediaSourceInfo {
selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1, selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1,
selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1, selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1,
chapters: item.fullChapterInfo, chapters: item.fullChapterInfo,
streamType: streamType playMethod: playMethod
) )
} }
@ -70,16 +70,16 @@ extension MediaSourceInfo {
let userSession: UserSession! = Container.shared.currentUserSession() let userSession: UserSession! = Container.shared.currentUserSession()
let playbackURL: URL let playbackURL: URL
let streamType: StreamType let playMethod: PlayMethod
if let transcodingURL { if let transcodingURL {
guard let fullTranscodeURL = URL(string: transcodingURL, relativeTo: userSession.server.currentURL) guard let fullTranscodeURL = URL(string: transcodingURL, relativeTo: userSession.server.currentURL)
else { throw JellyfinAPIError("Unable to construct transcoded url") } else { throw JellyfinAPIError("Unable to construct transcoded url") }
playbackURL = fullTranscodeURL playbackURL = fullTranscodeURL
streamType = .transcode playMethod = .transcode
} else if self.isSupportsDirectPlay ?? false, let path = self.path, let playbackUrl = URL(string: path) { } else if self.isSupportsDirectPlay ?? false, let path = self.path, let playbackUrl = URL(string: path) {
playbackURL = playbackUrl playbackURL = playbackUrl
streamType = .direct playMethod = .directPlay
} else { } else {
let videoStreamParameters = Paths.GetVideoStreamParameters( let videoStreamParameters = Paths.GetVideoStreamParameters(
isStatic: true, isStatic: true,
@ -97,7 +97,7 @@ extension MediaSourceInfo {
throw JellyfinAPIError("Unable to construct transcoded url") throw JellyfinAPIError("Unable to construct transcoded url")
} }
playbackURL = fullURL playbackURL = fullURL
streamType = .direct playMethod = .directPlay
} }
let videoStreams = mediaStreams?.filter { $0.type == .video } ?? [] let videoStreams = mediaStreams?.filter { $0.type == .video } ?? []
@ -115,7 +115,7 @@ extension MediaSourceInfo {
selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1, selectedAudioStreamIndex: defaultAudioStreamIndex ?? -1,
selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1, selectedSubtitleStreamIndex: defaultSubtitleStreamIndex ?? -1,
chapters: item.fullChapterInfo, chapters: item.fullChapterInfo,
streamType: streamType playMethod: playMethod
) )
} }
} }

View File

@ -75,7 +75,7 @@ extension MediaStream {
} }
if let value = videoRange { 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 { if let value = isInterlaced {
@ -223,7 +223,7 @@ extension [MediaStream] {
/// For transcode stream type: /// For transcode stream type:
/// Only the first internal video track and the first internal audio track are included, in that order. /// 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. /// 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 internalTracks = self.filter { !($0.isExternal ?? false) }
let externalTracks = 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. // 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 } 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. // Only include the first video and first audio track for transcode.
let videoInternal = internalTracks.filter { $0.type == .video } let videoInternal = internalTracks.filter { $0.type == .video }
let audioInternal = internalTracks.filter { $0.type == .audio } let audioInternal = internalTracks.filter { $0.type == .audio }
@ -288,11 +288,11 @@ extension [MediaStream] {
} }
var hasHDRVideo: Bool { var hasHDRVideo: Bool {
contains { VideoRangeType(from: $0.videoRangeType).isHDR } contains { $0.videoRangeType?.isHDR == true }
} }
var hasDolbyVision: Bool { var hasDolbyVision: Bool {
contains { VideoRangeType(from: $0.videoRangeType).isDolbyVision } contains { $0.videoRangeType?.isDolbyVision == true }
} }
var hasSubtitles: Bool { var hasSubtitles: Bool {

View File

@ -9,37 +9,7 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
// TODO: No longer needed in 10.9+ extension PersonKind: Displayable, SupportedCaseIterable {
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 {
var displayTitle: String { var displayTitle: String {
switch self { switch self {
case .unknown: case .unknown:
@ -94,4 +64,8 @@ extension PersonKind: Displayable {
return L10n.translator return L10n.translator
} }
} }
static var supportedCases: [PersonKind] {
[.actor, .director, .writer, .producer]
}
} }

View File

@ -17,7 +17,6 @@ extension RemoteSearchResult: Displayable {
} }
} }
// TODO: fix in SDK, should already be equatable
extension RemoteSearchResult: @retroactive Hashable, @retroactive Identifiable { extension RemoteSearchResult: @retroactive Hashable, @retroactive Identifiable {
public var id: Int { public var id: Int {

View File

@ -9,7 +9,6 @@
import Foundation import Foundation
// TODO: remove and have sdk use strong types instead // TODO: remove and have sdk use strong types instead
typealias ServerTicks = Int typealias ServerTicks = Int
extension ServerTicks { extension ServerTicks {

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
extension SessionInfo { extension SessionInfoDto {
var device: DeviceType { var device: DeviceType {
DeviceType( DeviceType(

View File

@ -9,31 +9,34 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
extension SpecialFeatureType: Displayable { extension ExtraType: Displayable {
// TODO: localize
var displayTitle: String { var displayTitle: String {
switch self { switch self {
case .unknown: case .unknown:
return L10n.unknown return L10n.unknown
case .clip: case .clip:
return "Clip" return L10n.clip
case .trailer: case .trailer:
return "Trailer" return L10n.trailer
case .behindTheScenes: case .behindTheScenes:
return "Behind the Scenes" return L10n.behindTheScenes
case .deletedScene: case .deletedScene:
return "Deleted Scene" return L10n.deletedScene
case .interview: case .interview:
return "Interview" return L10n.interview
case .scene: case .scene:
return "Scene" return L10n.scene
case .sample: case .sample:
return "Sample" return L10n.sample
case .themeSong: case .themeSong:
return "Theme Song" return L10n.themeSong
case .themeVideo: case .themeVideo:
return "Theme Video" return L10n.themeVideo
case .featurette:
return L10n.featurette
case .short:
return L10n.short
} }
} }

View File

@ -7,16 +7,9 @@
// //
import Foundation import Foundation
import JellyfinAPI
// TODO: move to SDK as patch file extension TaskTriggerType: Displayable, SystemImageable {
enum TaskTriggerType: String, Codable, CaseIterable, Displayable, SystemImageable {
case daily = "DailyTrigger"
case weekly = "WeeklyTrigger"
case interval = "IntervalTrigger"
case startup = "StartupTrigger"
var displayTitle: String { var displayTitle: String {
switch self { switch self {
case .daily: case .daily:

View File

@ -64,6 +64,8 @@ extension TranscodeReason: Displayable, SystemImageable {
return L10n.directPlayError return L10n.directPlayError
case .videoRangeTypeNotSupported: case .videoRangeTypeNotSupported:
return L10n.videoRangeTypeNotSupported return L10n.videoRangeTypeNotSupported
case .videoCodecTagNotSupported:
return L10n.videoCodecTagNotSupported
} }
} }
@ -93,6 +95,7 @@ extension TranscodeReason: Displayable, SystemImageable {
.interlacedVideoNotSupported, .interlacedVideoNotSupported,
.videoBitrateNotSupported, .videoBitrateNotSupported,
.unknownVideoStreamInfo, .unknownVideoStreamInfo,
.videoCodecTagNotSupported,
.videoRangeTypeNotSupported: .videoRangeTypeNotSupported:
return "photo.tv" return "photo.tv"
case .subtitleCodecNotSupported: case .subtitleCodecNotSupported:

View File

@ -21,7 +21,7 @@ extension TranscodingProfile {
isEstimateContentLength: Bool? = nil, isEstimateContentLength: Bool? = nil,
maxAudioChannels: String? = nil, maxAudioChannels: String? = nil,
minSegments: Int? = nil, minSegments: Int? = nil,
protocol: String? = nil, protocol: MediaStreamProtocol? = nil,
segmentLength: Int? = nil, segmentLength: Int? = nil,
transcodeSeekInfo: TranscodeSeekInfo? = nil, transcodeSeekInfo: TranscodeSeekInfo? = nil,
type: DlnaProfileType? = nil, type: DlnaProfileType? = nil,

View File

@ -7,39 +7,20 @@
// //
import Foundation import Foundation
import JellyfinAPI
// TODO: 10.10+ Replace with extension of https://github.com/jellyfin/jellyfin-sdk-swift/blob/main/Sources/Entities/VideoRangeType.swift extension VideoRangeType: Displayable {
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.
/// Dolby Vision is a proper noun so it is not localized /// Dolby Vision is a proper noun so it is not localized
var displayTitle: String { var displayTitle: String {
switch self { switch self {
case .unknown: case .unknown:
return L10n.unknown return L10n.unknown
case .sdr:
return "SDR"
case .hdr10:
return "HDR10"
case .hlg:
return "HLG"
case .dovi: case .dovi:
return "Dolby Vision" return "Dolby Vision"
case .doviWithHDR10: case .doviWithHDR10:
@ -50,18 +31,16 @@ enum VideoRangeType: String, Displayable {
return "Dolby Vision / SDR" return "Dolby Vision / SDR"
case .hdr10Plus: case .hdr10Plus:
return "HDR10+" return "HDR10+"
default:
return self.rawValue
} }
} }
/// Returns `true` if the video format is HDR (including Dolby Vision). /// Returns `true` if the video format is HDR (including Dolby Vision).
var isHDR: Bool { var isHDR: Bool {
switch self { switch self {
case .unknown, .sdr: case .hdr10, .hlg, .hdr10Plus, .dovi, .doviWithHDR10, .doviWithHLG, .doviWithSDR:
return false
default:
return true return true
default:
return false
} }
} }

View File

@ -52,7 +52,7 @@ enum ItemArrayElements: Displayable {
name: String, name: String,
id: String?, id: String?,
personRole: String?, personRole: String?,
personKind: String? personKind: PersonKind?
) -> T { ) -> T {
switch self { switch self {
case .genres, .tags: case .genres, .tags:

View File

@ -16,7 +16,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
var genres: [ItemGenre] = [] var genres: [ItemGenre] = []
var itemTypes: [BaseItemKind] = [] var itemTypes: [BaseItemKind] = []
var letter: [ItemLetter] = [] var letter: [ItemLetter] = []
var sortBy: [ItemSortBy] = [ItemSortBy.name] var sortBy: [ItemSortBy] = [ItemSortBy.sortName]
var sortOrder: [ItemSortOrder] = [ItemSortOrder.ascending] var sortOrder: [ItemSortOrder] = [ItemSortOrder.ascending]
var tags: [ItemTag] = [] var tags: [ItemTag] = []
var traits: [ItemTrait] = [] var traits: [ItemTrait] = []
@ -29,7 +29,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
traits: [ItemTrait.isFavorite] traits: [ItemTrait.isFavorite]
) )
static let recent: ItemFilterCollection = .init( static let recent: ItemFilterCollection = .init(
sortBy: [ItemSortBy.dateAdded], sortBy: [ItemSortBy.dateLastContentAdded],
sortOrder: [ItemSortOrder.descending] sortOrder: [ItemSortOrder.descending]
) )
@ -39,7 +39,7 @@ struct ItemFilterCollection: Codable, Defaults.Serializable, Hashable {
/// available values within the current context. /// available values within the current context.
static let all: ItemFilterCollection = .init( static let all: ItemFilterCollection = .init(
letter: ItemLetter.allCases, letter: ItemLetter.allCases,
sortBy: ItemSortBy.allCases, sortBy: ItemSortBy.supportedCases,
sortOrder: ItemSortOrder.allCases, sortOrder: ItemSortOrder.allCases,
traits: ItemTrait.supportedCases traits: ItemTrait.supportedCases
) )

View File

@ -9,28 +9,85 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
// TODO: Remove when JellyfinAPI generates 10.9.0 schema extension ItemSortBy: Displayable, SupportedCaseIterable {
enum ItemSortBy: String, CaseIterable, Displayable, Codable {
case premiereDate = "PremiereDate"
case name = "SortName"
case dateAdded = "DateCreated"
case random = "Random"
// TODO: Localize
var displayTitle: String { var displayTitle: String {
switch self { 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: case .premiereDate:
return L10n.premiereDate return L10n.premiereDate
case .startDate:
return L10n.startDate
case .sortName:
return L10n.sortName
case .name: case .name:
return L10n.name return L10n.name
case .dateAdded:
return L10n.dateAdded
case .random: case .random:
return L10n.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 { extension ItemSortBy: ItemFilter {

View File

@ -33,7 +33,7 @@ extension PlaybackCompatibility {
context: .streaming, context: .streaming,
maxAudioChannels: "8", maxAudioChannels: "8",
minSegments: 2, minSegments: 2,
protocol: StreamType.hls.rawValue, protocol: MediaStreamProtocol.hls,
type: .video type: .video
) { ) {
AudioCodec.aac AudioCodec.aac

View File

@ -56,7 +56,7 @@ struct CustomDeviceProfile: Hashable, Storable {
context: .streaming, context: .streaming,
maxAudioChannels: "8", maxAudioChannels: "8",
minSegments: 2, minSegments: 2,
protocol: StreamType.hls.rawValue, protocol: MediaStreamProtocol.hls,
type: .video type: .video
) { ) {
audio audio

View File

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

View File

@ -18,3 +18,10 @@ protocol SupportedCaseIterable: CaseIterable {
static var supportedCases: Self.SupportedCases { get } static var supportedCases: Self.SupportedCases { get }
} }
extension SupportedCaseIterable where SupportedCases.Element: Equatable {
var isSupported: Bool {
Self.supportedCases.contains(self)
}
}

View File

@ -31,11 +31,9 @@ struct UserPermissions {
self.canDelete = policy?.enableContentDeletion ?? false || policy?.enableContentDeletionFromFolders != [] self.canDelete = policy?.enableContentDeletion ?? false || policy?.enableContentDeletionFromFolders != []
self.canDownload = policy?.enableContentDownloading ?? false self.canDownload = policy?.enableContentDownloading ?? false
self.canEditMetadata = isAdministrator self.canEditMetadata = isAdministrator
// TODO: SDK 10.9 Enable Comments self.canManageSubtitles = isAdministrator || policy?.enableSubtitleManagement ?? false
self.canManageSubtitles = isAdministrator // || policy?.enableSubtitleManagement ?? false self.canManageCollections = isAdministrator || policy?.enableCollectionManagement ?? false
self.canManageCollections = isAdministrator // || policy?.enableCollectionManagement ?? false self.canManageLyrics = isAdministrator || policy?.enableSubtitleManagement ?? false
// TODO: SDK 10.10 Enable Comments
self.canManageLyrics = isAdministrator // || policy?.enableSubtitleManagement ?? false
} }
} }
} }

View File

@ -101,7 +101,7 @@ extension VideoPlayerType {
enableSubtitlesInManifest: true, enableSubtitlesInManifest: true,
maxAudioChannels: "8", maxAudioChannels: "8",
minSegments: 2, minSegments: 2,
protocol: "hls", protocol: MediaStreamProtocol.hls,
type: .video type: .video
) { ) {
AudioCodec.aac AudioCodec.aac

View File

@ -83,15 +83,4 @@ extension VideoPlayerType {
} }
) )
} }
// MARK: - response profiles
@ArrayBuilder<ResponseProfile>
var responseProfiles: [ResponseProfile] {
ResponseProfile(
container: MediaContainer.m4v.rawValue,
mimeType: "video/mp4",
type: .video
)
}
} }

View File

@ -57,7 +57,7 @@ extension VideoPlayerType {
context: .streaming, context: .streaming,
maxAudioChannels: "8", maxAudioChannels: "8",
minSegments: 2, minSegments: 2,
protocol: StreamType.hls.rawValue, protocol: MediaStreamProtocol.hls,
type: .video type: .video
) { ) {
AudioCodec.aac AudioCodec.aac

View File

@ -70,12 +70,16 @@ internal enum L10n {
} }
/// Aired /// Aired
internal static let aired = L10n.tr("Localizable", "aired", fallback: "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 /// Air Time
internal static let airTime = L10n.tr("Localizable", "airTime", fallback: "Air Time") internal static let airTime = L10n.tr("Localizable", "airTime", fallback: "Air Time")
/// Airs %s /// Airs %s
internal static func airWithDate(_ p1: UnsafePointer<CChar>) -> String { internal static func airWithDate(_ p1: UnsafePointer<CChar>) -> String {
return L10n.tr("Localizable", "airWithDate", p1, fallback: "Airs %s") return L10n.tr("Localizable", "airWithDate", p1, fallback: "Airs %s")
} }
/// Album
internal static let album = L10n.tr("Localizable", "album", fallback: "Album")
/// Album Artist /// Album Artist
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist") internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
/// All /// All
@ -170,6 +174,8 @@ internal enum L10n {
internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons") internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons")
/// Behavior /// Behavior
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "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. /// 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.") internal static let birateAutoDescription = L10n.tr("Localizable", "birateAutoDescription", fallback: "Tests your server connection to assess internet speed and adjust bandwidth automatically.")
/// Birthday /// Birthday
@ -268,6 +274,8 @@ internal enum L10n {
internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background") internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background")
/// Client /// Client
internal static let client = L10n.tr("Localizable", "client", fallback: "Client") internal static let client = L10n.tr("Localizable", "client", fallback: "Client")
/// Clip
internal static let clip = L10n.tr("Localizable", "clip", fallback: "Clip")
/// Close /// Close
internal static let close = L10n.tr("Localizable", "close", fallback: "Close") internal static let close = L10n.tr("Localizable", "close", fallback: "Close")
/// Collections /// 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.") 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. /// 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.") 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. /// Your custom device name '%1$@' has been saved.
internal static func customDeviceNameSaved(_ p1: Any) -> String { internal static func customDeviceNameSaved(_ p1: Any) -> String {
return L10n.tr("Localizable", "customDeviceNameSaved", String(describing: p1), fallback: "Your custom device name '%1$@' has been saved.") 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") internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
/// Date created /// Date created
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "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 /// Date modified
internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date modified") internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date modified")
/// Date of death /// Date of death
internal static let dateOfDeath = L10n.tr("Localizable", "dateOfDeath", fallback: "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 /// Dates
internal static let dates = L10n.tr("Localizable", "dates", fallback: "Dates") internal static let dates = L10n.tr("Localizable", "dates", fallback: "Dates")
/// Day of Week /// 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. /// 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.") 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 /// Delete image
internal static let deleteImage = L10n.tr("Localizable", "deleteImage", fallback: "Delete image") internal static let deleteImage = L10n.tr("Localizable", "deleteImage", fallback: "Delete image")
/// Are you sure you want to delete this item? /// 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") internal static let external = L10n.tr("Localizable", "external", fallback: "External")
/// Failed logins /// Failed logins
internal static let failedLogins = L10n.tr("Localizable", "failedLogins", fallback: "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 /// Favorited
internal static let favorited = L10n.tr("Localizable", "favorited", fallback: "Favorited") internal static let favorited = L10n.tr("Localizable", "favorited", fallback: "Favorited")
/// Favorites /// Favorites
internal static let favorites = L10n.tr("Localizable", "favorites", fallback: "Favorites") internal static let favorites = L10n.tr("Localizable", "favorites", fallback: "Favorites")
/// Featurette
internal static let featurette = L10n.tr("Localizable", "featurette", fallback: "Featurette")
/// Filters /// Filters
internal static let filters = L10n.tr("Localizable", "filters", fallback: "Filters") internal static let filters = L10n.tr("Localizable", "filters", fallback: "Filters")
/// Find Missing /// Find Missing
internal static let findMissing = L10n.tr("Localizable", "findMissing", fallback: "Find Missing") internal static let findMissing = L10n.tr("Localizable", "findMissing", fallback: "Find Missing")
/// Find missing metadata and images. /// Find missing metadata and images.
internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "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 /// Force remote media transcoding
internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding") internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding")
/// Format /// Format
@ -670,6 +688,8 @@ internal enum L10n {
internal static let imageSource = L10n.tr("Localizable", "imageSource", fallback: "Image source") internal static let imageSource = L10n.tr("Localizable", "imageSource", fallback: "Image source")
/// Index /// Index
internal static let index = L10n.tr("Localizable", "index", fallback: "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 /// Indicators
internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators") internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators")
/// Inker /// Inker
@ -678,6 +698,8 @@ internal enum L10n {
internal static let interlacedVideoNotSupported = L10n.tr("Localizable", "interlacedVideoNotSupported", fallback: "Interlaced video is not supported") internal static let interlacedVideoNotSupported = L10n.tr("Localizable", "interlacedVideoNotSupported", fallback: "Interlaced video is not supported")
/// Interval /// Interval
internal static let interval = L10n.tr("Localizable", "interval", fallback: "Interval") internal static let interval = L10n.tr("Localizable", "interval", fallback: "Interval")
/// Interview
internal static let interview = L10n.tr("Localizable", "interview", fallback: "Interview")
/// Inverted Dark /// Inverted Dark
internal static let invertedDark = L10n.tr("Localizable", "invertedDark", fallback: "Inverted Dark") internal static let invertedDark = L10n.tr("Localizable", "invertedDark", fallback: "Inverted Dark")
/// Inverted Light /// Inverted Light
@ -932,6 +954,8 @@ internal enum L10n {
internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls") internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls")
/// Parental rating /// Parental rating
internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "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 /// Password
internal static let password = L10n.tr("Localizable", "password", fallback: "Password") internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
/// User password has been changed. /// User password has been changed.
@ -966,6 +990,8 @@ internal enum L10n {
internal static let playbackQuality = L10n.tr("Localizable", "playbackQuality", fallback: "Playback Quality") internal static let playbackQuality = L10n.tr("Localizable", "playbackQuality", fallback: "Playback Quality")
/// Playback Speed /// Playback Speed
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed", fallback: "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 /// Played
internal static let played = L10n.tr("Localizable", "played", fallback: "Played") internal static let played = L10n.tr("Localizable", "played", fallback: "Played")
/// Play From Beginning /// Play From Beginning
@ -1144,10 +1170,14 @@ internal enum L10n {
internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") internal static let running = L10n.tr("Localizable", "running", fallback: "Running...")
/// Runtime /// Runtime
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
/// Sample
internal static let sample = L10n.tr("Localizable", "sample", fallback: "Sample")
/// Save /// Save
internal static let save = L10n.tr("Localizable", "save", fallback: "Save") internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
/// Save the user to this device without any local authentication. /// 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.") 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 /// Schedule already exists
internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists") internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists")
/// Score /// Score
@ -1158,6 +1188,8 @@ internal enum L10n {
internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time") internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time")
/// Search /// Search
internal static let search = L10n.tr("Localizable", "search", fallback: "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 /// Season
internal static let season = L10n.tr("Localizable", "season", fallback: "Season") internal static let season = L10n.tr("Localizable", "season", fallback: "Season")
/// S%1$@:E%2$@ /// S%1$@:E%2$@
@ -1182,6 +1214,10 @@ internal enum L10n {
internal static let series = L10n.tr("Localizable", "series", fallback: "Series") internal static let series = L10n.tr("Localizable", "series", fallback: "Series")
/// Series Backdrop /// Series Backdrop
internal static let seriesBackdrop = L10n.tr("Localizable", "seriesBackdrop", fallback: "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 /// Server
internal static let server = L10n.tr("Localizable", "server", fallback: "Server") internal static let server = L10n.tr("Localizable", "server", fallback: "Server")
/// %@ is already connected. /// %@ 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.") internal static let setPinHintDescription = L10n.tr("Localizable", "setPinHintDescription", fallback: "Set a hint when prompting for the pin.")
/// Settings /// Settings
internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings") internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings")
/// Short
internal static let short = L10n.tr("Localizable", "short", fallback: "Short")
/// Show Favorited /// Show Favorited
internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited") internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited")
/// Show Favorites /// Show Favorites
@ -1248,6 +1286,8 @@ internal enum L10n {
internal static let signoutClose = L10n.tr("Localizable", "signoutClose", fallback: "Sign out on close") internal static let signoutClose = L10n.tr("Localizable", "signoutClose", fallback: "Sign out on close")
/// Signs out the last user when Swiftfin has been force closed /// 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") 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 /// Slider
internal static let slider = L10n.tr("Localizable", "slider", fallback: "Slider") internal static let slider = L10n.tr("Localizable", "slider", fallback: "Slider")
/// Slider Color /// 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") 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 /// Sports
internal static let sports = L10n.tr("Localizable", "sports", fallback: "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 /// Start Time
internal static let startTime = L10n.tr("Localizable", "startTime", fallback: "Start Time") internal static let startTime = L10n.tr("Localizable", "startTime", fallback: "Start Time")
/// Status /// Status
@ -1284,6 +1326,8 @@ internal enum L10n {
internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc") internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc")
/// Streams /// Streams
internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams") internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams")
/// Studio
internal static let studio = L10n.tr("Localizable", "studio", fallback: "Studio")
/// Studios /// Studios
internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios") internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios")
/// Studio(s) involved in the creation of media. /// 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") internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: "Subtitle Size")
/// Success /// Success
internal static let success = L10n.tr("Localizable", "success", fallback: "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 /// Media Control
internal static let supportsMediaControl = L10n.tr("Localizable", "supportsMediaControl", fallback: "Media Control") internal static let supportsMediaControl = L10n.tr("Localizable", "supportsMediaControl", fallback: "Media Control")
/// Persistent Identifier /// Persistent Identifier
internal static let supportsPersistentIdentifier = L10n.tr("Localizable", "supportsPersistentIdentifier", fallback: "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 /// Switch User
internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User") internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User")
/// SyncPlay /// SyncPlay
@ -1352,6 +1392,10 @@ internal enum L10n {
internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps") internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps")
/// Test Size /// Test Size
internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "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 /// Thumb
internal static let thumb = L10n.tr("Localizable", "thumb", fallback: "Thumb") internal static let thumb = L10n.tr("Localizable", "thumb", fallback: "Thumb")
/// Time /// Time
@ -1464,10 +1508,14 @@ internal enum L10n {
internal static let video = L10n.tr("Localizable", "video", fallback: "Video") internal static let video = L10n.tr("Localizable", "video", fallback: "Video")
/// The video bit depth is not supported /// The video bit depth is not supported
internal static let videoBitDepthNotSupported = L10n.tr("Localizable", "videoBitDepthNotSupported", fallback: "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 /// The video bitrate is not supported
internal static let videoBitrateNotSupported = L10n.tr("Localizable", "videoBitrateNotSupported", fallback: "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 /// The video codec is not supported
internal static let videoCodecNotSupported = L10n.tr("Localizable", "videoCodecNotSupported", fallback: "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 /// The video framerate is not supported
internal static let videoFramerateNotSupported = L10n.tr("Localizable", "videoFramerateNotSupported", fallback: "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 /// The video level is not supported

View File

@ -152,13 +152,10 @@ extension UserState {
let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!) let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!)
let parameters = Paths.GetUserImageParameters( let parameters = Paths.GetUserImageParameters(
userID: id,
maxWidth: scaleWidth maxWidth: scaleWidth
) )
let request = Paths.getUserImage( let request = Paths.getUserImage(parameters: parameters)
userID: id,
imageType: "Primary",
parameters: parameters
)
let profileImageURL = client.fullURL(with: request) let profileImageURL = client.fullURL(with: request)

View File

@ -38,7 +38,7 @@ final class ActiveSessionsViewModel: ViewModel, Stateful {
@Published @Published
var backgroundStates: Set<BackgroundState> = [] var backgroundStates: Set<BackgroundState> = []
@Published @Published
var sessions: OrderedDictionary<String, BindingBox<SessionInfo?>> = [:] var sessions: OrderedDictionary<String, BindingBox<SessionInfoDto?>> = [:]
@Published @Published
var state: State = .initial var state: State = .initial
@ -119,7 +119,7 @@ final class ActiveSessionsViewModel: ViewModel, Stateful {
return !sessions.keys.contains(id) return !sessions.keys.contains(id)
} }
.map { s in .map { s in
BindingBox<SessionInfo?>( BindingBox<SessionInfoDto?>(
source: .init( source: .init(
get: { s }, get: { s },
set: { _ in } set: { _ in }

View File

@ -36,7 +36,7 @@ final class DeviceDetailViewModel: ViewModel, Stateful, Eventful {
var state: State = .initial var state: State = .initial
@Published @Published
private(set) var device: DeviceInfo private(set) var device: DeviceInfoDto
var events: AnyPublisher<Event, Never> { var events: AnyPublisher<Event, Never> {
eventSubject eventSubject
@ -46,7 +46,7 @@ final class DeviceDetailViewModel: ViewModel, Stateful, Eventful {
private var eventSubject: PassthroughSubject<Event, Never> = .init() private var eventSubject: PassthroughSubject<Event, Never> = .init()
init(device: DeviceInfo) { init(device: DeviceInfoDto) {
self.device = device self.device = device
} }
@ -87,6 +87,10 @@ final class DeviceDetailViewModel: ViewModel, Stateful, Eventful {
let request = Paths.updateDeviceOptions(id: id, .init(customName: newName)) let request = Paths.updateDeviceOptions(id: id, .init(customName: newName))
try await userSession.client.send(request) try await userSession.client.send(request)
await MainActor.run {
self.device.customName = newName
}
} }
private func getDeviceInfo() async throws { private func getDeviceInfo() async throws {

View File

@ -49,7 +49,7 @@ final class DevicesViewModel: ViewModel, Eventful, Stateful {
@Published @Published
var backgroundStates: Set<BackgroundState> = [] var backgroundStates: Set<BackgroundState> = []
@Published @Published
var devices: [DeviceInfo] = [] var devices: [DeviceInfoDto] = []
@Published @Published
var state: State = .initial var state: State = .initial

View File

@ -17,7 +17,7 @@ final class ChannelLibraryViewModel: PagingLibraryViewModel<ChannelProgram> {
var parameters = Paths.GetLiveTvChannelsParameters() var parameters = Paths.GetLiveTvChannelsParameters()
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
parameters.userID = userSession.user.id parameters.userID = userSession.user.id
parameters.sortBy = [ItemSortBy.name.rawValue] parameters.sortBy = [ItemSortBy.name]
parameters.limit = pageSize parameters.limit = pageSize
parameters.startIndex = page * pageSize parameters.startIndex = page * pageSize
@ -40,7 +40,7 @@ final class ChannelLibraryViewModel: PagingLibraryViewModel<ChannelProgram> {
parameters.userID = userSession.user.id parameters.userID = userSession.user.id
parameters.maxStartDate = maxStartDate parameters.maxStartDate = maxStartDate
parameters.minEndDate = minEndDate parameters.minEndDate = minEndDate
parameters.sortBy = ["StartDate"] parameters.sortBy = [ItemSortBy.startDate]
let request = Paths.getLiveTvPrograms(parameters: parameters) let request = Paths.getLiveTvPrograms(parameters: parameters)
let response = try await userSession.client.send(request) let response = try await userSession.client.send(request)

View File

@ -174,12 +174,13 @@ final class HomeViewModel: ViewModel, Stateful {
private func getResumeItems() async throws -> [BaseItemDto] { private func getResumeItems() async throws -> [BaseItemDto] {
var parameters = Paths.GetResumeItemsParameters() var parameters = Paths.GetResumeItemsParameters()
parameters.userID = userSession.user.id
parameters.enableUserData = true parameters.enableUserData = true
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
parameters.includeItemTypes = [.movie, .episode] parameters.includeItemTypes = [.movie, .episode]
parameters.limit = 20 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) let response = try await userSession.client.send(request)
return response.value.items ?? [] return response.value.items ?? []
@ -187,13 +188,14 @@ final class HomeViewModel: ViewModel, Stateful {
private func getLibraries() async throws -> [LatestInLibraryViewModel] { 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 userViews = userSession.client.send(userViewsPath)
async let excludedLibraryIDs = getExcludedLibraries() async let excludedLibraryIDs = getExcludedLibraries()
return try await (userViews.value.items ?? []) return try await (userViews.value.items ?? [])
.intersection(["movies", "tvshows"], using: \.collectionType) .intersection([.movies, .tvshows], using: \.collectionType)
.subtracting(excludedLibraryIDs, using: \.id) .subtracting(excludedLibraryIDs, using: \.id)
.map { LatestInLibraryViewModel(parent: $0) } .map { LatestInLibraryViewModel(parent: $0) }
} }
@ -211,13 +213,13 @@ final class HomeViewModel: ViewModel, Stateful {
if isPlayed { if isPlayed {
request = Paths.markPlayedItem( request = Paths.markPlayedItem(
userID: userSession.user.id, itemID: item.id!,
itemID: item.id! userID: userSession.user.id
) )
} else { } else {
request = Paths.markUnplayedItem( request = Paths.markUnplayedItem(
userID: userSession.user.id, itemID: item.id!,
itemID: item.id! userID: userSession.user.id
) )
} }

View File

@ -213,7 +213,10 @@ final class IdentifyItemViewModel: ViewModel, Stateful, Eventful {
private func refreshItem() async throws { private func refreshItem() async throws {
guard let itemID = item.id else { return } 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) let response = try await userSession.client.send(request)
await MainActor.run { await MainActor.run {

View File

@ -250,7 +250,10 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
_ = self.backgroundStates.insert(.refreshing) _ = 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) let response = try await userSession.client.send(request)
await MainActor.run { await MainActor.run {

View File

@ -388,8 +388,8 @@ final class ItemImagesViewModel: ViewModel, Stateful, Eventful {
} }
let request = Paths.getItem( let request = Paths.getItem(
userID: userSession.user.id, itemID: itemID,
itemID: itemID userID: userSession.user.id
) )
let response = try await userSession.client.send(request) let response = try await userSession.client.send(request)

View File

@ -136,7 +136,10 @@ final class RefreshMetadataViewModel: ViewModel, Stateful, Eventful {
try await pollRefreshProgress() 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) let response = try await userSession.client.send(request)
await MainActor.run { await MainActor.run {
@ -149,7 +152,7 @@ final class RefreshMetadataViewModel: ViewModel, Stateful, Eventful {
// MARK: - Poll Progress // 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 { private func pollRefreshProgress() async throws {
let totalDuration: Double = 5.0 let totalDuration: Double = 5.0
let interval: Double = 0.05 let interval: Double = 0.05

View File

@ -325,8 +325,8 @@ class ItemViewModel: ViewModel, Stateful {
private func getSpecialFeatures() async -> [BaseItemDto] { private func getSpecialFeatures() async -> [BaseItemDto] {
let request = Paths.getSpecialFeatures( 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) let response = try? await userSession.client.send(request)
@ -338,7 +338,7 @@ class ItemViewModel: ViewModel, Stateful {
guard let itemID = item.id else { return [] } 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) let response = try? await userSession.client.send(request)
return response?.value ?? [] return response?.value ?? []
@ -352,13 +352,13 @@ class ItemViewModel: ViewModel, Stateful {
if isPlayed { if isPlayed {
request = Paths.markPlayedItem( request = Paths.markPlayedItem(
userID: userSession.user.id, itemID: item.id!,
itemID: item.id! userID: userSession.user.id
) )
} else { } else {
request = Paths.markUnplayedItem( 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 { if isFavorite {
request = Paths.markFavoriteItem( request = Paths.markFavoriteItem(
userID: userSession.user.id, itemID: item.id!,
itemID: item.id! userID: userSession.user.id
) )
} else { } else {
request = Paths.unmarkFavoriteItem( request = Paths.unmarkFavoriteItem(
userID: userSession.user.id, itemID: item.id!,
itemID: item.id! userID: userSession.user.id
) )
} }

View File

@ -87,11 +87,12 @@ final class SeriesItemViewModel: ItemViewModel {
private func getResumeItem() async throws -> BaseItemDto? { private func getResumeItem() async throws -> BaseItemDto? {
var parameters = Paths.GetResumeItemsParameters() var parameters = Paths.GetResumeItemsParameters()
parameters.userID = userSession.user.id
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
parameters.limit = 1 parameters.limit = 1
parameters.parentID = item.id 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) let response = try await userSession.client.send(request)
return response.value.items?.first return response.value.items?.first

View File

@ -30,7 +30,7 @@ final class ItemLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
let items = (response.value.items ?? []) let items = (response.value.items ?? [])
.filter { item in .filter { item in
if let collectionType = item.collectionType { if let collectionType = item.collectionType {
return ["movies", "tvshows", "mixed", "boxsets"].contains(collectionType) return CollectionType.supportedCases.contains(collectionType)
} }
return true return true

View File

@ -14,7 +14,7 @@ final class LatestInLibraryViewModel: PagingLibraryViewModel<BaseItemDto>, Ident
override func get(page: Int) async throws -> [BaseItemDto] { override func get(page: Int) async throws -> [BaseItemDto] {
let parameters = parameters() 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) let response = try await userSession.client.send(request)
return response.value return response.value
@ -23,6 +23,7 @@ final class LatestInLibraryViewModel: PagingLibraryViewModel<BaseItemDto>, Ident
private func parameters() -> Paths.GetLatestMediaParameters { private func parameters() -> Paths.GetLatestMediaParameters {
var parameters = Paths.GetLatestMediaParameters() var parameters = Paths.GetLatestMediaParameters()
parameters.userID = userSession.user.id
parameters.parentID = parent?.id parameters.parentID = parent?.id
parameters.fields = .MinimumFields parameters.fields = .MinimumFields
parameters.enableUserData = true parameters.enableUserData = true

View File

@ -42,7 +42,7 @@ final class RecentlyAddedLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
parameters.includeItemTypes = [.movie, .series] parameters.includeItemTypes = [.movie, .series]
parameters.isRecursive = true parameters.isRecursive = true
parameters.limit = pageSize parameters.limit = pageSize
parameters.sortBy = [ItemSortBy.dateAdded.rawValue] parameters.sortBy = [ItemSortBy.dateLastContentAdded.rawValue]
parameters.sortOrder = [.descending] parameters.sortOrder = [.descending]
parameters.startIndex = page parameters.startIndex = page

View File

@ -13,9 +13,6 @@ import OrderedCollections
final class MediaViewModel: ViewModel, Stateful { final class MediaViewModel: ViewModel, Stateful {
// TODO: remove once collection types become an enum
static let supportedCollectionTypes: [String] = ["boxsets", "folders", "movies", "tvshows", "livetv"]
// MARK: Action // MARK: Action
enum Action: Equatable { enum Action: Equatable {
@ -75,7 +72,7 @@ final class MediaViewModel: ViewModel, Stateful {
let media: [MediaType] = try await getUserViews() let media: [MediaType] = try await getUserViews()
.compactMap { userView in .compactMap { userView in
if userView.collectionType == "livetv" { if userView.collectionType == .livetv {
return .liveTV(userView) return .liveTV(userView)
} }
@ -90,7 +87,8 @@ final class MediaViewModel: ViewModel, Stateful {
private func getUserViews() async throws -> [BaseItemDto] { 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 userViews = userSession.client.send(userViewsPath)
async let excludedLibraryIDs = getExcludedLibraries() async let excludedLibraryIDs = getExcludedLibraries()
@ -98,11 +96,11 @@ final class MediaViewModel: ViewModel, Stateful {
// folders has `type = UserView`, but we manually // folders has `type = UserView`, but we manually
// force it to `folders` for better view handling // force it to `folders` for better view handling
let supportedUserViews = try await (userViews.value.items ?? []) let supportedUserViews = try await (userViews.value.items ?? [])
.intersection(Self.supportedCollectionTypes, using: \.collectionType) .intersection(CollectionType.supportedCases, using: \.collectionType)
.subtracting(excludedLibraryIDs, using: \.id) .subtracting(excludedLibraryIDs, using: \.id)
.map { item in .map { item in
if item.type == .userView, item.collectionType == "folders" { if item.type == .userView, item.collectionType == .folders {
return item.mutating(\.type, with: .folder) return item.mutating(\.type, with: .folder)
} }

View File

@ -146,7 +146,6 @@ final class UserProfileImageViewModel: ViewModel, Eventful, Stateful {
var request = Paths.postUserImage( var request = Paths.postUserImage(
userID: userID, userID: userID,
imageType: "Primary",
imageData imageData
) )
request.headers = ["Content-Type": contentType] request.headers = ["Content-Type": contentType]
@ -172,10 +171,7 @@ final class UserProfileImageViewModel: ViewModel, Eventful, Stateful {
guard let userID = user.id else { return } guard let userID = user.id else { return }
let request = Paths.deleteUserImage( let request = Paths.deleteUserImage(userID: userID)
userID: userID,
imageType: "Primary"
)
let _ = try await userSession.client.send(request) let _ = try await userSession.client.send(request)
sweepProfileImageCache() sweepProfileImageCache()

View File

@ -31,7 +31,7 @@ final class DownloadVideoPlayerManager: VideoPlayerManager {
selectedAudioStreamIndex: 1, selectedAudioStreamIndex: 1,
selectedSubtitleStreamIndex: 1, selectedSubtitleStreamIndex: 1,
chapters: downloadTask.item.fullChapterInfo, chapters: downloadTask.item.fullChapterInfo,
streamType: .direct playMethod: .directPlay
) )
} }

View File

@ -26,14 +26,14 @@ final class VideoPlayerViewModel: ViewModel {
let selectedAudioStreamIndex: Int let selectedAudioStreamIndex: Int
let selectedSubtitleStreamIndex: Int let selectedSubtitleStreamIndex: Int
let chapters: [ChapterInfo.FullInfo] let chapters: [ChapterInfo.FullInfo]
let streamType: StreamType let playMethod: PlayMethod
var hlsPlaybackURL: URL { var hlsPlaybackURL: URL {
let parameters = Paths.GetMasterHlsVideoPlaylistParameters( let parameters = Paths.GetMasterHlsVideoPlaylistParameters(
isStatic: true, isStatic: true,
tag: mediaSource.eTag, tag: mediaSource.eTag,
playSessionID: playSessionID, playSessionID: playSessionID,
segmentContainer: "mp4", segmentContainer: MediaContainer.mp4.rawValue,
minSegments: 2, minSegments: 2,
mediaSourceID: mediaSource.id!, mediaSourceID: mediaSource.id!,
deviceID: UIDevice.vendorUUIDString, deviceID: UIDevice.vendorUUIDString,
@ -97,7 +97,7 @@ final class VideoPlayerViewModel: ViewModel {
selectedAudioStreamIndex: Int, selectedAudioStreamIndex: Int,
selectedSubtitleStreamIndex: Int, selectedSubtitleStreamIndex: Int,
chapters: [ChapterInfo.FullInfo], chapters: [ChapterInfo.FullInfo],
streamType: StreamType playMethod: PlayMethod
) { ) {
self.item = item self.item = item
self.mediaSource = mediaSource self.mediaSource = mediaSource
@ -108,16 +108,16 @@ final class VideoPlayerViewModel: ViewModel {
fatalError("Media source does not have any streams") 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.videoStreams = adjustedStreams.filter { $0.type == MediaStreamType.video }
self.audioStreams = adjustedStreams.filter { $0.type == .audio } self.audioStreams = adjustedStreams.filter { $0.type == MediaStreamType.audio }
self.subtitleStreams = adjustedStreams.filter { $0.type == .subtitle } self.subtitleStreams = adjustedStreams.filter { $0.type == MediaStreamType.subtitle }
self.selectedAudioStreamIndex = selectedAudioStreamIndex self.selectedAudioStreamIndex = selectedAudioStreamIndex
self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex
self.chapters = chapters self.chapters = chapters
self.streamType = streamType self.playMethod = playMethod
super.init() super.init()
} }

View File

@ -22,7 +22,9 @@ extension ItemView {
PosterHStack( PosterHStack(
title: L10n.castAndCrew, title: L10n.castAndCrew,
type: .portrait, type: .portrait,
items: people.filter(\.isDisplayed) items: people.filter { person in
person.type?.isSupported ?? false
}
) )
.onSelect { person in .onSelect { person in
let viewModel = ItemLibraryViewModel(parent: person) let viewModel = ItemLibraryViewModel(parent: person)

View File

@ -31,8 +31,8 @@
4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; }; 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; };
4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; }; 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; };
4E16FD582C01A32700110147 /* 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 */; }; 4E17498E2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */; };
4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; }; 4E17498F2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */; };
4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; }; 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; };
4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; }; 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; };
4E1A39332D56C84200BAC1C7 /* ItemViewAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1A39322D56C83E00BAC1C7 /* ItemViewAttributes.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 */; }; 4E90F7672CC72B1F00417C31 /* TriggerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75F2CC72B1F00417C31 /* TriggerRow.swift */; };
4E90F7682CC72B1F00417C31 /* TriggersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */; }; 4E90F7682CC72B1F00417C31 /* TriggersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F75D2CC72B1F00417C31 /* TriggersSection.swift */; };
4E90F76A2CC72B1F00417C31 /* DetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F7592CC72B1F00417C31 /* DetailsSection.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 */; }; 4E97D1832D064748004B89AD /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1822D064748004B89AD /* ItemSection.swift */; };
4E97D1852D064B43004B89AD /* RefreshMetadataButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */; }; 4E97D1852D064B43004B89AD /* RefreshMetadataButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */; };
4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.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 */; }; 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B252CB9934700343666 /* LibraryRow.swift */; };
4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; }; 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */; };
4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.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 */; }; 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; };
4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */; }; 4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */; };
4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.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 */; }; 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 */; }; 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 */; }; 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 */; }; E149CCAD2BE6ECC8008B9331 /* Storable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E149CCAC2BE6ECC8008B9331 /* Storable.swift */; };
E149CCAE2BE6ECC8008B9331 /* 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 */; }; E14A08CB28E6831D004FC984 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14A08CA28E6831D004FC984 /* VideoPlayerViewModel.swift */; };
@ -785,13 +790,11 @@
E1575E58293E7685001665B1 /* Files in Frameworks */ = {isa = PBXBuildFile; productRef = E1575E57293E7685001665B1 /* Files */; }; E1575E58293E7685001665B1 /* Files in Frameworks */ = {isa = PBXBuildFile; productRef = E1575E57293E7685001665B1 /* Files */; };
E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; E1575E5C293E77B5001665B1 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
E1575E5D293E77B5001665B1 /* ItemViewType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C925F328875037002A7A66 /* ItemViewType.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 */; }; E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129429728F4785200796AC6 /* CaseIterablePicker.swift */; };
E1575E65293E77B5001665B1 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; }; E1575E65293E77B5001665B1 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1575E66293E77B5001665B1 /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; }; E1575E66293E77B5001665B1 /* Poster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1937A60288F32DB00CB80AA /* Poster.swift */; };
E1575E67293E77B5001665B1 /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; }; E1575E67293E77B5001665B1 /* OverlayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AA331E2782639D00F6439C /* OverlayType.swift */; };
E1575E68293E77B5001665B1 /* LibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113133728BEADBA00930F75 /* LibraryParent.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 */; }; E1575E6A293E77B5001665B1 /* RoundedCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */; };
E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; }; E1575E6B293E77B5001665B1 /* Displayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55128C119D400311DFE /* Displayable.swift */; };
E1575E6C293E77B5001665B1 /* SliderType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E129429228F2845000796AC6 /* SliderType.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 */; }; E1ED91182B95993300802036 /* TitledLibraryParent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ED91172B95993300802036 /* TitledLibraryParent.swift */; };
E1ED91192B95993300802036 /* 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 */; }; 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 */; }; E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; }; E1F5CF052CB09EA000607465 /* CurrentDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F5CF042CB09EA000607465 /* CurrentDate.swift */; };
E1F5CF062CB09EA000607465 /* 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; E1F5CF072CB0A04500607465 /* Text.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = "<group>"; };
@ -3420,7 +3422,6 @@
E129429228F2845000796AC6 /* SliderType.swift */, E129429228F2845000796AC6 /* SliderType.swift */,
E11042742B8013DF00821020 /* Stateful.swift */, E11042742B8013DF00821020 /* Stateful.swift */,
E149CCAC2BE6ECC8008B9331 /* Storable.swift */, E149CCAC2BE6ECC8008B9331 /* Storable.swift */,
E1EF4C402911B783008CC695 /* StreamType.swift */,
E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */, E11BDF792B85529D0045C54A /* SupportedCaseIterable.swift */,
E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */, E15D63EE2BD6DFC200AA665D /* SystemImageable.swift */,
E1A1528428FD191A00600579 /* TextPair.swift */, E1A1528428FD191A00600579 /* TextPair.swift */,
@ -4598,7 +4599,7 @@
E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */, E14EDEC72B8FB65F000F00A4 /* ItemFilterType.swift */,
E11BDF762B8513B40045C54A /* ItemGenre.swift */, E11BDF762B8513B40045C54A /* ItemGenre.swift */,
E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */, E14E9DF02BCF7A99004E3371 /* ItemLetter.swift */,
E148128A28C15526003B8787 /* ItemSortBy.swift */, 4EF36F632D962A430065BB79 /* ItemSortBy.swift */,
E11BDF962B865F550045C54A /* ItemTag.swift */, E11BDF962B865F550045C54A /* ItemTag.swift */,
E14EDECB2B8FB709000F00A4 /* ItemYear.swift */, E14EDECB2B8FB709000F00A4 /* ItemYear.swift */,
); );
@ -5083,8 +5084,9 @@
E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */, E1D37F5A2B9CF01F00343D2B /* BaseItemPerson */,
E1002B632793CEE700E47059 /* ChapterInfo.swift */, E1002B632793CEE700E47059 /* ChapterInfo.swift */,
E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */, E1CB758A2C80F9EC00217C76 /* CodecProfile.swift */,
4E9654472D99C551006CB024 /* CollectionType.swift */,
4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */, 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */,
4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */, 4E17498D2CC00A2E00DD07D1 /* DeviceInfoDto.swift */,
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */, 4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */,
4E12F9152CBE9615006C217E /* DeviceType.swift */, 4E12F9152CBE9615006C217E /* DeviceType.swift */,
E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */, E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */,
@ -5109,8 +5111,7 @@
4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */, 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */,
4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */, 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */,
4E35CE652CBED8B300DBD886 /* ServerTicks.swift */, 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */,
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */, 4EDBDCD02CBDD6510033D347 /* SessionInfoDto.swift */,
4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */,
E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */, E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */,
E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */, E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */,
E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */, E1CB757E2C80F28F00217C76 /* SubtitleProfile.swift */,
@ -5973,7 +5974,6 @@
E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */, E193D53327193F7D00900D82 /* FilterCoordinator.swift in Sources */,
E18E021E2887492B0022598C /* RowDivider.swift in Sources */, E18E021E2887492B0022598C /* RowDivider.swift in Sources */,
4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */, 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */,
4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */,
E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */,
4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */, 4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */,
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */, 4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */,
@ -6011,6 +6011,7 @@
E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, E1A1529128FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */,
E187A60529AD2E25008387E6 /* StepperView.swift in Sources */, E187A60529AD2E25008387E6 /* StepperView.swift in Sources */,
E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */, E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */,
4EF36F652D962A430065BB79 /* ItemSortBy.swift in Sources */,
E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */,
E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */, E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */,
E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */, E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */,
@ -6107,7 +6108,6 @@
E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */, E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */,
E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */, E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */,
E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */, E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */,
E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */,
4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */, 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */,
E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */, E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */,
E1575E93293E7B1E001665B1 /* Double.swift in Sources */, E1575E93293E7B1E001665B1 /* Double.swift in Sources */,
@ -6117,8 +6117,10 @@
E17AC96B2954D00E003D2BC2 /* URLResponse.swift in Sources */, E17AC96B2954D00E003D2BC2 /* URLResponse.swift in Sources */,
E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */, E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */,
E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */, E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */,
4EF36F672D9649050065BB79 /* SessionInfoDto.swift in Sources */,
E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */, E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */,
4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */, 4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */,
4E9654482D99C553006CB024 /* CollectionType.swift in Sources */,
E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */, E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */,
E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */, E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */,
E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */, E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */,
@ -6180,13 +6182,12 @@
4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */, 4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */,
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */, E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */,
E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */, E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */,
E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */,
E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */, E1B490482967E2E500D3EDCE /* CoreStore.swift in Sources */,
4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */, 4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */,
E1DC9845296DECB600982F06 /* ProgressIndicator.swift in Sources */, E1DC9845296DECB600982F06 /* ProgressIndicator.swift in Sources */,
E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */, E1C925F928875647002A7A66 /* LatestInLibraryView.swift in Sources */,
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */,
4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */, 4E17498F2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */,
E12CC1C928D132B800678D5D /* RecentlyAddedView.swift in Sources */, E12CC1C928D132B800678D5D /* RecentlyAddedView.swift in Sources */,
E19D41B32BF2BFEF0082B8B2 /* URLSessionConfiguration.swift in Sources */, E19D41B32BF2BFEF0082B8B2 /* URLSessionConfiguration.swift in Sources */,
E10B1ECE2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */, E10B1ECE2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */,
@ -6432,7 +6433,6 @@
621338932660107500A81A2A /* String.swift in Sources */, 621338932660107500A81A2A /* String.swift in Sources */,
E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */, E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */,
BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */, BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */,
4EB4ECE32CBEFC4D002FF2FC /* SessionInfo.swift in Sources */,
4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */, 4EC6C16B2C92999800FC904B /* TranscodeSection.swift in Sources */,
62C83B08288C6A630004ED0C /* FontPickerView.swift in Sources */, 62C83B08288C6A630004ED0C /* FontPickerView.swift in Sources */,
E122A9132788EAAD0060FA63 /* MediaStream.swift in Sources */, E122A9132788EAAD0060FA63 /* MediaStream.swift in Sources */,
@ -6724,7 +6724,6 @@
C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */, C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */, E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */,
4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */, 4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */,
E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */,
E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */, E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */,
E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */, E1A3E4CB2BB74EFD005C59F8 /* EpisodeHStack.swift in Sources */,
@ -6740,6 +6739,7 @@
4E8F74AC2CE03DD300CC8969 /* DeleteItemViewModel.swift in Sources */, 4E8F74AC2CE03DD300CC8969 /* DeleteItemViewModel.swift in Sources */,
4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */, 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */,
E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */, E1DA656F28E78C9900592A73 /* EpisodeSelector.swift in Sources */,
4E9654492D99C553006CB024 /* CollectionType.swift in Sources */,
4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */, 4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */,
E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */, E18E01E0288747230022598C /* iPadOSMovieItemContentView.swift in Sources */,
E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */, E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */,
@ -6750,7 +6750,7 @@
4EC2B19E2CC96EAB00D866BE /* ServerUsersRow.swift in Sources */, 4EC2B19E2CC96EAB00D866BE /* ServerUsersRow.swift in Sources */,
E1C8CE7C28FF015000DF5D7B /* TrailingTimestampType.swift in Sources */, E1C8CE7C28FF015000DF5D7B /* TrailingTimestampType.swift in Sources */,
C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */, C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */,
4E17498E2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */, 4E17498E2CC00A3100DD07D1 /* DeviceInfoDto.swift in Sources */,
4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */, 4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */,
4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */, 4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */,
4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */, 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */,
@ -6775,8 +6775,8 @@
E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */, E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */,
E1DE84142B9531C1008CCE21 /* OrderedSectionSelectorView.swift in Sources */, E1DE84142B9531C1008CCE21 /* OrderedSectionSelectorView.swift in Sources */,
E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */, E13DD3FC2717EAE8009D4DAF /* SelectUserView.swift in Sources */,
4EF36F642D962A430065BB79 /* ItemSortBy.swift in Sources */,
E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */, E18E01DE288747230022598C /* iPadOSSeriesItemView.swift in Sources */,
E1EF4C412911B783008CC695 /* StreamType.swift in Sources */,
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */, 6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */, E1A3E4CF2BB7E02B005C59F8 /* DelayedProgressView.swift in Sources */,
E1EA09672BED6815004CDE76 /* UserSignInSecurityView.swift in Sources */, E1EA09672BED6815004CDE76 /* UserSignInSecurityView.swift in Sources */,
@ -6951,6 +6951,7 @@
4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */, 4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */,
62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */, 62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */,
E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */, E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */,
4EF36F662D9649050065BB79 /* SessionInfoDto.swift in Sources */,
E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */, E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */,
4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */, 4EC1C8692C808FBB00E2879E /* CustomDeviceProfileSettingsView.swift in Sources */,
E18E01E6288747230022598C /* CollectionItemView.swift in Sources */, E18E01E6288747230022598C /* CollectionItemView.swift in Sources */,
@ -7805,7 +7806,7 @@
repositoryURL = "https://github.com/jellyfin/jellyfin-sdk-swift.git"; repositoryURL = "https://github.com/jellyfin/jellyfin-sdk-swift.git";
requirement = { requirement = {
kind = upToNextMinorVersion; kind = upToNextMinorVersion;
minimumVersion = 0.3.0; minimumVersion = 0.5.1;
}; };
}; };
E15210522946DF1B00375CC2 /* XCRemoteSwiftPackageReference "Pulse" */ = { E15210522946DF1B00375CC2 /* XCRemoteSwiftPackageReference "Pulse" */ = {

View File

@ -1,5 +1,5 @@
{ {
"originHash" : "66bff9f26defe8d2dfa92b4e65d0ae348e3b586d0fbb7de49c9c937459e6b55c", "originHash" : "59e91adc6b66cec011d85f8b5356b72b51a6e772e85bad7fbeac490d39e91f45",
"pins" : [ "pins" : [
{ {
"identity" : "blurhashkit", "identity" : "blurhashkit",
@ -87,8 +87,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Get", "location" : "https://github.com/kean/Get",
"state" : { "state" : {
"revision" : "74dba201ebe42e9c15c1db6ee1cc893025bbef94", "revision" : "31249885da1052872e0ac91a2943f62567c0d96d",
"version" : "2.2.0" "version" : "2.2.1"
} }
}, },
{ {
@ -96,8 +96,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/jellyfin/jellyfin-sdk-swift.git", "location" : "https://github.com/jellyfin/jellyfin-sdk-swift.git",
"state" : { "state" : {
"revision" : "eae2ab5ed7caf770d79afbcdae08aab48df27a6e", "revision" : "10624671970ee1ad49ce817ee7b8ee3074f89a32",
"version" : "0.3.4" "version" : "0.5.1"
} }
}, },
{ {

View File

@ -17,12 +17,12 @@ struct ActiveSessionDetailView: View {
private var router: AdminDashboardCoordinator.Router private var router: AdminDashboardCoordinator.Router
@ObservedObject @ObservedObject
var box: BindingBox<SessionInfo?> var box: BindingBox<SessionInfoDto?>
// MARK: Create Idle Content View // MARK: Create Idle Content View
@ViewBuilder @ViewBuilder
private func idleContent(session: SessionInfo) -> some View { private func idleContent(session: SessionInfoDto) -> some View {
List { List {
if let userID = session.userID { if let userID = session.userID {
let user = UserDto(id: userID, name: session.userName) let user = UserDto(id: userID, name: session.userName)
@ -47,7 +47,7 @@ struct ActiveSessionDetailView: View {
@ViewBuilder @ViewBuilder
private func sessionContent( private func sessionContent(
session: SessionInfo, session: SessionInfoDto,
nowPlayingItem: BaseItemDto, nowPlayingItem: BaseItemDto,
playState: PlayerStateInfo playState: PlayerStateInfo
) -> some View { ) -> some View {

View File

@ -18,15 +18,15 @@ extension ActiveSessionsView {
private var currentDate: Date private var currentDate: Date
@ObservedObject @ObservedObject
private var box: BindingBox<SessionInfo?> private var box: BindingBox<SessionInfoDto?>
private let onSelect: () -> Void private let onSelect: () -> Void
private var session: SessionInfo { private var session: SessionInfoDto {
box.value ?? .init() box.value ?? .init()
} }
init(box: BindingBox<SessionInfo?>, onSelect action: @escaping () -> Void) { init(box: BindingBox<SessionInfoDto?>, onSelect action: @escaping () -> Void) {
self.box = box self.box = box
self.onSelect = action self.onSelect = action
} }

View File

@ -11,14 +11,10 @@ import SwiftUI
extension DeviceDetailsView { extension DeviceDetailsView {
struct CapabilitiesSection: View { struct CapabilitiesSection: View {
var device: DeviceInfo var device: DeviceInfoDto
var body: some View { var body: some View {
Section(L10n.capabilities) { 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 { if let supportsMediaControl = device.capabilities?.isSupportsMediaControl {
TextPairView(leading: L10n.supportsMediaControl, trailing: supportsMediaControl ? L10n.yes : L10n.no) TextPairView(leading: L10n.supportsMediaControl, trailing: supportsMediaControl ? L10n.yes : L10n.no)
} }
@ -26,10 +22,6 @@ extension DeviceDetailsView {
if let supportsPersistentIdentifier = device.capabilities?.isSupportsPersistentIdentifier { if let supportsPersistentIdentifier = device.capabilities?.isSupportsPersistentIdentifier {
TextPairView(leading: L10n.supportsPersistentIdentifier, trailing: supportsPersistentIdentifier ? L10n.yes : L10n.no) 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)
}
} }
} }
} }

View File

@ -17,7 +17,7 @@ extension DeviceDetailsView {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
Section(L10n.customDeviceName) { Section(L10n.name) {
TextField( TextField(
L10n.name, L10n.name,
text: $customName text: $customName

View File

@ -10,8 +10,6 @@ import Defaults
import JellyfinAPI import JellyfinAPI
import SwiftUI import SwiftUI
// TODO: Enable for CustomNames for Devices with SDK Changes
struct DeviceDetailsView: View { struct DeviceDetailsView: View {
// MARK: - Current Date // MARK: - Current Date
@ -44,11 +42,9 @@ struct DeviceDetailsView: View {
// MARK: - Initializer // MARK: - Initializer
init(device: DeviceInfo) { init(device: DeviceInfoDto) {
_viewModel = StateObject(wrappedValue: DeviceDetailViewModel(device: device)) _viewModel = StateObject(wrappedValue: DeviceDetailViewModel(device: device))
self.temporaryCustomName = device.customName ?? device.name ?? ""
// TODO: Enable with SDK Change
self.temporaryCustomName = device.name ?? "" // device.customName ?? device.name
} }
// MARK: - Body // MARK: - Body
@ -69,8 +65,7 @@ struct DeviceDetailsView: View {
} }
} }
// TODO: Enable with SDK Change CustomDeviceNameSection(customName: $temporaryCustomName)
// CustomDeviceNameSection(customName: $temporaryCustomName)
AdminDashboardView.DeviceSection( AdminDashboardView.DeviceSection(
client: viewModel.device.appName, client: viewModel.device.appName,
@ -94,22 +89,15 @@ struct DeviceDetailsView: View {
.topBarTrailing { .topBarTrailing {
if viewModel.backgroundStates.contains(.updating) { if viewModel.backgroundStates.contains(.updating) {
ProgressView() 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( .alert(
L10n.success.text, L10n.success.text,

View File

@ -32,14 +32,14 @@ extension DevicesView {
// MARK: - Properties // MARK: - Properties
let device: DeviceInfo let device: DeviceInfoDto
let onSelect: () -> Void let onSelect: () -> Void
let onDelete: (() -> Void)? let onDelete: (() -> Void)?
// MARK: - Initializer // MARK: - Initializer
init( init(
device: DeviceInfo, device: DeviceInfoDto,
onSelect: @escaping () -> Void, onSelect: @escaping () -> Void,
onDelete: (() -> Void)? = nil onDelete: (() -> Void)? = nil
) { ) {
@ -83,7 +83,7 @@ extension DevicesView {
private var rowContent: some View { private var rowContent: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(device.name ?? L10n.unknown) Text(device.customName ?? device.name ?? L10n.unknown)
.font(.headline) .font(.headline)
.lineLimit(2) .lineLimit(2)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)

View File

@ -11,8 +11,6 @@ import JellyfinAPI
import OrderedCollections import OrderedCollections
import SwiftUI import SwiftUI
// TODO: Replace with CustomName when Available
struct DevicesView: View { struct DevicesView: View {
@EnvironmentObject @EnvironmentObject

View File

@ -45,7 +45,7 @@ struct AddTaskTriggerView: View {
intervalTicks: nil, intervalTicks: nil,
maxRuntimeTicks: nil, maxRuntimeTicks: nil,
timeOfDayTicks: nil, timeOfDayTicks: nil,
type: TaskTriggerType.startup.rawValue type: TaskTriggerType.startup
) )
_taskTriggerInfo = State(initialValue: newTrigger) _taskTriggerInfo = State(initialValue: newTrigger)
@ -82,11 +82,11 @@ struct AddTaskTriggerView: View {
TriggerTypeRow(taskTriggerInfo: $taskTriggerInfo) TriggerTypeRow(taskTriggerInfo: $taskTriggerInfo)
if let taskType = taskTriggerInfo.type { if let taskType = taskTriggerInfo.type {
if taskType == TaskTriggerType.daily.rawValue { if taskType == TaskTriggerType.daily {
dailyView dailyView
} else if taskType == TaskTriggerType.weekly.rawValue { } else if taskType == TaskTriggerType.weekly {
weeklyView weeklyView
} else if taskType == TaskTriggerType.interval.rawValue { } else if taskType == TaskTriggerType.interval {
intervalView intervalView
} }
} }

View File

@ -19,30 +19,20 @@ extension AddTaskTriggerView {
var body: some View { var body: some View {
Picker( Picker(
L10n.type, L10n.type,
selection: Binding<TaskTriggerType?>( selection: $taskTriggerInfo.type
get: {
if let t = taskTriggerInfo.type {
return TaskTriggerType(rawValue: t)
} else {
return nil
}
},
set: { newValue in
if taskTriggerInfo.type != newValue?.rawValue {
resetValuesForNewType(newType: newValue)
}
}
)
) { ) {
ForEach(TaskTriggerType.allCases, id: \.self) { type in ForEach(TaskTriggerType.allCases, id: \.self) { type in
Text(type.displayTitle) Text(type.displayTitle)
.tag(type as TaskTriggerType?) .tag(type as TaskTriggerType?)
} }
} }
.onChange(of: taskTriggerInfo.type) { newType in
resetValuesForNewType(newType: newType)
}
} }
private func resetValuesForNewType(newType: TaskTriggerType?) { private func resetValuesForNewType(newType: TaskTriggerType?) {
taskTriggerInfo.type = newType?.rawValue taskTriggerInfo.type = newType
let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks let maxRuntimeTicks = taskTriggerInfo.maxRuntimeTicks
switch newType { switch newType {

View File

@ -16,23 +16,13 @@ extension EditServerTaskView {
let taskTriggerInfo: TaskTriggerInfo 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 // MARK: - Body
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(triggerDisplayText) Text(triggerDisplayText(for: taskTriggerInfo.type))
.fontWeight(.semibold) .fontWeight(.semibold)
Group { Group {
@ -52,7 +42,7 @@ extension EditServerTaskView {
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: taskTriggerType.systemImage) Image(systemName: (taskTriggerInfo.type ?? .startup).systemImage)
.backport .backport
.fontWeight(.bold) .fontWeight(.bold)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@ -61,12 +51,15 @@ extension EditServerTaskView {
// MARK: - Trigger Display Text // MARK: - Trigger Display Text
private var triggerDisplayText: String { private func triggerDisplayText(for triggerType: TaskTriggerType?) -> String {
switch taskTriggerType {
guard let triggerType else { return L10n.unknown }
switch triggerType {
case .daily: case .daily:
if let timeOfDayTicks = taskTriggerInfo.timeOfDayTicks { if let timeOfDayTicks = taskTriggerInfo.timeOfDayTicks {
return L10n.itemAtItem( return L10n.itemAtItem(
taskTriggerType.displayTitle, triggerType.displayTitle,
ServerTicks(timeOfDayTicks) ServerTicks(timeOfDayTicks)
.date.formatted(date: .omitted, time: .shortened) .date.formatted(date: .omitted, time: .shortened)
) )
@ -89,7 +82,7 @@ extension EditServerTaskView {
) )
} }
case .startup: case .startup:
return taskTriggerType.displayTitle return triggerType.displayTitle
} }
return L10n.unknown return L10n.unknown

View File

@ -45,8 +45,7 @@ struct AddServerUserAccessTagsView: View {
// MARK: - Tag is Already Blocked/Allowed // MARK: - Tag is Already Blocked/Allowed
private var tagIsDuplicate: Bool { private var tagIsDuplicate: Bool {
viewModel.user.policy!.blockedTags!.contains(tempTag) // && viewModel.user.policy!.blockedTags!.contains(tempTag) || viewModel.user.policy!.allowedTags!.contains(tempTag)
//! viewModel.user.policy!.allowedTags!.contains(tempTag)
} }
// MARK: - Tag Already Exists on Jellyfin // MARK: - Tag Already Exists on Jellyfin
@ -84,9 +83,8 @@ struct AddServerUserAccessTagsView: View {
} else { } else {
Button(L10n.save) { Button(L10n.save) {
if access { if access {
// TODO: Enable on 10.10 tempPolicy.allowedTags = tempPolicy.allowedTags
/* tempPolicy.allowedTags = tempPolicy.allowedTags .appendedOrInit(tempTag)
.appendedOrInit(tempTag) */
} else { } else {
tempPolicy.blockedTags = tempPolicy.blockedTags tempPolicy.blockedTags = tempPolicy.blockedTags
.appendedOrInit(tempTag) .appendedOrInit(tempTag)

View File

@ -29,27 +29,25 @@ extension AddServerUserAccessTagsView {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
// TODO: Enable on 10.10 Section {
// Section { Picker(L10n.access, selection: $access) {
// Picker(L10n.access, selection: $access) { Text(L10n.allowed).tag(true)
// Text(L10n.allowed).tag(true) Text(L10n.blocked).tag(false)
// Text(L10n.blocked).tag(false) }
// } } header: {
// .disabled(true) Text(L10n.access)
// } header: { } footer: {
// Text(L10n.access) LearnMoreButton(L10n.accessTags) {
// } footer: { TextPair(
// LearnMoreButton(L10n.accessTags) { title: L10n.allowed,
// TextPair( subtitle: L10n.accessTagAllowDescription
// title: L10n.allowed, )
// subtitle: L10n.accessTagAllowDescription TextPair(
// ) title: L10n.blocked,
// TextPair( subtitle: L10n.accessTagBlockDescription
// title: L10n.blocked, )
// subtitle: L10n.accessTagBlockDescription }
// ) }
// }
// }
Section { Section {
TextField(L10n.name, text: $tag) TextField(L10n.name, text: $tag)

View File

@ -42,18 +42,18 @@ struct EditServerUserAccessTagsView: View {
@State @State
private var error: Error? private var error: Error?
private var allowedTags: [TagWithAccess] {
viewModel.user.policy?.allowedTags?
.sorted()
.map { TagWithAccess(tag: $0, access: true) } ?? []
}
private var blockedTags: [TagWithAccess] { private var blockedTags: [TagWithAccess] {
viewModel.user.policy?.blockedTags? viewModel.user.policy?.blockedTags?
.sorted() .sorted()
.map { TagWithAccess(tag: $0, access: false) } ?? [] .map { TagWithAccess(tag: $0, access: false) } ?? []
} }
// private var allowedTags: [TagWithAccess] {
// viewModel.user.policy?.allowedTags?
// .sorted()
// .map { TagWithAccess(tag: $0, access: true) } ?? []
// }
// MARK: - Initializera // MARK: - Initializera
init(viewModel: ServerUserAdminViewModel) { init(viewModel: ServerUserAdminViewModel) {
@ -173,22 +173,29 @@ struct EditServerUserAccessTagsView: View {
UIApplication.shared.open(.jellyfinDocsManagingUsers) UIApplication.shared.open(.jellyfinDocsManagingUsers)
} }
if blockedTags.isEmpty { if blockedTags.isEmpty, allowedTags.isEmpty {
Button(L10n.add) { Button(L10n.add) {
router.route(to: \.userAddAccessTag, viewModel) router.route(to: \.userAddAccessTag, viewModel)
} }
} else { } else {
if allowedTags.isNotEmpty {
// TODO: with allowed, use `DisclosureGroup` instead DisclosureGroup(L10n.allowed) {
Section(L10n.blocked) { ForEach(
ForEach( allowedTags,
blockedTags, id: \.self,
id: \.self, content: makeRow
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.cancel, role: .cancel) {}
Button(L10n.delete, role: .destructive) { 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 { for tag in selectedTags {
if tag.access { if tag.access {
// tempPolicy.allowedTags?.removeAll { $0 == tag.tag } tempPolicy.allowedTags?.removeAll(equalTo: tag.tag)
} else { } else {
tempPolicy.blockedTags?.removeAll { $0 == tag.tag } tempPolicy.blockedTags?.removeAll(equalTo: tag.tag)
} }
} }

View File

@ -34,7 +34,12 @@ struct ServerUserMediaAccessView: View {
init(viewModel: ServerUserAdminViewModel) { init(viewModel: ServerUserAdminViewModel) {
self.viewModel = viewModel 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 // MARK: - Body
@ -123,7 +128,7 @@ struct ServerUserMediaAccessView: View {
if tempPolicy.enableContentDeletion == false { if tempPolicy.enableContentDeletion == false {
Section { Section {
ForEach( ForEach(
viewModel.libraries.filter { $0.collectionType != "boxsets" }, viewModel.libraries.filter { $0.collectionType != .boxsets },
id: \.id id: \.id
) { library in ) { library in
Toggle( Toggle(

View File

@ -41,7 +41,12 @@ struct ServerUserDeviceAccessView: View {
init(viewModel: ServerUserAdminViewModel) { init(viewModel: ServerUserAdminViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel) 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 // MARK: - Body

View File

@ -39,7 +39,12 @@ struct ServerUserLiveTVAccessView: View {
init(viewModel: ServerUserAdminViewModel) { init(viewModel: ServerUserAdminViewModel) {
self.viewModel = viewModel 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 // MARK: - Body

View File

@ -36,7 +36,12 @@ struct ServerUserParentalRatingView: View {
init(viewModel: ServerUserAdminViewModel) { init(viewModel: ServerUserAdminViewModel) {
self._viewModel = StateObject(wrappedValue: viewModel) 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 // MARK: - Body

View File

@ -24,22 +24,20 @@ extension ServerUserPermissionsView {
isOn: $policy.isAdministrator.coalesce(false) isOn: $policy.isAdministrator.coalesce(false)
) )
// TODO: Enable for 10.9 Toggle(
/* Toggle(L10n.collections, isOn: Binding( L10n.collections,
get: { policy.enableCollectionManagement ?? false }, isOn: $policy.enableCollectionManagement
set: { policy.enableCollectionManagement = $0 } )
))
Toggle(L10n.subtitles, isOn: Binding( Toggle(
get: { policy.enableSubtitleManagement ?? false }, L10n.subtitles,
set: { policy.enableSubtitleManagement = $0 } isOn: $policy.enableSubtitleManagement
)) */ )
// TODO: Enable for 10.10 Toggle(
/* Toggle(L10n.lyrics, isOn: Binding( L10n.lyrics,
get: { policy.enableLyricManagement ?? false }, isOn: $policy.enableLyricManagement
set: { policy.enableLyricManagement = $0 } )
)) */
} }
} }
} }

View File

@ -35,7 +35,12 @@ struct ServerUserPermissionsView: View {
init(viewModel: ServerUserAdminViewModel) { init(viewModel: ServerUserAdminViewModel) {
self._viewModel = ObservedObject(wrappedValue: viewModel) 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 // MARK: - Body

View File

@ -87,7 +87,7 @@ struct AddItemElementView<Element: Hashable>: View {
name: name, name: name,
id: id, id: id,
personRole: personRole.isEmpty ? (personKind == .unknown ? nil : personKind.rawValue) : personRole, personRole: personRole.isEmpty ? (personKind == .unknown ? nil : personKind.rawValue) : personRole,
personKind: personKind.rawValue personKind: personKind
)])) )]))
} }
.buttonStyle(.toolbarPill) .buttonStyle(.toolbarPill)

View File

@ -24,8 +24,6 @@ extension AddItemElementView {
let type: ItemArrayElements let type: ItemArrayElements
let population: [Element] let population: [Element]
// TODO: Why doesn't environment(\.isSearching) work?
let isSearching: Bool let isSearching: Bool
// MARK: - Body // MARK: - Body

View File

@ -66,7 +66,7 @@ extension EditItemElementView {
let person = (item as! BaseItemPerson) let person = (item as! BaseItemPerson)
TextPairView( TextPairView(
leading: person.type ?? .emptyDash, leading: person.type?.displayTitle ?? .emptyDash,
trailing: person.role ?? .emptyDash trailing: person.role ?? .emptyDash
) )
.foregroundStyle( .foregroundStyle(

View File

@ -17,7 +17,6 @@ extension EditMetadataView {
@Binding @Binding
var item: BaseItemDto var item: BaseItemDto
// TODO: Animation when lockAllFields is selected
var body: some View { var body: some View {
Section(L10n.lockedFields) { Section(L10n.lockedFields) {
Toggle( Toggle(

View File

@ -118,7 +118,7 @@ struct EditMetadataView: View {
ParentalRatingSection(item: $tempItem) ParentalRatingSection(item: $tempItem)
if [BaseItemKind.movie, .episode].contains(itemType) { if [.movie, .episode].contains(itemType) {
MediaFormatSection(item: $tempItem) MediaFormatSection(item: $tempItem)
} }

View File

@ -22,7 +22,9 @@ extension ItemView {
PosterHStack( PosterHStack(
title: L10n.castAndCrew, title: L10n.castAndCrew,
type: .portrait, type: .portrait,
items: people.filter(\.isDisplayed) items: people.filter { person in
person.type?.isSupported ?? false
}
) )
.trailing { .trailing {
SeeAllButton() SeeAllButton()

View File

@ -85,12 +85,18 @@
/// Aired /// Aired
"aired" = "Aired"; "aired" = "Aired";
/// Aired episode order
"airedEpisodeOrder" = "Aired episode order";
/// Air Time /// Air Time
"airTime" = "Air Time"; "airTime" = "Air Time";
/// Airs %s /// Airs %s
"airWithDate" = "Airs %s"; "airWithDate" = "Airs %s";
/// Album
"album" = "Album";
/// Album Artist /// Album Artist
"albumArtist" = "Album Artist"; "albumArtist" = "Album Artist";
@ -232,6 +238,9 @@
/// Behavior /// Behavior
"behavior" = "Behavior"; "behavior" = "Behavior";
/// Behind the Scenes
"behindTheScenes" = "Behind the Scenes";
/// Tests your server connection to assess internet speed and adjust bandwidth automatically. /// 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."; "birateAutoDescription" = "Tests your server connection to assess internet speed and adjust bandwidth automatically.";
@ -376,6 +385,9 @@
/// Client /// Client
"client" = "Client"; "client" = "Client";
/// Clip
"clip" = "Clip";
/// Close /// Close
"close" = "Close"; "close" = "Close";
@ -505,9 +517,6 @@
/// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback. /// 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."; "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. /// Your custom device name '%1$@' has been saved.
"customDeviceNameSaved" = "Your custom device name '%1$@' has been saved."; "customDeviceNameSaved" = "Your custom device name '%1$@' has been saved.";
@ -553,12 +562,18 @@
/// Date created /// Date created
"dateCreated" = "Date created"; "dateCreated" = "Date created";
/// Date added
"dateLastContentAdded" = "Date added";
/// Date modified /// Date modified
"dateModified" = "Date modified"; "dateModified" = "Date modified";
/// Date of death /// Date of death
"dateOfDeath" = "Date of death"; "dateOfDeath" = "Date of death";
/// Date played
"datePlayed" = "Date played";
/// Dates /// Dates
"dates" = "Dates"; "dates" = "Dates";
@ -592,6 +607,9 @@
/// Are you sure you wish to delete this device? This session will be logged out. /// 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."; "deleteDeviceWarning" = "Are you sure you wish to delete this device? This session will be logged out.";
/// Deleted Scene
"deletedScene" = "Deleted Scene";
/// Delete image /// Delete image
"deleteImage" = "Delete image"; "deleteImage" = "Delete image";
@ -847,12 +865,18 @@
/// Failed logins /// Failed logins
"failedLogins" = "Failed logins"; "failedLogins" = "Failed logins";
/// Favorite
"favorite" = "Favorite";
/// Favorited /// Favorited
"favorited" = "Favorited"; "favorited" = "Favorited";
/// Favorites /// Favorites
"favorites" = "Favorites"; "favorites" = "Favorites";
/// Featurette
"featurette" = "Featurette";
/// Filters /// Filters
"filters" = "Filters"; "filters" = "Filters";
@ -862,6 +886,9 @@
/// Find missing metadata and images. /// Find missing metadata and images.
"findMissingDescription" = "Find missing metadata and images."; "findMissingDescription" = "Find missing metadata and images.";
/// Folder
"folder" = "Folder";
/// Force remote media transcoding /// Force remote media transcoding
"forceRemoteTranscoding" = "Force remote media transcoding"; "forceRemoteTranscoding" = "Force remote media transcoding";
@ -949,6 +976,9 @@
/// Index /// Index
"index" = "Index"; "index" = "Index";
/// Index number
"indexNumber" = "Index number";
/// Indicators /// Indicators
"indicators" = "Indicators"; "indicators" = "Indicators";
@ -961,6 +991,9 @@
/// Interval /// Interval
"interval" = "Interval"; "interval" = "Interval";
/// Interview
"interview" = "Interview";
/// Inverted Dark /// Inverted Dark
"invertedDark" = "Inverted Dark"; "invertedDark" = "Inverted Dark";
@ -1330,6 +1363,9 @@
/// Parental rating /// Parental rating
"parentalRating" = "Parental rating"; "parentalRating" = "Parental rating";
/// Parent index
"parentIndexNumber" = "Parent index";
/// Password /// Password
"password" = "Password"; "password" = "Password";
@ -1381,6 +1417,9 @@
/// Playback Speed /// Playback Speed
"playbackSpeed" = "Playback Speed"; "playbackSpeed" = "Playback Speed";
/// Play count
"playCount" = "Play count";
/// Played /// Played
"played" = "Played"; "played" = "Played";
@ -1642,12 +1681,18 @@
/// Runtime /// Runtime
"runtime" = "Runtime"; "runtime" = "Runtime";
/// Sample
"sample" = "Sample";
/// Save /// Save
"save" = "Save"; "save" = "Save";
/// Save the user to this device without any local authentication. /// Save the user to this device without any local authentication.
"saveUserWithoutAuthDescription" = "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 /// Schedule already exists
"scheduleAlreadyExists" = "Schedule already exists"; "scheduleAlreadyExists" = "Schedule already exists";
@ -1663,6 +1708,9 @@
/// Search /// Search
"search" = "Search"; "search" = "Search";
/// Search score
"searchScore" = "Search score";
/// Season /// Season
"season" = "Season"; "season" = "Season";
@ -1696,6 +1744,12 @@
/// Series Backdrop /// Series Backdrop
"seriesBackdrop" = "Series Backdrop"; "seriesBackdrop" = "Series Backdrop";
/// Series date played
"seriesDatePlayed" = "Series date played";
/// Series name
"seriesName" = "Series name";
/// Server /// Server
"server" = "Server"; "server" = "Server";
@ -1735,6 +1789,9 @@
/// Settings /// Settings
"settings" = "Settings"; "settings" = "Settings";
/// Short
"short" = "Short";
/// Show Favorited /// Show Favorited
"showFavorited" = "Show Favorited"; "showFavorited" = "Show Favorited";
@ -1786,6 +1843,9 @@
/// Signs out the last user when Swiftfin has been force closed /// Signs out the last user when Swiftfin has been force closed
"signoutCloseFooter" = "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" = "Slider"; "slider" = "Slider";
@ -1825,6 +1885,9 @@
/// Sports /// Sports
"sports" = "Sports"; "sports" = "Sports";
/// Start date
"startDate" = "Start date";
/// Start Time /// Start Time
"startTime" = "Start Time"; "startTime" = "Start Time";
@ -1840,6 +1903,9 @@
/// Streams /// Streams
"streams" = "Streams"; "streams" = "Streams";
/// Studio
"studio" = "Studio";
/// Studios /// Studios
"studios" = "Studios"; "studios" = "Studios";
@ -1873,18 +1939,12 @@
/// Success /// Success
"success" = "Success"; "success" = "Success";
/// Content Uploading
"supportsContentUploading" = "Content Uploading";
/// Media Control /// Media Control
"supportsMediaControl" = "Media Control"; "supportsMediaControl" = "Media Control";
/// Persistent Identifier /// Persistent Identifier
"supportsPersistentIdentifier" = "Persistent Identifier"; "supportsPersistentIdentifier" = "Persistent Identifier";
/// Sync
"supportsSync" = "Sync";
/// Switch User /// Switch User
"switchUser" = "Switch User"; "switchUser" = "Switch User";
@ -1942,6 +2002,12 @@
/// Test Size /// Test Size
"testSize" = "Test Size"; "testSize" = "Test Size";
/// Theme Song
"themeSong" = "Theme Song";
/// Theme Video
"themeVideo" = "Theme Video";
/// Thumb /// Thumb
"thumb" = "Thumb"; "thumb" = "Thumb";
@ -2104,12 +2170,18 @@
/// The video bit depth is not supported /// The video bit depth is not supported
"videoBitDepthNotSupported" = "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 /// The video bitrate is not supported
"videoBitrateNotSupported" = "The video bitrate is not supported"; "videoBitrateNotSupported" = "The video bitrate is not supported";
/// The video codec is not supported /// The video codec is not supported
"videoCodecNotSupported" = "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 /// The video framerate is not supported
"videoFramerateNotSupported" = "The video framerate is not supported"; "videoFramerateNotSupported" = "The video framerate is not supported";