diff --git a/Shared/Extensions/FormatStyle.swift b/Shared/Extensions/FormatStyle.swift index 82d2fb68..01788fbf 100644 --- a/Shared/Extensions/FormatStyle.swift +++ b/Shared/Extensions/FormatStyle.swift @@ -129,3 +129,29 @@ extension FormatStyle where Self == LastSeenFormatStyle { static var lastSeen: LastSeenFormatStyle { LastSeenFormatStyle() } } + +struct IntBitRateFormatStyle: FormatStyle { + func format(_ value: Int) -> String { + let units = [ + L10n.bitsPerSecond, + L10n.kilobitsPerSecond, + L10n.megabitsPerSecond, + L10n.gigabitsPerSecond, + L10n.terabitsPerSecond, + ] + var adjustedValue = Double(value) + var unitIndex = 0 + + while adjustedValue >= 1000, unitIndex < units.count - 1 { + adjustedValue /= 1000 + unitIndex += 1 + } + + let formattedValue = String(format: "%.1f", adjustedValue) + return "\(formattedValue) \(units[unitIndex])" + } +} + +extension FormatStyle where Self == IntBitRateFormatStyle { + static var bitRate: IntBitRateFormatStyle { IntBitRateFormatStyle() } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index f61d1987..de3c575e 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -108,6 +108,8 @@ internal enum L10n { internal static let authorize = L10n.tr("Localizable", "authorize", fallback: "Authorize") /// PlaybackCompatibility Default Category 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.") /// Auto Play internal static let autoPlay = L10n.tr("Localizable", "autoPlay", fallback: "Auto Play") /// Back @@ -116,12 +118,14 @@ internal enum L10n { internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons") /// Behavior internal static let behavior = L10n.tr("Localizable", "behavior", fallback: "Behavior") + /// 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 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 - internal static let bitrateDefaultDescription = L10n.tr("Localizable", "bitrateDefaultDescription", fallback: "Limits the internet bandwidth used during video 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 internal static let bitrateKbps1500 = L10n.tr("Localizable", "bitrateKbps1500", fallback: "480p - 1.5 Mbps") /// Option to set the bitrate to 360p quality at 420 Kbps @@ -130,6 +134,10 @@ internal enum L10n { internal static let bitrateKbps720 = L10n.tr("Localizable", "bitrateKbps720", fallback: "480p - 720 Kbps") /// Option for the maximum bitrate 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 internal static let bitrateMbps10 = L10n.tr("Localizable", "bitrateMbps10", fallback: "1080p - 10 Mbps") /// Option to set the bitrate to 4K quality at 120 Mbps @@ -154,10 +162,10 @@ internal enum L10n { internal static let bitrateMbps80 = L10n.tr("Localizable", "bitrateMbps80", fallback: "4K - 80 Mbps") /// Bitrate Automatic Section Header internal static let bitrateTest = L10n.tr("Localizable", "bitrateTest", fallback: "Bitrate Test") - /// Description for bitrate test duration description - internal static let bitrateTestDescription = L10n.tr("Localizable", "bitrateTestDescription", fallback: "Determines the length of the 'Auto' bitrate test used to find the available internet bandwidth") /// Description for bitrate test duration indicating longer tests provide more accurate bitrates but may delay playback - internal static let bitrateTestDisclaimer = L10n.tr("Localizable", "bitrateTestDisclaimer", fallback: "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") /// Blue internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue") /// Bugs and Features @@ -220,6 +228,8 @@ internal enum L10n { internal static let compatibility = L10n.tr("Localizable", "compatibility", fallback: "Compatibility") /// PlaybackCompatibility Compatible Category 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 internal static let confirm = L10n.tr("Localizable", "confirm", fallback: "Confirm") /// Confirm Close @@ -262,6 +272,8 @@ internal enum L10n { internal static let currentPosition = L10n.tr("Localizable", "currentPosition", fallback: "Current Position") /// PlaybackCompatibility Custom Category internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom") + /// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback. + internal static let customDescription = L10n.tr("Localizable", "customDescription", fallback: "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback.") /// Custom Device Name internal static let customDeviceName = L10n.tr("Localizable", "customDeviceName", fallback: "Custom Device Name") /// Your custom device name '%1$@' has been saved. @@ -269,11 +281,11 @@ internal enum L10n { 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 - internal static let customDeviceProfileAdd = L10n.tr("Localizable", "customDeviceProfileAdd", fallback: "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 - internal static let customDeviceProfileDescription = L10n.tr("Localizable", "customDeviceProfileDescription", fallback: "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 - internal static let customDeviceProfileReplace = L10n.tr("Localizable", "customDeviceProfileReplace", fallback: "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.") /// Settings View - Customize internal static let customize = L10n.tr("Localizable", "customize", fallback: "Customize") /// Section Header for a Custom Device Profile @@ -342,10 +354,14 @@ internal enum L10n { internal static let device = L10n.tr("Localizable", "device", fallback: "Device") /// Section Header for Device Profiles 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 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 @@ -428,6 +444,8 @@ internal enum L10n { internal static let genres = L10n.tr("Localizable", "genres", fallback: "Genres") /// Gestures internal static let gestures = L10n.tr("Localizable", "gestures", fallback: "Gestures") + /// Gbps + internal static let gigabitsPerSecond = L10n.tr("Localizable", "gigabitsPerSecond", fallback: "Gbps") /// Green internal static let green = L10n.tr("Localizable", "green", fallback: "Green") /// Grid @@ -486,6 +504,8 @@ internal enum L10n { } /// Kids internal static let kids = L10n.tr("Localizable", "kids", fallback: "Kids") + /// kbps + internal static let kilobitsPerSecond = L10n.tr("Localizable", "kilobitsPerSecond", fallback: "kbps") /// Larger internal static let larger = L10n.tr("Localizable", "larger", fallback: "Larger") /// Largest @@ -535,9 +555,11 @@ internal enum L10n { /// Option to set the maximum bitrate for playback internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate") /// Playback May Fail - internal static let mayResultInPlaybackFailure = L10n.tr("Localizable", "mayResultInPlaybackFailure", fallback: "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") + /// Mbps + internal static let megabitsPerSecond = L10n.tr("Localizable", "megabitsPerSecond", fallback: "Mbps") /// Menu Buttons internal static let menuButtons = L10n.tr("Localizable", "menuButtons", fallback: "Menu Buttons") /// Metadata @@ -1016,6 +1038,8 @@ internal enum L10n { internal static let taskTriggerInterval = L10n.tr("Localizable", "taskTriggerInterval", fallback: "Sets the duration (in minutes) in between task triggers.") /// Sets the maximum runtime (in hours) for this task trigger. 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 internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size") /// Time diff --git a/Swiftfin tvOS/Views/LearnMoreModal.swift b/Swiftfin tvOS/Views/LearnMoreModal.swift new file mode 100644 index 00000000..ae551b31 --- /dev/null +++ b/Swiftfin tvOS/Views/LearnMoreModal.swift @@ -0,0 +1,45 @@ +// +// 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 LearnMoreModal: View { + + private let items: [TextPair] + + // MARK: - Initializer + + init(@ArrayBuilder items: () -> [TextPair]) { + self.items = items() + } + + // MARK: - Body + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + ForEach(items) { content in + VStack(alignment: .leading, spacing: 8) { + Text(content.title) + .font(.headline) + .foregroundStyle(.primary) + + Text(content.subtitle) + .font(.subheadline) + .foregroundStyle(.secondary) + } + .padding(8) + } + } + .padding(24) + .background { + RoundedRectangle(cornerRadius: 10) + .fill(Material.regular) + } + .padding() + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift b/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift index 46c67610..9bb4369e 100644 --- a/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift @@ -20,13 +20,22 @@ struct PlaybackQualitySettingsView: View { @EnvironmentObject private var router: PlaybackQualitySettingsCoordinator.Router + // MARK: - Focus Management + + @FocusState + private var focusedItem: FocusableItem? + + private enum FocusableItem: Hashable { + case maximumBitrate + case compatibility + } + + // MARK: - Body + var body: some View { SplitFormWindowView() .descriptionView { - Image(systemName: "play.rectangle.on.rectangle") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 400) + descriptionView } .contentView { Section { @@ -34,12 +43,11 @@ struct PlaybackQualitySettingsView: View { title: L10n.maximumBitrate, selection: $appMaximumBitrate ) + .focused($focusedItem, equals: .maximumBitrate) } header: { L10n.bitrateDefault.text } footer: { - VStack(alignment: .leading) { - L10n.bitrateDefaultDescription.text - } + L10n.bitrateDefaultDescription.text } .animation(.none, value: appMaximumBitrate) @@ -49,13 +57,8 @@ struct PlaybackQualitySettingsView: View { title: L10n.testSize, selection: $appMaximumBitrateTest ) - } header: { - L10n.bitrateTest.text } footer: { - VStack(alignment: .leading, spacing: 8) { - L10n.bitrateTestDescription.text - L10n.bitrateTestDisclaimer.text - } + L10n.bitrateTestDisclaimer.text } } @@ -64,7 +67,7 @@ struct PlaybackQualitySettingsView: View { title: L10n.compatibility, selection: $compatibilityMode ) - .animation(.none, value: compatibilityMode) + .focused($focusedItem, equals: .compatibility) if compatibilityMode == .custom { ChevronButton(L10n.profiles) @@ -74,8 +77,66 @@ struct PlaybackQualitySettingsView: View { } } header: { L10n.deviceProfile.text + } footer: { + L10n.deviceProfileDescription.text } } .navigationTitle(L10n.playbackQuality) } + + // MARK: - Description View Icon + + private var descriptionView: some View { + ZStack { + Image(systemName: "play.rectangle.on.rectangle") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + + focusedDescription + .transition(.opacity.animation(.linear(duration: 0.2))) + } + } + + // MARK: - Description View on Focus + + @ViewBuilder + private var focusedDescription: some View { + switch focusedItem { + case .maximumBitrate: + LearnMoreModal { + TextPair( + title: L10n.auto, + subtitle: L10n.birateAutoDescription + ) + TextPair( + title: L10n.bitrateMax, + subtitle: L10n.bitrateMaxDescription(PlaybackBitrate.max.rawValue.formatted(.bitRate)) + ) + } + + case .compatibility: + LearnMoreModal { + TextPair( + title: L10n.auto, + subtitle: L10n.autoDescription + ) + TextPair( + title: L10n.compatible, + subtitle: L10n.compatibleDescription + ) + TextPair( + title: L10n.direct, + subtitle: L10n.directDescription + ) + TextPair( + title: L10n.custom, + subtitle: L10n.customDescription + ) + } + + case nil: + EmptyView() + } + } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 965bd330..2065061e 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ 4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; 4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; + 4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */; }; 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 4E8F74A22CE03C9000CC8969 /* ItemEditorCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */; }; @@ -1121,6 +1122,7 @@ 4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = ""; }; 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = ""; }; 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = ""; }; + 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreModal.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = ""; }; 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorCoordinator.swift; sourceTree = ""; }; 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemEditorView.swift; sourceTree = ""; }; @@ -3269,6 +3271,7 @@ E1A42E4D28CBD3B200A14DCB /* HomeView */, E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */, E193D54E271942C000900D82 /* ItemView */, + 4E884C642CEBB2FF004CF6AD /* LearnMoreModal.swift */, E158C8D02A31947500C527C5 /* MediaSourceInfoView.swift */, E103DF932BCF31C5000229B2 /* MediaView */, E10231572BCF8AF8009D71FC /* ProgramsView */, @@ -4831,6 +4834,7 @@ E14EDEC62B8FB64E000F00A4 /* AnyItemFilter.swift in Sources */, E14EDEC92B8FB65F000F00A4 /* ItemFilterType.swift in Sources */, E1D37F4C2B9CEA5C00343D2B /* ImageSource.swift in Sources */, + 4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */, E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */, E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */, diff --git a/Swiftfin/Components/LearnMoreButton.swift b/Swiftfin/Components/LearnMoreButton.swift index e3830045..37097847 100644 --- a/Swiftfin/Components/LearnMoreButton.swift +++ b/Swiftfin/Components/LearnMoreButton.swift @@ -30,32 +30,38 @@ struct LearnMoreButton: View { isPresented = true } .foregroundStyle(Color.accentColor) - .font(.subheadline) + .buttonStyle(.plain) + .frame(maxWidth: .infinity, alignment: .leading) .sheet(isPresented: $isPresented) { - NavigationView { - ScrollView { - VStack(alignment: .leading, spacing: 16) { - ForEach(items) { content in - VStack(alignment: .leading, spacing: 8) { - Text(content.title) - .font(.headline) - .foregroundStyle(.primary) + learnMoreView + } + } - Text(content.subtitle) - .font(.subheadline) - .foregroundStyle(.secondary) - } + // MARK: - Learn More View - Divider() + private var learnMoreView: some View { + NavigationView { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + ForEach(items) { content in + VStack(alignment: .leading, spacing: 8) { + Text(content.title) + .font(.headline) + .foregroundStyle(.primary) + + Text(content.subtitle) + .font(.subheadline) + .foregroundStyle(.secondary) } + Divider() } - .edgePadding() - } - .navigationTitle(title) - .navigationBarTitleDisplayMode(.inline) - .navigationBarCloseButton { - isPresented = false } + .edgePadding() + } + .navigationTitle(title) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + isPresented = false } } } diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift index 6e881ad3..45e5c296 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift @@ -72,7 +72,7 @@ struct CustomDeviceProfileSettingsView: View { .navigationTitle(L10n.profiles) .topBarTrailing { if customProfiles.isNotEmpty { - Button("Add") { + Button(L10n.add) { UIDevice.impact(.light) router.route(to: \.createCustomDeviceProfile) } diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index 64e3dc01..4bede18b 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -150,7 +150,7 @@ struct CustomizeViewsSettings: View { if libraryDisplayType == .list, UIDevice.isPad { BasicStepper( - title: "Columns", + title: L10n.columns, value: $listColumnCount, range: 1 ... 4, step: 1 diff --git a/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift b/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift index 11e7130e..332a1464 100644 --- a/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift +++ b/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift @@ -31,7 +31,20 @@ struct PlaybackQualitySettingsView: View { } header: { L10n.bitrateDefault.text } footer: { - L10n.bitrateDefaultDescription.text + VStack(alignment: .leading) { + Text(L10n.bitrateDefaultDescription) + LearnMoreButton(L10n.bitrateDefault) { + TextPair( + title: L10n.auto, + subtitle: L10n.birateAutoDescription + ) + TextPair( + title: L10n.bitrateMax, + subtitle: L10n.bitrateMaxDescription(PlaybackBitrate.max.rawValue.formatted(.bitRate)) + ) + } + .foregroundStyle(.foreground, .primary) + } } .animation(.none, value: appMaximumBitrate) @@ -44,18 +57,12 @@ struct PlaybackQualitySettingsView: View { } header: { L10n.bitrateTest.text } footer: { - VStack(alignment: .leading, spacing: 8) { - L10n.bitrateTestDescription.text + VStack(alignment: .leading) { L10n.bitrateTestDisclaimer.text } } } - // TODO: Have a small description and a "Learn More..." - // button that will open a page for longer descriptions - // of each option. See: iOS Settings/Accessibility/VoiceOver - // for reference - Section { CaseIterablePicker( L10n.compatibility, @@ -70,7 +77,30 @@ struct PlaybackQualitySettingsView: View { } } } header: { - L10n.deviceProfile.text + Text(L10n.deviceProfile) + } footer: { + VStack(alignment: .leading) { + Text(L10n.deviceProfileDescription) + LearnMoreButton(L10n.deviceProfile) { + TextPair( + title: L10n.auto, + subtitle: L10n.autoDescription + ) + TextPair( + title: L10n.compatible, + subtitle: L10n.compatibleDescription + ) + TextPair( + title: L10n.direct, + subtitle: L10n.directDescription + ) + TextPair( + title: L10n.custom, + subtitle: L10n.customDescription + ) + } + .foregroundStyle(.foreground, .primary) + } } } .animation(.linear, value: appMaximumBitrate) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 70e9cb5c..b46b2794 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -364,11 +364,8 @@ /* Option to set the bitrate to 360p quality at 420 Kbps */ "bitrateKbps420" = "360p - 420 Kbps"; -/* Description for bitrate test duration description */ -"bitrateTestDescription" = "Determines the length of the 'Auto' bitrate test used to find the available internet bandwidth"; - /* Description for bitrate test duration indicating longer tests provide more accurate bitrates but may delay playback */ -"bitrateTestDisclaimer" = "Longer tests are more accurate but may result in a delayed playback"; +"bitrateTestDisclaimer" = "Longer tests are more accurate but may result in a delayed playback."; /* Select Server View */ "servers" = "Servers"; @@ -464,10 +461,10 @@ "deviceProfile" = "Device Profile"; /* Custom profile is Added to the Existing Profiles */ -"customDeviceProfileAdd" = "The custom device profiles will be added to the default Swiftfin device profiles"; +"customDeviceProfileAdd" = "The custom device profiles will be added to the default Swiftfin device profiles."; /* Custom profile will replace the Existing Profiles */ -"customDeviceProfileReplace" = "The custom device profiles will replace the default Swiftfin device profiles"; +"customDeviceProfileReplace" = "The custom device profiles will replace the default Swiftfin device profiles."; /* Section for Playback Quality Settings */ "playbackQuality" = "Playback Quality"; @@ -503,13 +500,13 @@ "bitrateDefault" = "Default Bitrate"; /* Default Bitrate Description */ -"bitrateDefaultDescription" = "Limits the internet bandwidth used during video playback"; +"bitrateDefaultDescription" = "Limits the internet bandwidth used during playback."; /* Playback May Fail */ -"mayResultInPlaybackFailure" = "This setting may result in media failing to start playback"; +"mayResultInPlaybackFailure" = "This setting may result in media failing to start playback."; /* Device Profile Section Description */ -"customDeviceProfileDescription" = "Dictates back to the Jellyfin Server what this device hardware is capable of playing"; +"customDeviceProfileDescription" = "Dictates back to the Jellyfin Server what this device hardware is capable of playing."; /* Session Device Section Label */ "device" = "Device"; @@ -1322,3 +1319,63 @@ /// Replace all metadata and images /// Full refresh that replaces all unlocked 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 +"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 +"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 +"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 +"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 +"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 +"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 +"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";