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";