[iOS & tvOS] Upgrade SDK to 10.10 (#1463)

* Buildable!

* Update file names.

* Default sort to sort name NOT name.

* SessionInfoDto vs SessionInfo

* Targetting

* Fix many invalid `ItemSortBy` existing. Will need to revisit later to see which can still be used!

* ExtraTypes Patch.

* Move from Binding to OnChange. Tested and Working.

* Update README.md

Update README to use 10.10.6. Bumped up from 10.8.13

* Update to Main on https://github.com/jellyfin/jellyfin-sdk-swift.git

* Now using https://github.com/jellyfin/jellyfin-sdk-swift.git again!

* Paths.getUserViews() userId moved to parameters

* Fix ViewModels where -Dto suffixes were removed by https://github.com/jellyfin/Swiftfin/pull/1465 auto-merge.

* SupportedCaseIterable

* tvOS supportedCases fixes for build issue.

* cleanup

* update API to 0.5.1 and correct VideoRangeTypes.

* Remove deviceProfile.responseProfiles = videoPlayer.responseProfiles

* Second to last adjustment:
Resolved: // TODO: 10.10 - Filter to only valid SortBy's for each BaseItemKind.
Last outstanding item: // TODO: 10.10 - What should authenticationProviderID & passwordResetProviderID be?

* Trailers itemID must precede userID

* Force User Policy to exist.

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe Kribs 2025-04-06 21:42:47 -06:00 committed by GitHub
parent 0845545417
commit 0025422634
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
78 changed files with 551 additions and 446 deletions

View File

@ -4,7 +4,7 @@
<h1>Swiftfin</h1>
<img src="https://img.shields.io/badge/iOS-15+-red"/>
<img src="https://img.shields.io/badge/tvOS-17+-red"/>
<img src="https://img.shields.io/badge/Jellyfin-10.8.13-9962be"/>
<img src="https://img.shields.io/badge/Jellyfin-10.10.6-9962be"/>
<a href="https://translate.jellyfin.org/engage/swiftfin/">
<img src="https://translate.jellyfin.org/widgets/swiftfin/-/svg-badge.svg"/>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,23 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
extension CollectionType: SupportedCaseIterable {
static var supportedCases: [CollectionType] {
[
.boxsets,
.folders,
.movies,
.tvshows,
.livetv,
]
}
}

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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:

View File

@ -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:

View File

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

View File

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

View File

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

View File

@ -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
)

View File

@ -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 {

View File

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

View File

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

View File

@ -1,27 +0,0 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Foundation
enum StreamType: String, Displayable {
case direct
case transcode
case hls
var displayTitle: String {
switch self {
case .direct:
return L10n.direct
case .transcode:
return L10n.transcode
case .hls:
return "HLS"
}
}
}

View File

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

View File

@ -31,11 +31,9 @@ struct UserPermissions {
self.canDelete = policy?.enableContentDeletion ?? false || policy?.enableContentDeletionFromFolders != []
self.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
}
}
}

View File

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

View File

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

View File

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

View File

@ -70,12 +70,16 @@ internal enum L10n {
}
/// Aired
internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired")
/// Aired episode order
internal static let airedEpisodeOrder = L10n.tr("Localizable", "airedEpisodeOrder", fallback: "Aired episode order")
/// Air Time
internal static let airTime = L10n.tr("Localizable", "airTime", fallback: "Air Time")
/// Airs %s
internal static func airWithDate(_ p1: UnsafePointer<CChar>) -> String {
return L10n.tr("Localizable", "airWithDate", p1, fallback: "Airs %s")
}
/// Album
internal static let album = L10n.tr("Localizable", "album", fallback: "Album")
/// Album Artist
internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist")
/// All
@ -170,6 +174,8 @@ internal enum L10n {
internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons")
/// Behavior
internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior")
/// Behind the Scenes
internal static let behindTheScenes = L10n.tr("Localizable", "behindTheScenes", fallback: "Behind the Scenes")
/// Tests your server connection to assess internet speed and adjust bandwidth automatically.
internal static let birateAutoDescription = L10n.tr("Localizable", "birateAutoDescription", fallback: "Tests your server connection to assess internet speed and adjust bandwidth automatically.")
/// Birthday
@ -268,6 +274,8 @@ internal enum L10n {
internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background")
/// Client
internal static let client = L10n.tr("Localizable", "client", fallback: "Client")
/// Clip
internal static let clip = L10n.tr("Localizable", "clip", fallback: "Clip")
/// Close
internal static let close = L10n.tr("Localizable", "close", fallback: "Close")
/// Collections
@ -356,8 +364,6 @@ internal enum L10n {
internal static let customConnectionsDescription = L10n.tr("Localizable", "customConnectionsDescription", fallback: "Manually set the maximum number of connections a user can have to the server.")
/// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.
internal static let customDescription = L10n.tr("Localizable", "customDescription", fallback: "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.")
/// Custom Device Name
internal static let customDeviceName = L10n.tr("Localizable", "customDeviceName", fallback: "Custom Device Name")
/// Your custom device name '%1$@' has been saved.
internal static func customDeviceNameSaved(_ p1: Any) -> String {
return L10n.tr("Localizable", "customDeviceNameSaved", String(describing: p1), fallback: "Your custom device name '%1$@' has been saved.")
@ -390,10 +396,14 @@ internal enum L10n {
internal static let dateAdded = L10n.tr("Localizable", "dateAdded", fallback: "Date Added")
/// Date created
internal static let dateCreated = L10n.tr("Localizable", "dateCreated", fallback: "Date created")
/// Date added
internal static let dateLastContentAdded = L10n.tr("Localizable", "dateLastContentAdded", fallback: "Date added")
/// Date modified
internal static let dateModified = L10n.tr("Localizable", "dateModified", fallback: "Date modified")
/// Date of death
internal static let dateOfDeath = L10n.tr("Localizable", "dateOfDeath", fallback: "Date of death")
/// Date played
internal static let datePlayed = L10n.tr("Localizable", "datePlayed", fallback: "Date played")
/// Dates
internal static let dates = L10n.tr("Localizable", "dates", fallback: "Dates")
/// Day of Week
@ -418,6 +428,8 @@ internal enum L10n {
}
/// Are you sure you wish to delete this device? This session will be logged out.
internal static let deleteDeviceWarning = L10n.tr("Localizable", "deleteDeviceWarning", fallback: "Are you sure you wish to delete this device? This session will be logged out.")
/// Deleted Scene
internal static let deletedScene = L10n.tr("Localizable", "deletedScene", fallback: "Deleted Scene")
/// Delete image
internal static let deleteImage = L10n.tr("Localizable", "deleteImage", fallback: "Delete image")
/// Are you sure you want to delete this item?
@ -602,16 +614,22 @@ internal enum L10n {
internal static let external = L10n.tr("Localizable", "external", fallback: "External")
/// Failed logins
internal static let failedLogins = L10n.tr("Localizable", "failedLogins", fallback: "Failed logins")
/// Favorite
internal static let favorite = L10n.tr("Localizable", "favorite", fallback: "Favorite")
/// Favorited
internal static let favorited = L10n.tr("Localizable", "favorited", fallback: "Favorited")
/// Favorites
internal static let favorites = L10n.tr("Localizable", "favorites", fallback: "Favorites")
/// Featurette
internal static let featurette = L10n.tr("Localizable", "featurette", fallback: "Featurette")
/// Filters
internal static let filters = L10n.tr("Localizable", "filters", fallback: "Filters")
/// Find Missing
internal static let findMissing = L10n.tr("Localizable", "findMissing", fallback: "Find Missing")
/// Find missing metadata and images.
internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.")
/// Folder
internal static let folder = L10n.tr("Localizable", "folder", fallback: "Folder")
/// Force remote media transcoding
internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding")
/// Format
@ -670,6 +688,8 @@ internal enum L10n {
internal static let imageSource = L10n.tr("Localizable", "imageSource", fallback: "Image source")
/// Index
internal static let index = L10n.tr("Localizable", "index", fallback: "Index")
/// Index number
internal static let indexNumber = L10n.tr("Localizable", "indexNumber", fallback: "Index number")
/// Indicators
internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators")
/// Inker
@ -678,6 +698,8 @@ internal enum L10n {
internal static let interlacedVideoNotSupported = L10n.tr("Localizable", "interlacedVideoNotSupported", fallback: "Interlaced video is not supported")
/// Interval
internal static let interval = L10n.tr("Localizable", "interval", fallback: "Interval")
/// Interview
internal static let interview = L10n.tr("Localizable", "interview", fallback: "Interview")
/// Inverted Dark
internal static let invertedDark = L10n.tr("Localizable", "invertedDark", fallback: "Inverted Dark")
/// Inverted Light
@ -932,6 +954,8 @@ internal enum L10n {
internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls")
/// Parental rating
internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental rating")
/// Parent index
internal static let parentIndexNumber = L10n.tr("Localizable", "parentIndexNumber", fallback: "Parent index")
/// Password
internal static let password = L10n.tr("Localizable", "password", fallback: "Password")
/// User password has been changed.
@ -966,6 +990,8 @@ internal enum L10n {
internal static let playbackQuality = L10n.tr("Localizable", "playbackQuality", fallback: "Playback Quality")
/// Playback Speed
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed", fallback: "Playback Speed")
/// Play count
internal static let playCount = L10n.tr("Localizable", "playCount", fallback: "Play count")
/// Played
internal static let played = L10n.tr("Localizable", "played", fallback: "Played")
/// Play From Beginning
@ -1144,10 +1170,14 @@ internal enum L10n {
internal static let running = L10n.tr("Localizable", "running", fallback: "Running...")
/// Runtime
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
/// Sample
internal static let sample = L10n.tr("Localizable", "sample", fallback: "Sample")
/// Save
internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
/// Save the user to this device without any local authentication.
internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.")
/// Scene
internal static let scene = L10n.tr("Localizable", "scene", fallback: "Scene")
/// Schedule already exists
internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists")
/// Score
@ -1158,6 +1188,8 @@ internal enum L10n {
internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time")
/// Search
internal static let search = L10n.tr("Localizable", "search", fallback: "Search")
/// Search score
internal static let searchScore = L10n.tr("Localizable", "searchScore", fallback: "Search score")
/// Season
internal static let season = L10n.tr("Localizable", "season", fallback: "Season")
/// S%1$@:E%2$@
@ -1182,6 +1214,10 @@ internal enum L10n {
internal static let series = L10n.tr("Localizable", "series", fallback: "Series")
/// Series Backdrop
internal static let seriesBackdrop = L10n.tr("Localizable", "seriesBackdrop", fallback: "Series Backdrop")
/// Series date played
internal static let seriesDatePlayed = L10n.tr("Localizable", "seriesDatePlayed", fallback: "Series date played")
/// Series name
internal static let seriesName = L10n.tr("Localizable", "seriesName", fallback: "Series name")
/// Server
internal static let server = L10n.tr("Localizable", "server", fallback: "Server")
/// %@ is already connected.
@ -1212,6 +1248,8 @@ internal enum L10n {
internal static let setPinHintDescription = L10n.tr("Localizable", "setPinHintDescription", fallback: "Set a hint when prompting for the pin.")
/// Settings
internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings")
/// Short
internal static let short = L10n.tr("Localizable", "short", fallback: "Short")
/// Show Favorited
internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited")
/// Show Favorites
@ -1248,6 +1286,8 @@ internal enum L10n {
internal static let signoutClose = L10n.tr("Localizable", "signoutClose", fallback: "Sign out on close")
/// Signs out the last user when Swiftfin has been force closed
internal static let signoutCloseFooter = L10n.tr("Localizable", "signoutCloseFooter", fallback: "Signs out the last user when Swiftfin has been force closed")
/// Similarity score
internal static let similarityScore = L10n.tr("Localizable", "similarityScore", fallback: "Similarity score")
/// Slider
internal static let slider = L10n.tr("Localizable", "slider", fallback: "Slider")
/// Slider Color
@ -1274,6 +1314,8 @@ internal enum L10n {
internal static let splashscreenFooter = L10n.tr("Localizable", "splashscreenFooter", fallback: "When All Servers is selected, use the splashscreen from a single server or a random server")
/// Sports
internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports")
/// Start date
internal static let startDate = L10n.tr("Localizable", "startDate", fallback: "Start date")
/// Start Time
internal static let startTime = L10n.tr("Localizable", "startTime", fallback: "Start Time")
/// Status
@ -1284,6 +1326,8 @@ internal enum L10n {
internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc")
/// Streams
internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams")
/// Studio
internal static let studio = L10n.tr("Localizable", "studio", fallback: "Studio")
/// Studios
internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios")
/// Studio(s) involved in the creation of media.
@ -1306,14 +1350,10 @@ internal enum L10n {
internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: "Subtitle Size")
/// Success
internal static let success = L10n.tr("Localizable", "success", fallback: "Success")
/// Content Uploading
internal static let supportsContentUploading = L10n.tr("Localizable", "supportsContentUploading", fallback: "Content Uploading")
/// Media Control
internal static let supportsMediaControl = L10n.tr("Localizable", "supportsMediaControl", fallback: "Media Control")
/// Persistent Identifier
internal static let supportsPersistentIdentifier = L10n.tr("Localizable", "supportsPersistentIdentifier", fallback: "Persistent Identifier")
/// Sync
internal static let supportsSync = L10n.tr("Localizable", "supportsSync", fallback: "Sync")
/// Switch User
internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User")
/// SyncPlay
@ -1352,6 +1392,10 @@ internal enum L10n {
internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps")
/// Test Size
internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size")
/// Theme Song
internal static let themeSong = L10n.tr("Localizable", "themeSong", fallback: "Theme Song")
/// Theme Video
internal static let themeVideo = L10n.tr("Localizable", "themeVideo", fallback: "Theme Video")
/// Thumb
internal static let thumb = L10n.tr("Localizable", "thumb", fallback: "Thumb")
/// Time
@ -1464,10 +1508,14 @@ internal enum L10n {
internal static let video = L10n.tr("Localizable", "video", fallback: "Video")
/// The video bit depth is not supported
internal static let videoBitDepthNotSupported = L10n.tr("Localizable", "videoBitDepthNotSupported", fallback: "The video bit depth is not supported")
/// Video bitrate
internal static let videoBitRate = L10n.tr("Localizable", "videoBitRate", fallback: "Video bitrate")
/// The video bitrate is not supported
internal static let videoBitrateNotSupported = L10n.tr("Localizable", "videoBitrateNotSupported", fallback: "The video bitrate is not supported")
/// The video codec is not supported
internal static let videoCodecNotSupported = L10n.tr("Localizable", "videoCodecNotSupported", fallback: "The video codec is not supported")
/// Video codec tag is not supported
internal static let videoCodecTagNotSupported = L10n.tr("Localizable", "videoCodecTagNotSupported", fallback: "Video codec tag is not supported")
/// The video framerate is not supported
internal static let videoFramerateNotSupported = L10n.tr("Localizable", "videoFramerateNotSupported", fallback: "The video framerate is not supported")
/// The video level is not supported

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -250,7 +250,10 @@ class ItemEditorViewModel<Element: Equatable>: ViewModel, Stateful, Eventful {
_ = self.backgroundStates.insert(.refreshing)
}
let request = Paths.getItem(userID: userSession.user.id, itemID: itemId)
let request = Paths.getItem(
itemID: itemId,
userID: userSession.user.id
)
let response = try await userSession.client.send(request)
await MainActor.run {

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -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()
}

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(

View File

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

View File

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

View File

@ -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()

View File

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