diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index 8a028185..ff902321 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -80,8 +80,8 @@ final class ItemCoordinator: NavigationCoordinatable { } #if os(iOS) - func makeItemEditor(item: BaseItemDto) -> NavigationViewCoordinator { - NavigationViewCoordinator(ItemEditorCoordinator(item: item)) + func makeItemEditor(viewModel: ItemViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemEditorCoordinator(viewModel: viewModel)) } func makeDownloadTask(downloadTask: DownloadTask) -> NavigationViewCoordinator { diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index c7b785b0..f682cc8a 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -17,14 +17,23 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { @Root var start = makeStart - private let item: BaseItemDto + private let viewModel: ItemViewModel - init(item: BaseItemDto) { - self.item = item + @Route(.modal) + var editMetadata = makeEditMetadata + + init(viewModel: ItemViewModel) { + self.viewModel = viewModel + } + + func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator { + NavigationViewCoordinator { + EditMetadataView(viewModel: ItemEditorViewModel(item: item)) + } } @ViewBuilder func makeStart() -> some View { - ItemEditorView(item: item) + ItemEditorView(viewModel: viewModel) } } diff --git a/Shared/Extensions/Binding.swift b/Shared/Extensions/Binding.swift index cb41dc82..f4f652f3 100644 --- a/Shared/Extensions/Binding.swift +++ b/Shared/Extensions/Binding.swift @@ -42,3 +42,21 @@ extension Binding { map(getter: { !$0 }, setter: { $0 }) } } + +extension Binding where Value: RangeReplaceableCollection, Value.Element: Equatable { + + func contains(_ element: Value.Element) -> Binding { + Binding( + get: { wrappedValue.contains(element) }, + set: { newValue in + if newValue { + if !wrappedValue.contains(element) { + wrappedValue.append(element) + } + } else { + wrappedValue.removeAll { $0 == element } + } + } + ) + } +} diff --git a/Shared/Extensions/FormatStyle.swift b/Shared/Extensions/FormatStyle.swift index 01788fbf..95652cdc 100644 --- a/Shared/Extensions/FormatStyle.swift +++ b/Shared/Extensions/FormatStyle.swift @@ -8,6 +8,8 @@ import SwiftUI +// TODO: break into separate files + struct HourMinuteFormatStyle: FormatStyle { func format(_ value: TimeInterval) -> String { @@ -78,6 +80,29 @@ extension ParseableFormatStyle where Self == DayIntervalParseableFormatStyle { } } +struct NilIfEmptyStringFormatStyle: ParseableFormatStyle { + + var parseStrategy: NilIfEmptyStringParseStrategy = .init() + + func format(_ value: String?) -> String { + value ?? "" + } +} + +struct NilIfEmptyStringParseStrategy: ParseStrategy { + + func parse(_ value: String) -> String? { + value.isEmpty ? nil : value + } +} + +extension ParseableFormatStyle where Self == NilIfEmptyStringFormatStyle { + + static var nilIfEmptyString: NilIfEmptyStringFormatStyle { + .init() + } +} + extension FormatStyle where Self == TimeIntervalFormatStyle { static func interval( diff --git a/Shared/Extensions/JellyfinAPI/MetadataField.swift b/Shared/Extensions/JellyfinAPI/MetadataField.swift new file mode 100644 index 00000000..a1c71907 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/MetadataField.swift @@ -0,0 +1,35 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension MetadataField: Displayable { + var displayTitle: String { + switch self { + case .cast: + return L10n.people + case .genres: + return L10n.genres + case .productionLocations: + return L10n.productionLocations + case .studios: + return L10n.studios + case .tags: + return L10n.tags + case .name: + return L10n.name + case .overview: + return L10n.overview + case .runtime: + return L10n.runtime + case .officialRating: + return L10n.officialRating + } + } +} diff --git a/Shared/Extensions/JellyfinAPI/Video3DFormat.swift b/Shared/Extensions/JellyfinAPI/Video3DFormat.swift new file mode 100644 index 00000000..f8073b14 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/Video3DFormat.swift @@ -0,0 +1,27 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension Video3DFormat { + var displayTitle: String { + switch self { + case .halfSideBySide: + return L10n.halfSideBySide + case .fullSideBySide: + return L10n.fullSideBySide + case .fullTopAndBottom: + return L10n.fullTopAndBottom + case .halfTopAndBottom: + return L10n.halfTopAndBottom + case .mvc: + return L10n.mvc + } + } +} diff --git a/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift b/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift new file mode 100644 index 00000000..fc544424 --- /dev/null +++ b/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift @@ -0,0 +1,28 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation + +enum BoxSetDisplayOrder: String, CaseIterable, Identifiable { + case dateModified = "DateModified" + case sortName = "SortName" + case premiereDate = "PremiereDate" + + var id: String { rawValue } + + var displayTitle: String { + switch self { + case .dateModified: + return L10n.dateModified + case .sortName: + return L10n.sortName + case .premiereDate: + return L10n.premiereDate + } + } +} diff --git a/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift b/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift new file mode 100644 index 00000000..0c7cdbab --- /dev/null +++ b/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift @@ -0,0 +1,52 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation + +enum SeriesDisplayOrder: String, CaseIterable, Identifiable { + case aired = "Aired" + case originalAirDate + case absolute + case dvd + case digital + case storyArc + case production + case tv + case alternate + case regional + case alternateDVD = "altdvd" + + var id: String { rawValue } + + var displayTitle: String { + switch self { + case .aired: + return L10n.aired + case .originalAirDate: + return L10n.originalAirDate + case .absolute: + return L10n.absolute + case .dvd: + return L10n.dvd + case .digital: + return L10n.digital + case .storyArc: + return L10n.storyArc + case .production: + return L10n.production + case .tv: + return L10n.tv + case .alternate: + return L10n.alternate + case .regional: + return L10n.regional + case .alternateDVD: + return L10n.alternateDVD + } + } +} diff --git a/Shared/Objects/SeriesStatus.swift b/Shared/Objects/SeriesStatus.swift new file mode 100644 index 00000000..a4548bbc --- /dev/null +++ b/Shared/Objects/SeriesStatus.swift @@ -0,0 +1,26 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Foundation + +enum SeriesStatus: String, CaseIterable { + case continuing = "Continuing" + case ended = "Ended" + case unreleased = "Unreleased" + + var displayTitle: String { + switch self { + case .continuing: + return L10n.continuing + case .ended: + return L10n.ended + case .unreleased: + return L10n.unreleased + } + } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 328e2209..e46e111b 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -12,6 +12,8 @@ import Foundation internal enum L10n { /// About internal static let about = L10n.tr("Localizable", "about", fallback: "About") + /// Absolute + internal static let absolute = L10n.tr("Localizable", "absolute", fallback: "Absolute") /// Accent Color internal static let accentColor = L10n.tr("Localizable", "accentColor", fallback: "Accent Color") /// Some views may need an app restart to update. @@ -20,13 +22,15 @@ internal enum L10n { internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility") /// Active internal static let active = L10n.tr("Localizable", "active", fallback: "Active") + /// Active Devices + internal static let activeDevices = L10n.tr("Localizable", "activeDevices", fallback: "Active Devices") /// Activity internal static let activity = L10n.tr("Localizable", "activity", fallback: "Activity") /// Add internal static let add = L10n.tr("Localizable", "add", fallback: "Add") /// Add API key internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key") - /// Select Server View - Add Server + /// Add Server internal static let addServer = L10n.tr("Localizable", "addServer", fallback: "Add Server") /// Add trigger internal static let addTrigger = L10n.tr("Localizable", "addTrigger", fallback: "Add trigger") @@ -34,12 +38,16 @@ internal enum L10n { internal static let addURL = L10n.tr("Localizable", "addURL", fallback: "Add URL") /// Add User internal static let addUser = L10n.tr("Localizable", "addUser", fallback: "Add User") - /// Administration Dashboard Section + /// Administration internal static let administration = L10n.tr("Localizable", "administration", fallback: "Administration") /// Administrator internal static let administrator = L10n.tr("Localizable", "administrator", fallback: "Administrator") /// Advanced internal static let advanced = L10n.tr("Localizable", "advanced", fallback: "Advanced") + /// Aired + internal static let aired = L10n.tr("Localizable", "aired", fallback: "Aired") + /// Air Time + internal static let airTime = L10n.tr("Localizable", "airTime", fallback: "Air Time") /// Airs %s internal static func airWithDate(_ p1: UnsafePointer) -> String { return L10n.tr("Localizable", "airWithDate", p1, fallback: "Airs %s") @@ -54,11 +62,15 @@ internal enum L10n { internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion") /// Allow media item editing internal static let allowItemEditing = L10n.tr("Localizable", "allowItemEditing", fallback: "Allow media item editing") - /// Select Server View - Select All Servers + /// All Servers internal static let allServers = L10n.tr("Localizable", "allServers", fallback: "All Servers") /// View and manage all registered users on the server, including their permissions and activity status. internal static let allUsersDescription = L10n.tr("Localizable", "allUsersDescription", fallback: "View and manage all registered users on the server, including their permissions and activity status.") - /// TranscodeReason - Anamorphic Video Not Supported + /// Alternate + internal static let alternate = L10n.tr("Localizable", "alternate", fallback: "Alternate") + /// Alternate DVD + internal static let alternateDVD = L10n.tr("Localizable", "alternateDVD", fallback: "Alternate DVD") + /// Anamorphic video is not supported internal static let anamorphicVideoNotSupported = L10n.tr("Localizable", "anamorphicVideoNotSupported", fallback: "Anamorphic video is not supported") /// API Key Copied internal static let apiKeyCopied = L10n.tr("Localizable", "apiKeyCopied", fallback: "API Key Copied") @@ -84,21 +96,21 @@ internal enum L10n { internal static let audio = L10n.tr("Localizable", "audio", fallback: "Audio") /// Audio & Captions internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions", fallback: "Audio & Captions") - /// TranscodeReason - Audio Bit Depth Not Supported + /// The audio bit depth is not supported internal static let audioBitDepthNotSupported = L10n.tr("Localizable", "audioBitDepthNotSupported", fallback: "The audio bit depth is not supported") - /// TranscodeReason - Audio Bitrate Not Supported + /// The audio bitrate is not supported internal static let audioBitrateNotSupported = L10n.tr("Localizable", "audioBitrateNotSupported", fallback: "The audio bitrate is not supported") - /// TranscodeReason - Audio Channels Not Supported + /// The number of audio channels is not supported internal static let audioChannelsNotSupported = L10n.tr("Localizable", "audioChannelsNotSupported", fallback: "The number of audio channels is not supported") - /// TranscodeReason - Audio Codec Not Supported + /// The audio codec is not supported internal static let audioCodecNotSupported = L10n.tr("Localizable", "audioCodecNotSupported", fallback: "The audio codec is not supported") - /// TranscodeReason - Audio Is External + /// The audio track is external and requires transcoding internal static let audioIsExternal = L10n.tr("Localizable", "audioIsExternal", fallback: "The audio track is external and requires transcoding") /// Audio Offset internal static let audioOffset = L10n.tr("Localizable", "audioOffset", fallback: "Audio Offset") - /// TranscodeReason - Audio Profile Not Supported + /// The audio profile is not supported internal static let audioProfileNotSupported = L10n.tr("Localizable", "audioProfileNotSupported", fallback: "The audio profile is not supported") - /// TranscodeReason - Audio Sample Rate Not Supported + /// The audio sample rate is not supported internal static let audioSampleRateNotSupported = L10n.tr("Localizable", "audioSampleRateNotSupported", fallback: "The audio sample rate is not supported") /// Audio Track internal static let audioTrack = L10n.tr("Localizable", "audioTrack", fallback: "Audio Track") @@ -106,7 +118,7 @@ internal enum L10n { internal static let audioTranscoding = L10n.tr("Localizable", "audioTranscoding", fallback: "Audio transcoding") /// Authorize internal static let authorize = L10n.tr("Localizable", "authorize", fallback: "Authorize") - /// PlaybackCompatibility Default Category + /// Auto internal static let auto = L10n.tr("Localizable", "auto", fallback: "Auto") /// Optimizes playback using default settings for most devices. Some formats may require server transcoding for non-compatible media types. internal static let autoDescription = L10n.tr("Localizable", "autoDescription", fallback: "Optimizes playback using default settings for most devices. Some formats may require server transcoding for non-compatible media types.") @@ -120,49 +132,53 @@ internal enum L10n { internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior") /// 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.") - /// Option for automatic bitrate selection + /// Birthday + internal static let birthday = L10n.tr("Localizable", "birthday", fallback: "Birthday") + /// Birth year + internal static let birthYear = L10n.tr("Localizable", "birthYear", fallback: "Birth year") + /// Auto internal static let bitrateAuto = L10n.tr("Localizable", "bitrateAuto", fallback: "Auto") /// Default Bitrate internal static let bitrateDefault = L10n.tr("Localizable", "bitrateDefault", fallback: "Default Bitrate") - /// Default Bitrate Description + /// Limits the internet bandwidth used during playback. internal static let bitrateDefaultDescription = L10n.tr("Localizable", "bitrateDefaultDescription", fallback: "Limits the internet bandwidth used during playback.") - /// Option to set the bitrate to 480p quality at 1.5 Mbps + /// 480p - 1.5 Mbps internal static let bitrateKbps1500 = L10n.tr("Localizable", "bitrateKbps1500", fallback: "480p - 1.5 Mbps") - /// Option to set the bitrate to 360p quality at 420 Kbps + /// 360p - 420 Kbps internal static let bitrateKbps420 = L10n.tr("Localizable", "bitrateKbps420", fallback: "360p - 420 Kbps") - /// Option to set the bitrate to 480p quality at 720 Kbps + /// 480p - 720 Kbps internal static let bitrateKbps720 = L10n.tr("Localizable", "bitrateKbps720", fallback: "480p - 720 Kbps") - /// Option for the maximum bitrate + /// Maximum internal static let bitrateMax = L10n.tr("Localizable", "bitrateMax", fallback: "Maximum") /// Maximizes bandwidth usage, up to %@, for each playback stream to ensure the highest quality. internal static func bitrateMaxDescription(_ p1: Any) -> String { return L10n.tr("Localizable", "bitrateMaxDescription", String(describing: p1), fallback: "Maximizes bandwidth usage, up to %@, for each playback stream to ensure the highest quality.") } - /// Option to set the bitrate to 1080p quality at 10 Mbps + /// 1080p - 10 Mbps internal static let bitrateMbps10 = L10n.tr("Localizable", "bitrateMbps10", fallback: "1080p - 10 Mbps") - /// Option to set the bitrate to 4K quality at 120 Mbps + /// 4K - 120 Mbps internal static let bitrateMbps120 = L10n.tr("Localizable", "bitrateMbps120", fallback: "4K - 120 Mbps") - /// Option to set the bitrate to 1080p quality at 15 Mbps + /// 1080p - 15 Mbps internal static let bitrateMbps15 = L10n.tr("Localizable", "bitrateMbps15", fallback: "1080p - 15 Mbps") - /// Option to set the bitrate to 1080p quality at 20 Mbps + /// 1080p - 20 Mbps internal static let bitrateMbps20 = L10n.tr("Localizable", "bitrateMbps20", fallback: "1080p - 20 Mbps") - /// Option to set the bitrate to 480p quality at 3 Mbps + /// 480p - 3 Mbps internal static let bitrateMbps3 = L10n.tr("Localizable", "bitrateMbps3", fallback: "480p - 3 Mbps") - /// Option to set the bitrate to 720p quality at 4 Mbps + /// 720p - 4 Mbps internal static let bitrateMbps4 = L10n.tr("Localizable", "bitrateMbps4", fallback: "720p - 4 Mbps") - /// Option to set the bitrate to 1080p quality at 40 Mbps + /// 1080p - 40 Mbps internal static let bitrateMbps40 = L10n.tr("Localizable", "bitrateMbps40", fallback: "1080p - 40 Mbps") - /// Option to set the bitrate to 720p quality at 6 Mbps + /// 720p - 6 Mbps internal static let bitrateMbps6 = L10n.tr("Localizable", "bitrateMbps6", fallback: "720p - 6 Mbps") - /// Option to set the bitrate to 1080p quality at 60 Mbps + /// 1080p - 60 Mbps internal static let bitrateMbps60 = L10n.tr("Localizable", "bitrateMbps60", fallback: "1080p - 60 Mbps") - /// Option to set the bitrate to 720p quality at 8 Mbps + /// 720p - 8 Mbps internal static let bitrateMbps8 = L10n.tr("Localizable", "bitrateMbps8", fallback: "720p - 8 Mbps") - /// Option to set the bitrate to 4K quality at 80 Mbps + /// 4K - 80 Mbps internal static let bitrateMbps80 = L10n.tr("Localizable", "bitrateMbps80", fallback: "4K - 80 Mbps") - /// Bitrate Automatic Section Header + /// Bitrate Test internal static let bitrateTest = L10n.tr("Localizable", "bitrateTest", fallback: "Bitrate Test") - /// Description for bitrate test duration indicating longer tests provide more accurate bitrates but may delay playback + /// Longer tests are more accurate but may result in a delayed playback. internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "Longer tests are more accurate but may result in a delayed playback.") /// bps internal static let bitsPerSecond = L10n.tr("Localizable", "bitsPerSecond", fallback: "bps") @@ -174,9 +190,9 @@ internal enum L10n { internal static let buttons = L10n.tr("Localizable", "buttons", fallback: "Buttons") /// Cancel internal static let cancel = L10n.tr("Localizable", "cancel", fallback: "Cancel") - /// Task was Canceled + /// Cancelled internal static let canceled = L10n.tr("Localizable", "canceled", fallback: "Cancelled") - /// Status label for when a task is cancelling + /// Cancelling... internal static let cancelling = L10n.tr("Localizable", "cancelling", fallback: "Cancelling...") /// Cannot connect to host internal static let cannotConnectToHost = L10n.tr("Localizable", "cannotConnectToHost", fallback: "Cannot connect to host") @@ -186,7 +202,7 @@ internal enum L10n { internal static let cast = L10n.tr("Localizable", "cast", fallback: "CAST") /// Cast & Crew internal static let castAndCrew = L10n.tr("Localizable", "castAndCrew", fallback: "Cast & Crew") - /// The category label for tasks + /// Category internal static let category = L10n.tr("Localizable", "category", fallback: "Category") /// Change Server internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server") @@ -200,11 +216,11 @@ internal enum L10n { internal static let chapterSlider = L10n.tr("Localizable", "chapterSlider", fallback: "Chapter Slider") /// Cinematic internal static let cinematic = L10n.tr("Localizable", "cinematic", fallback: "Cinematic") - /// Customize Server View - Cinematic Background + /// Cinematic Background internal static let cinematicBackground = L10n.tr("Localizable", "cinematicBackground", fallback: "Cinematic Background") /// Cinematic Views internal static let cinematicViews = L10n.tr("Localizable", "cinematicViews", fallback: "Cinematic Views") - /// The client used for the session + /// Client internal static let client = L10n.tr("Localizable", "client", fallback: "Client") /// Close internal static let close = L10n.tr("Localizable", "close", fallback: "Close") @@ -214,23 +230,25 @@ internal enum L10n { internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections") /// Color internal static let color = L10n.tr("Localizable", "color", fallback: "Color") - /// Section Title for Column Configuration + /// Columns internal static let columns = L10n.tr("Localizable", "columns", fallback: "Columns") /// Coming soon internal static let comingSoon = L10n.tr("Localizable", "comingSoon", fallback: "Coming soon") + /// Community + internal static let community = L10n.tr("Localizable", "community", fallback: "Community") /// Compact internal static let compact = L10n.tr("Localizable", "compact", fallback: "Compact") /// Compact Logo internal static let compactLogo = L10n.tr("Localizable", "compactLogo", fallback: "Compact Logo") /// Compact Poster internal static let compactPoster = L10n.tr("Localizable", "compactPoster", fallback: "Compact Poster") - /// PlaybackCompatibility Section Title + /// Compatibility internal static let compatibility = L10n.tr("Localizable", "compatibility", fallback: "Compatibility") - /// PlaybackCompatibility Compatible Category + /// Most Compatible internal static let compatible = L10n.tr("Localizable", "compatible", fallback: "Most Compatible") /// Converts all media to H.264 video and AAC audio for maximum compatibility. May require server transcoding for non-compatible media types. internal static let compatibleDescription = L10n.tr("Localizable", "compatibleDescription", fallback: "Converts all media to H.264 video and AAC audio for maximum compatibility. May require server transcoding for non-compatible media types.") - /// Confirm Task Fuction + /// Confirm internal static let confirm = L10n.tr("Localizable", "confirm", fallback: "Confirm") /// Confirm Close internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: "Confirm Close") @@ -250,9 +268,9 @@ internal enum L10n { internal static let connectToJellyfinServerStart = L10n.tr("Localizable", "connectToJellyfinServerStart", fallback: "Connect to a Jellyfin server to get started") /// Connect to Server internal static let connectToServer = L10n.tr("Localizable", "connectToServer", fallback: "Connect to Server") - /// TranscodeReason - Container Bitrate Exceeds Limit + /// The container bitrate exceeds the allowed limit internal static let containerBitrateExceedsLimit = L10n.tr("Localizable", "containerBitrateExceedsLimit", fallback: "The container bitrate exceeds the allowed limit") - /// TranscodeReason - Container Not Supported + /// The container format is not supported internal static let containerNotSupported = L10n.tr("Localizable", "containerNotSupported", fallback: "The container format is not supported") /// Containers internal static let containers = L10n.tr("Localizable", "containers", fallback: "Containers") @@ -260,23 +278,29 @@ internal enum L10n { internal static let `continue` = L10n.tr("Localizable", "continue", fallback: "Continue") /// Continue Watching internal static let continueWatching = L10n.tr("Localizable", "continueWatching", fallback: "Continue Watching") + /// Continuing + internal static let continuing = L10n.tr("Localizable", "continuing", fallback: "Continuing") /// Control other users internal static let controlOtherUsers = L10n.tr("Localizable", "controlOtherUsers", fallback: "Control other users") /// Control shared devices internal static let controlSharedDevices = L10n.tr("Localizable", "controlSharedDevices", fallback: "Control shared devices") + /// Country + internal static let country = L10n.tr("Localizable", "country", fallback: "Country") /// Create & Join Groups internal static let createAndJoinGroups = L10n.tr("Localizable", "createAndJoinGroups", fallback: "Create & Join Groups") /// Create API Key internal static let createAPIKey = L10n.tr("Localizable", "createAPIKey", fallback: "Create API Key") /// Enter the application name for the new API key. internal static let createAPIKeyMessage = L10n.tr("Localizable", "createAPIKeyMessage", fallback: "Enter the application name for the new API key.") + /// Critics + internal static let critics = L10n.tr("Localizable", "critics", fallback: "Critics") /// Current internal static let current = L10n.tr("Localizable", "current", fallback: "Current") /// Current Password internal static let currentPassword = L10n.tr("Localizable", "currentPassword", fallback: "Current Password") /// Current Position internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: "Current Position") - /// PlaybackCompatibility Custom Category + /// Custom internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom") /// Custom bitrate internal static let customBitrate = L10n.tr("Localizable", "customBitrate", fallback: "Custom bitrate") @@ -290,35 +314,45 @@ internal enum L10n { internal static func customDeviceNameSaved(_ p1: Any) -> String { return L10n.tr("Localizable", "customDeviceNameSaved", String(describing: p1), fallback: "Your custom device name '%1$@' has been saved.") } - /// Custom profile is Added to the Existing Profiles + /// The custom device profiles will be added to the default Swiftfin device profiles. internal static let customDeviceProfileAdd = L10n.tr("Localizable", "customDeviceProfileAdd", fallback: "The custom device profiles will be added to the default Swiftfin device profiles.") - /// Device Profile Section Description + /// Dictates back to the Jellyfin Server what this device hardware is capable of playing. internal static let customDeviceProfileDescription = L10n.tr("Localizable", "customDeviceProfileDescription", fallback: "Dictates back to the Jellyfin Server what this device hardware is capable of playing.") - /// Custom profile will replace the Existing Profiles + /// The custom device profiles will replace the default Swiftfin device profiles. internal static let customDeviceProfileReplace = L10n.tr("Localizable", "customDeviceProfileReplace", fallback: "The custom device profiles will replace the default Swiftfin device profiles.") /// Manually set the number of failed login attempts allowed before locking the user. internal static let customFailedLoginDescription = L10n.tr("Localizable", "customFailedLoginDescription", fallback: "Manually set the number of failed login attempts allowed before locking the user.") /// Custom failed logins internal static let customFailedLogins = L10n.tr("Localizable", "customFailedLogins", fallback: "Custom failed logins") - /// Settings View - Customize + /// Customize internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize") - /// Section Header for a Custom Device Profile + /// Custom Profile internal static let customProfile = L10n.tr("Localizable", "customProfile", fallback: "Custom Profile") + /// Custom Rating + internal static let customRating = L10n.tr("Localizable", "customRating", fallback: "Custom Rating") /// Custom sessions internal static let customSessions = L10n.tr("Localizable", "customSessions", fallback: "Custom sessions") /// Daily internal static let daily = L10n.tr("Localizable", "daily", fallback: "Daily") /// Represents the dark theme setting internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark") - /// UserDashboardView Header + /// Dashboard internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard") - /// Description for the dashboard section + /// Perform administrative tasks for your Jellyfin server. internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.") + /// Date Added + 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 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") + /// Dates + internal static let dates = L10n.tr("Localizable", "dates", fallback: "Dates") /// Day of Week internal static let dayOfWeek = L10n.tr("Localizable", "dayOfWeek", fallback: "Day of Week") - /// Time Interval Help Text - Days + /// Days internal static let days = L10n.tr("Localizable", "days", fallback: "Days") /// Default internal static let `default` = L10n.tr("Localizable", "default", fallback: "Default") @@ -350,7 +384,7 @@ internal enum L10n { internal static let deleteSelectionDevicesWarning = L10n.tr("Localizable", "deleteSelectionDevicesWarning", fallback: "Are you sure you wish to delete all selected devices? All selected sessions will be logged out.") /// Are you sure you wish to delete all selected users? internal static let deleteSelectionUsersWarning = L10n.tr("Localizable", "deleteSelectionUsersWarning", fallback: "Are you sure you wish to delete all selected users?") - /// Server Detail View - Delete Server + /// Delete Server internal static let deleteServer = L10n.tr("Localizable", "deleteServer", fallback: "Delete Server") /// Delete Trigger internal static let deleteTrigger = L10n.tr("Localizable", "deleteTrigger", fallback: "Delete Trigger") @@ -370,25 +404,27 @@ internal enum L10n { internal static let delivery = L10n.tr("Localizable", "delivery", fallback: "Delivery") /// Details internal static let details = L10n.tr("Localizable", "details", fallback: "Details") - /// Session Device Section Label + /// Device internal static let device = L10n.tr("Localizable", "device", fallback: "Device") - /// Section Header for Device Profiles + /// Device Profile internal static let deviceProfile = L10n.tr("Localizable", "deviceProfile", fallback: "Device Profile") /// Decide which media plays natively or requires server transcoding for compatibility. internal static let deviceProfileDescription = L10n.tr("Localizable", "deviceProfileDescription", fallback: "Decide which media plays natively or requires server transcoding for compatibility.") /// Devices internal static let devices = L10n.tr("Localizable", "devices", fallback: "Devices") - /// PlaybackCompatibility DirectPlay Category + /// Digital + internal static let digital = L10n.tr("Localizable", "digital", fallback: "Digital") + /// Direct Play internal static let direct = L10n.tr("Localizable", "direct", fallback: "Direct Play") /// Plays content in its original format. May cause playback issues on unsupported media types. internal static let directDescription = L10n.tr("Localizable", "directDescription", fallback: "Plays content in its original format. May cause playback issues on unsupported media types.") /// DIRECTOR internal static let director = L10n.tr("Localizable", "director", fallback: "DIRECTOR") - /// PlayMethod - Direct Play + /// Direct Play internal static let directPlay = L10n.tr("Localizable", "directPlay", fallback: "Direct Play") - /// TranscodeReason - Direct Play Error + /// An error occurred during direct play internal static let directPlayError = L10n.tr("Localizable", "directPlayError", fallback: "An error occurred during direct play") - /// PlayMethod - Direct Stream + /// Direct Stream internal static let directStream = L10n.tr("Localizable", "directStream", fallback: "Direct Stream") /// Disabled internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled") @@ -398,17 +434,19 @@ internal enum L10n { internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers", fallback: "Discovered Servers") /// Dismiss internal static let dismiss = L10n.tr("Localizable", "dismiss", fallback: "Dismiss") - /// Display order - internal static let displayOrder = L10n.tr("Localizable", "displayOrder", fallback: "Display order") - /// Done - Completed, end, or save + /// Display Order + internal static let displayOrder = L10n.tr("Localizable", "displayOrder", fallback: "Display Order") + /// Done internal static let done = L10n.tr("Localizable", "done", fallback: "Done") /// Downloads internal static let downloads = L10n.tr("Localizable", "downloads", fallback: "Downloads") - /// Button label to edit a task + /// DVD + internal static let dvd = L10n.tr("Localizable", "dvd", fallback: "DVD") + /// Edit internal static let edit = L10n.tr("Localizable", "edit", fallback: "Edit") /// Edit Jump Lengths internal static let editJumpLengths = L10n.tr("Localizable", "editJumpLengths", fallback: "Edit Jump Lengths") - /// Select Server View - Edit an Existing Server + /// Edit Server internal static let editServer = L10n.tr("Localizable", "editServer", fallback: "Edit Server") /// Edit Users internal static let editUsers = L10n.tr("Localizable", "editUsers", fallback: "Edit Users") @@ -416,18 +454,30 @@ internal enum L10n { internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up") /// Enabled internal static let enabled = L10n.tr("Localizable", "enabled", fallback: "Enabled") + /// End Date + internal static let endDate = L10n.tr("Localizable", "endDate", fallback: "End Date") + /// Ended + internal static let ended = L10n.tr("Localizable", "ended", fallback: "Ended") /// Enter custom bitrate in Mbps internal static let enterCustomBitrate = L10n.tr("Localizable", "enterCustomBitrate", fallback: "Enter custom bitrate in Mbps") /// Enter custom failed logins limit internal static let enterCustomFailedLogins = L10n.tr("Localizable", "enterCustomFailedLogins", fallback: "Enter custom failed logins limit") /// Enter custom max sessions internal static let enterCustomMaxSessions = L10n.tr("Localizable", "enterCustomMaxSessions", fallback: "Enter custom max sessions") + /// Enter the episode number. + internal static let enterEpisodeNumber = L10n.tr("Localizable", "enterEpisodeNumber", fallback: "Enter the episode number.") + /// Enter the season number. + internal static let enterSeasonNumber = L10n.tr("Localizable", "enterSeasonNumber", fallback: "Enter the season number.") + /// Episode + internal static let episode = L10n.tr("Localizable", "episode", fallback: "Episode") /// Episode Landscape Poster internal static let episodeLandscapePoster = L10n.tr("Localizable", "episodeLandscapePoster", fallback: "Episode Landscape Poster") /// Episode %1$@ internal static func episodeNumber(_ p1: Any) -> String { return L10n.tr("Localizable", "episodeNumber", String(describing: p1), fallback: "Episode %1$@") } + /// Episode runtime in minutes. + internal static let episodeRuntimeDescription = L10n.tr("Localizable", "episodeRuntimeDescription", fallback: "Episode runtime in minutes.") /// Episodes internal static let episodes = L10n.tr("Localizable", "episodes", fallback: "Episodes") /// Error @@ -458,6 +508,8 @@ internal enum L10n { internal static let featureAccess = L10n.tr("Localizable", "featureAccess", fallback: "Feature access") /// File internal static let file = L10n.tr("Localizable", "file", fallback: "File") + /// File Path + internal static let filePath = L10n.tr("Localizable", "filePath", fallback: "File Path") /// Filter Results internal static let filterResults = L10n.tr("Localizable", "filterResults", fallback: "Filter Results") /// Filters @@ -466,12 +518,22 @@ internal enum L10n { 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.") + /// Find Missing Metadata + internal static let findMissingMetadata = L10n.tr("Localizable", "findMissingMetadata", fallback: "Find Missing Metadata") /// Force remote media transcoding internal static let forceRemoteTranscoding = L10n.tr("Localizable", "forceRemoteTranscoding", fallback: "Force remote media transcoding") - /// Transcode FPS + /// Format + internal static let format = L10n.tr("Localizable", "format", fallback: "Format") + /// 3D Format + internal static let format3D = L10n.tr("Localizable", "format3D", fallback: "3D Format") + /// %@fps internal static func fpsWithString(_ p1: Any) -> String { return L10n.tr("Localizable", "fpsWithString", String(describing: p1), fallback: "%@fps") } + /// Full Side-by-Side + internal static let fullSideBySide = L10n.tr("Localizable", "fullSideBySide", fallback: "Full Side-by-Side") + /// Full Top and Bottom + internal static let fullTopAndBottom = L10n.tr("Localizable", "fullTopAndBottom", fallback: "Full Top and Bottom") /// Genres internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres") /// Gestures @@ -482,6 +544,10 @@ internal enum L10n { internal static let green = L10n.tr("Localizable", "green", fallback: "Green") /// Grid internal static let grid = L10n.tr("Localizable", "grid", fallback: "Grid") + /// Half Side-by-Side + internal static let halfSideBySide = L10n.tr("Localizable", "halfSideBySide", fallback: "Half Side-by-Side") + /// Half Top and Bottom + internal static let halfTopAndBottom = L10n.tr("Localizable", "halfTopAndBottom", fallback: "Half Top and Bottom") /// Haptic Feedback internal static let hapticFeedback = L10n.tr("Localizable", "hapticFeedback", fallback: "Haptic Feedback") /// Hidden @@ -494,11 +560,11 @@ internal enum L10n { internal static let hours = L10n.tr("Localizable", "hours", fallback: "Hours") /// Idle internal static let idle = L10n.tr("Localizable", "idle", fallback: "Idle") - /// Customize Server View - Indicators + /// Indicators internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators") /// Information internal static let information = L10n.tr("Localizable", "information", fallback: "Information") - /// TranscodeReason - Interlaced Video Not Supported + /// Interlaced video is not supported 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") @@ -512,13 +578,13 @@ internal enum L10n { } /// You do not have permission to delete this item. internal static let itemDeletionPermissionFailure = L10n.tr("Localizable", "itemDeletionPermissionFailure", fallback: "You do not have permission to delete this item.") - /// SessionPlaybackMethod Remaining Time + /// %1$@ / %2$@ internal static func itemOverItem(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "itemOverItem", String(describing: p1), String(describing: p2), fallback: "%1$@ / %2$@") } /// Items internal static let items = L10n.tr("Localizable", "items", fallback: "Items") - /// General + /// Jellyfin internal static let jellyfin = L10n.tr("Localizable", "jellyfin", fallback: "Jellyfin") /// Join Groups internal static let joinGroups = L10n.tr("Localizable", "joinGroups", fallback: "Join Groups") @@ -542,17 +608,19 @@ internal enum L10n { internal static let kids = L10n.tr("Localizable", "kids", fallback: "Kids") /// kbps internal static let kilobitsPerSecond = L10n.tr("Localizable", "kilobitsPerSecond", fallback: "kbps") + /// Language + internal static let language = L10n.tr("Localizable", "language", fallback: "Language") /// Larger internal static let larger = L10n.tr("Localizable", "larger", fallback: "Larger") /// Largest internal static let largest = L10n.tr("Localizable", "largest", fallback: "Largest") - /// The label for the last run time of a task + /// Last run internal static let lastRun = L10n.tr("Localizable", "lastRun", fallback: "Last run") - /// Last run message with time + /// Last ran %@ internal static func lastRunTime(_ p1: Any) -> String { return L10n.tr("Localizable", "lastRunTime", String(describing: p1), fallback: "Last ran %@") } - /// Session Client Last Seen Section Label + /// Last Seen internal static let lastSeen = L10n.tr("Localizable", "lastSeen", fallback: "Last Seen") /// Latest %@ internal static func latestWithString(_ p1: Any) -> String { @@ -562,6 +630,8 @@ internal enum L10n { internal static let learnMoreEllipsis = L10n.tr("Localizable", "learnMoreEllipsis", fallback: "Learn more...") /// Left internal static let `left` = L10n.tr("Localizable", "left", fallback: "Left") + /// Letter + internal static let letter = L10n.tr("Localizable", "letter", fallback: "Letter") /// Letter Picker internal static let letterPicker = L10n.tr("Localizable", "letterPicker", fallback: "Letter Picker") /// Library @@ -582,6 +652,10 @@ internal enum L10n { internal static let loading = L10n.tr("Localizable", "loading", fallback: "Loading") /// Local Servers internal static let localServers = L10n.tr("Localizable", "localServers", fallback: "Local Servers") + /// Lock All Fields + internal static let lockAllFields = L10n.tr("Localizable", "lockAllFields", fallback: "Lock All Fields") + /// Locked Fields + internal static let lockedFields = L10n.tr("Localizable", "lockedFields", fallback: "Locked Fields") /// Locked users internal static let lockedUsers = L10n.tr("Localizable", "lockedUsers", fallback: "Locked users") /// Login @@ -590,7 +664,7 @@ internal enum L10n { internal static func loginToWithString(_ p1: Any) -> String { return L10n.tr("Localizable", "loginToWithString", String(describing: p1), fallback: "Login to %@") } - /// Settings View - Logs + /// Logs internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs") /// Access the Jellyfin server logs for troubleshooting and monitoring purposes. internal static let logsDescription = L10n.tr("Localizable", "logsDescription", fallback: "Access the Jellyfin server logs for troubleshooting and monitoring purposes.") @@ -598,7 +672,7 @@ internal enum L10n { internal static let lyrics = L10n.tr("Localizable", "lyrics", fallback: "Lyrics") /// Management internal static let management = L10n.tr("Localizable", "management", fallback: "Management") - /// Option to set the maximum bitrate for playback + /// Maximum Bitrate internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate") /// Limits the total number of connections a user can have to the server. internal static let maximumConnectionsDescription = L10n.tr("Localizable", "maximumConnectionsDescription", fallback: "Limits the total number of connections a user can have to the server.") @@ -614,7 +688,7 @@ internal enum L10n { internal static let maximumSessions = L10n.tr("Localizable", "maximumSessions", fallback: "Maximum sessions") /// Maximum sessions policy internal static let maximumSessionsPolicy = L10n.tr("Localizable", "maximumSessionsPolicy", fallback: "Maximum sessions policy") - /// Playback May Fail + /// This setting may result in media failing to start playback. internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "This setting may result in media failing to start playback.") /// Media internal static let media = L10n.tr("Localizable", "media", fallback: "Media") @@ -628,7 +702,9 @@ internal enum L10n { internal static let menuButtons = L10n.tr("Localizable", "menuButtons", fallback: "Menu Buttons") /// Metadata internal static let metadata = L10n.tr("Localizable", "metadata", fallback: "Metadata") - /// The play method (e.g., Direct Play, Transcoding) + /// Metadata preferences + internal static let metadataPreferences = L10n.tr("Localizable", "metadataPreferences", fallback: "Metadata preferences") + /// Method internal static let method = L10n.tr("Localizable", "method", fallback: "Method") /// Minutes internal static let minutes = L10n.tr("Localizable", "minutes", fallback: "Minutes") @@ -644,6 +720,8 @@ internal enum L10n { internal static func multipleUsers(_ p1: Int) -> String { return L10n.tr("Localizable", "multipleUsers", p1, fallback: "%d users") } + /// MVC + internal static let mvc = L10n.tr("Localizable", "mvc", fallback: "MVC") /// Name internal static let name = L10n.tr("Localizable", "name", fallback: "Name") /// Native Player @@ -654,7 +732,7 @@ internal enum L10n { internal static let networkTimedOut = L10n.tr("Localizable", "networkTimedOut", fallback: "Network timed out") /// Never internal static let never = L10n.tr("Localizable", "never", fallback: "Never") - /// Message shown when a task has never run + /// Never run internal static let neverRun = L10n.tr("Localizable", "neverRun", fallback: "Never run") /// New Password internal static let newPassword = L10n.tr("Localizable", "newPassword", fallback: "New Password") @@ -668,11 +746,11 @@ internal enum L10n { internal static let nextItem = L10n.tr("Localizable", "nextItem", fallback: "Next Item") /// Next Up internal static let nextUp = L10n.tr("Localizable", "nextUp", fallback: "Next Up") - /// Settings Description for the day limit in Next Up + /// Days in Next Up internal static let nextUpDays = L10n.tr("Localizable", "nextUpDays", fallback: "Days in Next Up") - /// Description for how the nextUpDays setting works + /// Set the maximum amount of days a show should stay in the 'Next Up' list without watching it. internal static let nextUpDaysDescription = L10n.tr("Localizable", "nextUpDaysDescription", fallback: "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.") - /// Settings Description for enabling rewatching in Next Up + /// Rewatching in Next Up internal static let nextUpRewatch = L10n.tr("Localizable", "nextUpRewatch", fallback: "Rewatching in Next Up") /// No internal static let no = L10n.tr("Localizable", "no", fallback: "No") @@ -696,7 +774,7 @@ internal enum L10n { internal static let normal = L10n.tr("Localizable", "normal", fallback: "Normal") /// No runtime limit internal static let noRuntimeLimit = L10n.tr("Localizable", "noRuntimeLimit", fallback: "No runtime limit") - /// No active session available + /// No session internal static let noSession = L10n.tr("Localizable", "noSession", fallback: "No session") /// N/A internal static let notAvailableSlash = L10n.tr("Localizable", "notAvailableSlash", fallback: "N/A") @@ -706,7 +784,9 @@ internal enum L10n { } /// No title internal static let noTitle = L10n.tr("Localizable", "noTitle", fallback: "No title") - /// Video Player Settings View - Offset + /// Official Rating + internal static let officialRating = L10n.tr("Localizable", "officialRating", fallback: "Official Rating") + /// Offset internal static let offset = L10n.tr("Localizable", "offset", fallback: "Offset") /// OK internal static let ok = L10n.tr("Localizable", "ok", fallback: "OK") @@ -714,7 +794,7 @@ internal enum L10n { internal static let onApplicationStartup = L10n.tr("Localizable", "onApplicationStartup", fallback: "On application startup") /// 1 user internal static let oneUser = L10n.tr("Localizable", "oneUser", fallback: "1 user") - /// Indicates that something is Online + /// Online internal static let online = L10n.tr("Localizable", "online", fallback: "Online") /// On Now internal static let onNow = L10n.tr("Localizable", "onNow", fallback: "On Now") @@ -728,6 +808,12 @@ internal enum L10n { internal static let order = L10n.tr("Localizable", "order", fallback: "Order") /// Orientation internal static let orientation = L10n.tr("Localizable", "orientation", fallback: "Orientation") + /// Original Air Date + internal static let originalAirDate = L10n.tr("Localizable", "originalAirDate", fallback: "Original Air Date") + /// Original aspect ratio + internal static let originalAspectRatio = L10n.tr("Localizable", "originalAspectRatio", fallback: "Original aspect ratio") + /// Original Title + internal static let originalTitle = L10n.tr("Localizable", "originalTitle", fallback: "Original Title") /// Other internal static let other = L10n.tr("Localizable", "other", fallback: "Other") /// Other User @@ -742,6 +828,8 @@ internal enum L10n { internal static func pageOfWithNumbers(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "pageOfWithNumbers", String(describing: p1), String(describing: p2), fallback: "Page %1$@ of %2$@") } + /// Parental Rating + internal static let parentalRating = L10n.tr("Localizable", "parentalRating", fallback: "Parental Rating") /// Password internal static let password = L10n.tr("Localizable", "password", fallback: "Password") /// User password has been changed. @@ -750,7 +838,7 @@ internal enum L10n { internal static let passwordChangeWarning = L10n.tr("Localizable", "passwordChangeWarning", fallback: "Changes the Jellyfin server user password. This does not change any Swiftfin settings.") /// New passwords do not match. internal static let passwordsDoNotMatch = L10n.tr("Localizable", "passwordsDoNotMatch", fallback: "New passwords do not match.") - /// Video Player Settings View - Pause on Background + /// Pause on background internal static let pauseOnBackground = L10n.tr("Localizable", "pauseOnBackground", fallback: "Pause on background") /// People internal static let people = L10n.tr("Localizable", "people", fallback: "People") @@ -760,11 +848,11 @@ internal enum L10n { internal static let play = L10n.tr("Localizable", "play", fallback: "Play") /// Play / Pause internal static let playAndPause = L10n.tr("Localizable", "playAndPause", fallback: "Play / Pause") - /// Video Player Settings View - Playback Header + /// Playback internal static let playback = L10n.tr("Localizable", "playback", fallback: "Playback") /// Playback Buttons internal static let playbackButtons = L10n.tr("Localizable", "playbackButtons", fallback: "Playback Buttons") - /// Section for Playback Quality Settings + /// Playback Quality internal static let playbackQuality = L10n.tr("Localizable", "playbackQuality", fallback: "Playback Quality") /// Playback settings internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings", fallback: "Playback settings") @@ -780,12 +868,14 @@ internal enum L10n { internal static let playNext = L10n.tr("Localizable", "playNext", fallback: "Play Next") /// Play Next Item internal static let playNextItem = L10n.tr("Localizable", "playNextItem", fallback: "Play Next Item") - /// Video Player Settings View - Play on Active + /// Play on active internal static let playOnActive = L10n.tr("Localizable", "playOnActive", fallback: "Play on active") /// Play Previous Item internal static let playPreviousItem = L10n.tr("Localizable", "playPreviousItem", fallback: "Play Previous Item") - /// Customize Server View - Posters + /// Posters internal static let posters = L10n.tr("Localizable", "posters", fallback: "Posters") + /// Premiere Date + internal static let premiereDate = L10n.tr("Localizable", "premiereDate", fallback: "Premiere Date") /// Present internal static let present = L10n.tr("Localizable", "present", fallback: "Present") /// Press Down for Menu @@ -794,7 +884,11 @@ internal enum L10n { internal static let previousItem = L10n.tr("Localizable", "previousItem", fallback: "Previous Item") /// Primary internal static let primary = L10n.tr("Localizable", "primary", fallback: "Primary") - /// PlaybackCompatibility Profile Sections + /// Production + internal static let production = L10n.tr("Localizable", "production", fallback: "Production") + /// Production Locations + internal static let productionLocations = L10n.tr("Localizable", "productionLocations", fallback: "Production Locations") + /// Profiles internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles") /// Programs internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs") @@ -820,10 +914,16 @@ internal enum L10n { internal static let quickConnectSuccessMessage = L10n.tr("Localizable", "quickConnectSuccessMessage", fallback: "Authorizing Quick Connect successful. Please continue on your other device.") /// Random internal static let random = L10n.tr("Localizable", "random", fallback: "Random") - /// Customize Server View - Random Image + /// Random Image internal static let randomImage = L10n.tr("Localizable", "randomImage", fallback: "Random Image") /// Rated internal static let rated = L10n.tr("Localizable", "rated", fallback: "Rated") + /// Rating + internal static let rating = L10n.tr("Localizable", "rating", fallback: "Rating") + /// %@ rating on a scale from 1 to 10. + internal static func ratingDescription(_ p1: Any) -> String { + return L10n.tr("Localizable", "ratingDescription", String(describing: p1), fallback: "%@ rating on a scale from 1 to 10.") + } /// Ratings internal static let ratings = L10n.tr("Localizable", "ratings", fallback: "Ratings") /// Recently Added @@ -832,16 +932,20 @@ internal enum L10n { internal static let recommended = L10n.tr("Localizable", "recommended", fallback: "Recommended") /// Red internal static let red = L10n.tr("Localizable", "red", fallback: "Red") - /// TranscodeReason - Reference Frames Not Supported + /// The number of reference frames is not supported internal static let refFramesNotSupported = L10n.tr("Localizable", "refFramesNotSupported", fallback: "The number of reference frames is not supported") /// Refresh internal static let refresh = L10n.tr("Localizable", "refresh", fallback: "Refresh") /// Refresh Metadata internal static let refreshMetadata = L10n.tr("Localizable", "refreshMetadata", fallback: "Refresh Metadata") + /// Regional + internal static let regional = L10n.tr("Localizable", "regional", fallback: "Regional") /// Regular internal static let regular = L10n.tr("Localizable", "regular", fallback: "Regular") /// Released internal static let released = L10n.tr("Localizable", "released", fallback: "Released") + /// Release Date + internal static let releaseDate = L10n.tr("Localizable", "releaseDate", fallback: "Release Date") /// Reload internal static let reload = L10n.tr("Localizable", "reload", fallback: "Reload") /// Remaining Time @@ -860,7 +964,7 @@ internal enum L10n { internal static let removeAllUsers = L10n.tr("Localizable", "removeAllUsers", fallback: "Remove All Users") /// Remove From Resume internal static let removeFromResume = L10n.tr("Localizable", "removeFromResume", fallback: "Remove From Resume") - /// PlayMethod - Remux + /// Remux internal static let remux = L10n.tr("Localizable", "remux", fallback: "Remux") /// Replace All internal static let replaceAll = L10n.tr("Localizable", "replaceAll", fallback: "Replace All") @@ -886,39 +990,43 @@ internal enum L10n { internal static let resetAppSettings = L10n.tr("Localizable", "resetAppSettings", fallback: "Reset App Settings") /// Reset User Settings internal static let resetUserSettings = L10n.tr("Localizable", "resetUserSettings", fallback: "Reset User Settings") - /// Restart Server Label + /// Restart Server internal static let restartServer = L10n.tr("Localizable", "restartServer", fallback: "Restart Server") - /// Restart Warning Label + /// Are you sure you want to restart the server? internal static let restartWarning = L10n.tr("Localizable", "restartWarning", fallback: "Are you sure you want to restart the server?") - /// Video Player Settings View - Resume + /// Resume internal static let resume = L10n.tr("Localizable", "resume", fallback: "Resume") /// Resume 5 Second Offset internal static let resume5SecondOffset = L10n.tr("Localizable", "resume5SecondOffset", fallback: "Resume 5 Second Offset") /// Resume Offset internal static let resumeOffset = L10n.tr("Localizable", "resumeOffset", fallback: "Resume Offset") - /// Video Player Settings View - Resume Offset Description + /// Resume content seconds before the recorded resume time internal static let resumeOffsetDescription = L10n.tr("Localizable", "resumeOffsetDescription", fallback: "Resume content seconds before the recorded resume time") - /// Video Player Settings View - Resume Offset Title + /// Resume Offset internal static let resumeOffsetTitle = L10n.tr("Localizable", "resumeOffsetTitle", fallback: "Resume Offset") /// Retrieving media information internal static let retrievingMediaInformation = L10n.tr("Localizable", "retrievingMediaInformation", fallback: "Retrieving media information") /// Retry internal static let retry = L10n.tr("Localizable", "retry", fallback: "Retry") + /// Reviews + internal static let reviews = L10n.tr("Localizable", "reviews", fallback: "Reviews") /// Right internal static let `right` = L10n.tr("Localizable", "right", fallback: "Right") /// Role internal static let role = L10n.tr("Localizable", "role", fallback: "Role") - /// Button label to run a task + /// Run internal static let run = L10n.tr("Localizable", "run", fallback: "Run") - /// Status label for when a task is running + /// Running... internal static let running = L10n.tr("Localizable", "running", fallback: "Running...") + /// Run Time + internal static let runTime = L10n.tr("Localizable", "runTime", fallback: "Run Time") /// Runtime internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime") /// Save internal static let save = L10n.tr("Localizable", "save", fallback: "Save") - /// Administration Dashboard Scan All Libraries Button + /// Scan All Libraries internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries") - /// Administration Dashboard Scheduled Tasks + /// Scheduled Tasks internal static let scheduledTasks = L10n.tr("Localizable", "scheduledTasks", fallback: "Scheduled Tasks") /// Scrub Current Time internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time") @@ -936,7 +1044,7 @@ internal enum L10n { } /// Seasons internal static let seasons = L10n.tr("Localizable", "seasons", fallback: "Seasons") - /// TranscodeReason - Secondary Audio Not Supported + /// Secondary audio is not supported internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported") /// See All internal static let seeAll = L10n.tr("Localizable", "seeAll", fallback: "See All") @@ -966,9 +1074,9 @@ internal enum L10n { internal static let serverDetails = L10n.tr("Localizable", "serverDetails", fallback: "Server Details") /// Server Information internal static let serverInformation = L10n.tr("Localizable", "serverInformation", fallback: "Server Information") - /// Title for the server logs section + /// Server Logs internal static let serverLogs = L10n.tr("Localizable", "serverLogs", fallback: "Server Logs") - /// Select Server View + /// Servers internal static let servers = L10n.tr("Localizable", "servers", fallback: "Servers") /// A new trigger was created for '%1$@'. internal static func serverTriggerCreated(_ p1: Any) -> String { @@ -980,7 +1088,7 @@ internal enum L10n { } /// Server URL internal static let serverURL = L10n.tr("Localizable", "serverURL", fallback: "Server URL") - /// The title for the session view + /// Session internal static let session = L10n.tr("Localizable", "session", fallback: "Session") /// Sessions internal static let sessions = L10n.tr("Localizable", "sessions", fallback: "Sessions") @@ -990,9 +1098,9 @@ internal enum L10n { internal static let showCastAndCrew = L10n.tr("Localizable", "showCastAndCrew", fallback: "Show Cast & Crew") /// Show Chapters Info In Bottom Overlay internal static let showChaptersInfoInBottomOverlay = L10n.tr("Localizable", "showChaptersInfoInBottomOverlay", fallback: "Show Chapters Info In Bottom Overlay") - /// Indicators View - Show Favorited + /// Show Favorited internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited") - /// Customize Server View - Show Favorites + /// Show Favorites internal static let showFavorites = L10n.tr("Localizable", "showFavorites", fallback: "Show Favorites") /// Flatten Library Items internal static let showFlattenView = L10n.tr("Localizable", "showFlattenView", fallback: "Flatten Library Items") @@ -1002,17 +1110,17 @@ internal enum L10n { internal static let showMissingSeasons = L10n.tr("Localizable", "showMissingSeasons", fallback: "Show Missing Seasons") /// Show Poster Labels internal static let showPosterLabels = L10n.tr("Localizable", "showPosterLabels", fallback: "Show Poster Labels") - /// Indicators View - Show Progress + /// Show Progress internal static let showProgress = L10n.tr("Localizable", "showProgress", fallback: "Show Progress") - /// Customize Server View - Show Recently Added + /// Show Recently Added internal static let showRecentlyAdded = L10n.tr("Localizable", "showRecentlyAdded", fallback: "Show Recently Added") - /// Indicators View - Show Unwatched + /// Show Unwatched internal static let showUnwatched = L10n.tr("Localizable", "showUnwatched", fallback: "Show Unwatched") - /// Indicators View - Show Watched + /// Show Watched internal static let showWatched = L10n.tr("Localizable", "showWatched", fallback: "Show Watched") - /// Shutdown Server Label + /// Shutdown Server internal static let shutdownServer = L10n.tr("Localizable", "shutdownServer", fallback: "Shutdown Server") - /// Shutdown Warning Label + /// Are you sure you want to shutdown the server? internal static let shutdownWarning = L10n.tr("Localizable", "shutdownWarning", fallback: "Are you sure you want to shutdown the server?") /// Signed in as %@ internal static func signedInAsWithString(_ p1: Any) -> String { @@ -1040,6 +1148,10 @@ internal enum L10n { internal static let sort = L10n.tr("Localizable", "sort", fallback: "Sort") /// Sort by internal static let sortBy = L10n.tr("Localizable", "sortBy", fallback: "Sort by") + /// Sort Name + internal static let sortName = L10n.tr("Localizable", "sortName", fallback: "Sort Name") + /// Sort Title + internal static let sortTitle = L10n.tr("Localizable", "sortTitle", fallback: "Sort Title") /// Source Code internal static let sourceCode = L10n.tr("Localizable", "sourceCode", fallback: "Source Code") /// Special Features @@ -1048,9 +1160,11 @@ internal enum L10n { internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports") /// Status internal static let status = L10n.tr("Localizable", "status", fallback: "Status") - /// Button label to stop a task + /// Stop internal static let stop = L10n.tr("Localizable", "stop", fallback: "Stop") - /// Session Streaming Clients + /// Story Arc + 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") @@ -1058,7 +1172,7 @@ internal enum L10n { internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios") /// Subtitle internal static let subtitle = L10n.tr("Localizable", "subtitle", fallback: "Subtitle") - /// TranscodeReason - Subtitle Codec Not Supported + /// The subtitle codec is not supported internal static let subtitleCodecNotSupported = L10n.tr("Localizable", "subtitleCodecNotSupported", fallback: "The subtitle codec is not supported") /// Subtitle Color internal static let subtitleColor = L10n.tr("Localizable", "subtitleColor", fallback: "Subtitle Color") @@ -1068,7 +1182,7 @@ internal enum L10n { internal static let subtitleOffset = L10n.tr("Localizable", "subtitleOffset", fallback: "Subtitle Offset") /// Subtitles internal static let subtitles = L10n.tr("Localizable", "subtitles", fallback: "Subtitles") - /// Video Player Settings View - Disclaimer + /// Settings only affect some subtitle types internal static let subtitlesDisclaimer = L10n.tr("Localizable", "subtitlesDisclaimer", fallback: "Settings only affect some subtitle types") /// Subtitle Size internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: "Subtitle Size") @@ -1092,21 +1206,25 @@ internal enum L10n { internal static let system = L10n.tr("Localizable", "system", fallback: "System") /// System Control Gestures Enabled internal static let systemControlGesturesEnabled = L10n.tr("Localizable", "systemControlGesturesEnabled", fallback: "System Control Gestures Enabled") + /// Tagline + internal static let tagline = L10n.tr("Localizable", "tagline", fallback: "Tagline") + /// Taglines + internal static let taglines = L10n.tr("Localizable", "taglines", fallback: "Taglines") /// Tags internal static let tags = L10n.tr("Localizable", "tags", fallback: "Tags") - /// The navigation title for the task view + /// Task internal static let task = L10n.tr("Localizable", "task", fallback: "Task") - /// Status message for an aborted task + /// Aborted internal static let taskAborted = L10n.tr("Localizable", "taskAborted", fallback: "Aborted") - /// Status message for a cancelled task + /// Cancelled internal static let taskCancelled = L10n.tr("Localizable", "taskCancelled", fallback: "Cancelled") - /// Status message for a completed task + /// Completed internal static let taskCompleted = L10n.tr("Localizable", "taskCompleted", fallback: "Completed") - /// Status message for a failed task + /// Failed internal static let taskFailed = L10n.tr("Localizable", "taskFailed", fallback: "Failed") - /// Title for the tasks section + /// Tasks internal static let tasks = L10n.tr("Localizable", "tasks", fallback: "Tasks") - /// Description for the tasks section + /// Tasks are operations that are scheduled to run periodically or can be triggered manually. internal static let tasksDescription = L10n.tr("Localizable", "tasksDescription", fallback: "Tasks are operations that are scheduled to run periodically or can be triggered manually.") /// Sets the duration (in minutes) in between task triggers. internal static let taskTriggerInterval = L10n.tr("Localizable", "taskTriggerInterval", fallback: "Sets the duration (in minutes) in between task triggers.") @@ -1114,7 +1232,7 @@ internal enum L10n { internal static let taskTriggerTimeLimit = L10n.tr("Localizable", "taskTriggerTimeLimit", fallback: "Sets the maximum runtime (in hours) for this task trigger.") /// Tbps internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps") - /// Option to set the test size for bitrate testing + /// Test Size internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size") /// Time internal static let time = L10n.tr("Localizable", "time", fallback: "Time") @@ -1132,13 +1250,15 @@ internal enum L10n { internal static let timestamp = L10n.tr("Localizable", "timestamp", fallback: "Timestamp") /// Timestamp Type internal static let timestampType = L10n.tr("Localizable", "timestampType", fallback: "Timestamp Type") + /// Title + internal static let title = L10n.tr("Localizable", "title", fallback: "Title") /// Too Many Redirects internal static let tooManyRedirects = L10n.tr("Localizable", "tooManyRedirects", fallback: "Too Many Redirects") /// Trailing Value internal static let trailingValue = L10n.tr("Localizable", "trailingValue", fallback: "Trailing Value") - /// PlayMethod - Transcode + /// Transcode internal static let transcode = L10n.tr("Localizable", "transcode", fallback: "Transcode") - /// Transcode Reason(s) Section Label + /// Transcode Reason(s) internal static let transcodeReasons = L10n.tr("Localizable", "transcodeReasons", fallback: "Transcode Reason(s)") /// Transition internal static let transition = L10n.tr("Localizable", "transition", fallback: "Transition") @@ -1148,9 +1268,11 @@ internal enum L10n { internal static let triggers = L10n.tr("Localizable", "triggers", fallback: "Triggers") /// Try again internal static let tryAgain = L10n.tr("Localizable", "tryAgain", fallback: "Try again") + /// TV + internal static let tv = L10n.tr("Localizable", "tv", fallback: "TV") /// TV Shows internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows") - /// Indicate a type + /// Type internal static let type = L10n.tr("Localizable", "type", fallback: "Type") /// Unable to connect to server internal static let unableToConnectServer = L10n.tr("Localizable", "unableToConnectServer", fallback: "Unable to connect to server") @@ -1164,11 +1286,11 @@ internal enum L10n { internal static let unauthorizedUser = L10n.tr("Localizable", "unauthorizedUser", fallback: "Unauthorized user") /// Unknown internal static let unknown = L10n.tr("Localizable", "unknown", fallback: "Unknown") - /// TranscodeReason - Unknown Audio Stream Info + /// The audio stream information is unknown internal static let unknownAudioStreamInfo = L10n.tr("Localizable", "unknownAudioStreamInfo", fallback: "The audio stream information is unknown") /// Unknown Error internal static let unknownError = L10n.tr("Localizable", "unknownError", fallback: "Unknown Error") - /// TranscodeReason - Unknown Video Stream Info + /// The video stream information is unknown internal static let unknownVideoStreamInfo = L10n.tr("Localizable", "unknownVideoStreamInfo", fallback: "The video stream information is unknown") /// Unlimited internal static let unlimited = L10n.tr("Localizable", "unlimited", fallback: "Unlimited") @@ -1178,11 +1300,13 @@ internal enum L10n { internal static let unlimitedFailedLoginDescription = L10n.tr("Localizable", "unlimitedFailedLoginDescription", fallback: "Allows unlimited failed login attempts without locking the user.") /// Unplayed internal static let unplayed = L10n.tr("Localizable", "unplayed", fallback: "Unplayed") + /// Unreleased + internal static let unreleased = L10n.tr("Localizable", "unreleased", fallback: "Unreleased") /// You have unsaved changes. Are you sure you want to discard them? internal static let unsavedChangesMessage = L10n.tr("Localizable", "unsavedChangesMessage", fallback: "You have unsaved changes. Are you sure you want to discard them?") /// URL internal static let url = L10n.tr("Localizable", "url", fallback: "URL") - /// Override Transcoding Profile + /// Use as Transcoding Profile internal static let useAsTranscodingProfile = L10n.tr("Localizable", "useAsTranscodingProfile", fallback: "Use as Transcoding Profile") /// Use Primary Image internal static let usePrimaryImage = L10n.tr("Localizable", "usePrimaryImage", fallback: "Use Primary Image") @@ -1204,27 +1328,27 @@ internal enum L10n { internal static let version = L10n.tr("Localizable", "version", fallback: "Version") /// Video internal static let video = L10n.tr("Localizable", "video", fallback: "Video") - /// TranscodeReason - Video Bit Depth Not Supported + /// The video bit depth is not supported internal static let videoBitDepthNotSupported = L10n.tr("Localizable", "videoBitDepthNotSupported", fallback: "The video bit depth is not supported") - /// TranscodeReason - Video Bitrate Not Supported + /// The video bitrate is not supported internal static let videoBitrateNotSupported = L10n.tr("Localizable", "videoBitrateNotSupported", fallback: "The video bitrate is not supported") - /// TranscodeReason - Video Codec Not Supported + /// The video codec is not supported internal static let videoCodecNotSupported = L10n.tr("Localizable", "videoCodecNotSupported", fallback: "The video codec is not supported") - /// TranscodeReason - Video Framerate Not Supported + /// The video framerate is not supported internal static let videoFramerateNotSupported = L10n.tr("Localizable", "videoFramerateNotSupported", fallback: "The video framerate is not supported") - /// TranscodeReason - Video Level Not Supported + /// The video level is not supported internal static let videoLevelNotSupported = L10n.tr("Localizable", "videoLevelNotSupported", fallback: "The video level is not supported") - /// Settings View - Video Player + /// Video Player internal static let videoPlayer = L10n.tr("Localizable", "videoPlayer", fallback: "Video Player") /// Video Player Type internal static let videoPlayerType = L10n.tr("Localizable", "videoPlayerType", fallback: "Video Player Type") - /// TranscodeReason - Video Profile Not Supported + /// The video profile is not supported internal static let videoProfileNotSupported = L10n.tr("Localizable", "videoProfileNotSupported", fallback: "The video profile is not supported") - /// TranscodeReason - Video Range Type Not Supported + /// The video range type is not supported internal static let videoRangeTypeNotSupported = L10n.tr("Localizable", "videoRangeTypeNotSupported", fallback: "The video range type is not supported") /// Video remuxing internal static let videoRemuxing = L10n.tr("Localizable", "videoRemuxing", fallback: "Video remuxing") - /// TranscodeReason - Video Resolution Not Supported + /// The video resolution is not supported internal static let videoResolutionNotSupported = L10n.tr("Localizable", "videoResolutionNotSupported", fallback: "The video resolution is not supported") /// Video transcoding internal static let videoTranscoding = L10n.tr("Localizable", "videoTranscoding", fallback: "Video transcoding") @@ -1234,6 +1358,10 @@ internal enum L10n { internal static let whosWatching = L10n.tr("Localizable", "WhosWatching", fallback: "Who's watching?") /// WIP internal static let wip = L10n.tr("Localizable", "wip", fallback: "WIP") + /// Year + internal static let year = L10n.tr("Localizable", "year", fallback: "Year") + /// Years + internal static let years = L10n.tr("Localizable", "years", fallback: "Years") /// Yellow internal static let yellow = L10n.tr("Localizable", "yellow", fallback: "Yellow") /// Yes diff --git a/Shared/ViewModels/ItemEditorViewModel/ItemEditorViewModel.swift b/Shared/ViewModels/ItemEditorViewModel/ItemEditorViewModel.swift new file mode 100644 index 00000000..146c52d5 --- /dev/null +++ b/Shared/ViewModels/ItemEditorViewModel/ItemEditorViewModel.swift @@ -0,0 +1,195 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI +import OrderedCollections + +class ItemEditorViewModel: ViewModel, Stateful, Eventful { + + // MARK: - Events + + enum Event: Equatable { + case updated + case error(JellyfinAPIError) + } + + // MARK: - Actions + + enum Action: Equatable { + case add([ItemType]) + case remove([ItemType]) + case update(BaseItemDto) + } + + // MARK: BackgroundState + + enum BackgroundState: Hashable { + case refreshing + } + + // MARK: - State + + enum State: Hashable { + case initial + case error(JellyfinAPIError) + case updating + } + + @Published + var backgroundStates: OrderedSet = [] + + @Published + var item: BaseItemDto + + @Published + var state: State = .initial + + private var task: AnyCancellable? + private let eventSubject = PassthroughSubject() + + var events: AnyPublisher { + eventSubject.receive(on: RunLoop.main).eraseToAnyPublisher() + } + + // MARK: - Init + + init(item: BaseItemDto) { + self.item = item + super.init() + } + + // MARK: - Respond to Actions + + func respond(to action: Action) -> State { + switch action { + case let .add(items): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { self.state = .updating } + + try await self.addComponents(items) + + await MainActor.run { + self.state = .initial + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + + return state + + case let .remove(items): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { self.state = .updating } + + try await self.removeComponents(items) + + await MainActor.run { + self.state = .initial + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + + return state + + case let .update(newItem): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { self.state = .updating } + + try await self.updateItem(newItem) + + await MainActor.run { + self.state = .initial + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + + return state + } + } + + // MARK: - Save Updated Item to Server + + func updateItem(_ newItem: BaseItemDto, refresh: Bool = false) async throws { + guard let itemId = item.id else { return } + + let request = Paths.updateItem(itemID: itemId, newItem) + _ = try await userSession.client.send(request) + + if refresh { + try await refreshItem() + } + + await MainActor.run { + Notifications[.itemMetadataDidChange].post(object: newItem) + } + } + + // MARK: - Refresh Item + + private func refreshItem() async throws { + guard let itemId = item.id else { return } + + await MainActor.run { + _ = backgroundStates.append(.refreshing) + } + + let request = Paths.getItem(userID: userSession.user.id, itemID: itemId) + let response = try await userSession.client.send(request) + + await MainActor.run { + self.item = response.value + _ = backgroundStates.remove(.refreshing) + } + } + + // MARK: - Add Items (To be overridden) + + func addComponents(_ items: [ItemType]) async throws { + fatalError("This method should be overridden in subclasses") + } + + // MARK: - Remove Items (To be overridden) + + func removeComponents(_ items: [ItemType]) async throws { + fatalError("This method should be overridden in subclasses") + } +} diff --git a/Shared/ViewModels/ItemEditorViewModel/RefreshMetadataViewModel.swift b/Shared/ViewModels/ItemEditorViewModel/RefreshMetadataViewModel.swift index 575c9b1e..5d3e9014 100644 --- a/Shared/ViewModels/ItemEditorViewModel/RefreshMetadataViewModel.swift +++ b/Shared/ViewModels/ItemEditorViewModel/RefreshMetadataViewModel.swift @@ -169,7 +169,7 @@ class RefreshMetadataViewModel: ViewModel, Stateful, Eventful { self.item = response.value self.progress = 0.0 - Notifications[.itemMetadataDidChange].post(object: itemId) + Notifications[.itemMetadataDidChange].post(object: item) } } } diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index 2291f664..7dfa36f8 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -22,6 +22,7 @@ class ItemViewModel: ViewModel, Stateful { case backgroundRefresh case error(JellyfinAPIError) case refresh + case replace(BaseItemDto) case toggleIsFavorite case toggleIsPlayed } @@ -91,16 +92,19 @@ class ItemViewModel: ViewModel, Stateful { // TODO: should replace with a more robust "PlaybackManager" Notifications[.itemMetadataDidChange].publisher .sink { [weak self] notification in - - guard let userInfo = notification.object as? [String: String] else { return } - - if let itemID = userInfo["itemID"], itemID == item.id { - Task { [weak self] in - await self?.send(.backgroundRefresh) + if let userInfo = notification.object as? [String: String] { + if let itemID = userInfo["itemID"], itemID == item.id { + Task { [weak self] in + await self?.send(.backgroundRefresh) + } + } else if let seriesID = userInfo["seriesID"], seriesID == item.id { + Task { [weak self] in + await self?.send(.backgroundRefresh) + } } - } else if let seriesID = userInfo["seriesID"], seriesID == item.id { + } else if let newItem = notification.object as? BaseItemDto, newItem.id == self?.item.id { Task { [weak self] in - await self?.send(.backgroundRefresh) + await self?.send(.replace(newItem)) } } } @@ -195,6 +199,22 @@ class ItemViewModel: ViewModel, Stateful { .asAnyCancellable() return .refreshing + case let .replace(newItem): + + backgroundStates.append(.refresh) + + Task { [weak self] in + guard let self else { return } + do { + await MainActor.run { + self.backgroundStates.remove(.refresh) + self.item = newItem + } + } + } + .store(in: &cancellables) + + return state case .toggleIsFavorite: toggleIsFavoriteTask?.cancel() diff --git a/Shared/ViewModels/ParentalRatingsViewModel.swift b/Shared/ViewModels/ParentalRatingsViewModel.swift new file mode 100644 index 00000000..4dd943c7 --- /dev/null +++ b/Shared/ViewModels/ParentalRatingsViewModel.swift @@ -0,0 +1,81 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI + +final class ParentalRatingsViewModel: ViewModel, Stateful { + + // MARK: Action + + enum Action: Equatable { + case refresh + } + + // MARK: State + + enum State: Hashable { + case content + case error(JellyfinAPIError) + case initial + case refreshing + } + + @Published + private(set) var parentalRatings: [ParentalRating] = [] + + @Published + final var state: State = .initial + + private var currentRefreshTask: AnyCancellable? + + var hasNoResults: Bool { + parentalRatings.isEmpty + } + + func respond(to action: Action) -> State { + switch action { + case .refresh: + currentRefreshTask?.cancel() + + currentRefreshTask = Task { [weak self] in + guard let self else { return } + + do { + let parentalRatings = try await getParentalRatings() + + guard !Task.isCancelled else { return } + + await MainActor.run { + self.parentalRatings = parentalRatings + self.state = .content + } + } catch { + guard !Task.isCancelled else { return } + + await MainActor.run { + self.state = .error(.init(error.localizedDescription)) + } + } + } + .asAnyCancellable() + + return state + } + } + + // MARK: - Fetch Parental Ratings + + private func getParentalRatings() async throws -> [ParentalRating] { + let request = Paths.getParentalRatings + let response = try await userSession.client.send(request) + + return response.value + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index fc719264..184f58ec 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -79,6 +79,35 @@ 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; 4E63B9FA2C8A5BEF00C25378 /* AdminDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */; }; 4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; }; + 4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; }; + 4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */; }; + 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A002CEFE39900025C99 /* EditMetadataView.swift */; }; + 4E661A0F2CEFE46300025C99 /* SeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0D2CEFE46300025C99 /* SeriesSection.swift */; }; + 4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0E2CEFE46300025C99 /* TitleSection.swift */; }; + 4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A082CEFE46300025C99 /* LockMetadataSection.swift */; }; + 4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A092CEFE46300025C99 /* MediaFormatSection.swift */; }; + 4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A062CEFE46300025C99 /* EpisodeSection.swift */; }; + 4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A052CEFE46300025C99 /* DisplayOrderSection.swift */; }; + 4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A072CEFE46300025C99 /* LocalizationSection.swift */; }; + 4E661A162CEFE46300025C99 /* ParentialRatingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0B2CEFE46300025C99 /* ParentialRatingsSection.swift */; }; + 4E661A172CEFE46300025C99 /* OverviewSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0A2CEFE46300025C99 /* OverviewSection.swift */; }; + 4E661A182CEFE46300025C99 /* ReviewsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A0C2CEFE46300025C99 /* ReviewsSection.swift */; }; + 4E661A192CEFE46300025C99 /* DateSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A042CEFE46300025C99 /* DateSection.swift */; }; + 4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */; }; + 4E661A1C2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */; }; + 4E661A1F2CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */; }; + 4E661A202CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */; }; + 4E661A222CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */; }; + 4E661A232CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */; }; + 4E661A252CEFE64500025C99 /* CountryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A242CEFE64200025C99 /* CountryPicker.swift */; }; + 4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A262CEFE64D00025C99 /* LanguagePicker.swift */; }; + 4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A282CEFE68100025C99 /* Video3DFormatPicker.swift */; }; + 4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */; }; + 4E661A2C2CEFE6F400025C99 /* Video3DFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */; }; + 4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2D2CEFE77700025C99 /* MetadataField.swift */; }; + 4E661A2F2CEFE77700025C99 /* MetadataField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A2D2CEFE77700025C99 /* MetadataField.swift */; }; + 4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A302CEFE7B900025C99 /* SeriesStatus.swift */; }; + 4E661A322CEFE7BC00025C99 /* SeriesStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E661A302CEFE7B900025C99 /* SeriesStatus.swift */; }; 4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BB82CB33FB5007CBD5D /* HomeSection.swift */; }; 4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BBF2CB34775007CBD5D /* HomeSection.swift */; }; 4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */; }; @@ -149,6 +178,7 @@ 4EED874A2CBF824B002354D2 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87462CBF824B002354D2 /* DeviceRow.swift */; }; 4EED874B2CBF824B002354D2 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED87482CBF824B002354D2 /* DevicesView.swift */; }; 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */; }; + 4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */; }; 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 */; }; @@ -1146,6 +1176,28 @@ 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdminDashboardView.swift; sourceTree = ""; }; 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = ""; }; + 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorViewModel.swift; sourceTree = ""; }; + 4E661A002CEFE39900025C99 /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; + 4E661A042CEFE46300025C99 /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = ""; }; + 4E661A052CEFE46300025C99 /* DisplayOrderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayOrderSection.swift; sourceTree = ""; }; + 4E661A062CEFE46300025C99 /* EpisodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeSection.swift; sourceTree = ""; }; + 4E661A072CEFE46300025C99 /* LocalizationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationSection.swift; sourceTree = ""; }; + 4E661A082CEFE46300025C99 /* LockMetadataSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockMetadataSection.swift; sourceTree = ""; }; + 4E661A092CEFE46300025C99 /* MediaFormatSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFormatSection.swift; sourceTree = ""; }; + 4E661A0A2CEFE46300025C99 /* OverviewSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverviewSection.swift; sourceTree = ""; }; + 4E661A0B2CEFE46300025C99 /* ParentialRatingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentialRatingsSection.swift; sourceTree = ""; }; + 4E661A0C2CEFE46300025C99 /* ReviewsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewsSection.swift; sourceTree = ""; }; + 4E661A0D2CEFE46300025C99 /* SeriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesSection.swift; sourceTree = ""; }; + 4E661A0E2CEFE46300025C99 /* TitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleSection.swift; sourceTree = ""; }; + 4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxSetDisplayOrder.swift; sourceTree = ""; }; + 4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesDisplayOrder.swift; sourceTree = ""; }; + 4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentalRatingsViewModel.swift; sourceTree = ""; }; + 4E661A242CEFE64200025C99 /* CountryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryPicker.swift; sourceTree = ""; }; + 4E661A262CEFE64D00025C99 /* LanguagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguagePicker.swift; sourceTree = ""; }; + 4E661A282CEFE68100025C99 /* Video3DFormatPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video3DFormatPicker.swift; sourceTree = ""; }; + 4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video3DFormat.swift; sourceTree = ""; }; + 4E661A2D2CEFE77700025C99 /* MetadataField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetadataField.swift; sourceTree = ""; }; + 4E661A302CEFE7B900025C99 /* SeriesStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesStatus.swift; sourceTree = ""; }; 4E699BB82CB33FB5007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = ""; }; 4E699BBF2CB34775007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = ""; }; 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = ""; }; @@ -1205,6 +1257,7 @@ 4EED87462CBF824B002354D2 /* DeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRow.swift; sourceTree = ""; }; 4EED87482CBF824B002354D2 /* DevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesView.swift; sourceTree = ""; }; 4EED874F2CBF84AD002354D2 /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = ""; }; + 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = ""; }; 4EF18B252CB9934700343666 /* LibraryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryRow.swift; sourceTree = ""; }; 4EF18B272CB9936400343666 /* ListColumnsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListColumnsPickerView.swift; sourceTree = ""; }; 4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; @@ -2123,6 +2176,50 @@ path = AdminDashboardView; sourceTree = ""; }; + 4E6619FF2CEFE39000025C99 /* EditMetadataView */ = { + isa = PBXGroup; + children = ( + 4E661A022CEFE42200025C99 /* Components */, + 4E661A002CEFE39900025C99 /* EditMetadataView.swift */, + ); + path = EditMetadataView; + sourceTree = ""; + }; + 4E661A022CEFE42200025C99 /* Components */ = { + isa = PBXGroup; + children = ( + 4E661A032CEFE42800025C99 /* Sections */, + ); + path = Components; + sourceTree = ""; + }; + 4E661A032CEFE42800025C99 /* Sections */ = { + isa = PBXGroup; + children = ( + 4E661A042CEFE46300025C99 /* DateSection.swift */, + 4E661A052CEFE46300025C99 /* DisplayOrderSection.swift */, + 4E661A062CEFE46300025C99 /* EpisodeSection.swift */, + 4E661A072CEFE46300025C99 /* LocalizationSection.swift */, + 4E661A082CEFE46300025C99 /* LockMetadataSection.swift */, + 4E661A092CEFE46300025C99 /* MediaFormatSection.swift */, + 4E661A0A2CEFE46300025C99 /* OverviewSection.swift */, + 4E661A0B2CEFE46300025C99 /* ParentialRatingsSection.swift */, + 4E661A0C2CEFE46300025C99 /* ReviewsSection.swift */, + 4E661A0D2CEFE46300025C99 /* SeriesSection.swift */, + 4E661A0E2CEFE46300025C99 /* TitleSection.swift */, + ); + path = Sections; + sourceTree = ""; + }; + 4E661A1D2CEFE55200025C99 /* DisplayOrder */ = { + isa = PBXGroup; + children = ( + 4E661A1A2CEFE53A00025C99 /* BoxSetDisplayOrder.swift */, + 4E661A1E2CEFE56400025C99 /* SeriesDisplayOrder.swift */, + ); + path = DisplayOrder; + sourceTree = ""; + }; 4E699BB52CB33F4B007CBD5D /* CustomizeViewsSettings */ = { isa = PBXGroup; children = ( @@ -2188,6 +2285,7 @@ isa = PBXGroup; children = ( 4E8F74A62CE03D4C00CC8969 /* Components */, + 4E6619FF2CEFE39000025C99 /* EditMetadataView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, ); path = ItemEditorView; @@ -2205,6 +2303,7 @@ isa = PBXGroup; children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, + 4E6619FB2CEFE2B500025C99 /* ItemEditorViewModel.swift */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, ); path = ItemEditorViewModel; @@ -2456,6 +2555,7 @@ E1EDA8D52B924CA500F9A57E /* LibraryViewModel */, C45C36532A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift */, E1CAF65C2BA345830087D991 /* MediaViewModel */, + 4E661A212CEFE60C00025C99 /* ParentalRatingsViewModel.swift */, E10231472BCF8A6D009D71FC /* ProgramsViewModel.swift */, 6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */, E1BCDB4E2BE1F491009F6744 /* ResetUserPasswordViewModel.swift */, @@ -2564,6 +2664,7 @@ E1F5CF042CB09EA000607465 /* CurrentDate.swift */, 4E2AC4BD2C6C48D200DD600D /* CustomDeviceProfileAction.swift */, E17FB55128C119D400311DFE /* Displayable.swift */, + 4E661A1D2CEFE55200025C99 /* DisplayOrder */, E1579EA62B97DC1500A31CA1 /* Eventful.swift */, E1092F4B29106F9F00163F57 /* GestureAction.swift */, E1D37F4A2B9CEA5C00343D2B /* ImageSource.swift */, @@ -2584,6 +2685,7 @@ E1E9017A28DAAE4D001B1594 /* RoundedCorner.swift */, E18ACA8A2A14301800BB4F35 /* ScalingButtonStyle.swift */, E164A7F52BE4814700A54B18 /* SelectUserServerSelection.swift */, + 4E661A302CEFE7B900025C99 /* SeriesStatus.swift */, E129429228F2845000796AC6 /* SliderType.swift */, E11042742B8013DF00821020 /* Stateful.swift */, E149CCAC2BE6ECC8008B9331 /* Storable.swift */, @@ -2842,6 +2944,7 @@ children = ( E1D8429429346C6400D1041A /* BasicStepper.swift */, E133328C2953AE4B00EE76AB /* CircularProgressView.swift */, + 4E661A242CEFE64200025C99 /* CountryPicker.swift */, E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */, E18E01A7288746AF0022598C /* DotHStack.swift */, E1DE2B492B97ECB900F6715F /* ErrorView.swift */, @@ -2849,6 +2952,7 @@ E178B0752BE435D70023651B /* HourMinutePicker.swift */, E1DC7AC92C63337C00AEE368 /* iOS15View.swift */, E1FE69A928C29CC20021BC93 /* LandscapePosterProgressBar.swift */, + 4E661A262CEFE64D00025C99 /* LanguagePicker.swift */, 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */, 4E16FD4E2C0183B500110147 /* LetterPickerBar */, E1A8FDEB2C0574A800D0A51C /* ListRow.swift */, @@ -2866,6 +2970,7 @@ E1581E26291EF59800D6C640 /* SplitContentView.swift */, E1D27EE62BBC955F00152D16 /* UnmaskSecureField.swift */, E157562F29355B7900976E1F /* UpdateView.swift */, + 4E661A282CEFE68100025C99 /* Video3DFormatPicker.swift */, ); path = Components; sourceTree = ""; @@ -3979,6 +4084,7 @@ 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */, E1F5F9B12BA0200500BA5014 /* MediaSourceInfo */, E122A9122788EAAD0060FA63 /* MediaStream.swift */, + 4E661A2D2CEFE77700025C99 /* MetadataField.swift */, E1AD105E26D9ADDD003E4A08 /* NameGuidPair.swift */, E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */, 4E2182E42CAF67EF0094806B /* PlayMethod.swift */, @@ -3994,6 +4100,7 @@ 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */, E1CB757B2C80F00D00217C76 /* TranscodingProfile.swift */, E18CE0B128A229E70092E7F1 /* UserDto.swift */, + 4E661A2A2CEFE6F300025C99 /* Video3DFormat.swift */, ); path = JellyfinAPI; sourceTree = ""; @@ -4031,6 +4138,7 @@ children = ( E1CD13EE28EF364100CB46CA /* DetectOrientationModifier.swift */, E1194F4D2BEABA9100888DB6 /* NavigationBarCloseButton.swift */, + 4EEEEA232CFA8E1500527D79 /* NavigationBarMenuButton.swift */, E113133028BDB6D600930F75 /* NavigationBarDrawerButtons */, E11895B12893842D0042947B /* NavigationBarOffset */, ); @@ -4700,6 +4808,7 @@ E1DC983E296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, 4E2AC4BF2C6C48D200DD600D /* CustomDeviceProfileAction.swift in Sources */, 4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */, + 4E661A2B2CEFE6F400025C99 /* Video3DFormat.swift in Sources */, E107BB9427880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */, @@ -4730,6 +4839,7 @@ E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */, C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */, E1575E63293E77B5001665B1 /* CaseIterablePicker.swift in Sources */, + 4E6619FC2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */, E1CB757F2C80F28F00217C76 /* SubtitleProfile.swift in Sources */, E1E0BEB829EF450B0002E8D3 /* UIGestureRecognizer.swift in Sources */, E193D53527193F8100900D82 /* ItemCoordinator.swift in Sources */, @@ -4743,6 +4853,7 @@ E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */, E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */, E1575E78293E77B5001665B1 /* TrailingTimestampType.swift in Sources */, + 4E661A2F2CEFE77700025C99 /* MetadataField.swift in Sources */, E11CEB9128999D84003E74C7 /* EpisodeItemView.swift in Sources */, E14E9DF22BCF7A99004E3371 /* ItemLetter.swift in Sources */, E10B1EC82BD9AF6100A92EAF /* V2ServerModel.swift in Sources */, @@ -4786,6 +4897,7 @@ E1ED91162B95897500802036 /* LatestInLibraryViewModel.swift in Sources */, E12376B32A33DFAC001F5B44 /* ItemOverviewView.swift in Sources */, E1ED7FDB2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */, + 4E661A222CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */, E1A7F0E02BD4EC7400620DDD /* Dictionary.swift in Sources */, E1CAF6602BA345830087D991 /* MediaViewModel.swift in Sources */, E19D41A82BEEDC5F0082B8B2 /* UserLocalSecurityViewModel.swift in Sources */, @@ -4883,6 +4995,7 @@ E1575E75293E77B5001665B1 /* LibraryDisplayType.swift in Sources */, E193D53427193F7F00900D82 /* HomeCoordinator.swift in Sources */, E193D5502719430400900D82 /* ServerDetailView.swift in Sources */, + 4E661A312CEFE7BC00025C99 /* SeriesStatus.swift in Sources */, E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */, E1356E0429A731EB00382563 /* SeparatorHStack.swift in Sources */, E1575E69293E77B5001665B1 /* ItemSortBy.swift in Sources */, @@ -4948,6 +5061,7 @@ E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */, 4E2AC4CC2C6C494E00DD600D /* VideoCodec.swift in Sources */, 4EF18B262CB9934C00343666 /* LibraryRow.swift in Sources */, + 4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */, E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, @@ -4955,6 +5069,7 @@ C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */, E1575E9F293E7B1E001665B1 /* Int.swift in Sources */, E1D9F475296E86D400129AF3 /* NativeVideoPlayer.swift in Sources */, + 4E661A1F2CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */, E145EB462BE0AD4E003BF6F3 /* Set.swift in Sources */, E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, @@ -5104,6 +5219,7 @@ 4EC2B1A22CC96F6600D866BE /* ServerUsersViewModel.swift in Sources */, E1B90C6A2BBE68D5007027C8 /* OffsetScrollView.swift in Sources */, E18E01DB288747230022598C /* iPadOSEpisodeItemView.swift in Sources */, + 4E661A292CEFE68200025C99 /* Video3DFormatPicker.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, 621338932660107500A81A2A /* String.swift in Sources */, E17AC96F2954EE4B003D2BC2 /* DownloadListViewModel.swift in Sources */, @@ -5125,6 +5241,7 @@ 62E632DC267D2E130063E547 /* SearchViewModel.swift in Sources */, E1A1528A28FD22F600600579 /* TextPairView.swift in Sources */, E1BDF2E92951490400CC0294 /* ActionButtonSelectorView.swift in Sources */, + 4E661A232CEFE61000025C99 /* ParentalRatingsViewModel.swift in Sources */, E170D0E2294CC8000017224C /* VideoPlayer+Actions.swift in Sources */, 4E10C8112CC030CD0012CC9F /* DeviceDetailsView.swift in Sources */, E148128828C154BF003B8787 /* ItemFilter+ItemTrait.swift in Sources */, @@ -5176,6 +5293,17 @@ E15D63ED2BD622A700AA665D /* CompactChannelView.swift in Sources */, E18A8E8528D60D0000333B9A /* VideoPlayerCoordinator.swift in Sources */, E19E551F2897326C003CE330 /* BottomEdgeGradientModifier.swift in Sources */, + 4E661A0F2CEFE46300025C99 /* SeriesSection.swift in Sources */, + 4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */, + 4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */, + 4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */, + 4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */, + 4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */, + 4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */, + 4E661A162CEFE46300025C99 /* ParentialRatingsSection.swift in Sources */, + 4E661A172CEFE46300025C99 /* OverviewSection.swift in Sources */, + 4E661A182CEFE46300025C99 /* ReviewsSection.swift in Sources */, + 4E661A192CEFE46300025C99 /* DateSection.swift in Sources */, E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */, @@ -5184,8 +5312,10 @@ 4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, E154966A296CA2EF00C4EF88 /* DownloadManager.swift in Sources */, + 4E661A1C2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */, E133328829538D8D00EE76AB /* Files.swift in Sources */, C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */, + 4E661A322CEFE7BC00025C99 /* SeriesStatus.swift in Sources */, E1401CA02937DFF500E8B599 /* AppIconSelectorView.swift in Sources */, BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */, 4EC2B1A52CC96FA400D866BE /* ServerUserAdminViewModel.swift in Sources */, @@ -5205,6 +5335,7 @@ E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, 4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */, E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */, + 4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */, E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, @@ -5319,6 +5450,7 @@ E13F05F128BC9016003499D2 /* LibraryRow.swift in Sources */, 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */, E168BD10289A4162001A6922 /* HomeView.swift in Sources */, + 4EEEEA242CFA8E1500527D79 /* NavigationBarMenuButton.swift in Sources */, 4EC2B1A92CC97C0700D866BE /* ServerUserDetailsView.swift in Sources */, E11562952C818CB2001D5DE4 /* BindingBox.swift in Sources */, 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */, @@ -5333,6 +5465,7 @@ E15D63EF2BD6DFC200AA665D /* SystemImageable.swift in Sources */, E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, + 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, E18E01AD288746AF0022598C /* DotHStack.swift in Sources */, E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */, @@ -5350,6 +5483,7 @@ E152107C2947ACA000375CC2 /* InvertedLightAppIcon.swift in Sources */, E17AC9732955007A003D2BC2 /* DownloadTaskButton.swift in Sources */, E145EB4F2BE168AC003BF6F3 /* SwiftfinStore+ServerState.swift in Sources */, + 4E661A2C2CEFE6F400025C99 /* Video3DFormat.swift in Sources */, E1A1528228FD126C00600579 /* VerticalAlignment.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */, @@ -5425,10 +5559,12 @@ E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */, E1D4BF812719D22800A11E64 /* AppAppearance.swift in Sources */, + 4E6619FD2CEFE2BE00025C99 /* ItemEditorViewModel.swift in Sources */, E1BDF2EF29522A5900CC0294 /* AudioActionButton.swift in Sources */, E174120F29AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */, E10231392BCF8A3C009D71FC /* ProgramButtonContent.swift in Sources */, E1DC9844296DECB600982F06 /* ProgressIndicator.swift in Sources */, + 4E661A202CEFE56E00025C99 /* SeriesDisplayOrder.swift in Sources */, 6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */, 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */, E10B1ECA2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, @@ -5439,6 +5575,7 @@ 4E026A8B2CE804E7005471B5 /* ResetUserPasswordView.swift in Sources */, 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, + 4E661A252CEFE64500025C99 /* CountryPicker.swift in Sources */, E18A8E8328D60BC400333B9A /* VideoPlayer.swift in Sources */, 4E10C8192CC045700012CC9F /* CustomDeviceNameSection.swift in Sources */, 4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */, @@ -5561,6 +5698,7 @@ E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */, E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */, E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */, + 4E661A272CEFE65000025C99 /* LanguagePicker.swift in Sources */, 62E1DCC3273CE19800C9AE76 /* URL.swift in Sources */, E11BDF7A2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */, E170D0E4294CC8AB0017224C /* VideoPlayer+KeyCommands.swift in Sources */, diff --git a/Swiftfin/Components/CountryPicker.swift b/Swiftfin/Components/CountryPicker.swift new file mode 100644 index 00000000..2c903a55 --- /dev/null +++ b/Swiftfin/Components/CountryPicker.swift @@ -0,0 +1,47 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct CountryPicker: View { + let title: String + @Binding + var selectedCountryCode: String? + + // MARK: - Get all localized countries + + private var countries: [(code: String?, name: String)] { + var uniqueCountries = Set() + + var countryList: [(code: String?, name: String)] = Locale.isoRegionCodes.compactMap { code in + let locale = Locale.current + if let name = locale.localizedString(forRegionCode: code), + !uniqueCountries.contains(code) + { + uniqueCountries.insert(code) + return (code, name) + } + return nil + } + .sorted { $0.name < $1.name } + + // Add None as an option at the top of the list + countryList.insert((code: nil, name: L10n.none), at: 0) + return countryList + } + + // MARK: - Body + + var body: some View { + Picker(title, selection: $selectedCountryCode) { + ForEach(countries, id: \.code) { country in + Text(country.name).tag(country.code) + } + } + } +} diff --git a/Swiftfin/Components/LanguagePicker.swift b/Swiftfin/Components/LanguagePicker.swift new file mode 100644 index 00000000..796855dc --- /dev/null +++ b/Swiftfin/Components/LanguagePicker.swift @@ -0,0 +1,48 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct LanguagePicker: View { + let title: String + @Binding + var selectedLanguageCode: String? + + // MARK: - Get all localized languages + + private var languages: [(code: String?, name: String)] { + var uniqueLanguages = Set() + + var languageList: [(code: String?, name: String)] = Locale.availableIdentifiers.compactMap { identifier in + let locale = Locale(identifier: identifier) + if let code = locale.languageCode, + let name = locale.localizedString(forLanguageCode: code), + !uniqueLanguages.contains(code) + { + uniqueLanguages.insert(code) + return (code, name) + } + return nil + } + .sorted { $0.name < $1.name } + + // Add None as an option at the top of the list + languageList.insert((code: nil, name: L10n.none), at: 0) + return languageList + } + + // MARK: - Body + + var body: some View { + Picker(title, selection: $selectedLanguageCode) { + ForEach(languages, id: \.code) { language in + Text(language.name).tag(language.code) + } + } + } +} diff --git a/Swiftfin/Components/Video3DFormatPicker.swift b/Swiftfin/Components/Video3DFormatPicker.swift new file mode 100644 index 00000000..cc783d72 --- /dev/null +++ b/Swiftfin/Components/Video3DFormatPicker.swift @@ -0,0 +1,25 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct Video3DFormatPicker: View { + let title: String + @Binding + var selectedFormat: Video3DFormat? + + var body: some View { + Picker(title, selection: $selectedFormat) { + Text(L10n.none).tag(nil as Video3DFormat?) + ForEach(Video3DFormat.allCases, id: \.self) { format in + Text(format.displayTitle).tag(format as Video3DFormat?) + } + } + } +} diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift new file mode 100644 index 00000000..6eed9adf --- /dev/null +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift @@ -0,0 +1,41 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct NavigationBarMenuButtonModifier: ViewModifier { + + @Default(.accentColor) + private var accentColor + + let isLoading: Bool + let isHidden: Bool + let items: () -> Content + + func body(content: Self.Content) -> some View { + content.toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + + if isLoading { + ProgressView() + } + + if !isHidden { + Menu(L10n.options, systemImage: "ellipsis.circle") { + items() + } + .labelStyle(.iconOnly) + .backport + .fontWeight(.semibold) + .foregroundStyle(accentColor) + } + } + } + } +} diff --git a/Swiftfin/Extensions/View/View-iOS.swift b/Swiftfin/Extensions/View/View-iOS.swift index f4440f54..2d23e152 100644 --- a/Swiftfin/Extensions/View/View-iOS.swift +++ b/Swiftfin/Extensions/View/View-iOS.swift @@ -79,6 +79,22 @@ extension View { ) } + @ViewBuilder + func navigationBarMenuButton( + isLoading: Bool = false, + isHidden: Bool = false, + @ViewBuilder + _ items: @escaping () -> Content + ) -> some View { + modifier( + NavigationBarMenuButtonModifier( + isLoading: isLoading, + isHidden: isHidden, + items: items + ) + ) + } + @ViewBuilder func listRowCornerRadius(_ radius: CGFloat) -> some View { if #unavailable(iOS 16) { diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift index 89a5be92..8cf69690 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift @@ -61,8 +61,20 @@ struct ServerUsersView: View { navigationBarSelectView } } - ToolbarItemGroup(placement: .topBarTrailing) { - navigationBarEditView + ToolbarItem(placement: .topBarTrailing) { + if isEditing { + Button(isEditing ? L10n.cancel : L10n.edit) { + isEditing.toggle() + + UIDevice.impact(.light) + + if !isEditing { + selectedUsers.removeAll() + } + } + .buttonStyle(.toolbarPill) + .foregroundStyle(accentColor) + } } ToolbarItem(placement: .bottomBar) { if isEditing { @@ -75,6 +87,28 @@ struct ServerUsersView: View { } } } + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains(.gettingUsers), + isHidden: isEditing + ) { + Button(L10n.addUser, systemImage: "plus") { + router.route(to: \.addServerUser) + } + + if viewModel.users.isNotEmpty { + Button(L10n.editUsers, systemImage: "checkmark.circle") { + isEditing = true + } + } + + Divider() + + Section(L10n.filters) { + Toggle(L10n.hidden, systemImage: "eye.slash", isOn: $isHiddenFilterActive) + Toggle(L10n.disabled, systemImage: "person.slash", isOn: $isDisabledFilterActive) + } + } + .onChange(of: isDisabledFilterActive) { newValue in viewModel.send(.getUsers( isHidden: isHiddenFilterActive, @@ -175,51 +209,6 @@ struct ServerUsersView: View { } } - // MARK: - Navigation Bar Edit Content - - @ViewBuilder - private var navigationBarEditView: some View { - if viewModel.backgroundStates.contains(.gettingUsers) { - ProgressView() - } - - if isEditing { - Button(isEditing ? L10n.cancel : L10n.edit) { - isEditing.toggle() - - UIDevice.impact(.light) - - if !isEditing { - selectedUsers.removeAll() - } - } - .buttonStyle(.toolbarPill) - .foregroundStyle(accentColor) - } else { - Menu(L10n.options, systemImage: "ellipsis.circle") { - Button(L10n.addUser, systemImage: "plus") { - router.route(to: \.addServerUser) - } - - if viewModel.users.isNotEmpty { - Button(L10n.editUsers, systemImage: "checkmark.circle") { - isEditing = true - } - } - - Divider() - - Section(L10n.filters) { - Toggle(L10n.hidden, systemImage: "eye.slash", isOn: $isHiddenFilterActive) - Toggle(L10n.disabled, systemImage: "person.slash", isOn: $isDisabledFilterActive) - } - } - .labelStyle(.iconOnly) - .backport - .fontWeight(.semibold) - } - } - // MARK: - Navigation Bar Select/Remove All Content @ViewBuilder diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift new file mode 100644 index 00000000..ac4f54f0 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift @@ -0,0 +1,55 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct DateSection: View { + + @Binding + var item: BaseItemDto + + let itemType: BaseItemKind + + var body: some View { + Section(L10n.dates) { + DatePicker( + L10n.dateAdded, + selection: $item.dateCreated.coalesce(.now), + displayedComponents: .date + ) + + DatePicker( + itemType == .person ? L10n.birthday : L10n.releaseDate, + selection: $item.premiereDate.coalesce(.now), + displayedComponents: .date + ) + + if itemType == .series || itemType == .person { + DatePicker( + itemType == .person ? L10n.dateOfDeath : L10n.endDate, + selection: $item.endDate.coalesce(.now), + displayedComponents: .date + ) + } + } + + Section(L10n.year) { + TextField( + itemType == .person ? L10n.birthYear : L10n.year, + value: $item.productionYear, + format: .number.grouping(.never) + ) + .keyboardType(.numberPad) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift new file mode 100644 index 00000000..d00c7bc2 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift @@ -0,0 +1,61 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct DisplayOrderSection: View { + + @Binding + var item: BaseItemDto + + let itemType: BaseItemKind + + var body: some View { + Section(L10n.displayOrder) { + switch itemType { + case .boxSet: + Picker( + L10n.displayOrder, + selection: $item.displayOrder + .coalesce("") + .map( + getter: { BoxSetDisplayOrder(rawValue: $0) ?? .dateModified }, + setter: { $0.rawValue } + ) + ) { + ForEach(BoxSetDisplayOrder.allCases) { order in + Text(order.displayTitle).tag(order) + } + } + + case .series: + Picker( + L10n.displayOrder, + selection: $item.displayOrder + .coalesce("") + .map( + getter: { SeriesDisplayOrder(rawValue: $0) ?? .aired }, + setter: { $0.rawValue } + ) + ) { + ForEach(SeriesDisplayOrder.allCases) { order in + Text(order.displayTitle).tag(order) + } + } + + default: + EmptyView() + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift new file mode 100644 index 00000000..7f39e20c --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift @@ -0,0 +1,55 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct EpisodeSection: View { + + @Binding + var item: BaseItemDto + + var body: some View { + Section(L10n.season) { + + // MARK: Season Number + + ChevronAlertButton( + L10n.season, + subtitle: item.parentIndexNumber?.description, + description: L10n.enterSeasonNumber + ) { + TextField( + L10n.season, + value: $item.parentIndexNumber, + format: .number + ) + .keyboardType(.numberPad) + } + + // MARK: Episode Number + + ChevronAlertButton( + L10n.episode, + subtitle: item.indexNumber?.description, + description: L10n.enterEpisodeNumber + ) { + TextField( + L10n.episode, + value: $item.indexNumber, + format: .number + ) + .keyboardType(.numberPad) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift new file mode 100644 index 00000000..c789f0c6 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift @@ -0,0 +1,34 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct LocalizationSection: View { + + @Binding + var item: BaseItemDto + + var body: some View { + Section(L10n.metadataPreferences) { + LanguagePicker( + title: L10n.language, + selectedLanguageCode: $item.preferredMetadataLanguage + ) + + CountryPicker( + title: L10n.country, + selectedCountryCode: $item.preferredMetadataCountryCode + ) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift new file mode 100644 index 00000000..78b197fb --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift @@ -0,0 +1,43 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct LockMetadataSection: View { + + @Binding + var item: BaseItemDto + + // TODO: Animation when lockAllFields is selected + var body: some View { + Section(L10n.lockedFields) { + Toggle( + L10n.lockAllFields, + isOn: $item.lockData.coalesce(false) + ) + } + + if item.lockData != true { + Section { + ForEach(MetadataField.allCases, id: \.self) { field in + Toggle( + field.displayTitle, + isOn: $item.lockedFields + .coalesce([]) + .contains(field) + ) + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift new file mode 100644 index 00000000..560d5412 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift @@ -0,0 +1,35 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct MediaFormatSection: View { + + @Binding + var item: BaseItemDto + + var body: some View { + Section(L10n.format) { + TextField( + L10n.originalAspectRatio, + value: $item.aspectRatio, + format: .nilIfEmptyString + ) + + Video3DFormatPicker( + title: L10n.format3D, + selectedFormat: $item.video3DFormat + ) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift new file mode 100644 index 00000000..dc04c5ee --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift @@ -0,0 +1,58 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct OverviewSection: View { + + @Binding + var item: BaseItemDto + + let itemType: BaseItemKind + + private var showTaglines: Bool { + [ + BaseItemKind.movie, + .series, + .audioBook, + .book, + .audio, + ].contains(itemType) + } + + var body: some View { + if showTaglines { + // There doesn't seem to be a usage anywhere of more than 1 tagline? + Section(L10n.taglines) { + TextField( + L10n.tagline, + value: $item.taglines + .map( + getter: { $0 == nil ? "" : $0!.first }, + setter: { $0 == nil ? [] : [$0!] } + ), + format: .nilIfEmptyString + ) + } + } + + Section(L10n.overview) { + TextEditor(text: $item.overview.coalesce("")) + .onAppear { + // Workaround for iOS 17 and earlier bug + // where the row height won't be set properly + item.overview = item.overview + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift new file mode 100644 index 00000000..c1eb5452 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift @@ -0,0 +1,105 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +// TODO: Reimagine this whole thing to be much leaner. +extension EditMetadataView { + + struct ParentalRatingSection: View { + + @Binding + var item: BaseItemDto + + @ObservedObject + private var viewModel = ParentalRatingsViewModel() + + @State + private var officialRatings: [ParentalRating] = [] + @State + private var customRatings: [ParentalRating] = [] + + // MARK: - Body + + var body: some View { + Section(L10n.parentalRating) { + + // MARK: Official Rating Picker + + Picker( + L10n.officialRating, + selection: $item.officialRating + .map( + getter: { value in officialRatings.first { $0.name == value } }, + setter: { $0?.name } + ) + ) { + Text(L10n.none).tag(nil as ParentalRating?) + ForEach(officialRatings, id: \.self) { rating in + Text(rating.name ?? "").tag(rating as ParentalRating?) + } + } + .onAppear { + updateOfficialRatings() + } + .onChange(of: viewModel.parentalRatings) { _ in + updateOfficialRatings() + } + + // MARK: Custom Rating Picker + + Picker( + L10n.customRating, + selection: $item.customRating + .map( + getter: { value in customRatings.first { $0.name == value } }, + setter: { $0?.name } + ) + ) { + Text(L10n.none).tag(nil as ParentalRating?) + ForEach(customRatings, id: \.self) { rating in + Text(rating.name ?? "").tag(rating as ParentalRating?) + } + } + .onAppear { + updateCustomRatings() + } + .onChange(of: viewModel.parentalRatings) { _ in + updateCustomRatings() + } + } + .onFirstAppear { + viewModel.send(.refresh) + } + } + + // MARK: - Update Official Rating + + private func updateOfficialRatings() { + officialRatings = viewModel.parentalRatings + if let currentRatingName = item.officialRating, + !officialRatings.contains(where: { $0.name == currentRatingName }) + { + officialRatings.append(ParentalRating(name: currentRatingName)) + } + } + + // MARK: - Update Custom Rating + + private func updateCustomRatings() { + customRatings = viewModel.parentalRatings + if let currentRatingName = item.customRating, + !customRatings.contains(where: { $0.name == currentRatingName }) + { + customRatings.append(ParentalRating(name: currentRatingName)) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift new file mode 100644 index 00000000..db2319b9 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift @@ -0,0 +1,65 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct ReviewsSection: View { + + @Binding + var item: BaseItemDto + + var body: some View { + Section(L10n.reviews) { + + // MARK: Critics Rating + + ChevronAlertButton( + L10n.critics, + subtitle: item.criticRating.map { "\($0)" } ?? .emptyDash, + description: L10n.ratingDescription(L10n.critics) + ) { + TextField( + L10n.rating, + value: $item.criticRating, + format: .number.precision(.fractionLength(1)) + ) + .keyboardType(.decimalPad) + .onChange(of: item.criticRating) { _ in + if let rating = item.criticRating { + item.criticRating = min(max(rating, 0), 10) + } + } + } + + // MARK: Community Rating + + ChevronAlertButton( + L10n.community, + subtitle: item.communityRating.map { "\($0)" } ?? .emptyDash, + description: L10n.ratingDescription(L10n.community) + ) { + TextField( + L10n.rating, + value: $item.communityRating, + format: .number.precision(.fractionLength(1)) + ) + .keyboardType(.decimalPad) + .onChange(of: item.communityRating) { _ in + if let rating = item.communityRating { + item.communityRating = min(max(rating, 0), 10) + } + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift new file mode 100644 index 00000000..8f22a559 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift @@ -0,0 +1,147 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct SeriesSection: View { + + @Binding + private var item: BaseItemDto + + @State + private var tempRunTime: Int? + + // MARK: - Initializer + + init(item: Binding) { + self._item = item + self.tempRunTime = Int(ServerTicks(item.wrappedValue.runTimeTicks ?? 0).minutes) + } + + // MARK: - Body + + var body: some View { + + Section(L10n.series) { + seriesStatusView + } + + Section(L10n.episodes) { + airTimeView + + runTimeView + } + + Section(L10n.dayOfWeek) { + airDaysView + } + } + + // MARK: - Series Status View + + @ViewBuilder + private var seriesStatusView: some View { + Picker( + L10n.status, + selection: $item.status + .coalesce("") + .map( + getter: { SeriesStatus(rawValue: $0) ?? .continuing }, + setter: { $0.rawValue } + ) + ) { + ForEach(SeriesStatus.allCases, id: \.self) { status in + Text(status.displayTitle).tag(status) + } + } + } + + // MARK: - Air Time View + + @ViewBuilder + private var airTimeView: some View { + DatePicker( + L10n.airTime, + selection: $item.airTime + .coalesce("00:00") + .map( + getter: { parseAirTimeToDate($0) }, + setter: { formatDateToString($0) } + ), + displayedComponents: .hourAndMinute + ) + } + + // MARK: - Air Days View + + @ViewBuilder + private var airDaysView: some View { + ForEach(DayOfWeek.allCases, id: \.self) { field in + Toggle( + field.displayTitle ?? L10n.unknown, + isOn: $item.airDays + .coalesce([]) + .contains(field) + ) + } + } + + // MARK: - Run Time View + + @ViewBuilder + private var runTimeView: some View { + ChevronAlertButton( + L10n.runTime, + subtitle: ServerTicks(item.runTimeTicks ?? 0) + .seconds.formatted(.hourMinute), + description: L10n.episodeRuntimeDescription + ) { + TextField( + L10n.minutes, + value: $tempRunTime + .coalesce(0) + .min(0), + format: .number + ) + .keyboardType(.numberPad) + } onSave: { + if let tempRunTime, tempRunTime != 0 { + item.runTimeTicks = ServerTicks(minutes: tempRunTime).ticks + } else { + item.runTimeTicks = nil + } + } onCancel: { + if let originalRunTime = item.runTimeTicks { + tempRunTime = Int(ServerTicks(originalRunTime).minutes) + } else { + tempRunTime = nil + } + } + } + + // MARK: - Parse AirTime to Date + + private func parseAirTimeToDate(_ airTime: String) -> Date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter.date(from: airTime) ?? Date() + } + + // MARK: - Format Date to String + + private func formatDateToString(_ date: Date) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter.string(from: date) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift new file mode 100644 index 00000000..aa994e0a --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift @@ -0,0 +1,46 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension EditMetadataView { + + struct TitleSection: View { + + @Binding + var item: BaseItemDto + + var body: some View { + Section(L10n.title) { + TextField( + L10n.title, + value: $item.name, + format: .nilIfEmptyString + ) + } + + Section(L10n.originalTitle) { + TextField( + L10n.originalTitle, + value: $item.originalTitle, + format: .nilIfEmptyString + ) + } + + Section(L10n.sortTitle) { + TextField( + L10n.sortTitle, + value: $item.forcedSortName, + format: .nilIfEmptyString + ) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift b/Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift new file mode 100644 index 00000000..02e74798 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift @@ -0,0 +1,95 @@ +// +// 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) 2024 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +struct EditMetadataView: View { + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + private var viewModel: ItemEditorViewModel + + @Binding + var item: BaseItemDto + + @State + private var tempItem: BaseItemDto + + private let itemType: BaseItemKind + + // MARK: - Initializer + + init(viewModel: ItemEditorViewModel) { + self.viewModel = viewModel + self._item = Binding(get: { viewModel.item }, set: { viewModel.item = $0 }) + self._tempItem = State(initialValue: viewModel.item) + self.itemType = viewModel.item.type! + } + + // MARK: - Body + + @ViewBuilder + var body: some View { + contentView + .navigationBarTitle(L10n.metadata) + .navigationBarTitleDisplayMode(.inline) + .topBarTrailing { + Button(L10n.save) { + item = tempItem + viewModel.send(.update(tempItem)) + router.dismissCoordinator() + } + .buttonStyle(.toolbarPill) + .disabled(viewModel.item == tempItem) + } + .navigationBarCloseButton { + router.dismissCoordinator() + } + } + + // MARK: - Content View + + @ViewBuilder + private var contentView: some View { + Form { + TitleSection(item: $tempItem) + + DateSection( + item: $tempItem, + itemType: itemType + ) + + if itemType == .series { + SeriesSection(item: $tempItem) + } else if itemType == .episode { + EpisodeSection(item: $tempItem) + } + + OverviewSection( + item: $tempItem, + itemType: itemType + ) + + ReviewsSection(item: $tempItem) + + ParentalRatingSection(item: $tempItem) + + if [BaseItemKind.movie, .episode].contains(itemType) { + MediaFormatSection(item: $tempItem) + } + + LocalizationSection(item: $tempItem) + + LockMetadataSection(item: $tempItem) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index 01eba1b0..591148a2 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -18,8 +18,8 @@ struct ItemEditorView: View { @EnvironmentObject private var router: ItemEditorCoordinator.Router - @State - var item: BaseItemDto + @ObservedObject + var viewModel: ItemViewModel // MARK: - Body @@ -30,10 +30,6 @@ struct ItemEditorView: View { .navigationBarCloseButton { router.dismissCoordinator() } - .onNotification(.itemMetadataDidChange) { notification in - guard let newItem = notification.object as? BaseItemDto else { return } - item = newItem - } } // MARK: - Content View @@ -41,33 +37,50 @@ struct ItemEditorView: View { private var contentView: some View { List { ListTitleSection( - item.name ?? L10n.unknown, - description: item.path + viewModel.item.name ?? L10n.unknown, + description: viewModel.item.path ) - Section { - RefreshMetadataButton(item: item) - .environment(\.isEnabled, userSession?.user.isAdministrator ?? false) - } footer: { - LearnMoreButton(L10n.metadata) { - TextPair( - title: L10n.findMissing, - subtitle: L10n.findMissingDescription - ) - TextPair( - title: L10n.replaceMetadata, - subtitle: L10n.replaceMetadataDescription - ) - TextPair( - title: L10n.replaceImages, - subtitle: L10n.replaceImagesDescription - ) - TextPair( - title: L10n.replaceAll, - subtitle: L10n.replaceAllDescription - ) - } + refreshButtonView + + editView + } + } + + @ViewBuilder + private var refreshButtonView: some View { + Section { + RefreshMetadataButton(item: viewModel.item) + .environment(\.isEnabled, userSession?.user.isAdministrator ?? false) + } footer: { + LearnMoreButton(L10n.metadata) { + TextPair( + title: L10n.findMissing, + subtitle: L10n.findMissingDescription + ) + TextPair( + title: L10n.replaceMetadata, + subtitle: L10n.replaceMetadataDescription + ) + TextPair( + title: L10n.replaceImages, + subtitle: L10n.replaceImagesDescription + ) + TextPair( + title: L10n.replaceAll, + subtitle: L10n.replaceAllDescription + ) } } } + + @ViewBuilder + private var editView: some View { + Section(L10n.edit) { + ChevronButton(L10n.metadata) + .onSelect { + router.route(to: \.editMetadata, viewModel.item) + } + } + } } diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index b28c9538..2bd959d4 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -39,7 +39,12 @@ struct ItemView: View { enableItemDeletion && viewModel.item.canDelete ?? false } - // As more menu items exist, this can either be expanded to include more validation or removed if there are permanent menu items. + private var canDownload: Bool { + viewModel.item.canDownload ?? false + } + + // Use to hide the menu button when not needed. + // Add more checks as needed. For example, canDownload. private var enableMenu: Bool { canDelete || enableItemEditor } @@ -123,12 +128,21 @@ struct ItemView: View { .onFirstAppear { viewModel.send(.refresh) } - .topBarTrailing { - if viewModel.backgroundStates.contains(.refresh) { - ProgressView() + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains(.refresh), + isHidden: !enableMenu + ) { + if enableItemEditor { + Button(L10n.edit, systemImage: "pencil") { + router.route(to: \.itemEditor, viewModel) + } } - if enableMenu { - itemActionMenu + + if canDelete { + Divider() + Button(L10n.delete, systemImage: "trash", role: .destructive) { + showConfirmationDialog = true + } } } .confirmationDialog( @@ -159,27 +173,4 @@ struct ItemView: View { Text(error.localizedDescription) } } - - @ViewBuilder - private var itemActionMenu: some View { - - Menu(L10n.options, systemImage: "ellipsis.circle") { - - if enableItemEditor { - Button(L10n.edit, systemImage: "pencil") { - router.route(to: \.itemEditor, viewModel.item) - } - } - - if canDelete { - Divider() - Button(L10n.delete, systemImage: "trash", role: .destructive) { - showConfirmationDialog = true - } - } - } - .labelStyle(.iconOnly) - .backport - .fontWeight(.semibold) - } } diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index 0107d5e6..b259688e 100644 --- a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift @@ -471,35 +471,27 @@ struct PagingLibraryView: View { viewModel.send(.refresh) } } - .topBarTrailing { - - if viewModel.backgroundStates.contains(.gettingNextPage) { - ProgressView() + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains(.gettingNextPage) + ) { + if Defaults[.Customization.Library.rememberLayout] { + LibraryViewTypeToggle( + posterType: $posterType, + viewType: $displayType, + listColumnCount: $listColumnCount + ) + } else { + LibraryViewTypeToggle( + posterType: $defaultPosterType, + viewType: $defaultDisplayType, + listColumnCount: $defaultListColumnCount + ) } - Menu { - - if Defaults[.Customization.Library.rememberLayout] { - LibraryViewTypeToggle( - posterType: $posterType, - viewType: $displayType, - listColumnCount: $listColumnCount - ) - } else { - LibraryViewTypeToggle( - posterType: $defaultPosterType, - viewType: $defaultDisplayType, - listColumnCount: $defaultListColumnCount - ) - } - - Button(L10n.random, systemImage: "dice.fill") { - viewModel.send(.getRandomItem) - } - .disabled(viewModel.elements.isEmpty) - } label: { - Image(systemName: "ellipsis.circle") + Button(L10n.random, systemImage: "dice.fill") { + viewModel.send(.getRandomItem) } + .disabled(viewModel.elements.isEmpty) } } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 27eaf958..dd88b59e 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -301,1099 +301,1124 @@ "pauseOnBackground" = "Pause on background"; "playOnActive" = "Play on active"; +// Retrieving Media Information - Loading Message +// Text displayed to indicate that media information is being loaded. "retrievingMediaInformation" = "Retrieving media information"; +// Right - Direction +// Label for the right side or direction. "right" = "Right"; + +// Left - Direction +// Label for the left side or direction. "left" = "Left"; + +// Letter Picker - Option +// Picker used to select a letter for jumping to a specific section in a list. "letterPicker" = "Letter Picker"; + +// Orientation - Setting +// Setting to define how an item is oriented (e.g., portrait or landscape). "orientation" = "Orientation"; -/* MARK: Bitrate Options */ - -/* Option to set the test size for bitrate testing */ +// Test Size - Option +// Option to set the test size for bitrate testing. "testSize" = "Test Size"; -/* Option to set the maximum bitrate for playback */ +// Maximum Bitrate - Option +// Option to set the maximum bitrate for playback. "maximumBitrate" = "Maximum Bitrate"; -/* Option for automatic bitrate selection */ +// Auto - Option +// Option for automatic bitrate selection. "bitrateAuto" = "Auto"; -/* Option for the maximum bitrate */ +// Maximum - Option +// Option for the maximum bitrate. "bitrateMax" = "Maximum"; -/* Option to set the bitrate to 4K quality at 120 Mbps */ +// 4K - 120 Mbps - Option +// Option to set the bitrate to 4K quality at 120 Mbps. "bitrateMbps120" = "4K - 120 Mbps"; -/* Option to set the bitrate to 4K quality at 80 Mbps */ +// 4K - 80 Mbps - Option +// Option to set the bitrate to 4K quality at 80 Mbps. "bitrateMbps80" = "4K - 80 Mbps"; -/* Option to set the bitrate to 1080p quality at 60 Mbps */ +// 1080p - 60 Mbps - Option +// Option to set the bitrate to 1080p quality at 60 Mbps. "bitrateMbps60" = "1080p - 60 Mbps"; -/* Option to set the bitrate to 1080p quality at 40 Mbps */ +// 1080p - 40 Mbps - Option +// Option to set the bitrate to 1080p quality at 40 Mbps. "bitrateMbps40" = "1080p - 40 Mbps"; -/* Option to set the bitrate to 1080p quality at 20 Mbps */ +// 1080p - 20 Mbps - Option +// Option to set the bitrate to 1080p quality at 20 Mbps. "bitrateMbps20" = "1080p - 20 Mbps"; -/* Option to set the bitrate to 1080p quality at 15 Mbps */ +// 1080p - 15 Mbps - Option +// Option to set the bitrate to 1080p quality at 15 Mbps. "bitrateMbps15" = "1080p - 15 Mbps"; -/* Option to set the bitrate to 1080p quality at 10 Mbps */ +// 1080p - 10 Mbps - Option +// Option to set the bitrate to 1080p quality at 10 Mbps. "bitrateMbps10" = "1080p - 10 Mbps"; -/* Option to set the bitrate to 720p quality at 8 Mbps */ +// 720p - 8 Mbps - Option +// Option to set the bitrate to 720p quality at 8 Mbps. "bitrateMbps8" = "720p - 8 Mbps"; -/* Option to set the bitrate to 720p quality at 6 Mbps */ +// 720p - 6 Mbps - Option +// Option to set the bitrate to 720p quality at 6 Mbps. "bitrateMbps6" = "720p - 6 Mbps"; -/* Option to set the bitrate to 720p quality at 4 Mbps */ +// 720p - 4 Mbps - Option +// Option to set the bitrate to 720p quality at 4 Mbps. "bitrateMbps4" = "720p - 4 Mbps"; -/* Option to set the bitrate to 480p quality at 3 Mbps */ +// 480p - 3 Mbps - Option +// Option to set the bitrate to 480p quality at 3 Mbps. "bitrateMbps3" = "480p - 3 Mbps"; -/* Option to set the bitrate to 480p quality at 1.5 Mbps */ +// 480p - 1.5 Mbps - Option +// Option to set the bitrate to 480p quality at 1.5 Mbps. "bitrateKbps1500" = "480p - 1.5 Mbps"; -/* Option to set the bitrate to 480p quality at 720 Kbps */ +// 480p - 720 Kbps - Option +// Option to set the bitrate to 480p quality at 720 Kbps. "bitrateKbps720" = "480p - 720 Kbps"; -/* Option to set the bitrate to 360p quality at 420 Kbps */ +// 360p - 420 Kbps - Option +// Option to set the bitrate to 360p quality at 420 Kbps. "bitrateKbps420" = "360p - 420 Kbps"; -/* Description for bitrate test duration indicating longer tests provide more accurate bitrates but may delay playback */ +// Bitrate Test Disclaimer - Description +// Description for bitrate test duration indicating longer tests provide more accurate bitrates. "bitrateTestDisclaimer" = "Longer tests are more accurate but may result in a delayed playback."; -/* Select Server View */ +// Servers - Navigation Title +// Select Server View. "servers" = "Servers"; -/* Select Server View - Add Server */ +// Add Server - Button +// Select Server View - Add Server. "addServer" = "Add Server"; -/* Select Server View - Select All Servers */ +// All Servers - Button +// Select Server View - Select All Servers. "allServers" = "All Servers"; -/* Select Server View - Edit an Existing Server */ +// Edit Server - Button +// Select Server View - Edit an Existing Server. "editServer" = "Edit Server"; -/* Server Detail View - Delete */ +// Delete - Button +// Server Detail View - Delete. "delete" = "Delete"; -/* Server Detail View - Delete Server */ +// Delete Server - Button +// Server Detail View - Delete Server. "deleteServer" = "Delete Server"; -/* Customize Server View - Indicators */ +// Indicators - Button +// Customize Server View - Indicators. "indicators" = "Indicators"; -/* Customize Server View - Posters */ +// Posters - Button +// Customize Server View - Posters. "posters" = "Posters"; -/* Customize Server View - Cinematic Background */ +// Cinematic Background - Button +// Customize Server View - Cinematic Background. "cinematicBackground" = "Cinematic Background"; -/* Customize Server View - Random Image */ +// Random Image - Button +// Customize Server View - Random Image. "randomImage" = "Random Image"; -/* Customize Server View - Show Favorites */ +// Show Favorites - Button +// Customize Server View - Show Favorites. "showFavorites" = "Show Favorites"; -/* Customize Server View - Show Recently Added */ +// Show Recently Added - Button +// Customize Server View - Show Recently Added. "showRecentlyAdded" = "Show Recently Added"; -/* Indicators View - Show Favorited */ +// Show Favorited - Indicators View +// Option to show favorited items. "showFavorited" = "Show Favorited"; -/* Indicators View - Show Progress */ +// Show Progress - Indicators View +// Option to show progress indicators. "showProgress" = "Show Progress"; -/* Indicators View - Show Unwatched */ +// Show Unwatched - Indicators View +// Option to show unwatched items. "showUnwatched" = "Show Unwatched"; -/* Indicators View - Show Watched */ +// Show Watched - Indicators View +// Option to show watched items. "showWatched" = "Show Watched"; -/* Maximum Bitrate View - Playback Quality */ +// Playback Quality - Maximum Bitrate View +// Label for the playback quality setting. "playbackQuality" = "Playback Quality"; -/* Settings View - Video Player */ +// Video Player - Settings View +// Label for the video player settings. "videoPlayer" = "Video Player"; -/* Settings View - Customize */ +// Customize - Settings View +// Label for customization settings. "customize" = "Customize"; -/* Settings View - Logs */ +// Logs - Settings View +// Label for viewing logs. "logs" = "Logs"; -/* General */ +// Jellyfin - General +// Label for Jellyfin branding. "jellyfin" = "Jellyfin"; -/* Video Player Settings View - Offset */ +// Offset - Video Player Settings View +// Label for setting playback offset. "offset" = "Offset"; -/* Video Player Settings View - Resume */ +// Resume - Video Player Settings View +// Label for resuming playback. "resume" = "Resume"; -/* Video Player Settings View - Disclaimer */ +// Subtitles Disclaimer - Video Player Settings View +// Disclaimer about subtitle compatibility. "subtitlesDisclaimer" = "Settings only affect some subtitle types"; -/* Video Player Settings View - Pause on Background */ +// Pause on Background - Video Player Settings View +// Option to pause playback when app goes to the background. "pauseOnBackground" = "Pause on background"; -/* Video Player Settings View - Play on Active */ +// Play on Active - Video Player Settings View +// Option to resume playback when app becomes active. "playOnActive" = "Play on active"; -/* Video Player Settings View - Playback Header */ +// Playback - Video Player Settings View +// Header label for playback settings. "playback" = "Playback"; -/* Video Player Settings View - Resume Offset Title */ +// Resume Offset Title - Video Player Settings View +// Title for the resume offset setting. "resumeOffsetTitle" = "Resume Offset"; -/* Video Player Settings View - Resume Offset Description */ +// Resume Offset Description - Video Player Settings View +// Description for the resume offset setting. "resumeOffsetDescription" = "Resume content seconds before the recorded resume time"; -/* Section Header for a Custom Device Profile */ +// Custom Profile - Section Header +// Header label for custom device profiles. "customProfile" = "Custom Profile"; -/* Section Header for Device Profiles */ +// Device Profile - Section Header +// Header label for device profiles. "deviceProfile" = "Device Profile"; -/* Custom profile is Added to the Existing Profiles */ +// Add Custom Device Profiles - Description +// Description for adding custom profiles to default profiles. "customDeviceProfileAdd" = "The custom device profiles will be added to the default Swiftfin device profiles."; -/* Custom profile will replace the Existing Profiles */ +// Replace Custom Device Profiles - Description +// Description for replacing default profiles with custom profiles. "customDeviceProfileReplace" = "The custom device profiles will replace the default Swiftfin device profiles."; -/* Section for Playback Quality Settings */ +// Playback Quality - Section +// Label for the playback quality section. "playbackQuality" = "Playback Quality"; -/* Override Transcoding Profile */ +// Use as Transcoding Profile - Option +// Option to use a profile for transcoding. "useAsTranscodingProfile" = "Use as Transcoding Profile"; -/* PlaybackCompatibility Default Category */ +// Auto - PlaybackCompatibility Default Category +// Label for the default playback compatibility setting. "auto" = "Auto"; -/* PlaybackCompatibility Compatible Category */ +// Most Compatible - PlaybackCompatibility Compatible Category +// Label for the most compatible playback compatibility setting. "compatible" = "Most Compatible"; -/* PlaybackCompatibility DirectPlay Category */ +// Direct Play - PlaybackCompatibility DirectPlay Category +// Label for the direct play compatibility setting. "direct" = "Direct Play"; -/* PlaybackCompatibility Custom Category */ +// Custom - PlaybackCompatibility Custom Category +// Label for the custom playback compatibility setting. "custom" = "Custom"; -/* PlaybackCompatibility Section Title */ +// Compatibility - PlaybackCompatibility Section Title +// Label for the compatibility section. "compatibility" = "Compatibility"; -/* PlaybackCompatibility Profile Sections */ +// Profiles - PlaybackCompatibility Profile Sections +// Label for playback compatibility profiles. "profiles" = "Profiles"; -/* Behavior */ +// Behavior - General +// Label for behavior settings. "behavior" = "Behavior"; -/* Bitrate Automatic Section Header */ +// Bitrate Test - Bitrate Automatic Section Header +// Header label for bitrate testing settings. "bitrateTest" = "Bitrate Test"; -/* Default Bitrate */ +// Default Bitrate - General +// Label for the default bitrate setting. "bitrateDefault" = "Default Bitrate"; -/* Default Bitrate Description */ +// Default Bitrate Description - General +// Description for the default bitrate setting. "bitrateDefaultDescription" = "Limits the internet bandwidth used during playback."; -/* Playback May Fail */ +// Playback May Fail - Warning +// Warning message for potential playback failure. "mayResultInPlaybackFailure" = "This setting may result in media failing to start playback."; -/* Device Profile Section Description */ +// Custom Device Profile Description - General +// Description for custom device profiles. "customDeviceProfileDescription" = "Dictates back to the Jellyfin Server what this device hardware is capable of playing."; -/* Session Device Section Label */ +// Device - Session Device Section Label +// Label for the device in session details. "device" = "Device"; -/* Session Client Last Seen Section Label */ +// Last Seen - Session Client Last Seen Section Label +// Label for the last seen time in session details. "lastSeen" = "Last Seen"; -/* Transcode Reason(s) Section Label */ +// Transcode Reasons - Section Label +// Label for reasons why transcoding is required. "transcodeReasons" = "Transcode Reason(s)"; -/* Administration Dashboard Section */ +// Administration - Dashboard Section +// Label for the administration dashboard. "administration" = "Administration"; -/* Administration Dashboard Scan All Libraries Button */ +// Scan All Libraries - Button +// Button to trigger a scan for all libraries. "scanAllLibraries" = "Scan All Libraries"; -/* Administration Dashboard Scheduled Tasks */ +// Scheduled Tasks - Administration Dashboard +// Label for scheduled tasks in the administration dashboard. "scheduledTasks" = "Scheduled Tasks"; -/* Transcode FPS */ +// FPS with String - Transcode FPS +// Label for frames per second, formatted with a string. "fpsWithString" = "%@fps"; -/* Session Streaming Clients */ +// Streams - Session Streaming Clients +// Label for active streaming clients. "streams" = "Streams"; -/* Indicates that something is Online */ +// Online - General +// Label to indicate that something is online. "online" = "Online"; -/* Shutdown Server Label */ +// Shutdown Server - Button +// Button to shutdown the server. "shutdownServer" = "Shutdown Server"; -/* Shutdown Warning Label */ +// Shutdown Warning - Confirmation +// Warning message displayed when shutting down the server. "shutdownWarning" = "Are you sure you want to shutdown the server?"; -/* Restart Server Label */ +// Restart Server - Button +// Button to restart the server. "restartServer" = "Restart Server"; -/* Restart Warning Label */ +// Restart Warning - Confirmation +// Warning message displayed when restarting the server. "restartWarning" = "Are you sure you want to restart the server?"; -/* UserDashboardView Header */ +// Active Devices - ActiveSessionsView Header +// Header label for the active devices view. +"activeDevices" = "Active Devices"; + +// Dashboard - UserDashboardView Header +// Header label for the user dashboard view. "dashboard" = "Dashboard"; -/* SessionPlaybackMethod Remaining Time */ +// Remaining Time - SessionPlaybackMethod +// Label for displaying remaining playback time. "itemOverItem" = "%1$@ / %2$@"; -/* Task was Canceled */ +// Canceled - Task Status +// Label for a canceled task. "canceled" = "Cancelled"; -/* Confirm Task Fuction */ +// Confirm - Button +// Button to confirm an action. "confirm" = "Confirm"; -/* PlayMethod - Transcode */ +// Transcode - Play Method +// Label for the transcode play method. "transcode" = "Transcode"; -/* PlayMethod - Remux */ +// Remux - Play Method +// Label for the remux play method. "remux" = "Remux"; -/* PlayMethod - Direct Play */ +// Direct Play - Play Method +// Label for the direct play method. "directPlay" = "Direct Play"; -/* PlayMethod - Direct Stream */ +// Direct Stream - Play Method +// Label for the direct stream method. "directStream" = "Direct Stream"; -/* TranscodeReason - Container Not Supported */ +// Container Not Supported - TranscodeReason +// Error message for unsupported container format. "containerNotSupported" = "The container format is not supported"; -/* TranscodeReason - Video Codec Not Supported */ +// Video Codec Not Supported - TranscodeReason +// Error message for unsupported video codec. "videoCodecNotSupported" = "The video codec is not supported"; -/* TranscodeReason - Audio Codec Not Supported */ +// Audio Codec Not Supported - TranscodeReason +// Error message for unsupported audio codec. "audioCodecNotSupported" = "The audio codec is not supported"; -/* TranscodeReason - Subtitle Codec Not Supported */ +// Subtitle Codec Not Supported - TranscodeReason +// Error message for unsupported subtitle codec. "subtitleCodecNotSupported" = "The subtitle codec is not supported"; -/* TranscodeReason - Audio Is External */ +// Audio Is External - TranscodeReason +// Error message when audio track is external and requires transcoding. "audioIsExternal" = "The audio track is external and requires transcoding"; -/* TranscodeReason - Secondary Audio Not Supported */ +// Secondary Audio Not Supported - TranscodeReason +// Error message for unsupported secondary audio. "secondaryAudioNotSupported" = "Secondary audio is not supported"; -/* TranscodeReason - Video Profile Not Supported */ +// Video Profile Not Supported - TranscodeReason +// Error message for unsupported video profile. "videoProfileNotSupported" = "The video profile is not supported"; -/* TranscodeReason - Video Level Not Supported */ +// Video Level Not Supported - TranscodeReason +// Error message for unsupported video level. "videoLevelNotSupported" = "The video level is not supported"; -/* TranscodeReason - Video Resolution Not Supported */ +// Video Resolution Not Supported - TranscodeReason +// Error message for unsupported video resolution. "videoResolutionNotSupported" = "The video resolution is not supported"; -/* TranscodeReason - Video Bit Depth Not Supported */ +// Video Bit Depth Not Supported - TranscodeReason +// Error message for unsupported video bit depth. "videoBitDepthNotSupported" = "The video bit depth is not supported"; -/* TranscodeReason - Video Framerate Not Supported */ +// Video Framerate Not Supported - TranscodeReason +// Error message for unsupported video framerate. "videoFramerateNotSupported" = "The video framerate is not supported"; -/* TranscodeReason - Reference Frames Not Supported */ +// Reference Frames Not Supported - TranscodeReason +// Error message for unsupported number of reference frames. "refFramesNotSupported" = "The number of reference frames is not supported"; -/* TranscodeReason - Anamorphic Video Not Supported */ +// Anamorphic Video Not Supported - TranscodeReason +// Error message for unsupported anamorphic video. "anamorphicVideoNotSupported" = "Anamorphic video is not supported"; -/* TranscodeReason - Interlaced Video Not Supported */ +// Interlaced Video Not Supported - TranscodeReason +// Error message for unsupported interlaced video. "interlacedVideoNotSupported" = "Interlaced video is not supported"; -/* TranscodeReason - Audio Channels Not Supported */ +// Audio Channels Not Supported - TranscodeReason +// Error message for unsupported number of audio channels. "audioChannelsNotSupported" = "The number of audio channels is not supported"; -/* TranscodeReason - Audio Profile Not Supported */ +// Audio Profile Not Supported - TranscodeReason +// Error message for unsupported audio profile. "audioProfileNotSupported" = "The audio profile is not supported"; -/* TranscodeReason - Audio Sample Rate Not Supported */ +// Audio Sample Rate Not Supported - TranscodeReason +// Error message for unsupported audio sample rate. "audioSampleRateNotSupported" = "The audio sample rate is not supported"; -/* TranscodeReason - Audio Bit Depth Not Supported */ +// Audio Bit Depth Not Supported - TranscodeReason +// Error message for unsupported audio bit depth. "audioBitDepthNotSupported" = "The audio bit depth is not supported"; -/* TranscodeReason - Container Bitrate Exceeds Limit */ +// Container Bitrate Exceeds Limit - TranscodeReason +// Error message when container bitrate exceeds the allowed limit. "containerBitrateExceedsLimit" = "The container bitrate exceeds the allowed limit"; -/* TranscodeReason - Video Bitrate Not Supported */ +// Video Bitrate Not Supported - TranscodeReason +// Error message for unsupported video bitrate. "videoBitrateNotSupported" = "The video bitrate is not supported"; -/* TranscodeReason - Audio Bitrate Not Supported */ +// Audio Bitrate Not Supported - TranscodeReason +// Error message for unsupported audio bitrate. "audioBitrateNotSupported" = "The audio bitrate is not supported"; -/* TranscodeReason - Unknown Video Stream Info */ +// Unknown Video Stream Info - TranscodeReason +// Error message for unknown video stream information. "unknownVideoStreamInfo" = "The video stream information is unknown"; -/* TranscodeReason - Unknown Audio Stream Info */ +// Unknown Audio Stream Info - TranscodeReason +// Error message for unknown audio stream information. "unknownAudioStreamInfo" = "The audio stream information is unknown"; -/* TranscodeReason - Direct Play Error */ +// Direct Play Error - TranscodeReason +// Error message for a direct play failure. "directPlayError" = "An error occurred during direct play"; -/* TranscodeReason - Video Range Type Not Supported */ +// Video Range Type Not Supported - TranscodeReason +// Error message for unsupported video range type. "videoRangeTypeNotSupported" = "The video range type is not supported"; -/* No active session available */ +// No Session - General +// Message displayed when no active session is available. "noSession" = "No session"; -/* The title for the session view */ +// Session - General +// Title for the session view. "session" = "Session"; -/* The client used for the session */ +// Client - General +// Label for the client used in a session. "client" = "Client"; -/* The play method (e.g., Direct Play, Transcoding) */ +// Method - General +// Label for the playback method (e.g., Direct Play, Transcoding). "method" = "Method"; -/* The category label for tasks */ +// Category - General +// Label for the category of a task. "category" = "Category"; -/* The navigation title for the task view */ +// Task - Navigation Title +// Navigation title for the task view. "task" = "Task"; -/* The label for the last run time of a task */ +// Last Run - General +// Label for the last run time of a task. "lastRun" = "Last run"; -/* Description for the dashboard section */ +// Dashboard Description - General +// Description for the administration dashboard section. "dashboardDescription" = "Perform administrative tasks for your Jellyfin server."; -/* Title for the tasks section */ +// Tasks - Section Title +// Title for the tasks section. "tasks" = "Tasks"; -/* Title for the server logs section */ +// Server Logs - Section Title +// Title for the server logs section. "serverLogs" = "Server Logs"; -/* Description for the tasks section */ +// Tasks Description - General +// Description for the tasks section. "tasksDescription" = "Tasks are operations that are scheduled to run periodically or can be triggered manually."; -/* Status label for when a task is running */ +// Running - Status +// Label for a task that is currently running. "running" = "Running..."; -/* Button label to run a task */ +// Run - Button +// Label for the button to run a task. "run" = "Run"; -/* Button label to stop a task */ +// Stop - Button +// Label for the button to stop a task. "stop" = "Stop"; -/* Status label for when a task is cancelling */ +// Cancelling - Status +// Label for a task that is in the process of being canceled. "cancelling" = "Cancelling..."; -/* Last run message with time */ +// Last Run Time - General +// Message showing the last run time of a task. "lastRunTime" = "Last ran %@"; -/* Message shown when a task has never run */ +// Never Run - General +// Message displayed when a task has never run. "neverRun" = "Never run"; -/* Button label to edit a task */ +// Edit - Button +// Label for the button to edit a task. "edit" = "Edit"; -/* Status message for a completed task */ +// Task Completed - Status +// Message displayed when a task has completed successfully. "taskCompleted" = "Completed"; -/* Status message for a failed task */ +// Task Failed - Status +// Message displayed when a task has failed. "taskFailed" = "Failed"; -/* Status message for a cancelled task */ +// Task Cancelled - Status +// Message displayed when a task has been canceled. "taskCancelled" = "Cancelled"; -/* Status message for an aborted task */ +// Task Aborted - Status +// Message displayed when a task has been aborted. "taskAborted" = "Aborted"; -/* Settings Description for enabling rewatching in Next Up */ +// Rewatching in Next Up - Settings Description +// Description for enabling rewatching in the Next Up section. "nextUpRewatch" = "Rewatching in Next Up"; -/* Settings Description for the day limit in Next Up */ +// Days in Next Up - Settings Description +// Description for the day limit in the Next Up section. "nextUpDays" = "Days in Next Up"; -/* Description for how the nextUpDays setting works */ +// Next Up Days Description - General +// Description for the Next Up days setting. "nextUpDaysDescription" = "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it."; -/* Done - Completed, end, or save */ +// Done - General +// Label for completing an action. "done" = "Done"; -/* Save - Completed, end, or save */ +// Save - General +// Label for saving changes. "save" = "Save"; -/* Time Interval Help Text - Days */ +// Days - General +// Label for days as a time interval. "days" = "Days"; -/* Section Title for Column Configuration */ +// Columns - Section Title +// Title for the column configuration section. "columns" = "Columns"; // Date Created - Label // Label for displaying the date an API key was created -// Appears in the API key details "dateCreated" = "Date Created"; // API Keys - Title // Section Title for displaying API keys in the list -// Displays the title of the API key section "apiKeysTitle" = "API Keys"; // API Keys - Description // Explains the usage of API keys in external applications -// Displays below the title in the API key section "apiKeysDescription" = "External applications require an API key to communicate with your server."; // Add - Button // Adds a new record -// Appears in the toolbar "add" = "Add"; // API Key Copied - Alert // Informs the user that the API key was copied to the clipboard -// Displays an alert after the user copies the key "apiKeyCopied" = "API Key Copied"; // API Key Copied - Alert Message // Informs the user that the key was copied successfully -// Appears as a message in the alert "apiKeyCopiedMessage" = "Your API Key was copied to your clipboard!"; // OK - Button // Acknowledges an action -// Used to dismiss the alert "ok" = "OK"; // Delete API Key - Confirmation Message -// Warns the user that deletion is permanent -// Displays a warning message before deletion +// Warns the user that API Key deletion is permanent "deleteAPIKeyMessage" = "Are you sure you want to permanently delete this key?"; // Cancel - Button // Cancels the current action -// Appears in dialogs and alerts "cancel" = "Cancel"; // Delete - Button -// Confirms the deletion of an API key -// Appears in the delete confirmation dialog +// Confirms the deletion of an item "delete" = "Delete"; // Create API Key - Alert // Prompts the user to enter an app name to create an API key -// Appears when creating a new API key "createAPIKey" = "Create API Key"; // Create API Key - Message // Asks the user to enter the name of the application for the new API key -// Displays in the create API key dialog "createAPIKeyMessage" = "Enter the application name for the new API key."; // Application Name - Text Field // Placeholder text for entering the name of the application -// Appears in the create API key dialog "applicationName" = "Application Name"; // Save - Button // Confirms the creation of the new API key -// Appears in the create API key dialog "save" = "Save"; // API Keys - Screen Title // Title for the API keys management screen -// Appears in the navigation bar "apiKeys" = "API Keys"; // Devices - Section Header // Title for the devices section in the Admin Dashboard -// Used as the header for the devices section "devices" = "Devices"; // All Devices Description - Section Description // Description for the all devices section in the Admin Dashboard -// Provides information about the devices connected to the server, including past and current connections "allDevicesDescription" = "View all past and present devices that have connected."; // Delete Selected Devices - Button // Button label for deleting all selected devices -// Used in the all devices section to delete all selected devices "deleteSelectedDevices" = "Delete Selected Devices"; // Never - Filler Text // Text displayed when something has never or will never occur -// Used as placeholder text for events that never happen "never" = "Never"; // Delete Selected Devices Warning - Warning Message // Warning message displayed when deleting all devices -// Informs the user about the consequences of deleting all devices "deleteSelectionDevicesWarning" = "Are you sure you wish to delete all selected devices? All selected sessions will be logged out."; // Delete Device Warning - Warning Message // Warning message displayed when deleting a single device -// Informs the user about the consequences of deleting the device "deleteDeviceWarning" = "Are you sure you wish to delete this device? This session will be logged out."; // Delete Device - Action // Message for deleting a single device in the all devices section -// Used in the confirmation dialog to delete a single device "deleteDevice" = "Delete Device"; // Delete Device Self-Deletion - Error Message // Error message when attempting to delete the current session's device -// Used to inform the user that they cannot delete their own session "deleteDeviceSelfDeletion" = "Cannot delete a session from the same device (%1$@)."; // Delete Device Failed - Error Title // Title for the alert when device deletion fails -// Displayed when the system fails to delete a device "deleteDeviceFailed" = "Failed to Delete Device"; // Custom Device Name - Title // Title for setting a custom device name -// Used in the custom device name section "customDeviceName" = "Custom Device Name"; // Capabilities - Section Header // Title for the section showing the device capabilities -// Displayed as the header for the device capabilities section "capabilities" = "Capabilities"; // Supports Content Uploading - Label // Indicates whether the device supports uploading content -// Used in the capabilities section to display if content uploading is supported "supportsContentUploading" = "Content Uploading"; // Supports Media Control - Label // Indicates whether the device supports media control (e.g., play, pause, stop) -// Used in the capabilities section to display media control capability "supportsMediaControl" = "Media Control"; // Supports Persistent Identifier - Label // Indicates whether the device supports a persistent identifier -// Used in the capabilities section to display persistent identifier support "supportsPersistentIdentifier" = "Persistent Identifier"; // Supports Sync - Label // Indicates whether the device suppoTestrts syncing content (e.g., media sync across devices) -// Used in the capabilities section to display sync capability "supportsSync" = "Sync"; // Yes - Label // Indicates that a capability is supported -// Used in the capabilities section as a positive response "yes" = "Yes"; // No - Label // Indicates that a capability is not supported -// Used in the capabilities section as a negative response "no" = "No"; // Custom Device Name Saved - Label // Confirms that the custom device name was saved successfully -// Used after successfully saving a custom device name "customDeviceNameSaved" = "Your custom device name '%1$@' has been saved."; // Success - Label // Indicates that an operation was successful -// Used as a confirmation for successful actions "success" = "Success"; // Remove All - Button // Deselects all currently selected devices -// Used to clear all selections in selection mode "removeAll" = "Remove All"; // Select All - Button // Selects all available devices -// Used to select all items in selection mode "selectAll" = "Select All"; // Users - Section // Admin Dashboard Section with all Server Users -// Used as a header and a button for All Users "users" = "Users"; // Active - Label // Indication whether an item is active or inactive -// Used as a User describer and a button for All Users "active" = "Active"; // All Users Description - Section Description // Description for the all users section in the Admin Dashboard -// Provides information about the users on the server "allUsersDescription" = "View and manage all registered users on the server, including their permissions and activity status."; // Role - Label // Represents the role of the user -// Shown in user information "role" = "Role"; // Administrator - Title // Label for administrator role -// Indicates the user is an admin "administrator" = "Administrator"; // User - Title // Label for non-administrator users -// Indicates the user is a standard user "user" = "User"; // Activity - Label // Represents user activity status -// Shown in user information "activity" = "Activity"; // Logs Description - View // Access the Jellyfin server logs for troubleshooting and monitoring purposes -// Describes the logs view in settings "logsDescription" = "Access the Jellyfin server logs for troubleshooting and monitoring purposes."; -/* Indicate a type */ +// Type - Label +// Indicate a type "type" = "Type"; // Day of Week - Section Label // Specifies the day of the week for the trigger -// Label for the day of week section "dayOfWeek" = "Day of Week"; // Time - Section Label // Specifies the time for the trigger -// Label for the time section "time" = "Time"; // Daily - Description // Recurring trigger that runs daily -// Describes the daily trigger type "daily" = "Daily"; // Interval - Description // Recurring trigger based on time intervals -// Describes the interval trigger type "interval" = "Interval"; // Weekly - Description // Recurring trigger that runs weekly -// Describes the weekly trigger type "weekly" = "Weekly"; // On Application Startup - Description // Trigger that runs when the application starts -// Describes the startup trigger type "onApplicationStartup" = "On application startup"; // Task Trigger Time Limit - Section Description // Sets the maximum runtime (in hours) for this task trigger -// Description for the task trigger time limit section "taskTriggerTimeLimit" = "Sets the maximum runtime (in hours) for this task trigger."; // Task Trigger Interval - Section Description // Sets the duration (in minutes) in between task triggers -// Description for the task trigger interval section "taskTriggerInterval" = "Sets the duration (in minutes) in between task triggers."; // Every - Label // Used to select interval frequency -// Label for selecting interval frequency "every" = "Every"; // Time Limit with Unit - Label // Specifies time limit along with the unit -// Time limit label with descriptive unit "timeLimitWithUnit" = "Time Limit (%@)"; // Time Limit - Section Label // Specifies the time limit for the task -// Label for the time limit section "timeLimit" = "Time Limit"; // Hours - Input Field Placeholder // Placeholder for inputting hours -// Input field placeholder for hours "hours" = "Hours"; // Minutes - Input Field Placeholder // Placeholder for inputting minutes -// Input field placeholder for minutes "minutes" = "Minutes"; // Add Trigger - Title // Title for adding a new task trigger -// Title for adding a new task trigger "addTrigger" = "Add trigger"; // Save - Button Label // Button to save the current task trigger -// Save button label "save" = "Save"; // Changes Not Saved - Alert Title // Title for unsaved changes alert -// Title for the unsaved changes alert "changesNotSaved" = "Changes not saved"; // Discard Changes - Button Label // Button to discard unsaved changes -// Button label for discarding unsaved changes "discardChanges" = "Discard Changes"; // Unsaved Changes Message - Alert // Message for unsaved changes alert -// Alert message for unsaved changes "unsavedChangesMessage" = "You have unsaved changes. Are you sure you want to discard them?"; // Delete Trigger - Confirmation Dialog Title // Title for the delete trigger confirmation dialog -// Confirmation dialog title for deleting a trigger "deleteTrigger" = "Delete Trigger"; // Delete Trigger Confirmation - Message // Message for deleting a trigger confirmation dialog -// Confirmation dialog message for deleting a trigger "deleteTriggerConfirmationMessage" = "Are you sure you want to delete this trigger? This action cannot be undone."; // Item At Item - Label // Used to describe an item at another item -// Label for something at something else "itemAtItem" = "%1$@ at %2$@"; // Every Interval - Label // Describes an interval trigger with recurring time -// Label for interval trigger with recurring time "everyInterval" = "Every %1$@"; // Time Limit Label with Value - Label // Describes time limit with a value -// Label for time limit with value "timeLimitLabelWithValue" = "Time limit: %1$@"; // Add - Button Label // Button to add a new item -// Button label for adding a new item "add" = "Add"; // Idle - Task State // Describes the task state as idle -// Localized text for task state 'Idle' "idle" = "Idle"; // Status - Section Title // Title for the status section -// Section title for a status section "status" = "Status"; // Error Details - Section Title // Title for the error details section -// Section title for a task error details "errorDetails" = "Error Details"; // Details - Section Title // Title for the details section -// Section title for any details section "details" = "Details"; // Triggers - Section Header // Header for the scheduled task triggers section -// Section header for scheduled task triggers "triggers" = "Triggers"; // Executed - Section Title // Title for the task execution date section -// Section title for a task execution date "executed" = "Executed"; // No Runtime Limit - Label // Describes a task with no runtime limit -// No task trigger runtime limit set "noRuntimeLimit" = "No runtime limit"; // API Key Created - Success Message // A new Access Token was successfully created for the specified application -// Appears in success alert when a new API key is created "serverTriggerCreated" = "A new trigger was created for '%1$@'."; // API Key Deleted - Success Message // The Access Token was successfully deleted for the specified application -// Appears in success alert when an API key is deleted "serverTriggerDeleted" = "The selected trigger was deleted from '%1$@'."; // Save - Button // Confirms that something completed successfully or without error -// Appears in the views with eventful to indicate a task did not fail "success" = "Success"; // Trigger Already Exists - // Message to indicate that a Task Trigger already exists -// Appears in AddServerTask when there is an existing task with the same configuration "triggerAlreadyExists" = "Trigger already exists"; // Add API Key - Button // Creates an API Key if there are no keys available -// Appears in place of the API Key list if there are no API Keys "addAPIKey" = "Add API key"; // Hidden - Filter // Users with a policy of isHidden == True -// Appears on the ServerUsersView to filter Hidden vs Non-Hidden Users "hidden" = "Hidden"; // Delete Selected Users Warning - Warning Message // Warning message displayed when deleting all users -// Informs the user about the consequences of deleting all users "deleteSelectionUsersWarning" = "Are you sure you wish to delete all selected users?"; // Delete User Warning - Warning Message // Warning message displayed when deleting a single user -// Informs the user about the consequences of deleting the user "deleteUserWarning" = "Are you sure you wish to delete this user?"; // Delete User - Action // Message for deleting a single device in the all users section -// Used in the confirmation dialog to delete a single user "deleteUser" = "Delete User"; // Delete User Self-Deletion - Error Message // Error message when attempting to delete the current session's user -// Used to inform the user that they cannot delete their own user "deleteUserSelfDeletion" = "Cannot delete a user from the same user (%1$@)."; // Delete Selected Users - Button // Button label for deleting all selected users -// Used in the all devices section to delete all selected users "deleteSelectedUsers" = "Delete Selected Users"; // Delete User Failed - Error Title // Title for the alert when users deletion fails -// Displayed when the system fails to delete a user "deleteUserFailed" = "Failed to Delete User"; // Confirm Password - TextField // Placeholder and label for confirming the password -// Used in the New User creation form "confirmPassword" = "Confirm Password"; // A username is required - Footer // Validation message shown when the username field is empty -// Used in the New User creation form "usernameRequired" = "A username is required"; // New passwords do not match - Footer // Validation message shown when the new password and confirm password fields do not match -// Used in the New User creation form "passwordsDoNotMatch" = "New passwords do not match"; // New User - Title // Title for the new user creation view -// Used as the navigation title when creating a new user "newUser" = "New User"; // Options - Menu // Menu title for additional actions -// Used as the label for the options menu in the navigation bar "options" = "Options"; // Add User - Button // Button title to add a new user -// Used as the button label in the options menu "addUser" = "Add User"; // Edit Users - Button // Button title to edit existing users -// Used as the button label in the options menu when there are users to edit "editUsers" = "Edit Users"; // Bits Per Second - Unit // Represents a speed in bits per second -// Used for bandwidth display "bitsPerSecond" = "bps"; // Kilobits Per Second - Unit // Represents a speed in kilobits per second -// Used for bandwidth display "kilobitsPerSecond" = "kbps"; // Megabits Per Second - Unit // Represents a speed in megabits per second -// Used for bandwidth display "megabitsPerSecond" = "Mbps"; // Gigabits Per Second - Unit // Represents a speed in gigabits per second -// Used for bandwidth display "gigabitsPerSecond" = "Gbps"; // Terabits Per Second - Unit // Represents a speed in terabits per second -// Used for bandwidth display "terabitsPerSecond" = "Tbps"; // Default - Setting // Represents the default policy or limit -// Used for setting user policies to default values "default" = "Default"; // Unlimited - Setting // Represents no restriction or unlimited policy -// Used for setting user policies with no limits "unlimited" = "Unlimited"; // Create & Join Groups - Action // Allows the user to create and join groups -// Used for setting user permissions related to groups "createAndJoinGroups" = "Create & Join Groups"; // Join Groups - Action // Allows the user to join existing groups -// Used for setting user permissions related to group joining "joinGroups" = "Join Groups"; // Permissions - Section // Represents access control settings for users -// Used for managing user permissions in various sections "permissions" = "Permissions"; // SyncPlay - Feature // Represents the synchronized playback feature across multiple devices -// Used for enabling or managing synchronized streaming sessions "syncPlay" = "SyncPlay"; // Remote connections - Section & Toggle // Represents settings related to remote access -// Used in the external access section of user permissions "remoteConnections" = "Remote connections"; // Maximum remote bitrate - Picker // Represents the maximum bitrate allowed for remote connections -// Used in the external access section "maximumRemoteBitrate" = "Maximum remote bitrate"; // Custom bitrate - Button // Opens an alert to enter a custom bitrate value -// Used in the external access section "customBitrate" = "Custom bitrate"; // Enter custom bitrate in Mbps - Description // Describes the purpose of the custom bitrate entry -// Used in the custom bitrate alert "enterCustomBitrate" = "Enter custom bitrate in Mbps"; // Feature access - Section // Represents settings related to feature access for users -// Used in the feature access section of user permissions "featureAccess" = "Feature access"; // Live TV access - Toggle // Toggles access to live TV content -// Used in the feature access section "liveTvAccess" = "Live TV access"; // Live TV recording management - Toggle // Toggles management of live TV recordings -// Used in the feature access section "liveTvRecordingManagement" = "Live TV recording management"; // Management - Section // Represents settings related to management permissions -// Used in the management section of user permissions "management" = "Management"; // Lyrics - Toggle // Toggles permission to manage lyrics -// Used in the management section "lyrics" = "Lyrics"; // Media playback - Section & Toggle // Represents settings related to media playback permissions -// Used in the media playback section of user permissions "mediaPlayback" = "Media playback"; // Audio transcoding - Toggle // Toggles permission for audio transcoding -// Used in the media playback section "audioTranscoding" = "Audio transcoding"; // Video transcoding - Toggle // Toggles permission for video transcoding -// Used in the media playback section "videoTranscoding" = "Video transcoding"; // Video remuxing - Toggle // Toggles permission for video remuxing -// Used in the media playback section "videoRemuxing" = "Video remuxing"; // Force remote media transcoding - Toggle // Toggles whether remote media transcoding is forced -// Used in the media playback section "forceRemoteTranscoding" = "Force remote media transcoding"; // Media downloads - Toggle // Toggles permission to download media content -// Used in the permission section "mediaDownloads" = "Media downloads"; // Hide user from login screen - Toggle // Toggles whether the user is hidden from the login screen -// Used in the permission section "hideUserFromLoginScreen" = "Hide user from login screen"; // Remote control - Section // Represents settings related to remote control permissions -// Used in the remote control section of user permissions "remoteControl" = "Remote control"; // Control other users - Toggle // Toggles permission to control other users' sessions -// Used in the remote control section "controlOtherUsers" = "Control other users"; // Control shared devices - Toggle // Toggles permission to control shared devices -// Used in the remote control section "controlSharedDevices" = "Control shared devices"; // Sessions - Section // Represents settings related to session control -// Used in the sessions section of user permissions "sessions" = "Sessions"; // Maximum failed login policy - Picker // Represents the policy for maximum failed login attempts -// Used in the sessions section "maximumFailedLoginPolicy" = "Maximum failed login policy"; // Maximum sessions policy - Picker // Represents the policy for maximum active sessions -// Used in the sessions section "maximumSessionsPolicy" = "Maximum sessions policy"; // Custom failed logins - Button // Opens an alert to enter a custom failed login limit -// Used in the sessions section "customFailedLogins" = "Custom failed logins"; // Enter custom failed logins limit - Description // Describes the purpose of the custom failed logins entry -// Used in the custom failed logins alert "enterCustomFailedLogins" = "Enter custom failed logins limit"; // Failed logins - Text Field // Represents the input field for custom failed logins -// Used in the custom failed logins section "failedLogins" = "Failed logins"; // Custom sessions - Button // Opens an alert to enter a custom maximum session limit -// Used in the sessions section "customSessions" = "Custom sessions"; // Enter custom max sessions - Description // Describes the purpose of the custom max sessions entry -// Used in the custom sessions alert "enterCustomMaxSessions" = "Enter custom max sessions"; // Maximum sessions - Text Field @@ -1403,215 +1428,456 @@ // Refresh - Button // Button title for the menu to refresh metadata -// Used as the label for the refresh metadata button "refreshMetadata" = "Refresh Metadata"; // Find Missing - Menu Option // Menu option for finding missing metadata -// Used to trigger a full metadata refresh "findMissing" = "Find Missing"; // Replace Metadata - Menu Option // Menu option for replacing existing metadata -// Used to trigger replacing metadata only "replaceMetadata" = "Replace Metadata"; // Replace Images - Menu Option // Menu option for replacing existing images -// Used to trigger replacing images only "replaceImages" = "Replace Images"; // Replace All - Menu Option // Menu option for replacing both metadata and images -// Used to trigger a full replacement of metadata and images "replaceAll" = "Replace All"; // Delete Item Confirmation Message - Warning message // Warning message to confirm deleting a media item -// Used in a confirmation for item deletion "deleteItemConfirmationMessage" = "Are you sure you want to delete this item? This action cannot be undone."; // Allow Media Item Editing - Toggle // Toggle option for enabling media item editing -// Used to allow users to edit metadata of media items "allowItemEditing" = "Allow media item editing"; // Allow Media Item Deletion - Toggle // Toggle option for enabling media item deletion -// Used to allow users to delete media items "allowItemDeletion" = "Allow media item deletion"; // Item Deletion Permission Failure - Error Message // Alert the user they should not be able to delete something -// Used to inform the user a deletion failed and why it failed "itemDeletionPermissionFailure" = "You do not have permission to delete this item."; // Metadata - Section Title // Title for the ItemEditorView and Metadata related views -// Used as a title for sections/views related to Metadata "metadata" = "Metadata"; // Learn More - Button // Opens a modal with more information -// Used as a button to show details "learnMoreEllipsis" = "Learn more..."; -/// Current Password - Placeholder -/// Placeholder text for the current password input field -/// Used in the ResetUserPasswordView +// Current Password - Placeholder +// Placeholder text for the current password input field "currentPassword" = "Current Password"; -/// New Password - Placeholder -/// Placeholder text for the new password input field -/// Used in the ResetUserPasswordView +// New Password - Placeholder +// Placeholder text for the new password input field "newPassword" = "New Password"; -/// Confirm New Password - Placeholder -/// Placeholder text for confirming the new password input field -/// Used in the ResetUserPasswordView +// Confirm New Password - Placeholder +// Placeholder text for confirming the new password input field "confirmNewPassword" = "Confirm New Password"; -/// Password - Navigation Title -/// Title for the password reset view -/// Used in the navigation bar +// Password - Navigation Title +// Title for the password reset view. "password" = "Password"; -/// Password Changed - Alert Message -/// Message displayed in the success alert after changing the password -/// Used in the ResetUserPasswordView +// Password Changed - Alert Message +// Message displayed in the success alert after changing the password. "passwordChangedMessage" = "User password has been changed."; -/// Passwords Do Not Match - Footer -/// Error message displayed when new passwords do not match -/// Used in the ResetUserPasswordView +// Passwords Do Not Match - Footer +// Error message displayed when new passwords do not match. "passwordsDoNotMatch" = "New passwords do not match."; -/// Password Change Warning - Message -/// Message displayed to alert the user what the password change does and does not do -/// Used in the ResetUserPasswordView +// Password Change Warning - Message +// Message displayed to alert the user what the password change does and does not do. "passwordChangeWarning" = "Changes the Jellyfin server user password. This does not change any Swiftfin settings."; -/// Find Missing - Button -/// Search for missing metadata and images -/// Used to locate media files missing information or images +// Find Missing - Button +// Search for missing metadata and images. "findMissingDescription" = "Find missing metadata and images."; -/// Replace Metadata - Button -/// Overwrite metadata without affecting images -/// Used when updating only metadata information +// Replace Metadata - Button +// Overwrite metadata without affecting images. "replaceMetadataDescription" = "Replace unlocked metadata with new information."; -/// Replace Images - Button -/// Overwrite existing images with new ones -/// Used to refresh media artwork +// Replace Images - Button +// Overwrite existing images with new ones. "replaceImagesDescription" = "Replace all images with new images."; -/// Replace All - Button -/// Replace all metadata and images -/// Full refresh that replaces all unlocked metadata and images +// Replace All - Button +// Replace all metadata and images. "replaceAllDescription" = "Replace all unlocked metadata and images with new information."; -/// Device Profile - Description -/// Explains how device profiles control playback and transcoding behavior -/// Used in the device profile settings section +// Device Profile - Description +// Explains how device profiles control playback and transcoding behavior. "deviceProfileDescription" = "Decide which media plays natively or requires server transcoding for compatibility."; -/// Auto - Description -/// Optimizes playback using default settings for most devices -/// Some formats may require server transcoding +// Auto - Description +// Optimizes playback using default settings for most devices. "autoDescription" = "Optimizes playback using default settings for most devices. Some formats may require server transcoding for non-compatible media types."; -/// Compatible - Description -/// Converts media to H.264 video and AAC audio for compatibility -/// Requires server transcoding for all content +// Compatible - Description +// Converts media to H.264 video and AAC audio for compatibility. "compatibleDescription" = "Converts all media to H.264 video and AAC audio for maximum compatibility. May require server transcoding for non-compatible media types."; -/// Direct - Description -/// Plays content in its original format without transcoding -/// May cause playback issues on unsupported devices +// Direct - Description +// Plays content in its original format without transcoding. "directDescription" = "Plays content in its original format. May cause playback issues on unsupported media types."; -/// Custom - Description -/// Allows customization of device profiles for native playback -/// Incorrect settings may result in playback issues +// Custom - Description +// Allows customization of device profiles for native playback. "customDescription" = "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback."; -/// Server Connection Test - Description -/// Tests the connection to the server to assess internet speed -/// Used to adjust bandwidth settings automatically +// Server Connection Test - Description +// Tests the connection to the server to assess internet speed. "birateAutoDescription" = "Tests your server connection to assess internet speed and adjust bandwidth automatically."; -/// Bandwidth Usage - Description -/// Indicates the maximum bandwidth used per playback stream -/// Helps to manage data usage during streaming +// Bandwidth Usage - Description +// Indicates the maximum bandwidth used per playback stream. "bitrateMaxDescription" = "Maximizes bandwidth usage, up to %@, for each playback stream to ensure the highest quality."; // Bits Per Second - Unit // Represents a speed in bits per second -// Used for bandwidth display "bitsPerSecond" = "bps"; // Kilobits Per Second - Unit // Represents a speed in kilobits per second -// Used for bandwidth display "kilobitsPerSecond" = "kbps"; // Megabits Per Second - Unit // Represents a speed in megabits per second -// Used for bandwidth display "megabitsPerSecond" = "Mbps"; // Gigabits Per Second - Unit // Represents a speed in gigabits per second -// Used for bandwidth display "gigabitsPerSecond" = "Gbps"; // Terabits Per Second - Unit // Represents a speed in terabits per second -// Used for bandwidth display "terabitsPerSecond" = "Tbps"; +// Display Order - Section +// Label for the display order section in metadata settings. +"displayOrder" = "Display Order"; + +// Dates - Section +// Label for the dates section in metadata settings. +"dates" = "Dates"; + +// Date Added - Label +// Label for the date added field. +"dateAdded" = "Date Added"; + +// Release Date - Label +// Label for the release date field. +"releaseDate" = "Release Date"; + +// End Date - Label +// Label for the end date field. +"endDate" = "End Date"; + +// Year - Section +// Label for the year section in metadata settings. +"year" = "Year"; + +// Refresh - Button +// Standard button title to refresh content. +"refresh" = "Refresh"; + +// Find Missing Metadata - Button +// Button title to trigger a search for missing metadata. +"findMissingMetadata" = "Find Missing Metadata"; + +// Replace Metadata - Button +// Button title to replace existing metadata with new data. +"replaceMetadata" = "Replace Metadata"; + +// Replace Images - Button +// Button title to replace existing images with new images. +"replaceImages" = "Replace Images"; + +// Replace All - Button +// Button title to replace all metadata and images. +"replaceAll" = "Replace All"; + +// Refresh Metadata - Menu Title +// Title for the metadata refresh menu. +"refreshMetadata" = "Refresh Metadata"; + +// Official Rating - Label +// Label for the official rating field in media metadata +"officialRating" = "Official Rating"; + +// Production Locations - Label +// Label for production locations in media metadata +"productionLocations" = "Production Locations"; + +// Locked Fields - Section Title +// Section title for locked metadata fields +"lockedFields" = "Locked Fields"; + +// Lock All Fields - Toggle +// Toggle label to lock all metadata fields at once +"lockAllFields" = "Lock All Fields"; + +// Metadata preferences - Section title +// Title for the section displaying metadata localization options +"metadataPreferences" = "Metadata preferences"; + +// Language - Picker title +// Label for the language picker for metadata +"language" = "Language"; + +// Country - Picker title +// Label for the country picker for metadata +"country" = "Country"; + +// Format - Section title +// Title for the video format section in metadata editing +"format" = "Format"; + +// Original aspect ratio - Text field label +// Label for entering the original aspect ratio of the video +"originalAspectRatio" = "Original aspect ratio"; + +// 3D Format - Picker title +// Title for the 3D format picker in video metadata +"format3D" = "3D Format"; + +// Parental Rating - Section title +// Title for the parental rating section in metadata editing +"parentalRating" = "Parental Rating"; + +// Official Rating - Picker title +// Title for the picker to select an official rating +"officialRating" = "Official Rating"; + +// Custom Rating - Picker title +// Title for the picker to select a custom rating +"customRating" = "Custom Rating"; + +// None - Option label +// Label to represent a lack of selection or no value chosen +"none" = "None"; + +// Letter - Label +// Label for the letter category in metadata options +"letter" = "Letter"; + +// Years - Label +// Label for the years category in metadata options +"years" = "Years"; + +// Taglines - Section +// Title for the section displaying or editing taglines +"taglines" = "Taglines"; + +// Tagline - Label +// Label for individual tagline entries +"tagline" = "Tagline"; + +// Reviews - Section +// Title for the reviews section in metadata editing. +"reviews" = "Reviews"; + +// Critics - Label +// Label for critic rating field in metadata editing. +"critics" = "Critics"; + +// Community - Label +// Label for community rating field in metadata editing. +"community" = "Community"; + +// Rating - Placeholder +// Placeholder text for the rating input field. +"rating" = "Rating"; + +// Rating - Description +// Description for any rating field (1 to 10 scale). +"ratingDescription" = "%@ rating on a scale from 1 to 10."; + +// Half Side-by-Side - Format +// Display title for half side-by-side 3D format. +"halfSideBySide" = "Half Side-by-Side"; + +// Full Side-by-Side - Format +// Display title for full side-by-side 3D format. +"fullSideBySide" = "Full Side-by-Side"; + +// Full Top and Bottom - Format +// Display title for full top and bottom 3D format. +"fullTopAndBottom" = "Full Top and Bottom"; + +// Half Top and Bottom - Format +// Display title for half top and bottom 3D format. +"halfTopAndBottom" = "Half Top and Bottom"; + +// MVC - Format +// Display title for MVC 3D format. +"mvc" = "MVC"; + +// Continuing - Series Status +// Display title for series status when a series is ongoing. +"continuing" = "Continuing"; + +// Ended - Series Status +// Display title for series status when a series has ended. +"ended" = "Ended"; + +// Unreleased - Series Status +// Display title for series status when a series has not yet been released. +"unreleased" = "Unreleased"; + +// Aired - Series Display Order +// Display title for series display order "Aired". +"aired" = "Aired"; + +// Original Air Date - Series Display Order +// Display title for series display order "Original Air Date". +"originalAirDate" = "Original Air Date"; + +// Absolute - Series Display Order +// Display title for series display order "Absolute". +"absolute" = "Absolute"; + +// DVD - Series Display Order +// Display title for series display order "DVD". +"dvd" = "DVD"; + +// Digital - Series Display Order +// Display title for series display order "Digital". +"digital" = "Digital"; + +// Story Arc - Series Display Order +// Display title for series display order "Story Arc". +"storyArc" = "Story Arc"; + +// Production - Series Display Order +// Display title for series display order "Production". +"production" = "Production"; + +// TV - Series Display Order +// Display title for series display order "TV". +"tv" = "TV"; + +// Alternate - Series Display Order +// Display title for series display order "Alternate". +"alternate" = "Alternate"; + +// Regional - Series Display Order +// Display title for series display order "Regional". +"regional" = "Regional"; + +// Alternate DVD - Series Display Order +// Display title for series display order "Alternate DVD". +"alternateDVD" = "Alternate DVD"; + +// Date Modified - Box Set Display Order +// Display title for box set display order "Date Modified". +"dateModified" = "Date Modified"; + +// Sort Name - Box Set Display Order +// Display title for box set display order "Sort Name". +"sortName" = "Sort Name"; + +// Premiere Date - Box Set Display Order +// Display title for box set display order "Premiere Date". +"premiereDate" = "Premiere Date"; + +// File Path - Section Title +// Section title for displaying the file path of the item. +"filePath" = "File Path"; + +// Title - Section Title +// Section title for displaying or editing the main title of the item. +"title" = "Title"; + +// Original Title - Section Title +// Section title for displaying or editing the original title of the item. +"originalTitle" = "Original Title"; + +// Sort Title - Section Title +// Section title for displaying or editing the title used for sorting the item. +"sortTitle" = "Sort Title"; + +// Unknown - Placeholder +// Placeholder text for unknown file paths or values. +"unknown" = "Unknown"; + +// Air Time - Label for air time date picker +// Label for selecting the air time of an episode. +"airTime" = "Air Time"; + +// Run Time - Label for runtime input field +// Label for specifying episode runtime. +"runTime" = "Run Time"; + +// Episode Runtime Description - Description for runtime input +// Description displayed below runtime input for episodes. +"episodeRuntimeDescription" = "Episode runtime in minutes."; + +// Episode - Label for episode input +// Title for the episode input field. +"episode" = "Episode"; + +// Enter Season Number - Description for season input field +// Description explaining the purpose of the season input. +"enterSeasonNumber" = "Enter the season number."; + +// Enter Episode Number - Description for episode input field +// Description explaining the purpose of the episode input. +"enterEpisodeNumber" = "Enter the episode number."; + +// Birthday - Label for birthday input +// Title for the birthday input field. +"birthday" = "Birthday"; + +// Birth Year - Label for birth year input +// Title for the birth year input field. +"birthYear" = "Birth year"; + +// Date of Death - Label for date of death input +// Title for the date of death input field. +"dateOfDeath" = "Date of death"; + // Maximum Failed Login Policy - Description // Explanation of the maximum failed login attempts policy -// Used in the user settings view "maximumFailedLoginPolicyDescription" = "Sets the maximum failed login attempts before a user is locked out."; // Maximum Failed Login Policy Re-enable - Description // Explanation of the resetting locked users -// Used in the user settings view "maximumFailedLoginPolicyReenable" = "Locked users must be re-enabled by an Administrator."; // Locked Users - Title // Section title for description on Locked Users -// Used in the user settings view "lockedUsers" = "Locked users"; // Unlimited - Description // Explanation of the unlimited login attempts policy -// Used in the user settings view "unlimitedFailedLoginDescription" = "Allows unlimited failed login attempts without locking the user."; // Default - Description // Explanation of the default login attempts policy -// Used in the user settings view "defaultFailedLoginDescription" = "Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts."; // Custom - Description // Explanation of the custom login attempts policy -// Used in the user settings view "customFailedLoginDescription" = "Manually set the number of failed login attempts allowed before locking the user."; // Maximum Connections Policy - Description // Explanation of the maximum connections policy -// Used in the user settings view "maximumConnectionsDescription" = "Limits the total number of connections a user can have to the server."; // Unlimited Connections - Description // Explanation of unlimited connections policy -// Used in the user settings view "unlimitedConnectionsDescription" = "The user can connect to the server without any limits."; // Custom Connections - Description // Explanation of custom connections policy -// Used in the user settings view "customConnectionsDescription" = "Manually set the maximum number of connections a user can have to the server.";