diff --git a/README.md b/README.md index 1cf88b9b..b5e6eaf3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Swiftfin

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