diff --git a/Documentation/players.md b/Documentation/players.md index 44ebe0fe5..0a77146f3 100644 --- a/Documentation/players.md +++ b/Documentation/players.md @@ -46,7 +46,6 @@ Swiftfin offers two player options: Swiftfin (VLCKit) and Native (AVKit). The Sw **Notes** - Unsupported containers will require transcoding or remuxing to play. -- AV1 is disabled by default but can be enabled for Native (AVKit) using Custom Device Profiles. --- @@ -86,7 +85,7 @@ Swiftfin offers two player options: Swiftfin (VLCKit) and Native (AVKit). The Sw | Video Codec | Swiftfin (VLCKit) | Native (AVKit) | |-------------|-------------------|----------------| -| **AV1** | ✅ | ❌ | +| **AV1** | ✅ | 🟡 Limited support | | **H.264** | ✅ | ✅ | | **H.265** | ✅ | ✅ | | **MPEG-2** | ✅ | ❌ | @@ -94,6 +93,8 @@ Swiftfin offers two player options: Swiftfin (VLCKit) and Native (AVKit). The Sw | **VP8** | ✅ | ❌ | | **VP9** | ✅ | ❌ | +- AV1 is disabled by default but can be enabled for Native (AVKit) using Custom Device Profiles. Enabling AV1 may result in a [poor experience for SOCs prior to A17](https://en.wikipedia.org/wiki/Apple_A17). + --- ## Subtitle Support @@ -136,4 +137,4 @@ Swiftfin offers two player options: Swiftfin (VLCKit) and Native (AVKit). The Sw - Dolby Vision Profile 10 requires AV1 to be enabled to work in Native (AVKit). - Swiftfin (VLCKit) does not support HDR playback natively. HDR content may play back without the intended high dynamic range effect. ---- \ No newline at end of file +--- diff --git a/PreferencesView/Sources/PreferencesView/Box.swift b/PreferencesView/Sources/PreferencesView/Box.swift index dc054e087..99f822a1c 100644 --- a/PreferencesView/Sources/PreferencesView/Box.swift +++ b/PreferencesView/Sources/PreferencesView/Box.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // class Box { diff --git a/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift b/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift index 1227f481a..484d4a73f 100644 --- a/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift +++ b/PreferencesView/Sources/PreferencesView/KeyCommandAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift b/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift index f287d5fa2..41da3f77b 100644 --- a/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift +++ b/PreferencesView/Sources/PreferencesView/KeyCommandsBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift b/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift index a19e44797..16b4150c6 100644 --- a/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift +++ b/PreferencesView/Sources/PreferencesView/PreferenceKeys.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/PreferencesView.swift b/PreferencesView/Sources/PreferencesView/PreferencesView.swift index 2ed71ce90..031180aaa 100644 --- a/PreferencesView/Sources/PreferencesView/PreferencesView.swift +++ b/PreferencesView/Sources/PreferencesView/PreferencesView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/PressCommandAction.swift b/PreferencesView/Sources/PreferencesView/PressCommandAction.swift index f9e5af83f..98d028ba7 100644 --- a/PreferencesView/Sources/PreferencesView/PressCommandAction.swift +++ b/PreferencesView/Sources/PreferencesView/PressCommandAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift b/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift index 5d67c31c9..ddc7d2b97 100644 --- a/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift +++ b/PreferencesView/Sources/PreferencesView/PressCommandBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift b/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift index abe471a97..8bc6bac58 100644 --- a/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift +++ b/PreferencesView/Sources/PreferencesView/UIPreferencesHostingController.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift b/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift index b35c85c38..b4f53b3bc 100644 --- a/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift +++ b/PreferencesView/Sources/PreferencesView/UIViewController+Swizzling.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwizzleSwift diff --git a/PreferencesView/Sources/PreferencesView/ViewExtensions.swift b/PreferencesView/Sources/PreferencesView/ViewExtensions.swift index 39ce78302..33e434f94 100644 --- a/PreferencesView/Sources/PreferencesView/ViewExtensions.swift +++ b/PreferencesView/Sources/PreferencesView/ViewExtensions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/RedrawOnNotificationView.swift b/RedrawOnNotificationView.swift index 9b86d4a66..1f1881af0 100644 --- a/RedrawOnNotificationView.swift +++ b/RedrawOnNotificationView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -13,10 +13,16 @@ struct RedrawOnNotificationView: View { @State private var id = 0 + private let filter: (P) -> Bool private let key: Notifications.Key

private let content: () -> Content - init(_ key: Notifications.Key

, @ViewBuilder content: @escaping () -> Content) { + init( + _ key: Notifications.Key

, + filter: @escaping (P) -> Bool = { _ in true }, + @ViewBuilder content: @escaping () -> Content + ) { + self.filter = filter self.key = key self.content = content } @@ -24,7 +30,8 @@ struct RedrawOnNotificationView: View { var body: some View { content() .id(id) - .onNotification(key) { _ in + .onNotification(key) { p in + guard filter(p) else { return } id += 1 } } diff --git a/Scripts/Translations/AlphabetizeStrings.swift b/Scripts/Translations/AlphabetizeStrings.swift new file mode 100644 index 000000000..bacdb0b48 --- /dev/null +++ b/Scripts/Translations/AlphabetizeStrings.swift @@ -0,0 +1,54 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation + +// Get the English localization file +let fileURL = URL(fileURLWithPath: "./Translations/en.lproj/Localizable.strings") + +// This regular expression pattern matches lines of the format: +// "Key" = "Value"; +let regex = #/^\"(?[^\"]+)\"\s*=\s*\"(?[^\"]+)\";/# + +// Attempt to read the file content. +guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { + print("Unable to read file: \(fileURL.path)") + exit(1) +} + +// Split file content by newlines to process line by line. +let strings = content.components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty && !$0.hasPrefix("//") } + +let entries = strings.reduce(into: [String: String]()) { + if let match = $1.firstMatch(of: regex) { + let key = String(match.output.key) + let value = String(match.output.value) + $0[key] = value + } else { + print("Error: Invalid line format in \(fileURL.path): \($1)") + exit(1) + } +} + +// Sort the keys alphabetically for consistent ordering. +let sortedKeys = entries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } +let newContent = sortedKeys.map { "/// \(entries[$0]!)\n\"\($0)\" = \"\(entries[$0]!)\";" }.joined(separator: "\n\n") + +// Write the updated, sorted, and commented localizations back to the file. +do { + try newContent.write(to: fileURL, atomically: true, encoding: .utf8) + + if let derivedFileDirectory = ProcessInfo.processInfo.environment["DERIVED_FILE_DIR"] { + try? "".write(toFile: derivedFileDirectory + "/alphabetizeStrings.txt", atomically: true, encoding: .utf8) + } +} catch { + print("Error: Failed to write to \(fileURL.path)") + exit(1) +} diff --git a/Scripts/Translations/PurgeUnusedStrings.swift b/Scripts/Translations/PurgeUnusedStrings.swift new file mode 100755 index 000000000..ff9c9cc3d --- /dev/null +++ b/Scripts/Translations/PurgeUnusedStrings.swift @@ -0,0 +1,111 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation + +// Path to the English localization file +let localizationFile = "./Translations/en.lproj/Localizable.strings" + +// Directories to scan for Swift files +let directoriesToScan = ["./Shared", "./Swiftfin", "./Swiftfin tvOS"] + +// File to exclude from scanning +let excludedFile = "./Shared/Strings/Strings.swift" + +// Regular expressions to match localization entries and usage in Swift files +// Matches lines like "Key" = "Value"; +let localizationRegex = #/^\"(?[^\"]+)\"\s*=\s*\"(?[^\"]+)\";$/# + +// Matches usage like L10n.key in Swift files +let usageRegex = #/L10n\.(?[a-zA-Z0-9_]+)/# + +// Attempt to load the localization file's content +guard let localizationContent = try? String(contentsOfFile: localizationFile, encoding: .utf8) else { + print("Unable to read localization file at \(localizationFile)") + exit(1) +} + +// Split the file into lines and initialize a dictionary for localization entries +let localizationLines = localizationContent.components(separatedBy: .newlines) +var localizationEntries = [String: String]() + +// Parse each line to extract key-value pairs +for line in localizationLines { + let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) + + // Skip empty lines or comments + if trimmed.isEmpty || trimmed.hasPrefix("//") { continue } + + // Match valid localization entries and add them to the dictionary + if let match = line.firstMatch(of: localizationRegex) { + let key = String(match.output.key) + let value = String(match.output.value) + localizationEntries[key] = value + } +} + +// Set to store all keys found in the codebase +var usedKeys = Set() + +// Function to scan a directory recursively for Swift files +func scanDirectory(_ path: String) { + let fileManager = FileManager.default + guard let enumerator = fileManager.enumerator(atPath: path) else { return } + + for case let file as String in enumerator { + let filePath = "\(path)/\(file)" + + // Skip the excluded file + if filePath == excludedFile { continue } + + // Process only Swift files + if file.hasSuffix(".swift") { + if let fileContent = try? String(contentsOfFile: filePath, encoding: .utf8) { + for line in fileContent.components(separatedBy: .newlines) { + // Find all matches for L10n.key in each line + let matches = line.matches(of: usageRegex) + for match in matches { + let key = String(match.output.key) + usedKeys.insert(key) + } + } + } + } + } +} + +// Scan all specified directories +for directory in directoriesToScan { + scanDirectory(directory) +} + +// MARK: - Remove Unused Keys + +// Identify keys in the localization file that are not used in the codebase +let unusedKeys = localizationEntries.keys.filter { !usedKeys.contains($0) } + +// Remove unused keys from the dictionary +unusedKeys.forEach { localizationEntries.removeValue(forKey: $0) } + +// MARK: - Write Updated Localizable.strings + +// Sort keys alphabetically for consistent formatting +let sortedKeys = localizationEntries.keys.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } + +// Reconstruct the localization file with sorted and updated entries +let updatedContent = sortedKeys.map { "/// \(localizationEntries[$0]!)\n\"\($0)\" = \"\(localizationEntries[$0]!)\";" } + .joined(separator: "\n\n") + +// Attempt to write the updated content back to the localization file +do { + try updatedContent.write(toFile: localizationFile, atomically: true, encoding: .utf8) + print("Localization file updated. Removed \(unusedKeys.count) unused keys.") +} catch { + print("Error: Failed to write updated localization file.") + exit(1) +} diff --git a/Shared/AppIcons/AppIcons.swift b/Shared/AppIcons/AppIcons.swift index 743fc9873..847d22756 100644 --- a/Shared/AppIcons/AppIcons.swift +++ b/Shared/AppIcons/AppIcons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/DarkAppIcon.swift b/Shared/AppIcons/DarkAppIcon.swift index 443ce9b92..5192e9ef8 100644 --- a/Shared/AppIcons/DarkAppIcon.swift +++ b/Shared/AppIcons/DarkAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/InvertedDarkAppIcon.swift b/Shared/AppIcons/InvertedDarkAppIcon.swift index 20c3b1802..70ec89094 100644 --- a/Shared/AppIcons/InvertedDarkAppIcon.swift +++ b/Shared/AppIcons/InvertedDarkAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/InvertedLightAppIcon.swift b/Shared/AppIcons/InvertedLightAppIcon.swift index d9ca8fd70..3dacdfbbb 100644 --- a/Shared/AppIcons/InvertedLightAppIcon.swift +++ b/Shared/AppIcons/InvertedLightAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/LightAppIcon.swift b/Shared/AppIcons/LightAppIcon.swift index 02dc79de6..8a5b6244a 100644 --- a/Shared/AppIcons/LightAppIcon.swift +++ b/Shared/AppIcons/LightAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/AppIcons/PrimaryAppIcon.swift b/Shared/AppIcons/PrimaryAppIcon.swift index e0c0ca5f8..45de9b8f2 100644 --- a/Shared/AppIcons/PrimaryAppIcon.swift +++ b/Shared/AppIcons/PrimaryAppIcon.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Components/AlternateLayoutView.swift b/Shared/Components/AlternateLayoutView.swift index 6f0e2608c..ef3da0c41 100644 --- a/Shared/Components/AlternateLayoutView.swift +++ b/Shared/Components/AlternateLayoutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/AssertionFailureView.swift b/Shared/Components/AssertionFailureView.swift index b98205315..52f1e2935 100644 --- a/Shared/Components/AssertionFailureView.swift +++ b/Shared/Components/AssertionFailureView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/BlurView.swift b/Shared/Components/BlurView.swift index 5809525ad..e428860eb 100644 --- a/Shared/Components/BlurView.swift +++ b/Shared/Components/BlurView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/BulletedList.swift b/Shared/Components/BulletedList.swift index 8a760a085..8d3220cd4 100644 --- a/Shared/Components/BulletedList.swift +++ b/Shared/Components/BulletedList.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/ChevronAlertButton.swift b/Shared/Components/ChevronAlertButton.swift index 91b6d0dc1..dd157b7f7 100644 --- a/Shared/Components/ChevronAlertButton.swift +++ b/Shared/Components/ChevronAlertButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/ChevronButton.swift b/Shared/Components/ChevronButton.swift index 46be21aa1..c5fb3d3a9 100644 --- a/Shared/Components/ChevronButton.swift +++ b/Shared/Components/ChevronButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/FastSVGView.swift b/Shared/Components/FastSVGView.swift index 3a4c2f180..866f77def 100644 --- a/Shared/Components/FastSVGView.swift +++ b/Shared/Components/FastSVGView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SVGKit diff --git a/Shared/Components/ImageView.swift b/Shared/Components/ImageView.swift index cdf7665d6..793d2db5a 100644 --- a/Shared/Components/ImageView.swift +++ b/Shared/Components/ImageView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit @@ -60,6 +60,7 @@ struct ImageView: View { } } .pipeline(pipeline) + .onDisappear(.lowerPriority) } else { failure() .eraseToAnyView() diff --git a/Shared/Components/LetterPickerOrientation.swift b/Shared/Components/LetterPickerOrientation.swift index c5e55ecb6..0a7f9aa47 100644 --- a/Shared/Components/LetterPickerOrientation.swift +++ b/Shared/Components/LetterPickerOrientation.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/ListRowCheckbox.swift b/Shared/Components/ListRowCheckbox.swift index 01b01a70a..755b8ce83 100644 --- a/Shared/Components/ListRowCheckbox.swift +++ b/Shared/Components/ListRowCheckbox.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/MaxHeightText.swift b/Shared/Components/MaxHeightText.swift index a31126858..772c748a7 100644 --- a/Shared/Components/MaxHeightText.swift +++ b/Shared/Components/MaxHeightText.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/PosterIndicators/FavoriteIndicator.swift b/Shared/Components/PosterIndicators/FavoriteIndicator.swift index 0121117ad..7e9f67ce6 100644 --- a/Shared/Components/PosterIndicators/FavoriteIndicator.swift +++ b/Shared/Components/PosterIndicators/FavoriteIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/PosterIndicators/ProgressIndicator.swift b/Shared/Components/PosterIndicators/ProgressIndicator.swift index d01857f54..09477c30c 100644 --- a/Shared/Components/PosterIndicators/ProgressIndicator.swift +++ b/Shared/Components/PosterIndicators/ProgressIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/PosterIndicators/UnwatchedIndicator.swift b/Shared/Components/PosterIndicators/UnwatchedIndicator.swift index b8838cc9c..9d0abfc26 100644 --- a/Shared/Components/PosterIndicators/UnwatchedIndicator.swift +++ b/Shared/Components/PosterIndicators/UnwatchedIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/PosterIndicators/WatchedIndicator.swift b/Shared/Components/PosterIndicators/WatchedIndicator.swift index 1efd10271..8744feb22 100644 --- a/Shared/Components/PosterIndicators/WatchedIndicator.swift +++ b/Shared/Components/PosterIndicators/WatchedIndicator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/ProgressBar.swift b/Shared/Components/ProgressBar.swift index 5b505e7cb..b0b1816aa 100644 --- a/Shared/Components/ProgressBar.swift +++ b/Shared/Components/ProgressBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/RotateContentView.swift b/Shared/Components/RotateContentView.swift index 2ab74fcc1..8dd8625cb 100644 --- a/Shared/Components/RotateContentView.swift +++ b/Shared/Components/RotateContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/RowDivider.swift b/Shared/Components/RowDivider.swift index 50e62ca45..0d58f07fd 100644 --- a/Shared/Components/RowDivider.swift +++ b/Shared/Components/RowDivider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/SelectorView.swift b/Shared/Components/SelectorView.swift index a8c7666d2..ba7d2a2d8 100644 --- a/Shared/Components/SelectorView.swift +++ b/Shared/Components/SelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/SeparatorHStack.swift b/Shared/Components/SeparatorHStack.swift index c9b732ced..500227418 100644 --- a/Shared/Components/SeparatorHStack.swift +++ b/Shared/Components/SeparatorHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/SeparatorVStack.swift b/Shared/Components/SeparatorVStack.swift index 44721f997..b7db46b2c 100644 --- a/Shared/Components/SeparatorVStack.swift +++ b/Shared/Components/SeparatorVStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/SystemImageContentView.swift b/Shared/Components/SystemImageContentView.swift index dcf237ccf..b32c9eb6d 100644 --- a/Shared/Components/SystemImageContentView.swift +++ b/Shared/Components/SystemImageContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/TextPairView.swift b/Shared/Components/TextPairView.swift index 66c9ace9f..284063f3e 100644 --- a/Shared/Components/TextPairView.swift +++ b/Shared/Components/TextPairView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Components/TruncatedText.swift b/Shared/Components/TruncatedText.swift index a5ba38c50..d042a9239 100644 --- a/Shared/Components/TruncatedText.swift +++ b/Shared/Components/TruncatedText.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Components/UserProfileImage/UserProfileHeroImage.swift b/Shared/Components/UserProfileImage/UserProfileHeroImage.swift new file mode 100644 index 000000000..15d1b4fba --- /dev/null +++ b/Shared/Components/UserProfileImage/UserProfileHeroImage.swift @@ -0,0 +1,101 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Factory +import JellyfinAPI +import Nuke +import SwiftUI + +struct UserProfileHeroImage: View { + + // MARK: - Accent Color + + @Default(.accentColor) + private var accentColor + + // MARK: - User Session + + @Injected(\.currentUserSession) + private var userSession + + // MARK: - User Variables + + private let user: UserDto + private let source: ImageSource + private let pipeline: ImagePipeline + + // MARK: - User Actions + + private let onUpdate: () -> Void + private let onDelete: () -> Void + + // MARK: - Dialog State + + @State + private var isPresentingOptions: Bool = false + + // MARK: - Initializer + + init( + user: UserDto, + source: ImageSource, + pipeline: ImagePipeline = .Swiftfin.posters, + onUpdate: @escaping () -> Void, + onDelete: @escaping () -> Void + ) { + self.user = user + self.source = source + self.pipeline = pipeline + self.onUpdate = onUpdate + self.onDelete = onDelete + } + + // MARK: - Body + + var body: some View { + Section { + VStack(alignment: .center) { + Button { + isPresentingOptions = true + } label: { + ZStack(alignment: .bottomTrailing) { + UserProfileImage( + userID: user.id, + source: source, + pipeline: userSession?.user.id == user.id ? .Swiftfin.local : .Swiftfin.posters + ) + .frame(width: 150, height: 150) + + Image(systemName: "pencil.circle.fill") + .resizable() + .frame(width: 30, height: 30) + .shadow(radius: 10) + .symbolRenderingMode(.palette) + .foregroundStyle(accentColor.overlayColor, accentColor) + } + } + + Text(user.name ?? L10n.unknown) + .fontWeight(.semibold) + .font(.title2) + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + } + .confirmationDialog( + L10n.profileImage, + isPresented: $isPresentingOptions, + titleVisibility: .visible + ) { + Button(L10n.selectImage, action: onUpdate) + + Button(L10n.delete, role: .destructive, action: onDelete) + } + } +} diff --git a/Shared/Components/UserProfileImage/UserProfileImage.swift b/Shared/Components/UserProfileImage/UserProfileImage.swift new file mode 100644 index 000000000..eeabb7e72 --- /dev/null +++ b/Shared/Components/UserProfileImage/UserProfileImage.swift @@ -0,0 +1,86 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Factory +import JellyfinAPI +import Nuke +import SwiftUI + +struct UserProfileImage: View { + + // MARK: - Inject Logger + + @Injected(\.logService) + private var logger + + // MARK: - User Variables + + private let userID: String? + private let source: ImageSource + private let pipeline: ImagePipeline + private let placeholder: Placeholder + + // MARK: - Body + + var body: some View { + RedrawOnNotificationView( + .didChangeUserProfile, + filter: { + $0 == userID + } + ) { + ImageView(source) + .pipeline(pipeline) + .image { + $0.posterBorder(ratio: 1 / 2, of: \.width) + } + .placeholder { _ in + placeholder + } + .failure { + placeholder + } + .posterShadow() + .aspectRatio(1, contentMode: .fill) + .clipShape(Circle()) + .shadow(radius: 5) + } + } +} + +// MARK: - Initializer + +extension UserProfileImage { + + init( + userID: String?, + source: ImageSource, + pipeline: ImagePipeline = .Swiftfin.posters, + @ViewBuilder placeholder: @escaping () -> Placeholder + ) { + self.userID = userID + self.source = source + self.pipeline = pipeline + self.placeholder = placeholder() + } +} + +extension UserProfileImage where Placeholder == SystemImageContentView { + + init( + userID: String?, + source: ImageSource, + pipeline: ImagePipeline = .Swiftfin.posters + ) { + self.userID = userID + self.source = source + self.pipeline = pipeline + self.placeholder = SystemImageContentView(systemName: "person.fill", ratio: 0.5) + } +} diff --git a/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift b/Shared/Components/UserProfileRow.swift similarity index 68% rename from Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift rename to Shared/Components/UserProfileRow.swift index 7a04a3f8e..c43d5a3c9 100644 --- a/Swiftfin/Views/SettingsView/SettingsView/Components/UserProfileRow.swift +++ b/Shared/Components/UserProfileRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory @@ -20,20 +20,6 @@ extension SettingsView { private let user: UserDto private let action: (() -> Void)? - @ViewBuilder - private var imageView: some View { - RedrawOnNotificationView(.didChangeUserProfileImage) { - ImageView(user.profileImageSource(client: userSession.client, maxWidth: 120)) - .pipeline(.Swiftfin.branding) - .placeholder { _ in - SystemImageContentView(systemName: "person.fill", ratio: 0.5) - } - .failure { - SystemImageContentView(systemName: "person.fill", ratio: 0.5) - } - } - } - var body: some View { Button { guard let action else { return } @@ -44,10 +30,14 @@ extension SettingsView { // `.aspectRatio(contentMode: .fill)` on `imageView` alone // causes a crash on some iOS versions ZStack { - imageView + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: userSession.client, + maxWidth: 120 + ) + ) } - .aspectRatio(1, contentMode: .fill) - .clipShape(.circle) .frame(width: 50, height: 50) Text(user.name ?? L10n.unknown) diff --git a/Shared/Components/WrappedView.swift b/Shared/Components/WrappedView.swift index eaca6e7e2..38d78f23d 100644 --- a/Shared/Components/WrappedView.swift +++ b/Shared/Components/WrappedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Coordinators/AdminDashboardCoordinator.swift b/Shared/Coordinators/AdminDashboardCoordinator.swift index 4222421d9..c0fd43dff 100644 --- a/Shared/Coordinators/AdminDashboardCoordinator.swift +++ b/Shared/Coordinators/AdminDashboardCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -33,10 +33,10 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { // MARK: - Route: Server Tasks - @Route(.push) - var editServerTask = makeEditServerTask @Route(.push) var tasks = makeTasks + @Route(.push) + var editServerTask = makeEditServerTask @Route(.modal) var addServerTaskTrigger = makeAddServerTaskTrigger @@ -51,6 +51,11 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { var users = makeUsers @Route(.push) var userDetails = makeUserDetails + @Route(.modal) + var addServerUser = makeAddServerUser + + // MARK: - Route: User Policy + @Route(.modal) var userDeviceAccess = makeUserDeviceAccess @Route(.modal) @@ -63,8 +68,16 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { var userParentalRatings = makeUserParentalRatings @Route(.modal) var resetUserPassword = makeResetUserPassword + @Route(.push) + var userEditAccessSchedules = makeUserEditAccessSchedules @Route(.modal) - var addServerUser = makeAddServerUser + var userAddAccessSchedule = makeUserAddAccessSchedule + @Route(.push) + var userEditAccessTags = makeUserEditAccessTags + @Route(.modal) + var userAddAccessTag = makeUserAddAccessTag + @Route(.modal) + var userPhotoPicker = makeUserPhotoPicker // MARK: - Route: API Keys @@ -132,12 +145,18 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { ServerUserDetailsView(user: user) } + func makeUserPhotoPicker(viewModel: UserProfileImageViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator(UserProfileImageCoordinator(viewModel: viewModel)) + } + func makeAddServerUser() -> NavigationViewCoordinator { NavigationViewCoordinator { AddServerUserView() } } + // MARK: - Views: User Policy + func makeUserDeviceAccess(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator { ServerUserDeviceAccessView(viewModel: viewModel) @@ -162,6 +181,28 @@ final class AdminDashboardCoordinator: NavigationCoordinatable { } } + @ViewBuilder + func makeUserEditAccessSchedules(viewModel: ServerUserAdminViewModel) -> some View { + EditAccessScheduleView(viewModel: viewModel) + } + + @ViewBuilder + func makeUserEditAccessTags(viewModel: ServerUserAdminViewModel) -> some View { + EditServerUserAccessTagsView(viewModel: viewModel) + } + + func makeUserAddAccessTag(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator { + AddServerUserAccessTagsView(viewModel: viewModel) + } + } + + func makeUserAddAccessSchedule(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator { + AddAccessScheduleView(viewModel: viewModel) + } + } + func makeUserParentalRatings(viewModel: ServerUserAdminViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator { ServerUserParentalRatingView(viewModel: viewModel) diff --git a/Shared/Coordinators/AppSettingsCoordinator.swift b/Shared/Coordinators/AppSettingsCoordinator.swift index 8cc419a79..3c897e21c 100644 --- a/Shared/Coordinators/AppSettingsCoordinator.swift +++ b/Shared/Coordinators/AppSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import PulseUI diff --git a/Shared/Coordinators/BasicNavigationCoordinator.swift b/Shared/Coordinators/BasicNavigationCoordinator.swift index b3093cc75..86d00749e 100644 --- a/Shared/Coordinators/BasicNavigationCoordinator.swift +++ b/Shared/Coordinators/BasicNavigationCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/CustomDeviceProfileCoordinator.swift b/Shared/Coordinators/CustomDeviceProfileCoordinator.swift index 7af5a547a..998e110c5 100644 --- a/Shared/Coordinators/CustomDeviceProfileCoordinator.swift +++ b/Shared/Coordinators/CustomDeviceProfileCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/CustomizeSettingsCoordinator.swift b/Shared/Coordinators/CustomizeSettingsCoordinator.swift index 259bd0334..91cc4869a 100644 --- a/Shared/Coordinators/CustomizeSettingsCoordinator.swift +++ b/Shared/Coordinators/CustomizeSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/DownloadListCoordinator.swift b/Shared/Coordinators/DownloadListCoordinator.swift index fc9af3622..6706f84b0 100644 --- a/Shared/Coordinators/DownloadListCoordinator.swift +++ b/Shared/Coordinators/DownloadListCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // #if os(iOS) diff --git a/Shared/Coordinators/DownloadTaskCoordinator.swift b/Shared/Coordinators/DownloadTaskCoordinator.swift index 6a9b72bc0..b3eaa133d 100644 --- a/Shared/Coordinators/DownloadTaskCoordinator.swift +++ b/Shared/Coordinators/DownloadTaskCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // #if os(iOS) diff --git a/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift b/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift index f84149b5f..c414e2411 100644 --- a/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift +++ b/Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/FilterCoordinator.swift b/Shared/Coordinators/FilterCoordinator.swift index be8c8fae0..851f57891 100644 --- a/Shared/Coordinators/FilterCoordinator.swift +++ b/Shared/Coordinators/FilterCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/HomeCoordinator.swift b/Shared/Coordinators/HomeCoordinator.swift index a9c76ed54..cae2aff00 100644 --- a/Shared/Coordinators/HomeCoordinator.swift +++ b/Shared/Coordinators/HomeCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/ItemCoordinator.swift b/Shared/Coordinators/ItemCoordinator.swift index ff902321e..9a68e28a3 100644 --- a/Shared/Coordinators/ItemCoordinator.swift +++ b/Shared/Coordinators/ItemCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/ItemEditorCoordinator.swift b/Shared/Coordinators/ItemEditorCoordinator.swift index 37e698eac..29fec728f 100644 --- a/Shared/Coordinators/ItemEditorCoordinator.swift +++ b/Shared/Coordinators/ItemEditorCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -21,9 +21,16 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Route to Metadata + @Route(.push) + var identifyItem = makeIdentifyItem @Route(.modal) var editMetadata = makeEditMetadata + // MARK: - Route to Images + + @Route(.modal) + var editImages = makeEditImages + // MARK: - Route to Genres @Route(.push) @@ -60,12 +67,23 @@ final class ItemEditorCoordinator: ObservableObject, NavigationCoordinatable { // MARK: - Item Metadata + @ViewBuilder + func makeIdentifyItem(item: BaseItemDto) -> some View { + IdentifyItemView(item: item) + } + func makeEditMetadata(item: BaseItemDto) -> NavigationViewCoordinator { NavigationViewCoordinator { EditMetadataView(viewModel: ItemEditorViewModel(item: item)) } } + // MARK: - Item Images + + func makeEditImages(viewModel: ItemImagesViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemImagesCoordinator(viewModel: viewModel)) + } + // MARK: - Item Genres @ViewBuilder diff --git a/Shared/Coordinators/ItemImagePickerCoordinator.swift b/Shared/Coordinators/ItemImagePickerCoordinator.swift new file mode 100644 index 000000000..35353e8e3 --- /dev/null +++ b/Shared/Coordinators/ItemImagePickerCoordinator.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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import Stinsen +import SwiftUI + +final class ItemImagePickerCoordinator: NavigationCoordinatable { + + // MARK: - Navigation Stack + + let stack = Stinsen.NavigationStack(initial: \ItemImagePickerCoordinator.start) + + @Root + var start = makeStart + + // MARK: - Routes + + @Route(.push) + var cropImage = makeCropImage + + // MARK: - Observed Object + + private let viewModel: ItemImagesViewModel + + // MARK: - Image Variable + + let type: ImageType + + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel, type: ImageType) { + self.viewModel = viewModel + self.type = type + } + + // MARK: - Crop Image View + + func makeCropImage(image: UIImage) -> some View { + ItemPhotoCropView(viewModel: viewModel, image: image, type: type) + } + + @ViewBuilder + func makeStart() -> some View { + ItemImagePicker() + } +} diff --git a/Shared/Coordinators/ItemImagesCoordinator.swift b/Shared/Coordinators/ItemImagesCoordinator.swift new file mode 100644 index 000000000..24175a7ab --- /dev/null +++ b/Shared/Coordinators/ItemImagesCoordinator.swift @@ -0,0 +1,60 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Factory +import JellyfinAPI +import Stinsen +import SwiftUI + +final class ItemImagesCoordinator: ObservableObject, NavigationCoordinatable { + + // MARK: - Navigation Stack + + let stack = NavigationStack(initial: \ItemImagesCoordinator.start) + + @Root + var start = makeStart + + private let viewModel: ItemImagesViewModel + + // MARK: - Route to Add Remote Image + + @Route(.push) + var addImage = makeAddImage + + // MARK: - Route to Photo Picker + + @Route(.modal) + var photoPicker = makePhotoPicker + + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel) { + self.viewModel = viewModel + } + + // MARK: - Add Remote Images View + + @ViewBuilder + func makeAddImage(imageType: ImageType) -> some View { + AddItemImageView(viewModel: viewModel, imageType: imageType) + } + + // MARK: - Photo Picker View + + func makePhotoPicker(type: ImageType) -> NavigationViewCoordinator { + NavigationViewCoordinator(ItemImagePickerCoordinator(viewModel: self.viewModel, type: type)) + } + + // MARK: - Start + + @ViewBuilder + func makeStart() -> some View { + ItemImagesView(viewModel: self.viewModel) + } +} diff --git a/Shared/Coordinators/LibraryCoordinator.swift b/Shared/Coordinators/LibraryCoordinator.swift index 950263f25..0b708424f 100644 --- a/Shared/Coordinators/LibraryCoordinator.swift +++ b/Shared/Coordinators/LibraryCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift b/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift index 12ccd6fe6..fb5125e48 100644 --- a/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift +++ b/Shared/Coordinators/LiveTVCoordinator/iOSLiveTVCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift b/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift index 1e091649c..294146d28 100644 --- a/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift +++ b/Shared/Coordinators/LiveTVCoordinator/tvOSLiveTVCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/LiveVideoPlayerCoordinator.swift b/Shared/Coordinators/LiveVideoPlayerCoordinator.swift index 16c449ec8..cda151b2b 100644 --- a/Shared/Coordinators/LiveVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/LiveVideoPlayerCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift index 71c868c7f..d5566c06b 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift index 39d3f9b24..048b3e8e3 100644 --- a/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/iOSMainTabCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift index 2e93413d3..d23fc771a 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift index 3e9f28fe4..71159a29d 100644 --- a/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift +++ b/Shared/Coordinators/MainCoordinator/tvOSMainTabCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -48,7 +48,10 @@ final class MainTabCoordinator: TabCoordinatable { } func makeTVShows() -> NavigationViewCoordinator> { - let viewModel = ItemTypeLibraryViewModel(itemTypes: [.series]) + let viewModel = ItemTypeLibraryViewModel( + itemTypes: [.series], + filters: .default + ) return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel)) } @@ -62,7 +65,10 @@ final class MainTabCoordinator: TabCoordinatable { } func makeMovies() -> NavigationViewCoordinator> { - let viewModel = ItemTypeLibraryViewModel(itemTypes: [.movie]) + let viewModel = ItemTypeLibraryViewModel( + itemTypes: [.movie], + filters: .default + ) return NavigationViewCoordinator(LibraryCoordinator(viewModel: viewModel)) } diff --git a/Shared/Coordinators/MediaCoordinator.swift b/Shared/Coordinators/MediaCoordinator.swift index dcf178a09..83d0280df 100644 --- a/Shared/Coordinators/MediaCoordinator.swift +++ b/Shared/Coordinators/MediaCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/MediaSourceInfoCoordinator.swift b/Shared/Coordinators/MediaSourceInfoCoordinator.swift index b1d0df0a5..34bc36dda 100644 --- a/Shared/Coordinators/MediaSourceInfoCoordinator.swift +++ b/Shared/Coordinators/MediaSourceInfoCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift b/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift index 88d388b6e..2cc4d9d8c 100644 --- a/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift +++ b/Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Coordinators/PlaybackSettingsCoordinator.swift b/Shared/Coordinators/PlaybackSettingsCoordinator.swift index 28015f185..5c0b4cb5a 100644 --- a/Shared/Coordinators/PlaybackSettingsCoordinator.swift +++ b/Shared/Coordinators/PlaybackSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Coordinators/SearchCoordinator.swift b/Shared/Coordinators/SearchCoordinator.swift index 4f6f1e3f0..2e03217ae 100644 --- a/Shared/Coordinators/SearchCoordinator.swift +++ b/Shared/Coordinators/SearchCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/SelectUserCoordinator.swift b/Shared/Coordinators/SelectUserCoordinator.swift index c2fecfbe1..fc0e5459d 100644 --- a/Shared/Coordinators/SelectUserCoordinator.swift +++ b/Shared/Coordinators/SelectUserCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -12,11 +12,6 @@ import SwiftUI final class SelectUserCoordinator: NavigationCoordinatable { - struct SelectServerParameters { - let selection: Binding - let viewModel: SelectUserViewModel - } - let stack = NavigationStack(initial: \SelectUserCoordinator.start) @Root @@ -31,11 +26,6 @@ final class SelectUserCoordinator: NavigationCoordinatable { @Route(.modal) var userSignIn = makeUserSignIn - #if os(tvOS) - @Route(.fullScreen) - var selectServer = makeSelectServer - #endif - func makeAdvancedSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(AppSettingsCoordinator()) } @@ -62,15 +52,6 @@ final class SelectUserCoordinator: NavigationCoordinatable { NavigationViewCoordinator(UserSignInCoordinator(server: server)) } - #if os(tvOS) - func makeSelectServer(parameters: SelectServerParameters) -> some View { - SelectServerView( - selection: parameters.selection, - viewModel: parameters.viewModel - ) - } - #endif - @ViewBuilder func makeStart() -> some View { SelectUserView() diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index da42ffc74..56b5cd170 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -79,6 +79,8 @@ final class SettingsCoordinator: NavigationCoordinatable { var videoPlayerSettings = makeVideoPlayerSettings @Route(.modal) var playbackQualitySettings = makePlaybackQualitySettings + @Route(.modal) + var userProfile = makeUserProfileSettings #endif #if os(iOS) @@ -123,8 +125,8 @@ final class SettingsCoordinator: NavigationCoordinatable { UserLocalSecurityView() } - func makePhotoPicker(viewModel: SettingsViewModel) -> NavigationViewCoordinator { - NavigationViewCoordinator(UserProfileImageCoordinator()) + func makePhotoPicker(viewModel: UserProfileImageViewModel) -> NavigationViewCoordinator { + NavigationViewCoordinator(UserProfileImageCoordinator(viewModel: viewModel)) } @ViewBuilder @@ -181,14 +183,25 @@ final class SettingsCoordinator: NavigationCoordinatable { #endif #if os(tvOS) - func makeCustomizeViewsSettings() -> NavigationViewCoordinator { + + // MARK: - User Profile View + + func makeUserProfileSettings(viewModel: SettingsViewModel) -> NavigationViewCoordinator { NavigationViewCoordinator( - BasicNavigationViewCoordinator { - CustomizeViewsSettings() - } + UserProfileSettingsCoordinator(viewModel: viewModel) + ) + } + + // MARK: - Customize Settings View + + func makeCustomizeViewsSettings() -> NavigationViewCoordinator { + NavigationViewCoordinator( + CustomizeSettingsCoordinator() ) } + // MARK: - Experimental Settings View + func makeExperimentalSettings() -> NavigationViewCoordinator { NavigationViewCoordinator( BasicNavigationViewCoordinator { @@ -197,24 +210,32 @@ final class SettingsCoordinator: NavigationCoordinatable { ) } + // MARK: - Poster Indicator Settings View + func makeIndicatorSettings() -> NavigationViewCoordinator { NavigationViewCoordinator { IndicatorSettingsView() } } + // MARK: - Server Settings View + func makeServerDetail(server: ServerState) -> NavigationViewCoordinator { NavigationViewCoordinator { EditServerView(server: server) } } + // MARK: - Video Player Settings View + func makeVideoPlayerSettings() -> NavigationViewCoordinator { NavigationViewCoordinator( VideoPlayerSettingsCoordinator() ) } + // MARK: - Playback Settings View + func makePlaybackQualitySettings() -> NavigationViewCoordinator { NavigationViewCoordinator( PlaybackQualitySettingsCoordinator() diff --git a/Shared/Coordinators/UserProfileImageCoordinator.swift b/Shared/Coordinators/UserProfileImageCoordinator.swift index 5542d587c..e5188896e 100644 --- a/Shared/Coordinators/UserProfileImageCoordinator.swift +++ b/Shared/Coordinators/UserProfileImageCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen @@ -11,30 +11,37 @@ import SwiftUI final class UserProfileImageCoordinator: NavigationCoordinatable { + // MARK: - Navigation Stack + let stack = Stinsen.NavigationStack(initial: \UserProfileImageCoordinator.start) @Root var start = makeStart + // MARK: - Routes + @Route(.push) var cropImage = makeCropImage + // MARK: - Observed Object + + @ObservedObject + var viewModel: UserProfileImageViewModel + + // MARK: - Initializer + + init(viewModel: UserProfileImageViewModel) { + self.viewModel = viewModel + } + + // MARK: - Views + func makeCropImage(image: UIImage) -> some View { - #if os(iOS) - UserProfileImagePicker.SquareImageCropView( - image: image - ) - #else - AssertionFailureView("not implemented") - #endif + UserProfileImageCropView(viewModel: viewModel, image: image) } @ViewBuilder func makeStart() -> some View { - #if os(iOS) - UserProfileImagePicker() - #else - AssertionFailureView("not implemented") - #endif + UserProfileImagePickerView() } } diff --git a/Shared/Coordinators/UserProfileSettingsCoordinator.swift b/Shared/Coordinators/UserProfileSettingsCoordinator.swift new file mode 100644 index 000000000..7fada9bfd --- /dev/null +++ b/Shared/Coordinators/UserProfileSettingsCoordinator.swift @@ -0,0 +1,51 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Stinsen +import SwiftUI + +final class UserProfileSettingsCoordinator: NavigationCoordinatable { + + // MARK: - Navigation Components + + let stack = Stinsen.NavigationStack(initial: \UserProfileSettingsCoordinator.start) + + @Root + var start = makeStart + + // MARK: - Route to User Profile Security + + @Route(.modal) + var localSecurity = makeLocalSecurity + + // MARK: - Observed Object + + @ObservedObject + var viewModel: SettingsViewModel + + // MARK: - Initializer + + init(viewModel: SettingsViewModel) { + self.viewModel = viewModel + } + + // MARK: - User Security View + + func makeLocalSecurity() -> NavigationViewCoordinator { + NavigationViewCoordinator( + BasicNavigationViewCoordinator { + UserLocalSecurityView() + } + ) + } + + @ViewBuilder + func makeStart() -> some View { + UserProfileSettingsView(viewModel: viewModel) + } +} diff --git a/Shared/Coordinators/UserSignInCoordinator.swift b/Shared/Coordinators/UserSignInCoordinator.swift index adf88edc0..ecbbb3338 100644 --- a/Shared/Coordinators/UserSignInCoordinator.swift +++ b/Shared/Coordinators/UserSignInCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Coordinators/VideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator.swift index b5dcb37df..59fa79999 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift b/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift index 0133f055b..5ed3b9100 100644 --- a/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerSettingsCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift b/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift index 4ca46fe70..8d04118b7 100644 --- a/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerWrapperCoordinator.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Errors/NetworkError.swift b/Shared/Errors/NetworkError.swift index 8644e4529..9a7384586 100644 --- a/Shared/Errors/NetworkError.swift +++ b/Shared/Errors/NetworkError.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Array.swift b/Shared/Extensions/Array.swift index 202b471b8..3ec15df30 100644 --- a/Shared/Extensions/Array.swift +++ b/Shared/Extensions/Array.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Binding.swift b/Shared/Extensions/Binding.swift index 06a00e359..c698e748b 100644 --- a/Shared/Extensions/Binding.swift +++ b/Shared/Extensions/Binding.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Button.swift b/Shared/Extensions/Button.swift index 8e9358a4f..6428120be 100644 --- a/Shared/Extensions/Button.swift +++ b/Shared/Extensions/Button.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/CGPoint.swift b/Shared/Extensions/CGPoint.swift index e7de1d28e..6685e385e 100644 --- a/Shared/Extensions/CGPoint.swift +++ b/Shared/Extensions/CGPoint.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/CGSize.swift b/Shared/Extensions/CGSize.swift index fa2e9a3a7..68ce49b44 100644 --- a/Shared/Extensions/CGSize.swift +++ b/Shared/Extensions/CGSize.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/Collection.swift b/Shared/Extensions/Collection.swift index c56236a57..68089011e 100644 --- a/Shared/Extensions/Collection.swift +++ b/Shared/Extensions/Collection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Color.swift b/Shared/Extensions/Color.swift index a8940949d..49b865ee5 100644 --- a/Shared/Extensions/Color.swift +++ b/Shared/Extensions/Color.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -22,9 +22,9 @@ extension Color { // TODO: Correct and add colors #if os(tvOS) // tvOS doesn't have these - static let systemFill = Color(UIColor.white) - static let secondarySystemFill = Color(UIColor.gray) - static let tertiarySystemFill = Color(UIColor.black) + static let systemFill = Color.white + static let secondarySystemFill = Color.gray + static let tertiarySystemFill = Color.black static let lightGray = Color(UIColor.lightGray) #else diff --git a/Shared/Extensions/CoreStore.swift b/Shared/Extensions/CoreStore.swift index 6e5d27c9d..a8d2d4d38 100644 --- a/Shared/Extensions/CoreStore.swift +++ b/Shared/Extensions/CoreStore.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/Extensions/Dictionary.swift b/Shared/Extensions/Dictionary.swift index 7364e2766..710194807 100644 --- a/Shared/Extensions/Dictionary.swift +++ b/Shared/Extensions/Dictionary.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Double.swift b/Shared/Extensions/Double.swift index 7840f9271..bdeb29c10 100644 --- a/Shared/Extensions/Double.swift +++ b/Shared/Extensions/Double.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Edge.swift b/Shared/Extensions/Edge.swift index bc505bf4d..1c6915332 100644 --- a/Shared/Extensions/Edge.swift +++ b/Shared/Extensions/Edge.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/EdgeInsets.swift b/Shared/Extensions/EdgeInsets.swift index 65a2a671d..3118d74a5 100644 --- a/Shared/Extensions/EdgeInsets.swift +++ b/Shared/Extensions/EdgeInsets.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift index f39ae4533..11d15b73e 100644 --- a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift +++ b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Keys.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift index 6a0df59ce..bb197f892 100644 --- a/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift +++ b/Shared/Extensions/EnvironmentValue/EnvironmentValue+Values.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Equatable.swift b/Shared/Extensions/Equatable.swift index 23ce2b585..25925e3ea 100644 --- a/Shared/Extensions/Equatable.swift +++ b/Shared/Extensions/Equatable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Files.swift b/Shared/Extensions/Files.swift index 58e320e4b..d7f2dc77c 100644 --- a/Shared/Extensions/Files.swift +++ b/Shared/Extensions/Files.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Font.swift b/Shared/Extensions/Font.swift index 0338ef117..a27d6911d 100644 --- a/Shared/Extensions/Font.swift +++ b/Shared/Extensions/Font.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/FormatStyle.swift b/Shared/Extensions/FormatStyle.swift index 95652cdc9..78fa3bc69 100644 --- a/Shared/Extensions/FormatStyle.swift +++ b/Shared/Extensions/FormatStyle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Hashable.swift b/Shared/Extensions/Hashable.swift deleted file mode 100644 index 13970de47..000000000 --- a/Shared/Extensions/Hashable.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Foundation - -extension Hashable { - - var hashString: String { - "\(hashValue)" - } -} diff --git a/Shared/Extensions/HorizontalAlignment.swift b/Shared/Extensions/HorizontalAlignment.swift index 3cce5dd34..d9aa92a04 100644 --- a/Shared/Extensions/HorizontalAlignment.swift +++ b/Shared/Extensions/HorizontalAlignment.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/Int.swift b/Shared/Extensions/Int.swift index 227c9e8cf..a0942a732 100644 --- a/Shared/Extensions/Int.swift +++ b/Shared/Extensions/Int.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift b/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift index 1506ac82e..c7b2469e2 100644 --- a/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift +++ b/Shared/Extensions/JellyfinAPI/ActiveSessionsPolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift index 80f958ad3..b3225015a 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Images.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory @@ -58,7 +58,7 @@ extension BaseItemDto { maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "", - force: true + requireTag: false ) } @@ -70,7 +70,7 @@ extension BaseItemDto { maxWidth: maxWidth, maxHeight: maxHeight, itemID: seriesID ?? "", - force: true + requireTag: false ) return ImageSource( @@ -86,16 +86,14 @@ extension BaseItemDto { maxWidth: CGFloat?, maxHeight: CGFloat?, itemID: String, - force: Bool = false + requireTag: Bool = true ) -> URL? { let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!) let scaleHeight = maxHeight == nil ? nil : UIScreen.main.scale(maxHeight!) let tag = getImageTag(for: type) - if tag == nil && !force { - return nil - } + guard tag != nil || !requireTag else { return nil } // TODO: client passing for widget/shared group views? guard let client = Container.shared.currentUserSession()?.client else { return nil } diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift index 18e56f43a..03c20900d 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+Poster.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift index e4271a954..30294d02d 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift index 98c541abe..0f0d267fb 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Algorithms diff --git a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift index 3c7c7044b..df5c47a12 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson+Poster.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift index f21d27bb9..7d93f69fc 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemPerson/BaseItemPerson.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift index 86de41999..a044d06d2 100644 --- a/Shared/Extensions/JellyfinAPI/ChapterInfo.swift +++ b/Shared/Extensions/JellyfinAPI/ChapterInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/CodecProfile.swift b/Shared/Extensions/JellyfinAPI/CodecProfile.swift index b67674d96..c70de069c 100644 --- a/Shared/Extensions/JellyfinAPI/CodecProfile.swift +++ b/Shared/Extensions/JellyfinAPI/CodecProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DayOfWeek.swift b/Shared/Extensions/JellyfinAPI/DayOfWeek.swift index 75bfb3195..0959e1b9b 100644 --- a/Shared/Extensions/JellyfinAPI/DayOfWeek.swift +++ b/Shared/Extensions/JellyfinAPI/DayOfWeek.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DeviceInfo.swift b/Shared/Extensions/JellyfinAPI/DeviceInfo.swift index 9876bc15d..0dd2b9f2c 100644 --- a/Shared/Extensions/JellyfinAPI/DeviceInfo.swift +++ b/Shared/Extensions/JellyfinAPI/DeviceInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DeviceProfile.swift b/Shared/Extensions/JellyfinAPI/DeviceProfile.swift index df1ab0db5..8636feec2 100644 --- a/Shared/Extensions/JellyfinAPI/DeviceProfile.swift +++ b/Shared/Extensions/JellyfinAPI/DeviceProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/DeviceType.swift b/Shared/Extensions/JellyfinAPI/DeviceType.swift index 0c102c82b..143acbf82 100644 --- a/Shared/Extensions/JellyfinAPI/DeviceType.swift +++ b/Shared/Extensions/JellyfinAPI/DeviceType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -73,7 +73,7 @@ enum DeviceType: String, Displayable, Codable, CaseIterable { case .xbox: return "Xbox" case .other: - return "Other" + return L10n.other } } diff --git a/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift b/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift index 2c4c1d838..cbae5ec4e 100644 --- a/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift +++ b/Shared/Extensions/JellyfinAPI/DirectPlayProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift b/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift new file mode 100644 index 000000000..9cb3c6a02 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/DynamicDayOfWeek.swift @@ -0,0 +1,38 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension DynamicDayOfWeek { + + var displayTitle: String { + switch self { + case .sunday: + DayOfWeek.sunday.displayTitle ?? self.rawValue + case .monday: + DayOfWeek.monday.displayTitle ?? self.rawValue + case .tuesday: + DayOfWeek.tuesday.displayTitle ?? self.rawValue + case .wednesday: + DayOfWeek.wednesday.displayTitle ?? self.rawValue + case .thursday: + DayOfWeek.thursday.displayTitle ?? self.rawValue + case .friday: + DayOfWeek.friday.displayTitle ?? self.rawValue + case .saturday: + DayOfWeek.saturday.displayTitle ?? self.rawValue + case .everyday: + L10n.everyday + case .weekday: + L10n.weekday + case .weekend: + L10n.weekend + } + } +} diff --git a/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift b/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift index 63a6d99ed..b27087f96 100644 --- a/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift +++ b/Shared/Extensions/JellyfinAPI/ImageBlurHashes.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ImageInfo.swift b/Shared/Extensions/JellyfinAPI/ImageInfo.swift new file mode 100644 index 000000000..f8cfd6c10 --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/ImageInfo.swift @@ -0,0 +1,36 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension ImageInfo: @retroactive Identifiable { + + public var id: Int { + hashValue + } +} + +extension ImageInfo { + + func itemImageSource(itemID: String, client: JellyfinClient) -> ImageSource { + let parameters = Paths.GetItemImageParameters( + tag: imageTag, + imageIndex: imageIndex + ) + let request = Paths.getItemImage( + itemID: itemID, + imageType: imageType?.rawValue ?? "", + parameters: parameters + ) + + let itemImageURL = client.fullURL(with: request) + + return ImageSource(url: itemImageURL) + } +} diff --git a/Shared/Extensions/JellyfinAPI/ImageType.swift b/Shared/Extensions/JellyfinAPI/ImageType.swift new file mode 100644 index 000000000..d90ae1d2f --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/ImageType.swift @@ -0,0 +1,44 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +extension ImageType: Displayable { + + var displayTitle: String { + switch self { + case .primary: + return L10n.primary + case .art: + return L10n.art + case .backdrop: + return L10n.backdrop + case .banner: + return L10n.banner + case .logo: + return L10n.logo + case .thumb: + return L10n.thumb + case .disc: + return L10n.disc + case .box: + return L10n.box + case .screenshot: + return L10n.screenshot + case .menu: + return L10n.menu + case .chapter: + return L10n.chapter + case .boxRear: + return L10n.boxRear + case .profile: + return L10n.profile + } + } +} diff --git a/Shared/Extensions/JellyfinAPI/ItemFields.swift b/Shared/Extensions/JellyfinAPI/ItemFields.swift index 1e299f98e..974df1873 100644 --- a/Shared/Extensions/JellyfinAPI/ItemFields.swift +++ b/Shared/Extensions/JellyfinAPI/ItemFields.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift b/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift index f9a32404d..d234dad9f 100644 --- a/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift +++ b/Shared/Extensions/JellyfinAPI/ItemFilter+ItemTrait.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift b/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift index 0e8eb714c..6b7ababac 100644 --- a/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift +++ b/Shared/Extensions/JellyfinAPI/JellyfinAPIError.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/JellyfinClient.swift b/Shared/Extensions/JellyfinAPI/JellyfinClient.swift index bd238dc70..38155236c 100644 --- a/Shared/Extensions/JellyfinAPI/JellyfinClient.swift +++ b/Shared/Extensions/JellyfinAPI/JellyfinClient.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift b/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift index f8354c758..728163db8 100644 --- a/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift +++ b/Shared/Extensions/JellyfinAPI/LoginFailurePolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift b/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift index eb6435172..5988a749f 100644 --- a/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift +++ b/Shared/Extensions/JellyfinAPI/MaxBitratePolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift index 15ff3815f..0f0c321dd 100644 --- a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo+ItemVideoPlayerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift index d237c8821..117c55b1a 100644 --- a/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift +++ b/Shared/Extensions/JellyfinAPI/MediaSourceInfo/MediaSourceInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/MediaStream.swift b/Shared/Extensions/JellyfinAPI/MediaStream.swift index 7b702acf9..c9652a6a4 100644 --- a/Shared/Extensions/JellyfinAPI/MediaStream.swift +++ b/Shared/Extensions/JellyfinAPI/MediaStream.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Extensions/JellyfinAPI/MetadataField.swift b/Shared/Extensions/JellyfinAPI/MetadataField.swift index a1c719072..fcee7c435 100644 --- a/Shared/Extensions/JellyfinAPI/MetadataField.swift +++ b/Shared/Extensions/JellyfinAPI/MetadataField.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/NameGuidPair.swift b/Shared/Extensions/JellyfinAPI/NameGuidPair.swift index f9b81bbfd..c5c32a6ae 100644 --- a/Shared/Extensions/JellyfinAPI/NameGuidPair.swift +++ b/Shared/Extensions/JellyfinAPI/NameGuidPair.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/ParentalRating.swift b/Shared/Extensions/JellyfinAPI/ParentalRating.swift index dc57c7120..43ce6a667 100644 --- a/Shared/Extensions/JellyfinAPI/ParentalRating.swift +++ b/Shared/Extensions/JellyfinAPI/ParentalRating.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/PersonKind.swift b/Shared/Extensions/JellyfinAPI/PersonKind.swift index bdd953118..243fa8a74 100644 --- a/Shared/Extensions/JellyfinAPI/PersonKind.swift +++ b/Shared/Extensions/JellyfinAPI/PersonKind.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/PlayMethod.swift b/Shared/Extensions/JellyfinAPI/PlayMethod.swift index f18618c40..6fa9c3ef4 100644 --- a/Shared/Extensions/JellyfinAPI/PlayMethod.swift +++ b/Shared/Extensions/JellyfinAPI/PlayMethod.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift b/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift index f0058a7c7..c33d5e172 100644 --- a/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift +++ b/Shared/Extensions/JellyfinAPI/PlayerStateInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.swift new file mode 100644 index 000000000..8be12d1be --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/RemoteImageInfo.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) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import SwiftUI + +extension RemoteImageInfo: @retroactive Identifiable, Poster { + + var displayTitle: String { + providerName ?? L10n.unknown + } + + var unwrappedIDHashOrZero: Int { + id + } + + var subtitle: String? { + language + } + + var systemImage: String { + "photo" + } + + public var id: Int { + hashValue + } +} diff --git a/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift new file mode 100644 index 000000000..b1c05efcb --- /dev/null +++ b/Shared/Extensions/JellyfinAPI/RemoteSearchResult.swift @@ -0,0 +1,56 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI +import SwiftUI + +extension RemoteSearchResult: Displayable { + + var displayTitle: String { + name ?? L10n.unknown + } +} + +// TODO: fix in SDK, should already be equatable +extension RemoteSearchResult: @retroactive Hashable, @retroactive Identifiable { + + public var id: Int { + hashValue + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(albumArtist) + hasher.combine(artists) + hasher.combine(imageURL) + hasher.combine(indexNumber) + hasher.combine(indexNumberEnd) + hasher.combine(name) + hasher.combine(overview) + hasher.combine(parentIndexNumber) + hasher.combine(premiereDate) + hasher.combine(productionYear) + hasher.combine(providerIDs) + hasher.combine(searchProviderName) + } + + public static func == (lhs: RemoteSearchResult, rhs: RemoteSearchResult) -> Bool { + lhs.albumArtist == rhs.albumArtist && + lhs.artists == rhs.artists && + lhs.imageURL == rhs.imageURL && + lhs.indexNumber == rhs.indexNumber && + lhs.indexNumberEnd == rhs.indexNumberEnd && + lhs.name == rhs.name && + lhs.overview == rhs.overview && + lhs.parentIndexNumber == rhs.parentIndexNumber && + lhs.premiereDate == rhs.premiereDate && + lhs.productionYear == rhs.productionYear && + lhs.providerIDs == rhs.providerIDs && + lhs.searchProviderName == rhs.searchProviderName + } +} diff --git a/Shared/Extensions/JellyfinAPI/ServerTicks.swift b/Shared/Extensions/JellyfinAPI/ServerTicks.swift index 22aecfa74..6570f3746 100644 --- a/Shared/Extensions/JellyfinAPI/ServerTicks.swift +++ b/Shared/Extensions/JellyfinAPI/ServerTicks.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SessionInfo.swift b/Shared/Extensions/JellyfinAPI/SessionInfo.swift index 6c50f6afb..bf6495825 100644 --- a/Shared/Extensions/JellyfinAPI/SessionInfo.swift +++ b/Shared/Extensions/JellyfinAPI/SessionInfo.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift b/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift index c68e659a2..32b3d7705 100644 --- a/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift +++ b/Shared/Extensions/JellyfinAPI/SortOrder+ItemSortOrder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift b/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift index 483db9880..6ee865cd0 100644 --- a/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift +++ b/Shared/Extensions/JellyfinAPI/SpecialFeatureType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift b/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift index 6f124efc4..fd8ec3bc1 100644 --- a/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift +++ b/Shared/Extensions/JellyfinAPI/SubtitleProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift b/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift index 9e11c93e7..0537698a4 100644 --- a/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift +++ b/Shared/Extensions/JellyfinAPI/SyncPlayUserAccessType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift b/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift index a48b0777c..25046f0e9 100644 --- a/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift +++ b/Shared/Extensions/JellyfinAPI/TaskCompletionStatus.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TaskState.swift b/Shared/Extensions/JellyfinAPI/TaskState.swift index 50ba73b90..06f63a6a4 100644 --- a/Shared/Extensions/JellyfinAPI/TaskState.swift +++ b/Shared/Extensions/JellyfinAPI/TaskState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift b/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift index add2d13cf..1141fa55f 100644 --- a/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift +++ b/Shared/Extensions/JellyfinAPI/TaskTriggerType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TranscodeReason.swift b/Shared/Extensions/JellyfinAPI/TranscodeReason.swift index 6049e2efd..365831183 100644 --- a/Shared/Extensions/JellyfinAPI/TranscodeReason.swift +++ b/Shared/Extensions/JellyfinAPI/TranscodeReason.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift b/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift index 699b69ab8..39edbea68 100644 --- a/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift +++ b/Shared/Extensions/JellyfinAPI/TranscodingProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/UserDto.swift b/Shared/Extensions/JellyfinAPI/UserDto.swift index 59de3902e..61404ace8 100644 --- a/Shared/Extensions/JellyfinAPI/UserDto.swift +++ b/Shared/Extensions/JellyfinAPI/UserDto.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/JellyfinAPI/Video3DFormat.swift b/Shared/Extensions/JellyfinAPI/Video3DFormat.swift index f8073b14f..662152a61 100644 --- a/Shared/Extensions/JellyfinAPI/Video3DFormat.swift +++ b/Shared/Extensions/JellyfinAPI/Video3DFormat.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/NavigationCoordinatable.swift b/Shared/Extensions/NavigationCoordinatable.swift index dd327f7ea..276ca9f36 100644 --- a/Shared/Extensions/NavigationCoordinatable.swift +++ b/Shared/Extensions/NavigationCoordinatable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Shared/Extensions/Nuke/DataCache.swift b/Shared/Extensions/Nuke/DataCache.swift index 143b2282f..a57bd54e1 100644 --- a/Shared/Extensions/Nuke/DataCache.swift +++ b/Shared/Extensions/Nuke/DataCache.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore @@ -21,48 +21,49 @@ extension DataCache { extension DataCache.Swiftfin { - static let `default`: DataCache? = { - let dataCache = try? DataCache(name: "org.jellyfin.swiftfin") { name in - URL(string: name)?.pathAndQuery() ?? name + static let posters: DataCache? = { + + let dataCache = try? DataCache(name: "org.jellyfin.swiftfin/Posters") { name in + guard let url = name.url else { return nil } + return ImagePipeline.cacheKey(for: url) } - dataCache?.sizeLimit = 1024 * 1024 * 500 // 500 MB + dataCache?.sizeLimit = 1024 * 1024 * 1000 // 1000 MB return dataCache }() - /// The `DataCache` used for images that should have longer lifetimes, usable without a - /// connection, and not affected by other caching size limits. - /// - /// Current 150 MB is more than necessary. - static let branding: DataCache? = { + /// The `DataCache` used for server and user images that should be usable + /// without an active connection. + static let local: DataCache? = { guard let root = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil } - let path = root.appendingPathComponent("Cache/org.jellyfin.swiftfin.branding", isDirectory: true) + let path = root.appendingPathComponent("Caches/org.jellyfin.swiftfin.local", isDirectory: true) let dataCache = try? DataCache(path: path) { name in - // this adds some latency, but fine since - // this DataCache is special - if name.range(of: "Splashscreen") != nil { + guard let url = name.url else { return nil } + + // Since multi-url servers are supported, key splashscreens with the server ID. + // + // Additional latency from Core Data fetch is acceptable. + if url.path.contains("Splashscreen") { - // TODO: potential issue where url ends with `/`, if - // not found, retry with `/` appended - let prefix = name.trimmingSuffix("/Branding/Splashscreen?") + // Account for hosting at a path + guard let prefixURL = url.absoluteString.trimmingSuffix("/Branding/Splashscreen?").url else { return nil } - // can assume that we are only requesting a server with - // the key same as the current url - guard let prefixURL = URL(string: prefix) else { return name } + // We can assume that the request is from the current server + let urlFilter: Where = Where(\.$currentURL == prefixURL) guard let server = try? SwiftfinStore.dataStack.fetchOne( From() - .where(\.$currentURL == prefixURL) - ) else { return name } + .where(urlFilter) + ) else { return nil } - return "\(server.id)-splashscreen" + return "\(server.id)-splashscreen".sha1 } else { - return URL(string: name)?.pathAndQuery() ?? name + return ImagePipeline.cacheKey(for: url) } } diff --git a/Shared/Extensions/Nuke/ImagePipeline.swift b/Shared/Extensions/Nuke/ImagePipeline.swift index 836068044..28c55b64b 100644 --- a/Shared/Extensions/Nuke/ImagePipeline.swift +++ b/Shared/Extensions/Nuke/ImagePipeline.swift @@ -3,27 +3,67 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation import Nuke extension ImagePipeline { + enum Swiftfin {} + + static func cacheKey(for url: URL) -> String? { + guard var components = url.components else { return nil } + + var maxWidthValue: String? + + if let maxWidth = components.queryItems?.first(where: { $0.name == "maxWidth" }) { + maxWidthValue = maxWidth.value + components.queryItems = components.queryItems?.filter { $0.name != "maxWidth" } + } + + guard let newURL = components.url, let urlSHA = newURL.pathAndQuery?.sha1 else { return nil } + + if let maxWidthValue { + return urlSHA + "-\(maxWidthValue)" + } else { + return urlSHA + } + } + + func removeItem(for url: URL) { + let request = ImageRequest(url: url) + cache.removeCachedImage(for: request) + cache.removeCachedData(for: request) + + guard let dataCacheKey = Self.cacheKey(for: url) else { return } + configuration.dataCache?.removeData(for: dataCacheKey) + } } extension ImagePipeline.Swiftfin { - /// The default `ImagePipeline` to use for images that should be used - /// during normal usage with an active connection. - static let `default`: ImagePipeline = ImagePipeline { - $0.dataCache = DataCache.Swiftfin.default + /// The default `ImagePipeline` to use for images that are typically posters + /// or server user images that should be presentable with an active connection. + static let posters: ImagePipeline = ImagePipeline(delegate: SwiftfinImagePipelineDelegate()) { + $0.dataCache = DataCache.Swiftfin.posters } /// The `ImagePipeline` used for images that should have longer lifetimes and usable - /// without a connection, like user profile images and server splashscreens. - static let branding: ImagePipeline = ImagePipeline { - $0.dataCache = DataCache.Swiftfin.branding + /// without a connection, likes local user profile images and server splashscreens. + static let local: ImagePipeline = ImagePipeline(delegate: SwiftfinImagePipelineDelegate()) { + $0.dataCache = DataCache.Swiftfin.local + } + + /// An `ImagePipeline` for images to prevent more important images from losing their cache. + static let other: ImagePipeline = ImagePipeline(configuration: .withURLCache) +} + +final class SwiftfinImagePipelineDelegate: ImagePipelineDelegate { + + func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? { + guard let url = request.url else { return nil } + return ImagePipeline.cacheKey(for: url) } } diff --git a/Shared/Extensions/Optional.swift b/Shared/Extensions/Optional.swift new file mode 100644 index 000000000..1ad51f827 --- /dev/null +++ b/Shared/Extensions/Optional.swift @@ -0,0 +1,24 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation + +extension Optional where Wrapped: Collection { + + var isNilOrEmpty: Bool { + self?.isEmpty ?? true + } + + mutating func appendedOrInit(_ element: Wrapped.Element) -> [Wrapped.Element] { + if let self { + return self + [element] + } else { + return [element] + } + } +} diff --git a/Shared/Extensions/OrderedDictionary.swift b/Shared/Extensions/OrderedDictionary.swift index 302c1326d..13394084b 100644 --- a/Shared/Extensions/OrderedDictionary.swift +++ b/Shared/Extensions/OrderedDictionary.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections diff --git a/Shared/Extensions/PersistentLogHandler.swift b/Shared/Extensions/PersistentLogHandler.swift index 5b1d76e97..f3cdf3dea 100644 --- a/Shared/Extensions/PersistentLogHandler.swift +++ b/Shared/Extensions/PersistentLogHandler.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/RatingType.swift b/Shared/Extensions/RatingType.swift new file mode 100644 index 000000000..aecd287da --- /dev/null +++ b/Shared/Extensions/RatingType.swift @@ -0,0 +1,21 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI + +extension RatingType: Displayable { + + var displayTitle: String { + switch self { + case .score: + return L10n.score + case .likes: + return L10n.likes + } + } +} diff --git a/Shared/Extensions/Sequence.swift b/Shared/Extensions/Sequence.swift index afc8fee82..525adbc0b 100644 --- a/Shared/Extensions/Sequence.swift +++ b/Shared/Extensions/Sequence.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/Set.swift b/Shared/Extensions/Set.swift index bc6b9531a..f1786d8f8 100644 --- a/Shared/Extensions/Set.swift +++ b/Shared/Extensions/Set.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -17,4 +17,10 @@ extension Set { insert(value) } } + + mutating func insert(contentsOf elements: [Element]) { + for element in elements { + insert(element) + } + } } diff --git a/Shared/Extensions/String.swift b/Shared/Extensions/String.swift index 77813789c..80aba5400 100644 --- a/Shared/Extensions/String.swift +++ b/Shared/Extensions/String.swift @@ -3,10 +3,11 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Algorithms +import CryptoKit import Foundation import SwiftUI @@ -34,6 +35,14 @@ extension String { self + String(element) } + func appending(_ element: @autoclosure () -> String, if condition: Bool) -> String { + if condition { + return self + element() + } else { + return self + } + } + func prepending(_ element: String) -> String { element + self } @@ -117,6 +126,23 @@ extension String { return s } + var sha1: String? { + guard let input = data(using: .utf8) else { return nil } + return Insecure.SHA1.hash(data: input) + .reduce(into: "") { partialResult, byte in + partialResult += String(format: "%02x", byte) + } + } + + var base64: String? { + guard let input = data(using: .utf8) else { return nil } + return input.base64EncodedString() + } + + var url: URL? { + URL(string: self) + } + // TODO: remove after iOS 15 support removed func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat { diff --git a/Shared/Extensions/Task.swift b/Shared/Extensions/Task.swift index cff9cd90a..7e7302aa1 100644 --- a/Shared/Extensions/Task.swift +++ b/Shared/Extensions/Task.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Extensions/Text.swift b/Shared/Extensions/Text.swift index c49808884..c376aca59 100644 --- a/Shared/Extensions/Text.swift +++ b/Shared/Extensions/Text.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/UIApplication.swift b/Shared/Extensions/UIApplication.swift index fd83c1306..2cac26023 100644 --- a/Shared/Extensions/UIApplication.swift +++ b/Shared/Extensions/UIApplication.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/UIColor.swift b/Shared/Extensions/UIColor.swift index a41a6f4ea..f8b78a9ae 100644 --- a/Shared/Extensions/UIColor.swift +++ b/Shared/Extensions/UIColor.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/UIDevice.swift b/Shared/Extensions/UIDevice.swift index 2668ebcff..a8685f3c6 100644 --- a/Shared/Extensions/UIDevice.swift +++ b/Shared/Extensions/UIDevice.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/UIGestureRecognizer.swift b/Shared/Extensions/UIGestureRecognizer.swift index dbf46e426..b825df563 100644 --- a/Shared/Extensions/UIGestureRecognizer.swift +++ b/Shared/Extensions/UIGestureRecognizer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/UIHostingController.swift b/Shared/Extensions/UIHostingController.swift index da2258c7f..f633f8cc4 100644 --- a/Shared/Extensions/UIHostingController.swift +++ b/Shared/Extensions/UIHostingController.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/UIScreen.swift b/Shared/Extensions/UIScreen.swift index 70c5f8a79..6372d1c4e 100644 --- a/Shared/Extensions/UIScreen.swift +++ b/Shared/Extensions/UIScreen.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Extensions/URL.swift b/Shared/Extensions/URL.swift index 33be35a67..cffe97483 100644 --- a/Shared/Extensions/URL.swift +++ b/Shared/Extensions/URL.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -15,6 +15,14 @@ extension URL: Identifiable { } } +extension URL { + + init?(string: String?) { + guard let string = string else { return nil } + self.init(string: string) + } +} + extension URL { static var documents: URL { @@ -39,6 +47,8 @@ extension URL { static let jellyfinDocsUsers: URL = URL(string: "https://jellyfin.org/docs/general/server/users")! + static let jellyfinDocsManagingUsers: URL = URL(string: "https://jellyfin.org/docs/general/server/users/adding-managing-users")! + func isDirectoryAndReachable() throws -> Bool { guard try resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true else { return false @@ -66,7 +76,7 @@ extension URL { } // doesn't have `?` but doesn't matter - func pathAndQuery() -> String? { + var pathAndQuery: String? { path + (query ?? "") } @@ -78,4 +88,8 @@ extension URL { return -1 } } + + var components: URLComponents? { + URLComponents(url: self, resolvingAgainstBaseURL: false) + } } diff --git a/Shared/Extensions/URLComponents.swift b/Shared/Extensions/URLComponents.swift index c2e04cacf..a7cf37ec4 100644 --- a/Shared/Extensions/URLComponents.swift +++ b/Shared/Extensions/URLComponents.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/URLResponse.swift b/Shared/Extensions/URLResponse.swift index ab497e15b..c95234aa7 100644 --- a/Shared/Extensions/URLResponse.swift +++ b/Shared/Extensions/URLResponse.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/URLSessionConfiguration.swift b/Shared/Extensions/URLSessionConfiguration.swift index e683d1bf7..4d2bbf2d7 100644 --- a/Shared/Extensions/URLSessionConfiguration.swift +++ b/Shared/Extensions/URLSessionConfiguration.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/VerticalAlignment.swift b/Shared/Extensions/VerticalAlignment.swift index 5b0764db1..06e635fd5 100644 --- a/Shared/Extensions/VerticalAlignment.swift +++ b/Shared/Extensions/VerticalAlignment.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift b/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift index 875c5f952..fedb76a25 100644 --- a/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift +++ b/Shared/Extensions/ViewExtensions/Backport/BackPort+ScrollIndicatorVisibility.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Extensions/ViewExtensions/Backport/Backport.swift b/Shared/Extensions/ViewExtensions/Backport/Backport.swift index f3b8b369f..639db398e 100644 --- a/Shared/Extensions/ViewExtensions/Backport/Backport.swift +++ b/Shared/Extensions/ViewExtensions/Backport/Backport.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -109,7 +109,7 @@ extension Backport where Content: View { extension ButtonBorderShape { static let circleBackport: ButtonBorderShape = { - if #available(iOS 17, tvOS 16.4, *) { + if #available(iOS 17, *) { return ButtonBorderShape.circle } else { return ButtonBorderShape.roundedRectangle diff --git a/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift index 7bda41574..dfcfa7e2b 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/AttributeStyleModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift index 526377ea9..84a6daf09 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/BackgroundParallaxHeaderModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift index 757052d97..4cf312d4e 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/BottomEdgeGradientModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift b/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift index e151c38ae..8807fa085 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/ErrorMessage.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift index 888dda70c..8bbc1f329 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnFinalDisappearModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift index c85402d0b..a158440c6 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnFirstAppearModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift index 1fd538e70..945d1993f 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnReceiveNotificationModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift index 5ccc3ff7b..c27074867 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnScenePhaseChangedModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift index ef07415e6..6e014e07d 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/OnSizeChangedModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift index 5a524d70d..5a6225b4b 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/ScrollIfLargerThanContainerModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift index b098a68f0..09b9a7d85 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/ScrollViewOffsetModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift b/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift index 67a54155a..d66869d65 100644 --- a/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift +++ b/Shared/Extensions/ViewExtensions/Modifiers/SinceLastDisappearModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/PreferenceKeys.swift b/Shared/Extensions/ViewExtensions/PreferenceKeys.swift index 5afbcd35d..7357640e4 100644 --- a/Shared/Extensions/ViewExtensions/PreferenceKeys.swift +++ b/Shared/Extensions/ViewExtensions/PreferenceKeys.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Extensions/ViewExtensions/ViewExtensions.swift b/Shared/Extensions/ViewExtensions/ViewExtensions.swift index 3341c9210..7a663a1b9 100644 --- a/Shared/Extensions/ViewExtensions/ViewExtensions.swift +++ b/Shared/Extensions/ViewExtensions/ViewExtensions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/AppAppearance.swift b/Shared/Objects/AppAppearance.swift index 256ce227f..80ce18bdb 100644 --- a/Shared/Objects/AppAppearance.swift +++ b/Shared/Objects/AppAppearance.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ArrayBuilder.swift b/Shared/Objects/ArrayBuilder.swift index 0a8e34ad1..6a5a1ae00 100644 --- a/Shared/Objects/ArrayBuilder.swift +++ b/Shared/Objects/ArrayBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/BindingBox.swift b/Shared/Objects/BindingBox.swift index 175eac266..155fd8c4e 100644 --- a/Shared/Objects/BindingBox.swift +++ b/Shared/Objects/BindingBox.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Objects/CaseIterablePicker.swift b/Shared/Objects/CaseIterablePicker.swift index 538cfd723..e4ce6e4a7 100644 --- a/Shared/Objects/CaseIterablePicker.swift +++ b/Shared/Objects/CaseIterablePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/ChannelProgram.swift b/Shared/Objects/ChannelProgram.swift index 43dc567db..d5fd2e77b 100644 --- a/Shared/Objects/ChannelProgram.swift +++ b/Shared/Objects/ChannelProgram.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/CommaStringBuilder.swift b/Shared/Objects/CommaStringBuilder.swift index 7456844f8..bbaff28f8 100644 --- a/Shared/Objects/CommaStringBuilder.swift +++ b/Shared/Objects/CommaStringBuilder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/CurrentDate.swift b/Shared/Objects/CurrentDate.swift index 4d4b840c5..2242e9959 100644 --- a/Shared/Objects/CurrentDate.swift +++ b/Shared/Objects/CurrentDate.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Objects/CustomDeviceProfileAction.swift b/Shared/Objects/CustomDeviceProfileAction.swift index b43db7723..b9f6dcfeb 100644 --- a/Shared/Objects/CustomDeviceProfileAction.swift +++ b/Shared/Objects/CustomDeviceProfileAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -17,7 +17,7 @@ enum CustomDeviceProfileAction: String, CaseIterable, Displayable, Storable { var displayTitle: String { switch self { case .add: - return "Add" + return L10n.add case .replace: return "Replace" } diff --git a/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift b/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift index fc5444240..7e539798f 100644 --- a/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift +++ b/Shared/Objects/DisplayOrder/BoxSetDisplayOrder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift b/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift index 0c7cdbabf..fa0798e98 100644 --- a/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift +++ b/Shared/Objects/DisplayOrder/SeriesDisplayOrder.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Displayable.swift b/Shared/Objects/Displayable.swift index 935efd34f..c4278ba45 100644 --- a/Shared/Objects/Displayable.swift +++ b/Shared/Objects/Displayable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Eventful.swift b/Shared/Objects/Eventful.swift index 258d9f142..1bf7b7a15 100644 --- a/Shared/Objects/Eventful.swift +++ b/Shared/Objects/Eventful.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/Objects/GestureAction.swift b/Shared/Objects/GestureAction.swift index eeb1ee6a5..59403a628 100644 --- a/Shared/Objects/GestureAction.swift +++ b/Shared/Objects/GestureAction.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -41,7 +41,7 @@ enum MultiTapAction: String, GestureAction { case .none: return L10n.none case .jump: - return "Jump" + return L10n.jump } } } @@ -58,11 +58,11 @@ enum DoubleTouchAction: String, GestureAction { case .none: return L10n.none case .aspectFill: - return "Aspect Fill" + return L10n.aspectFill case .gestureLock: return "Gesture Lock" case .pausePlay: - return "Pause/Play" + return L10n.playAndPause } } } @@ -83,17 +83,17 @@ enum PanAction: String, GestureAction { case .none: return L10n.none case .audioffset: - return "Audio Offset" + return L10n.audioOffset case .brightness: return "Brightness" case .playbackSpeed: - return "Playback Speed" + return L10n.playbackSpeed case .scrub: return "Scrub" case .slowScrub: return "Slow Scrub" case .subtitleOffset: - return "Subtitle Offset" + return L10n.subtitleOffset case .volume: return "Volume" } @@ -110,7 +110,7 @@ enum PinchAction: String, GestureAction { case .none: return L10n.none case .aspectFill: - return "Aspect Fill" + return L10n.aspectFill } } } @@ -125,7 +125,7 @@ enum SwipeAction: String, GestureAction { case .none: return L10n.none case .jump: - return "Jump" + return L10n.jump } } } diff --git a/Shared/Objects/ImageSource.swift b/Shared/Objects/ImageSource.swift index b08c43c24..d131d81e7 100644 --- a/Shared/Objects/ImageSource.swift +++ b/Shared/Objects/ImageSource.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemArrayElements.swift b/Shared/Objects/ItemArrayElements.swift index 23b2df193..f9e00c8aa 100644 --- a/Shared/Objects/ItemArrayElements.swift +++ b/Shared/Objects/ItemArrayElements.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/AnyItemFilter.swift b/Shared/Objects/ItemFilter/AnyItemFilter.swift index 412ba1e4a..7b14470ce 100644 --- a/Shared/Objects/ItemFilter/AnyItemFilter.swift +++ b/Shared/Objects/ItemFilter/AnyItemFilter.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemFilter.swift b/Shared/Objects/ItemFilter/ItemFilter.swift index 1f31c1667..514b73aef 100644 --- a/Shared/Objects/ItemFilter/ItemFilter.swift +++ b/Shared/Objects/ItemFilter/ItemFilter.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ItemFilter/ItemFilterCollection.swift b/Shared/Objects/ItemFilter/ItemFilterCollection.swift index 3b3f4e9eb..c62c35964 100644 --- a/Shared/Objects/ItemFilter/ItemFilterCollection.swift +++ b/Shared/Objects/ItemFilter/ItemFilterCollection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/ItemFilter/ItemFilterType.swift b/Shared/Objects/ItemFilter/ItemFilterType.swift index 448bbd969..422fe3394 100644 --- a/Shared/Objects/ItemFilter/ItemFilterType.swift +++ b/Shared/Objects/ItemFilter/ItemFilterType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -55,7 +55,7 @@ extension ItemFilterType: Displayable { case .genres: L10n.genres case .letter: - "Letter" + L10n.letter case .sortBy: L10n.sort case .sortOrder: @@ -65,7 +65,7 @@ extension ItemFilterType: Displayable { case .traits: L10n.filters case .years: - "Years" + L10n.years } } } diff --git a/Shared/Objects/ItemFilter/ItemGenre.swift b/Shared/Objects/ItemFilter/ItemGenre.swift index 9433ab0ad..4c33b64cd 100644 --- a/Shared/Objects/ItemFilter/ItemGenre.swift +++ b/Shared/Objects/ItemFilter/ItemGenre.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemLetter.swift b/Shared/Objects/ItemFilter/ItemLetter.swift index 9b8156b3d..cba593c30 100644 --- a/Shared/Objects/ItemFilter/ItemLetter.swift +++ b/Shared/Objects/ItemFilter/ItemLetter.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemSortBy.swift b/Shared/Objects/ItemFilter/ItemSortBy.swift index f04649ca6..f1ef59c40 100644 --- a/Shared/Objects/ItemFilter/ItemSortBy.swift +++ b/Shared/Objects/ItemFilter/ItemSortBy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -22,13 +22,13 @@ enum ItemSortBy: String, CaseIterable, Displayable, Codable { var displayTitle: String { switch self { case .premiereDate: - return "Premiere date" + return L10n.premiereDate case .name: - return "Name" + return L10n.name case .dateAdded: - return "Date added" + return L10n.dateAdded case .random: - return "Random" + return L10n.random } } } diff --git a/Shared/Objects/ItemFilter/ItemTag.swift b/Shared/Objects/ItemFilter/ItemTag.swift index fc2b34221..4805cd9b2 100644 --- a/Shared/Objects/ItemFilter/ItemTag.swift +++ b/Shared/Objects/ItemFilter/ItemTag.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemFilter/ItemYear.swift b/Shared/Objects/ItemFilter/ItemYear.swift index 10abc7b98..30d1f2d05 100644 --- a/Shared/Objects/ItemFilter/ItemYear.swift +++ b/Shared/Objects/ItemFilter/ItemYear.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/ItemViewType.swift b/Shared/Objects/ItemViewType.swift index 0fefc53dd..e4e32e1a6 100644 --- a/Shared/Objects/ItemViewType.swift +++ b/Shared/Objects/ItemViewType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/LibraryDisplayType.swift b/Shared/Objects/LibraryDisplayType.swift index 472b8fd14..a8340c72e 100644 --- a/Shared/Objects/LibraryDisplayType.swift +++ b/Shared/Objects/LibraryDisplayType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/LibraryParent/LibraryParent.swift b/Shared/Objects/LibraryParent/LibraryParent.swift index 2e56805a0..0165513ba 100644 --- a/Shared/Objects/LibraryParent/LibraryParent.swift +++ b/Shared/Objects/LibraryParent/LibraryParent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/LibraryParent/TitledLibraryParent.swift b/Shared/Objects/LibraryParent/TitledLibraryParent.swift index 53101f8f3..706195861 100644 --- a/Shared/Objects/LibraryParent/TitledLibraryParent.swift +++ b/Shared/Objects/LibraryParent/TitledLibraryParent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/MediaComponents/AudoCodec.swift b/Shared/Objects/MediaComponents/AudoCodec.swift index 5339aa715..55f76f488 100644 --- a/Shared/Objects/MediaComponents/AudoCodec.swift +++ b/Shared/Objects/MediaComponents/AudoCodec.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/MediaComponents/MediaContainer.swift b/Shared/Objects/MediaComponents/MediaContainer.swift index db61d8c21..678b15e61 100644 --- a/Shared/Objects/MediaComponents/MediaContainer.swift +++ b/Shared/Objects/MediaComponents/MediaContainer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/MediaComponents/SubtitleFormat.swift b/Shared/Objects/MediaComponents/SubtitleFormat.swift index 0f3d55c09..c456512d1 100644 --- a/Shared/Objects/MediaComponents/SubtitleFormat.swift +++ b/Shared/Objects/MediaComponents/SubtitleFormat.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/MediaComponents/VideoCodec.swift b/Shared/Objects/MediaComponents/VideoCodec.swift index 93b92d947..4f5ef75ba 100644 --- a/Shared/Objects/MediaComponents/VideoCodec.swift +++ b/Shared/Objects/MediaComponents/VideoCodec.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/NotificationSet.swift b/Shared/Objects/NotificationSet.swift index 5dc37da92..8aa1934d7 100644 --- a/Shared/Objects/NotificationSet.swift +++ b/Shared/Objects/NotificationSet.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/OverlayType.swift b/Shared/Objects/OverlayType.swift index bb72c912f..53b726c1b 100644 --- a/Shared/Objects/OverlayType.swift +++ b/Shared/Objects/OverlayType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -34,7 +34,7 @@ enum PlaybackButtonType: String, CaseIterable, Displayable, Defaults.Serializabl case .large: return "Large" case .compact: - return "Compact" + return L10n.compact } } } diff --git a/Shared/Objects/PanDirectionGestureRecognizer.swift b/Shared/Objects/PanDirectionGestureRecognizer.swift index 751695119..97fbcb9df 100644 --- a/Shared/Objects/PanDirectionGestureRecognizer.swift +++ b/Shared/Objects/PanDirectionGestureRecognizer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import UIKit diff --git a/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift b/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift index a40a84cee..0cbbdcfc2 100644 --- a/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift +++ b/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift b/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift index 800e4c4f2..fb8903100 100644 --- a/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift +++ b/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift index 70dd0e001..a655a6fcc 100644 --- a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift +++ b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility+Video.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift index 5cc26a02e..e39647b35 100644 --- a/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift +++ b/Shared/Objects/PlaybackCompatibility/PlaybackCompatibility.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackDeviceProfile.swift b/Shared/Objects/PlaybackDeviceProfile.swift index cc3336e52..a3b1c8d89 100644 --- a/Shared/Objects/PlaybackDeviceProfile.swift +++ b/Shared/Objects/PlaybackDeviceProfile.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/PlaybackSpeed.swift b/Shared/Objects/PlaybackSpeed.swift index 32e52c405..40768f2d0 100644 --- a/Shared/Objects/PlaybackSpeed.swift +++ b/Shared/Objects/PlaybackSpeed.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Poster.swift b/Shared/Objects/Poster.swift index cdf18e0b5..b512334fd 100644 --- a/Shared/Objects/Poster.swift +++ b/Shared/Objects/Poster.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/PosterDisplayType.swift b/Shared/Objects/PosterDisplayType.swift index 4eaff949b..18e885257 100644 --- a/Shared/Objects/PosterDisplayType.swift +++ b/Shared/Objects/PosterDisplayType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/RepeatingTimer.swift b/Shared/Objects/RepeatingTimer.swift index 216ee63d7..c3cc8e12e 100644 --- a/Shared/Objects/RepeatingTimer.swift +++ b/Shared/Objects/RepeatingTimer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/RoundedCorner.swift b/Shared/Objects/RoundedCorner.swift index 2845e11a6..ccd5e0ba1 100644 --- a/Shared/Objects/RoundedCorner.swift +++ b/Shared/Objects/RoundedCorner.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/ScalingButtonStyle.swift b/Shared/Objects/ScalingButtonStyle.swift index ed89f273f..1cde10705 100644 --- a/Shared/Objects/ScalingButtonStyle.swift +++ b/Shared/Objects/ScalingButtonStyle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/SelectUserServerSelection.swift b/Shared/Objects/SelectUserServerSelection.swift index a1b83b050..ded2602b9 100644 --- a/Shared/Objects/SelectUserServerSelection.swift +++ b/Shared/Objects/SelectUserServerSelection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/SeriesStatus.swift b/Shared/Objects/SeriesStatus.swift index a4548bbc5..d82aae1c0 100644 --- a/Shared/Objects/SeriesStatus.swift +++ b/Shared/Objects/SeriesStatus.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/SliderType.swift b/Shared/Objects/SliderType.swift index e041b5203..fcf5bab04 100644 --- a/Shared/Objects/SliderType.swift +++ b/Shared/Objects/SliderType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/Stateful.swift b/Shared/Objects/Stateful.swift index 4647ed8a5..e2c602ac7 100644 --- a/Shared/Objects/Stateful.swift +++ b/Shared/Objects/Stateful.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/Storable.swift b/Shared/Objects/Storable.swift index 887bba6d4..6588e0ed6 100644 --- a/Shared/Objects/Storable.swift +++ b/Shared/Objects/Storable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/StreamType.swift b/Shared/Objects/StreamType.swift index 8241b696c..7d504fb72 100644 --- a/Shared/Objects/StreamType.swift +++ b/Shared/Objects/StreamType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -17,9 +17,9 @@ enum StreamType: String, Displayable { var displayTitle: String { switch self { case .direct: - return "Direct" + return L10n.direct case .transcode: - return "Transcode" + return L10n.transcode case .hls: return "HLS" } diff --git a/Shared/Objects/SupportedCaseIterable.swift b/Shared/Objects/SupportedCaseIterable.swift index 46fc0684e..4c5b5069e 100644 --- a/Shared/Objects/SupportedCaseIterable.swift +++ b/Shared/Objects/SupportedCaseIterable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/SystemImageable.swift b/Shared/Objects/SystemImageable.swift index 9dbef46f8..87c4242de 100644 --- a/Shared/Objects/SystemImageable.swift +++ b/Shared/Objects/SystemImageable.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/TextPair.swift b/Shared/Objects/TextPair.swift index 8c9d95713..fb09f75ca 100644 --- a/Shared/Objects/TextPair.swift +++ b/Shared/Objects/TextPair.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/TimeStampType.swift b/Shared/Objects/TimeStampType.swift index a41e7b22f..51847bac7 100644 --- a/Shared/Objects/TimeStampType.swift +++ b/Shared/Objects/TimeStampType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -19,7 +19,7 @@ enum TimestampType: String, CaseIterable, Defaults.Serializable, Displayable { case .split: return "Split" case .compact: - return "Compact" + return L10n.compact } } } diff --git a/Shared/Objects/TimerProxy.swift b/Shared/Objects/TimerProxy.swift index 803761307..12febe876 100644 --- a/Shared/Objects/TimerProxy.swift +++ b/Shared/Objects/TimerProxy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Shared/Objects/TrailingTimestampType.swift b/Shared/Objects/TrailingTimestampType.swift index 8bef539a4..916fb4948 100644 --- a/Shared/Objects/TrailingTimestampType.swift +++ b/Shared/Objects/TrailingTimestampType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/Trie.swift b/Shared/Objects/Trie.swift index eb194709b..35ece1e58 100644 --- a/Shared/Objects/Trie.swift +++ b/Shared/Objects/Trie.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // class Trie where Key.Element: Hashable { diff --git a/Shared/Objects/UserAccessPolicy.swift b/Shared/Objects/UserAccessPolicy.swift index 96c23bf50..d9fa13713 100644 --- a/Shared/Objects/UserAccessPolicy.swift +++ b/Shared/Objects/UserAccessPolicy.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -21,11 +21,11 @@ enum UserAccessPolicy: String, CaseIterable, Codable, Displayable { var displayTitle: String { switch self { case .none: - "None" + L10n.none case .requireDeviceAuthentication: "Device Authentication" case .requirePin: - "Pin" + L10n.pin } } } diff --git a/Shared/Objects/UserPermissions.swift b/Shared/Objects/UserPermissions.swift index 706f5c3e5..e86b9a81b 100644 --- a/Shared/Objects/UserPermissions.swift +++ b/Shared/Objects/UserPermissions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Shared/Objects/UserSignInState.swift b/Shared/Objects/UserSignInState.swift index 8c2cdee4e..c0e98f3a7 100644 --- a/Shared/Objects/UserSignInState.swift +++ b/Shared/Objects/UserSignInState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/Utilities.swift b/Shared/Objects/Utilities.swift index 108ffdc87..5f3eaa541 100644 --- a/Shared/Objects/Utilities.swift +++ b/Shared/Objects/Utilities.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerActionButton.swift b/Shared/Objects/VideoPlayerActionButton.swift index 97448e0ca..e38f2770c 100644 --- a/Shared/Objects/VideoPlayerActionButton.swift +++ b/Shared/Objects/VideoPlayerActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -25,23 +25,23 @@ enum VideoPlayerActionButton: String, CaseIterable, Defaults.Serializable, Displ var displayTitle: String { switch self { // case .advanced: -// return "Advanced" +// return L10n.advanced case .aspectFill: - return "Aspect Fill" + return L10n.aspectFill case .audio: - return "Audio" + return L10n.audio case .autoPlay: - return "Auto Play" + return L10n.autoPlay case .chapters: - return "Chapters" + return L10n.chapters case .playbackSpeed: - return "Playback Speed" + return L10n.playbackSpeed case .playNextItem: - return "Play Next Item" + return L10n.playNextItem case .playPreviousItem: - return "Play Previous Item" + return L10n.playPreviousItem case .subtitles: - return "Subtitles" + return L10n.subtitles } } diff --git a/Shared/Objects/VideoPlayerJumpLength.swift b/Shared/Objects/VideoPlayerJumpLength.swift index fffb6b7d8..25597486e 100644 --- a/Shared/Objects/VideoPlayerJumpLength.swift +++ b/Shared/Objects/VideoPlayerJumpLength.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift index 2e4c1f57e..d0f7e0b6c 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType+Native.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift index 9e42a4e89..d63935291 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType+Shared.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift index 1c2c12118..28ebeaee4 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType+Swiftfin.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Objects/VideoPlayerType/VideoPlayerType.swift b/Shared/Objects/VideoPlayerType/VideoPlayerType.swift index 38be61c58..f3d23c949 100644 --- a/Shared/Objects/VideoPlayerType/VideoPlayerType.swift +++ b/Shared/Objects/VideoPlayerType/VideoPlayerType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ServerDiscovery/ServerDiscovery.swift b/Shared/ServerDiscovery/ServerDiscovery.swift index 21b745182..61e229d74 100644 --- a/Shared/ServerDiscovery/ServerDiscovery.swift +++ b/Shared/ServerDiscovery/ServerDiscovery.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ServerDiscovery/ServerResponse.swift b/Shared/ServerDiscovery/ServerResponse.swift index dae85d03c..ed0ccc4bc 100644 --- a/Shared/ServerDiscovery/ServerResponse.swift +++ b/Shared/ServerDiscovery/ServerResponse.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/Services/DownloadManager.swift b/Shared/Services/DownloadManager.swift index 2bc7f36a7..055499c0e 100644 --- a/Shared/Services/DownloadManager.swift +++ b/Shared/Services/DownloadManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Services/DownloadTask.swift b/Shared/Services/DownloadTask.swift index 47fae8c46..59a639e5a 100644 --- a/Shared/Services/DownloadTask.swift +++ b/Shared/Services/DownloadTask.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Services/Keychain.swift b/Shared/Services/Keychain.swift index be4f83e10..d945fff4c 100644 --- a/Shared/Services/Keychain.swift +++ b/Shared/Services/Keychain.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/Services/LogManager.swift b/Shared/Services/LogManager.swift index f7b75567d..27c915477 100644 --- a/Shared/Services/LogManager.swift +++ b/Shared/Services/LogManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/Services/Notifications.swift b/Shared/Services/Notifications.swift index ce726b23e..cd0cbd51a 100644 --- a/Shared/Services/Notifications.swift +++ b/Shared/Services/Notifications.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -124,12 +124,15 @@ extension Notifications.Key { // MARK: - Media Items + // TODO: come up with a cleaner, more defined way for item update notifications + /// - Payload: The new item with updated metadata. static var itemMetadataDidChange: Key { Key("itemMetadataDidChange") } - static var itemShouldRefresh: Key<(itemID: String, parentID: String?)> { + /// - Payload: The ID of the item that should refresh + static var itemShouldRefreshMetadata: Key { Key("itemShouldRefresh") } @@ -150,8 +153,9 @@ extension Notifications.Key { // MARK: - User - static var didChangeUserProfileImage: Key { - Key("didChangeUserProfileImage") + /// - Payload: The ID of the user whose Profile Image changed. + static var didChangeUserProfile: Key { + Key("didChangeUserProfile") } static var didAddServerUser: Key { diff --git a/Shared/Services/SwiftfinDefaults.swift b/Shared/Services/SwiftfinDefaults.swift index 96a1153d4..a0e3822b3 100644 --- a/Shared/Services/SwiftfinDefaults.swift +++ b/Shared/Services/SwiftfinDefaults.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/Services/UserSession.swift b/Shared/Services/UserSession.swift index 952f7aee9..942341e94 100644 --- a/Shared/Services/UserSession.swift +++ b/Shared/Services/UserSession.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreData diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index fa079f746..c02049db0 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -16,20 +16,28 @@ internal enum L10n { 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. - internal static let accentColorDescription = L10n.tr("Localizable", "accentColorDescription", fallback: "Some views may need an app restart to update.") /// Access internal static let access = L10n.tr("Localizable", "access", fallback: "Access") /// Accessibility internal static let accessibility = L10n.tr("Localizable", "accessibility", fallback: "Accessibility") - /// Access schedule - internal static let accessSchedule = L10n.tr("Localizable", "accessSchedule", fallback: "Access schedule") - /// Create an access schedule to limit access to certain hours. - internal static let accessScheduleDescription = L10n.tr("Localizable", "accessScheduleDescription", fallback: "Create an access schedule to limit access to certain hours.") + /// The End Time must come after the Start Time. + internal static let accessScheduleInvalidTime = L10n.tr("Localizable", "accessScheduleInvalidTime", fallback: "The End Time must come after the Start Time.") + /// Access Schedules + internal static let accessSchedules = L10n.tr("Localizable", "accessSchedules", fallback: "Access Schedules") + /// Define the allowed hours for usage and restrict access outside those times. + internal static let accessSchedulesDescription = L10n.tr("Localizable", "accessSchedulesDescription", fallback: "Define the allowed hours for usage and restrict access outside those times.") + /// User will have access to no media unless it contains at least one allowed tag. + internal static let accessTagAllowDescription = L10n.tr("Localizable", "accessTagAllowDescription", fallback: "User will have access to no media unless it contains at least one allowed tag.") + /// Access tag already exists + internal static let accessTagAlreadyExists = L10n.tr("Localizable", "accessTagAlreadyExists", fallback: "Access tag already exists") + /// User will have access to all media except when it contains any blocked tag. + internal static let accessTagBlockDescription = L10n.tr("Localizable", "accessTagBlockDescription", fallback: "User will have access to all media except when it contains any blocked tag.") + /// Access Tags + internal static let accessTags = L10n.tr("Localizable", "accessTags", fallback: "Access Tags") + /// Use tags to grant or restrict this user's access to media. + internal static let accessTagsDescription = L10n.tr("Localizable", "accessTagsDescription", fallback: "Use tags to grant or restrict this user's access to media.") /// 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") /// Activity Indicator @@ -38,6 +46,10 @@ internal enum L10n { internal static let actor = L10n.tr("Localizable", "actor", fallback: "Actor") /// Add internal static let add = L10n.tr("Localizable", "add", fallback: "Add") + /// Add access schedule + internal static let addAccessSchedule = L10n.tr("Localizable", "addAccessSchedule", fallback: "Add access schedule") + /// Add access tag + internal static let addAccessTag = L10n.tr("Localizable", "addAccessTag", fallback: "Add access tag") /// Add API key internal static let addAPIKey = L10n.tr("Localizable", "addAPIKey", fallback: "Add API key") /// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings. @@ -50,8 +62,6 @@ 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 - internal static let administration = L10n.tr("Localizable", "administration", fallback: "Administration") /// Administrator internal static let administrator = L10n.tr("Localizable", "administrator", fallback: "Administrator") /// Advanced @@ -70,20 +80,20 @@ internal enum L10n { } /// Album Artist internal static let albumArtist = L10n.tr("Localizable", "albumArtist", fallback: "Album Artist") + /// All + internal static let all = L10n.tr("Localizable", "all", fallback: "All") /// All Audiences internal static let allAudiences = L10n.tr("Localizable", "allAudiences", fallback: "All Audiences") /// View all past and present devices that have connected. internal static let allDevicesDescription = L10n.tr("Localizable", "allDevicesDescription", fallback: "View all past and present devices that have connected.") - /// All Genres - internal static let allGenres = L10n.tr("Localizable", "allGenres", fallback: "All Genres") + /// All languages + internal static let allLanguages = L10n.tr("Localizable", "allLanguages", fallback: "All languages") /// All Media internal static let allMedia = L10n.tr("Localizable", "allMedia", fallback: "All Media") /// Allow collection management internal static let allowCollectionManagement = L10n.tr("Localizable", "allowCollectionManagement", fallback: "Allow collection management") - /// Allowed tags - internal static let allowedTags = L10n.tr("Localizable", "allowedTags", fallback: "Allowed tags") - /// Only show media to this user with at least one of the specified tags. - internal static let allowedTagsDescription = L10n.tr("Localizable", "allowedTagsDescription", fallback: "Only show media to this user with at least one of the specified tags.") + /// Allowed + internal static let allowed = L10n.tr("Localizable", "allowed", fallback: "Allowed") /// Allow media item deletion internal static let allowItemDeletion = L10n.tr("Localizable", "allowItemDeletion", fallback: "Allow media item deletion") /// Allow media item editing @@ -108,24 +118,22 @@ internal enum L10n { internal static let apiKeysDescription = L10n.tr("Localizable", "apiKeysDescription", fallback: "External applications require an API key to communicate with your server.") /// API Keys internal static let apiKeysTitle = L10n.tr("Localizable", "apiKeysTitle", fallback: "API Keys") - /// Represents the Appearance setting label + /// Appearance internal static let appearance = L10n.tr("Localizable", "appearance", fallback: "Appearance") /// App Icon internal static let appIcon = L10n.tr("Localizable", "appIcon", fallback: "App Icon") /// Application Name internal static let applicationName = L10n.tr("Localizable", "applicationName", fallback: "Application Name") - /// Apply - internal static let apply = L10n.tr("Localizable", "apply", fallback: "Apply") /// Arranger internal static let arranger = L10n.tr("Localizable", "arranger", fallback: "Arranger") + /// Art + internal static let art = L10n.tr("Localizable", "art", fallback: "Art") /// Artist internal static let artist = L10n.tr("Localizable", "artist", fallback: "Artist") /// Aspect Fill internal static let aspectFill = L10n.tr("Localizable", "aspectFill", fallback: "Aspect Fill") /// Audio internal static let audio = L10n.tr("Localizable", "audio", fallback: "Audio") - /// Audio & Captions - internal static let audioAndCaptions = L10n.tr("Localizable", "audioAndCaptions", fallback: "Audio & Captions") /// The audio bit depth is not supported internal static let audioBitDepthNotSupported = L10n.tr("Localizable", "audioBitDepthNotSupported", fallback: "The audio bit depth is not supported") /// The audio bitrate is not supported @@ -142,8 +150,6 @@ internal enum L10n { internal static let audioProfileNotSupported = L10n.tr("Localizable", "audioProfileNotSupported", fallback: "The audio profile is 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") /// Audio transcoding internal static let audioTranscoding = L10n.tr("Localizable", "audioTranscoding", fallback: "Audio transcoding") /// Author @@ -158,6 +164,10 @@ internal enum L10n { internal static let autoPlay = L10n.tr("Localizable", "autoPlay", fallback: "Auto Play") /// Back internal static let back = L10n.tr("Localizable", "back", fallback: "Back") + /// Backdrop + internal static let backdrop = L10n.tr("Localizable", "backdrop", fallback: "Backdrop") + /// Banner + internal static let banner = L10n.tr("Localizable", "banner", fallback: "Banner") /// Bar Buttons internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons") /// Behavior @@ -214,10 +224,8 @@ internal enum L10n { 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") - /// Blocked tags - internal static let blockedTags = L10n.tr("Localizable", "blockedTags", fallback: "Blocked tags") - /// Hide media with at least one of the specified tags. - internal static let blockedTagsDescription = L10n.tr("Localizable", "blockedTagsDescription", fallback: "Hide media with at least one of the specified tags.") + /// Blocked + internal static let blocked = L10n.tr("Localizable", "blocked", fallback: "Blocked") /// Block unrated items internal static let blockUnratedItems = L10n.tr("Localizable", "blockUnratedItems", fallback: "Block unrated items") /// Block items from this user with no or unrecognized rating information. @@ -226,34 +234,32 @@ internal enum L10n { internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue") /// Books internal static let books = L10n.tr("Localizable", "books", fallback: "Books") + /// Box + internal static let box = L10n.tr("Localizable", "box", fallback: "Box") + /// BoxRear + internal static let boxRear = L10n.tr("Localizable", "boxRear", fallback: "BoxRear") /// Bugs and Features internal static let bugsAndFeatures = L10n.tr("Localizable", "bugsAndFeatures", fallback: "Bugs and Features") /// Buttons internal static let buttons = L10n.tr("Localizable", "buttons", fallback: "Buttons") /// Cancel internal static let cancel = L10n.tr("Localizable", "cancel", fallback: "Cancel") - /// Cancelled - internal static let canceled = L10n.tr("Localizable", "canceled", fallback: "Cancelled") /// 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") /// Capabilities internal static let capabilities = L10n.tr("Localizable", "capabilities", fallback: "Capabilities") - /// CAST - internal static let cast = L10n.tr("Localizable", "cast", fallback: "CAST") /// Cast & Crew internal static let castAndCrew = L10n.tr("Localizable", "castAndCrew", fallback: "Cast & Crew") /// Category internal static let category = L10n.tr("Localizable", "category", fallback: "Category") /// Change Pin internal static let changePin = L10n.tr("Localizable", "changePin", fallback: "Change Pin") - /// Change Server - internal static let changeServer = L10n.tr("Localizable", "changeServer", fallback: "Change Server") - /// Changes not saved - internal static let changesNotSaved = L10n.tr("Localizable", "changesNotSaved", fallback: "Changes not saved") /// Channels internal static let channels = L10n.tr("Localizable", "channels", fallback: "Channels") + /// Chapter + internal static let chapter = L10n.tr("Localizable", "chapter", fallback: "Chapter") /// Chapters internal static let chapters = L10n.tr("Localizable", "chapters", fallback: "Chapters") /// Chapter Slider @@ -262,14 +268,10 @@ internal enum L10n { internal static let cinematic = L10n.tr("Localizable", "cinematic", fallback: "Cinematic") /// 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") /// Client internal static let client = L10n.tr("Localizable", "client", fallback: "Client") /// Close internal static let close = L10n.tr("Localizable", "close", fallback: "Close") - /// Closed Captions - internal static let closedCaptions = L10n.tr("Localizable", "closedCaptions", fallback: "Closed Captions") /// Collections internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections") /// Color @@ -278,8 +280,6 @@ internal enum L10n { internal static let colorist = L10n.tr("Localizable", "colorist", fallback: "Colorist") /// 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 @@ -300,20 +300,12 @@ internal enum L10n { internal static let conductor = L10n.tr("Localizable", "conductor", fallback: "Conductor") /// Confirm internal static let confirm = L10n.tr("Localizable", "confirm", fallback: "Confirm") - /// Confirm Close - internal static let confirmClose = L10n.tr("Localizable", "confirmClose", fallback: "Confirm Close") /// Confirm New Password internal static let confirmNewPassword = L10n.tr("Localizable", "confirmNewPassword", fallback: "Confirm New Password") /// Confirm Password internal static let confirmPassword = L10n.tr("Localizable", "confirmPassword", fallback: "Confirm Password") /// Connect internal static let connect = L10n.tr("Localizable", "connect", fallback: "Connect") - /// Connect Manually - internal static let connectManually = L10n.tr("Localizable", "connectManually", fallback: "Connect Manually") - /// Connect to Jellyfin - internal static let connectToJellyfin = L10n.tr("Localizable", "connectToJellyfin", fallback: "Connect to Jellyfin") - /// Connect to a Jellyfin server - internal static let connectToJellyfinServer = L10n.tr("Localizable", "connectToJellyfinServer", fallback: "Connect to a Jellyfin server") /// Connect to a Jellyfin server to get started internal static let connectToJellyfinServerStart = L10n.tr("Localizable", "connectToJellyfinServerStart", fallback: "Connect to a Jellyfin server to get started") /// Connect to Server @@ -326,8 +318,6 @@ internal enum L10n { internal static let containers = L10n.tr("Localizable", "containers", fallback: "Containers") /// Continue 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 @@ -356,8 +346,6 @@ internal enum L10n { 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") /// Custom internal static let custom = L10n.tr("Localizable", "custom", fallback: "Custom") /// Custom bitrate @@ -374,8 +362,6 @@ internal enum L10n { } /// 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.") - /// 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.") /// 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. @@ -392,7 +378,7 @@ internal enum L10n { 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 + /// Dark internal static let dark = L10n.tr("Localizable", "dark", fallback: "Dark") /// Dashboard internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard") @@ -416,8 +402,6 @@ internal enum L10n { internal static let `default` = L10n.tr("Localizable", "default", fallback: "Default") /// Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts. internal static let defaultFailedLoginDescription = L10n.tr("Localizable", "defaultFailedLoginDescription", fallback: "Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts.") - /// Default Scheme - internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme") /// Delete internal static let delete = L10n.tr("Localizable", "delete", fallback: "Delete") /// Are you sure you want to permanently delete this key? @@ -432,18 +416,28 @@ internal enum L10n { } /// Are you sure you wish to delete this device? This session will be logged out. internal static let deleteDeviceWarning = L10n.tr("Localizable", "deleteDeviceWarning", fallback: "Are you sure you wish to delete this device? This session will be logged out.") + /// Delete image + internal static let deleteImage = L10n.tr("Localizable", "deleteImage", fallback: "Delete image") /// Are you sure you want to delete this item? internal static let deleteItemConfirmation = L10n.tr("Localizable", "deleteItemConfirmation", fallback: "Are you sure you want to delete this item?") /// Are you sure you want to delete this item? This action cannot be undone. internal static let deleteItemConfirmationMessage = L10n.tr("Localizable", "deleteItemConfirmationMessage", fallback: "Are you sure you want to delete this item? This action cannot be undone.") + /// Delete Schedule + internal static let deleteSchedule = L10n.tr("Localizable", "deleteSchedule", fallback: "Delete Schedule") + /// Are you sure you wish to delete this schedule? + internal static let deleteScheduleWarning = L10n.tr("Localizable", "deleteScheduleWarning", fallback: "Are you sure you wish to delete this schedule?") /// Are you sure you want to delete the selected items? internal static let deleteSelectedConfirmation = L10n.tr("Localizable", "deleteSelectedConfirmation", fallback: "Are you sure you want to delete the selected items?") /// Delete Selected Devices internal static let deleteSelectedDevices = L10n.tr("Localizable", "deleteSelectedDevices", fallback: "Delete Selected Devices") + /// Delete Selected Schedules + internal static let deleteSelectedSchedules = L10n.tr("Localizable", "deleteSelectedSchedules", fallback: "Delete Selected Schedules") /// Delete Selected Users internal static let deleteSelectedUsers = L10n.tr("Localizable", "deleteSelectedUsers", fallback: "Delete Selected Users") /// Are you sure you wish to delete all selected devices? All selected sessions will be logged out. 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 schedules? + internal static let deleteSelectionSchedulesWarning = L10n.tr("Localizable", "deleteSelectionSchedulesWarning", fallback: "Are you sure you wish to delete all selected schedules?") /// 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?") /// Delete Server @@ -490,6 +484,8 @@ internal enum L10n { internal static let devices = L10n.tr("Localizable", "devices", fallback: "Devices") /// Digital internal static let digital = L10n.tr("Localizable", "digital", fallback: "Digital") + /// Dimensions + internal static let dimensions = L10n.tr("Localizable", "dimensions", fallback: "Dimensions") /// 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. @@ -504,12 +500,10 @@ internal enum L10n { internal static let directStream = L10n.tr("Localizable", "directStream", fallback: "Direct Stream") /// Disabled internal static let disabled = L10n.tr("Localizable", "disabled", fallback: "Disabled") - /// Discard Changes - internal static let discardChanges = L10n.tr("Localizable", "discardChanges", fallback: "Discard Changes") + /// Disc + internal static let disc = L10n.tr("Localizable", "disc", fallback: "Disc") /// Disclaimer internal static let disclaimer = L10n.tr("Localizable", "disclaimer", fallback: "Disclaimer") - /// Discovered Servers - internal static let discoveredServers = L10n.tr("Localizable", "discoveredServers", fallback: "Discovered Servers") /// Dismiss internal static let dismiss = L10n.tr("Localizable", "dismiss", fallback: "Dismiss") /// Display Order @@ -528,16 +522,12 @@ internal enum L10n { 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") /// Editor internal static let editor = L10n.tr("Localizable", "editor", fallback: "Editor") /// 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") - /// Empty Next Up - internal static let emptyNextUp = L10n.tr("Localizable", "emptyNextUp", fallback: "Empty Next Up") /// Enable all devices internal static let enableAllDevices = L10n.tr("Localizable", "enableAllDevices", fallback: "Enable all devices") /// Enable all libraries @@ -548,6 +538,8 @@ internal enum L10n { internal static let endDate = L10n.tr("Localizable", "endDate", fallback: "End Date") /// Ended internal static let ended = L10n.tr("Localizable", "ended", fallback: "Ended") + /// End Time + internal static let endTime = L10n.tr("Localizable", "endTime", fallback: "End Time") /// Engineer internal static let engineer = L10n.tr("Localizable", "engineer", fallback: "Engineer") /// Enter custom bitrate in Mbps @@ -584,6 +576,8 @@ internal enum L10n { internal static let errorDetails = L10n.tr("Localizable", "errorDetails", fallback: "Error Details") /// Every internal static let every = L10n.tr("Localizable", "every", fallback: "Every") + /// Everyday + internal static let everyday = L10n.tr("Localizable", "everyday", fallback: "Everyday") /// Every %1$@ internal static func everyInterval(_ p1: Any) -> String { return L10n.tr("Localizable", "everyInterval", String(describing: p1), fallback: "Every %1$@") @@ -592,10 +586,6 @@ internal enum L10n { internal static let executed = L10n.tr("Localizable", "executed", fallback: "Executed") /// Existing items internal static let existingItems = L10n.tr("Localizable", "existingItems", fallback: "Existing items") - /// Existing Server - internal static let existingServer = L10n.tr("Localizable", "existingServer", fallback: "Existing Server") - /// Existing User - internal static let existingUser = L10n.tr("Localizable", "existingUser", fallback: "Existing User") /// This item exists on your Jellyfin Server. internal static let existsOnServer = L10n.tr("Localizable", "existsOnServer", fallback: "This item exists on your Jellyfin Server.") /// Experimental @@ -606,32 +596,18 @@ internal enum L10n { internal static let favorited = L10n.tr("Localizable", "favorited", fallback: "Favorited") /// Favorites internal static let favorites = L10n.tr("Localizable", "favorites", fallback: "Favorites") - /// Feature access - 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 internal static let filters = L10n.tr("Localizable", "filters", fallback: "Filters") /// Find Missing internal static let findMissing = L10n.tr("Localizable", "findMissing", fallback: "Find Missing") /// Find missing metadata and images. internal static let findMissingDescription = L10n.tr("Localizable", "findMissingDescription", fallback: "Find missing metadata and images.") - /// 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") /// 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 @@ -654,8 +630,6 @@ internal enum L10n { 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 internal static let hidden = L10n.tr("Localizable", "hidden", fallback: "Hidden") /// Hide user from login screen @@ -666,14 +640,24 @@ internal enum L10n { internal static let home = L10n.tr("Localizable", "home", fallback: "Home") /// Hours internal static let hours = L10n.tr("Localizable", "hours", fallback: "Hours") + /// ID + internal static let id = L10n.tr("Localizable", "id", fallback: "ID") + /// Identify + internal static let identify = L10n.tr("Localizable", "identify", fallback: "Identify") /// Idle internal static let idle = L10n.tr("Localizable", "idle", fallback: "Idle") /// Illustrator internal static let illustrator = L10n.tr("Localizable", "illustrator", fallback: "Illustrator") + /// Images + internal static let image = L10n.tr("Localizable", "image", fallback: "Images") + /// Images + internal static let images = L10n.tr("Localizable", "images", fallback: "Images") + /// Image source + internal static let imageSource = L10n.tr("Localizable", "imageSource", fallback: "Image source") + /// Index + internal static let index = L10n.tr("Localizable", "index", fallback: "Index") /// Indicators internal static let indicators = L10n.tr("Localizable", "indicators", fallback: "Indicators") - /// Information - internal static let information = L10n.tr("Localizable", "information", fallback: "Information") /// Inker internal static let inker = L10n.tr("Localizable", "inker", fallback: "Inker") /// Interlaced video is not supported @@ -688,12 +672,6 @@ internal enum L10n { internal static func itemAtItem(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "itemAtItem", String(describing: p1), String(describing: p2), fallback: "%1$@ at %2$@") } - /// 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.") - /// %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") /// Jellyfin @@ -710,12 +688,6 @@ internal enum L10n { internal static let jumpForward = L10n.tr("Localizable", "jumpForward", fallback: "Jump Forward") /// Jump Forward Length internal static let jumpForwardLength = L10n.tr("Localizable", "jumpForwardLength", fallback: "Jump Forward Length") - /// Jump Gestures Enabled - internal static let jumpGesturesEnabled = L10n.tr("Localizable", "jumpGesturesEnabled", fallback: "Jump Gestures Enabled") - /// %s seconds - internal static func jumpLengthSeconds(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "jumpLengthSeconds", p1, fallback: "%s seconds") - } /// Kids internal static let kids = L10n.tr("Localizable", "kids", fallback: "Kids") /// kbps @@ -752,10 +724,12 @@ internal enum L10n { internal static let letterPicker = L10n.tr("Localizable", "letterPicker", fallback: "Letter Picker") /// Library internal static let library = L10n.tr("Localizable", "library", fallback: "Library") - /// Represents the light theme setting + /// Light internal static let light = L10n.tr("Localizable", "light", fallback: "Light") /// Liked Items internal static let likedItems = L10n.tr("Localizable", "likedItems", fallback: "Liked Items") + /// Likes + internal static let likes = L10n.tr("Localizable", "likes", fallback: "Likes") /// List internal static let list = L10n.tr("Localizable", "list", fallback: "List") /// Live TV @@ -768,8 +742,6 @@ internal enum L10n { internal static let liveTVPrograms = L10n.tr("Localizable", "liveTVPrograms", fallback: "Live TV Programs") /// Live TV recording management internal static let liveTvRecordingManagement = L10n.tr("Localizable", "liveTvRecordingManagement", fallback: "Live TV recording management") - /// Loading - internal static let loading = L10n.tr("Localizable", "loading", fallback: "Loading") /// Loading user failed internal static let loadingUserFailed = L10n.tr("Localizable", "loadingUserFailed", fallback: "Loading user failed") /// Local Servers @@ -780,12 +752,8 @@ internal enum L10n { 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 - internal static let login = L10n.tr("Localizable", "login", fallback: "Login") - /// Login to %@ - internal static func loginToWithString(_ p1: Any) -> String { - return L10n.tr("Localizable", "loginToWithString", String(describing: p1), fallback: "Login to %@") - } + /// Logo + internal static let logo = L10n.tr("Localizable", "logo", fallback: "Logo") /// Logs internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs") /// Access the Jellyfin server logs for troubleshooting and monitoring purposes. @@ -816,8 +784,6 @@ internal enum L10n { internal static let maxParentalRating = L10n.tr("Localizable", "maxParentalRating", fallback: "Maximum parental rating") /// Content with a higher rating will be hidden from this user. internal static let maxParentalRatingDescription = L10n.tr("Localizable", "maxParentalRatingDescription", fallback: "Content with a higher rating will be hidden from this user.") - /// 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") /// Media Access @@ -828,6 +794,8 @@ internal enum L10n { internal static let mediaPlayback = L10n.tr("Localizable", "mediaPlayback", fallback: "Media playback") /// Mbps internal static let megabitsPerSecond = L10n.tr("Localizable", "megabitsPerSecond", fallback: "Mbps") + /// Menu + internal static let menu = L10n.tr("Localizable", "menu", fallback: "Menu") /// Menu Buttons internal static let menuButtons = L10n.tr("Localizable", "menuButtons", fallback: "Menu Buttons") /// Metadata @@ -844,14 +812,8 @@ internal enum L10n { internal static let missingItems = L10n.tr("Localizable", "missingItems", fallback: "Missing Items") /// Mixer internal static let mixer = L10n.tr("Localizable", "mixer", fallback: "Mixer") - /// More Like This - internal static let moreLikeThis = L10n.tr("Localizable", "moreLikeThis", fallback: "More Like This") /// Movies internal static let movies = L10n.tr("Localizable", "movies", fallback: "Movies") - /// %d users - internal static func multipleUsers(_ p1: Int) -> String { - return L10n.tr("Localizable", "multipleUsers", p1, fallback: "%d users") - } /// Music internal static let music = L10n.tr("Localizable", "music", fallback: "Music") /// MVC @@ -860,8 +822,6 @@ internal enum L10n { internal static let name = L10n.tr("Localizable", "name", fallback: "Name") /// Native Player internal static let nativePlayer = L10n.tr("Localizable", "nativePlayer", fallback: "Native Player") - /// Networking - internal static let networking = L10n.tr("Localizable", "networking", fallback: "Networking") /// Network timed out internal static let networkTimedOut = L10n.tr("Localizable", "networkTimedOut", fallback: "Network timed out") /// Never @@ -888,10 +848,6 @@ internal enum L10n { internal static let nextUpRewatch = L10n.tr("Localizable", "nextUpRewatch", fallback: "Rewatching in Next Up") /// No internal static let no = L10n.tr("Localizable", "no", fallback: "No") - /// No Cast devices found.. - internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound", fallback: "No Cast devices found..") - /// No Codec - internal static let noCodec = L10n.tr("Localizable", "noCodec", fallback: "No Codec") /// No episodes available internal static let noEpisodesAvailable = L10n.tr("Localizable", "noEpisodesAvailable", fallback: "No episodes available") /// No local servers found @@ -910,8 +866,6 @@ internal enum L10n { internal static let noRuntimeLimit = L10n.tr("Localizable", "noRuntimeLimit", fallback: "No runtime limit") /// 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") /// Type: %@ not implemented yet :( internal static func notImplementedYetWithType(_ p1: Any) -> String { return L10n.tr("Localizable", "notImplementedYetWithType", String(describing: p1), fallback: "Type: %@ not implemented yet :(") @@ -926,14 +880,8 @@ internal enum L10n { internal static let ok = L10n.tr("Localizable", "ok", fallback: "OK") /// On application startup 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") - /// Online - internal static let online = L10n.tr("Localizable", "online", fallback: "Online") /// On Now internal static let onNow = L10n.tr("Localizable", "onNow", fallback: "On Now") - /// Operating System - internal static let operatingSystem = L10n.tr("Localizable", "operatingSystem", fallback: "Operating System") /// Options internal static let options = L10n.tr("Localizable", "options", fallback: "Options") /// Orange @@ -950,18 +898,8 @@ internal enum L10n { internal static let originalTitle = L10n.tr("Localizable", "originalTitle", fallback: "Original Title") /// Other internal static let other = L10n.tr("Localizable", "other", fallback: "Other") - /// Other User - internal static let otherUser = L10n.tr("Localizable", "otherUser", fallback: "Other User") - /// Overlay - internal static let overlay = L10n.tr("Localizable", "overlay", fallback: "Overlay") - /// Overlay Type - internal static let overlayType = L10n.tr("Localizable", "overlayType", fallback: "Overlay Type") /// Overview internal static let overview = L10n.tr("Localizable", "overview", fallback: "Overview") - /// Page %1$@ of %2$@ - 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 controls internal static let parentalControls = L10n.tr("Localizable", "parentalControls", fallback: "Parental controls") /// Parental Rating @@ -996,18 +934,12 @@ internal enum L10n { internal static let playbackButtons = L10n.tr("Localizable", "playbackButtons", fallback: "Playback Buttons") /// 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") /// Playback Speed internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed", fallback: "Playback Speed") /// Played internal static let played = L10n.tr("Localizable", "played", fallback: "Played") - /// Player Gestures Lock Gesture Enabled - internal static let playerGesturesLockGestureEnabled = L10n.tr("Localizable", "playerGesturesLockGestureEnabled", fallback: "Player Gestures Lock Gesture Enabled") /// Play From Beginning internal static let playFromBeginning = L10n.tr("Localizable", "playFromBeginning", fallback: "Play From Beginning") - /// Play Next - 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") /// Play on active @@ -1018,8 +950,6 @@ internal enum L10n { 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 internal static let pressDownForMenu = L10n.tr("Localizable", "pressDownForMenu", fallback: "Press Down for Menu") /// Previous Item @@ -1032,12 +962,20 @@ internal enum L10n { internal static let production = L10n.tr("Localizable", "production", fallback: "Production") /// Production Locations internal static let productionLocations = L10n.tr("Localizable", "productionLocations", fallback: "Production Locations") + /// Production Year + internal static let productionYear = L10n.tr("Localizable", "productionYear", fallback: "Production Year") + /// Profile + internal static let profile = L10n.tr("Localizable", "profile", fallback: "Profile") + /// Profile Image + internal static let profileImage = L10n.tr("Localizable", "profileImage", fallback: "Profile Image") /// Profiles internal static let profiles = L10n.tr("Localizable", "profiles", fallback: "Profiles") /// Programs internal static let programs = L10n.tr("Localizable", "programs", fallback: "Programs") /// Progress internal static let progress = L10n.tr("Localizable", "progress", fallback: "Progress") + /// Provider + internal static let provider = L10n.tr("Localizable", "provider", fallback: "Provider") /// Public Users internal static let publicUsers = L10n.tr("Localizable", "publicUsers", fallback: "Public Users") /// Quick Connect @@ -1046,10 +984,6 @@ internal enum L10n { internal static let quickConnectCode = L10n.tr("Localizable", "quickConnectCode", fallback: "Quick Connect code") /// Enter the 6 digit code from your other device. internal static let quickConnectCodeInstruction = L10n.tr("Localizable", "quickConnectCodeInstruction", fallback: "Enter the 6 digit code from your other device.") - /// Invalid Quick Connect code - internal static let quickConnectInvalidError = L10n.tr("Localizable", "quickConnectInvalidError", fallback: "Invalid Quick Connect code") - /// Note: Quick Connect not enabled - internal static let quickConnectNotEnabled = L10n.tr("Localizable", "quickConnectNotEnabled", fallback: "Note: Quick Connect not enabled") /// Open the Jellyfin app on your phone or web browser and sign in with your account internal static let quickConnectStep1 = L10n.tr("Localizable", "quickConnectStep1", fallback: "Open the Jellyfin app on your phone or web browser and sign in with your account") /// Open the user menu and go to the Quick Connect page @@ -1062,8 +996,6 @@ internal enum L10n { internal static let random = L10n.tr("Localizable", "random", fallback: "Random") /// 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. @@ -1080,38 +1012,32 @@ internal enum L10n { internal static let red = L10n.tr("Localizable", "red", fallback: "Red") /// 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 - internal static let remainingTime = L10n.tr("Localizable", "remainingTime", fallback: "Remaining Time") + /// Remember layout + internal static let rememberLayout = L10n.tr("Localizable", "rememberLayout", fallback: "Remember layout") + /// Remember layout for individual libraries + internal static let rememberLayoutFooter = L10n.tr("Localizable", "rememberLayoutFooter", fallback: "Remember layout for individual libraries") + /// Remember sorting + internal static let rememberSorting = L10n.tr("Localizable", "rememberSorting", fallback: "Remember sorting") + /// Remember sorting for individual libraries + internal static let rememberSortingFooter = L10n.tr("Localizable", "rememberSortingFooter", fallback: "Remember sorting for individual libraries") /// Remixer internal static let remixer = L10n.tr("Localizable", "remixer", fallback: "Remixer") /// Remote connections internal static let remoteConnections = L10n.tr("Localizable", "remoteConnections", fallback: "Remote connections") /// Remote control internal static let remoteControl = L10n.tr("Localizable", "remoteControl", fallback: "Remote control") - /// Remove - internal static let remove = L10n.tr("Localizable", "remove", fallback: "Remove") /// Remove All internal static let removeAll = L10n.tr("Localizable", "removeAll", fallback: "Remove All") /// Remove All Servers internal static let removeAllServers = L10n.tr("Localizable", "removeAllServers", fallback: "Remove All Servers") - /// Remove All Users - 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") /// Remux internal static let remux = L10n.tr("Localizable", "remux", fallback: "Remux") /// Reorder @@ -1128,10 +1054,6 @@ internal enum L10n { internal static let replaceMetadata = L10n.tr("Localizable", "replaceMetadata", fallback: "Replace Metadata") /// Replace unlocked metadata with new information. internal static let replaceMetadataDescription = L10n.tr("Localizable", "replaceMetadataDescription", fallback: "Replace unlocked metadata with new information.") - /// Report an Issue - internal static let reportIssue = L10n.tr("Localizable", "reportIssue", fallback: "Report an Issue") - /// Request a Feature - internal static let requestFeature = L10n.tr("Localizable", "requestFeature", fallback: "Request a Feature") /// Required internal static let `required` = L10n.tr("Localizable", "required", fallback: "Required") /// Require device authentication when signing in to the user. @@ -1148,8 +1070,12 @@ internal enum L10n { internal static let reset = L10n.tr("Localizable", "reset", fallback: "Reset") /// Reset all settings back to defaults. internal static let resetAllSettings = L10n.tr("Localizable", "resetAllSettings", fallback: "Reset all settings back to defaults.") - /// Reset App Settings - internal static let resetAppSettings = L10n.tr("Localizable", "resetAppSettings", fallback: "Reset App Settings") + /// Reset Settings + internal static let resetSettings = L10n.tr("Localizable", "resetSettings", fallback: "Reset Settings") + /// Reset Swiftfin user settings + internal static let resetSettingsDescription = L10n.tr("Localizable", "resetSettingsDescription", fallback: "Reset Swiftfin user settings") + /// Are you sure you want to reset all user settings? + internal static let resetSettingsMessage = L10n.tr("Localizable", "resetSettingsMessage", fallback: "Are you sure you want to reset all user settings?") /// Reset User Settings internal static let resetUserSettings = L10n.tr("Localizable", "resetUserSettings", fallback: "Reset User Settings") /// Restart Server @@ -1158,8 +1084,6 @@ internal enum L10n { internal static let restartWarning = L10n.tr("Localizable", "restartWarning", fallback: "Are you sure you want to restart the server?") /// 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") /// Resume content seconds before the recorded resume time @@ -1188,40 +1112,36 @@ internal enum L10n { internal static let save = L10n.tr("Localizable", "save", fallback: "Save") /// Save the user to this device without any local authentication. internal static let saveUserWithoutAuthDescription = L10n.tr("Localizable", "saveUserWithoutAuthDescription", fallback: "Save the user to this device without any local authentication.") - /// Scan All Libraries - internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries") - /// Scheduled Tasks - internal static let scheduledTasks = L10n.tr("Localizable", "scheduledTasks", fallback: "Scheduled Tasks") + /// Schedule already exists + internal static let scheduleAlreadyExists = L10n.tr("Localizable", "scheduleAlreadyExists", fallback: "Schedule already exists") + /// Score + internal static let score = L10n.tr("Localizable", "score", fallback: "Score") + /// Screenshot + internal static let screenshot = L10n.tr("Localizable", "screenshot", fallback: "Screenshot") /// Scrub Current Time internal static let scrubCurrentTime = L10n.tr("Localizable", "scrubCurrentTime", fallback: "Scrub Current Time") /// Search internal static let search = L10n.tr("Localizable", "search", fallback: "Search") - /// Search… - internal static let searchDots = L10n.tr("Localizable", "searchDots", fallback: "Search…") - /// Searching… - internal static let searchingDots = L10n.tr("Localizable", "searchingDots", fallback: "Searching…") /// Season internal static let season = L10n.tr("Localizable", "season", fallback: "Season") /// S%1$@:E%2$@ internal static func seasonAndEpisode(_ p1: Any, _ p2: Any) -> String { return L10n.tr("Localizable", "seasonAndEpisode", String(describing: p1), String(describing: p2), fallback: "S%1$@:E%2$@") } - /// Seasons - internal static let seasons = L10n.tr("Localizable", "seasons", fallback: "Seasons") /// Secondary audio is not supported internal static let secondaryAudioNotSupported = L10n.tr("Localizable", "secondaryAudioNotSupported", fallback: "Secondary audio is not supported") /// Security internal static let security = L10n.tr("Localizable", "security", fallback: "Security") /// See All internal static let seeAll = L10n.tr("Localizable", "seeAll", fallback: "See All") - /// Seek Slide Gesture Enabled - internal static let seekSlideGestureEnabled = L10n.tr("Localizable", "seekSlideGestureEnabled", fallback: "Seek Slide Gesture Enabled") /// See More internal static let seeMore = L10n.tr("Localizable", "seeMore", fallback: "See More") /// Select All internal static let selectAll = L10n.tr("Localizable", "selectAll", fallback: "Select All") - /// Select Cast Destination - internal static let selectCastDestination = L10n.tr("Localizable", "selectCastDestination", fallback: "Select Cast Destination") + /// Select Image + internal static let selectImage = L10n.tr("Localizable", "selectImage", fallback: "Select Image") + /// Select server + internal static let selectServer = L10n.tr("Localizable", "selectServer", fallback: "Select server") /// Series internal static let series = L10n.tr("Localizable", "series", fallback: "Series") /// Series Backdrop @@ -1236,22 +1156,10 @@ internal enum L10n { internal static func serverAlreadyExistsPrompt(_ p1: UnsafePointer) -> String { return L10n.tr("Localizable", "serverAlreadyExistsPrompt", p1, fallback: "Server %s already exists. Add new URL?") } - /// Server Details - internal static let serverDetails = L10n.tr("Localizable", "serverDetails", fallback: "Server Details") - /// Server Information - internal static let serverInformation = L10n.tr("Localizable", "serverInformation", fallback: "Server Information") /// Server Logs internal static let serverLogs = L10n.tr("Localizable", "serverLogs", fallback: "Server Logs") /// 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 { - return L10n.tr("Localizable", "serverTriggerCreated", String(describing: p1), fallback: "A new trigger was created for '%1$@'.") - } - /// The selected trigger was deleted from '%1$@'. - internal static func serverTriggerDeleted(_ p1: Any) -> String { - return L10n.tr("Localizable", "serverTriggerDeleted", String(describing: p1), fallback: "The selected trigger was deleted from '%1$@'.") - } /// Server URL internal static let serverURL = L10n.tr("Localizable", "serverURL", fallback: "Server URL") /// Session @@ -1268,16 +1176,10 @@ internal enum L10n { internal static let setPinHintDescription = L10n.tr("Localizable", "setPinHintDescription", fallback: "Set a hint when prompting for the pin.") /// Settings internal static let settings = L10n.tr("Localizable", "settings", fallback: "Settings") - /// Show Cast & Crew - 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") /// Show Favorited internal static let showFavorited = L10n.tr("Localizable", "showFavorited", fallback: "Show Favorited") /// 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") /// Show Missing Episodes internal static let showMissingEpisodes = L10n.tr("Localizable", "showMissingEpisodes", fallback: "Show Missing Episodes") /// Show Missing Seasons @@ -1296,14 +1198,8 @@ internal enum L10n { internal static let shutdownServer = L10n.tr("Localizable", "shutdownServer", fallback: "Shutdown Server") /// 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 { - return L10n.tr("Localizable", "signedInAsWithString", String(describing: p1), fallback: "Signed in as %@") - } /// Sign In internal static let signIn = L10n.tr("Localizable", "signIn", fallback: "Sign In") - /// Sign in to get started - internal static let signInGetStarted = L10n.tr("Localizable", "signInGetStarted", fallback: "Sign in to get started") /// Sign In to %s internal static func signInToServer(_ p1: UnsafePointer) -> String { return L10n.tr("Localizable", "signInToServer", p1, fallback: "Sign In to %s") @@ -1320,8 +1216,6 @@ internal enum L10n { internal static let smallest = L10n.tr("Localizable", "smallest", fallback: "Smallest") /// Sort 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 @@ -1332,6 +1226,8 @@ internal enum L10n { internal static let specialFeatures = L10n.tr("Localizable", "specialFeatures", fallback: "Special Features") /// Sports internal static let sports = L10n.tr("Localizable", "sports", fallback: "Sports") + /// Start Time + internal static let startTime = L10n.tr("Localizable", "startTime", fallback: "Start Time") /// Status internal static let status = L10n.tr("Localizable", "status", fallback: "Status") /// Stop @@ -1340,8 +1236,6 @@ internal enum L10n { internal static let storyArc = L10n.tr("Localizable", "storyArc", fallback: "Story Arc") /// Streams internal static let streams = L10n.tr("Localizable", "streams", fallback: "Streams") - /// STUDIO - internal static let studio = L10n.tr("Localizable", "studio", fallback: "STUDIO") /// Studios internal static let studios = L10n.tr("Localizable", "studios", fallback: "Studios") /// Studio(s) involved in the creation of media. @@ -1364,8 +1258,6 @@ internal enum L10n { internal static let subtitleSize = L10n.tr("Localizable", "subtitleSize", fallback: "Subtitle Size") /// Success internal static let success = L10n.tr("Localizable", "success", fallback: "Success") - /// Suggestions - internal static let suggestions = L10n.tr("Localizable", "suggestions", fallback: "Suggestions") /// Content Uploading internal static let supportsContentUploading = L10n.tr("Localizable", "supportsContentUploading", fallback: "Content Uploading") /// Media Control @@ -1378,10 +1270,10 @@ internal enum L10n { internal static let switchUser = L10n.tr("Localizable", "switchUser", fallback: "Switch User") /// SyncPlay internal static let syncPlay = L10n.tr("Localizable", "syncPlay", fallback: "SyncPlay") - /// Represents the system theme setting + /// System 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") + /// Tag + internal static let tag = L10n.tr("Localizable", "tag", fallback: "Tag") /// Tagline internal static let tagline = L10n.tr("Localizable", "tagline", fallback: "Tagline") /// Taglines @@ -1412,6 +1304,8 @@ internal enum L10n { internal static let terabitsPerSecond = L10n.tr("Localizable", "terabitsPerSecond", fallback: "Tbps") /// Test Size internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size") + /// Thumb + internal static let thumb = L10n.tr("Localizable", "thumb", fallback: "Thumb") /// Time internal static let time = L10n.tr("Localizable", "time", fallback: "Time") /// Time Limit @@ -1420,18 +1314,12 @@ internal enum L10n { internal static func timeLimitLabelWithValue(_ p1: Any) -> String { return L10n.tr("Localizable", "timeLimitLabelWithValue", String(describing: p1), fallback: "Time limit: %1$@") } - /// Time Limit (%@) - internal static func timeLimitWithUnit(_ p1: Any) -> String { - return L10n.tr("Localizable", "timeLimitWithUnit", String(describing: p1), fallback: "Time Limit (%@)") - } /// Timestamp 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") /// Trailers internal static let trailers = L10n.tr("Localizable", "trailers", fallback: "Trailers") /// Trailing Value @@ -1448,8 +1336,6 @@ internal enum L10n { internal static let triggerAlreadyExists = L10n.tr("Localizable", "triggerAlreadyExists", fallback: "Trigger already exists") /// Triggers 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 Access @@ -1458,8 +1344,6 @@ internal enum L10n { internal static let tvShows = L10n.tr("Localizable", "tvShows", fallback: "TV Shows") /// 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") /// Unable to find host internal static let unableToFindHost = L10n.tr("Localizable", "unableToFindHost", fallback: "Unable to find host") /// Unable to perform device authentication @@ -1492,6 +1376,10 @@ internal enum L10n { 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?") + /// Upload file + internal static let uploadFile = L10n.tr("Localizable", "uploadFile", fallback: "Upload file") + /// Upload photo + internal static let uploadPhoto = L10n.tr("Localizable", "uploadPhoto", fallback: "Upload photo") /// URL internal static let url = L10n.tr("Localizable", "url", fallback: "URL") /// Use as Transcoding Profile @@ -1502,10 +1390,6 @@ internal enum L10n { internal static let usePrimaryImageDescription = L10n.tr("Localizable", "usePrimaryImageDescription", fallback: "Uses the primary image and hides the logo.") /// User internal static let user = L10n.tr("Localizable", "user", fallback: "User") - /// User %s is already signed in - internal static func userAlreadySignedIn(_ p1: UnsafePointer) -> String { - return L10n.tr("Localizable", "userAlreadySignedIn", p1, fallback: "User %s is already signed in") - } /// This user will require device authentication. internal static let userDeviceAuthRequiredDescription = L10n.tr("Localizable", "userDeviceAuthRequiredDescription", fallback: "This user will require device authentication.") /// Username @@ -1548,14 +1432,18 @@ internal enum L10n { 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") + /// Some views may need an app restart to update. + internal static let viewsMayRequireRestart = L10n.tr("Localizable", "viewsMayRequireRestart", fallback: "Some views may need an app restart to update.") + /// Votes + internal static let votes = L10n.tr("Localizable", "votes", fallback: "Votes") + /// Weekday + internal static let weekday = L10n.tr("Localizable", "weekday", fallback: "Weekday") + /// Weekend + internal static let weekend = L10n.tr("Localizable", "weekend", fallback: "Weekend") /// Weekly internal static let weekly = L10n.tr("Localizable", "weekly", fallback: "Weekly") - /// Who's watching? - internal static let whosWatching = L10n.tr("Localizable", "WhosWatching", fallback: "Who's watching?") /// This will be created as a new item on your Jellyfin Server. internal static let willBeCreatedOnServer = L10n.tr("Localizable", "willBeCreatedOnServer", fallback: "This will be created as a new item on your Jellyfin Server.") - /// WIP - internal static let wip = L10n.tr("Localizable", "wip", fallback: "WIP") /// Writer internal static let writer = L10n.tr("Localizable", "writer", fallback: "Writer") /// Year @@ -1566,8 +1454,6 @@ internal enum L10n { internal static let yellow = L10n.tr("Localizable", "yellow", fallback: "Yellow") /// Yes internal static let yes = L10n.tr("Localizable", "yes", fallback: "Yes") - /// Your Favorites - internal static let yourFavorites = L10n.tr("Localizable", "yourFavorites", fallback: "Your Favorites") } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/Shared/SwiftfinStore/StoredValue/StoredValue.swift b/Shared/SwiftfinStore/StoredValue/StoredValue.swift index 5438c7eec..3085c1b21 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValue.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValue.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift index edf39eb37..f5d6e09bb 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+Server.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift index ef20d0c23..29c5ed265 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+Temp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift b/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift index 4f0d6cdb3..aa1458f6b 100644 --- a/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift +++ b/Shared/SwiftfinStore/StoredValue/StoredValues+User.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift b/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift index 14bc9c14f..8f9b01ba5 100644 --- a/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift +++ b/Shared/SwiftfinStore/SwiftfinStore+Mappings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift b/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift index 4467f35a4..ca8b0413c 100644 --- a/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift +++ b/Shared/SwiftfinStore/SwiftfinStore+ServerState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore @@ -78,4 +78,21 @@ extension ServerState { let request = Paths.getSplashscreen() return ImageSource(url: client.fullURL(with: request)) } + + func updateServerInfo() async throws { + guard let server = try? SwiftfinStore.dataStack.fetchOne( + From() + ) else { return } + + let publicInfo = try await getPublicSystemInfo() + + try SwiftfinStore.dataStack.perform { transaction in + guard let newServer = transaction.edit(server) else { return } + + newServer.name = publicInfo.serverName ?? newServer.name + newServer.id = publicInfo.id ?? newServer.id + } + + StoredValues[.Server.publicInfo(id: server.id)] = publicInfo + } } diff --git a/Shared/SwiftfinStore/SwiftfinStore.swift b/Shared/SwiftfinStore/SwiftfinStore.swift index 593b67ded..17e67b670 100644 --- a/Shared/SwiftfinStore/SwiftfinStore.swift +++ b/Shared/SwiftfinStore/SwiftfinStore.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/SwiftinStore+UserState.swift b/Shared/SwiftfinStore/SwiftinStore+UserState.swift index 91d53211a..de29c34f7 100644 --- a/Shared/SwiftfinStore/SwiftinStore+UserState.swift +++ b/Shared/SwiftfinStore/SwiftinStore+UserState.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore @@ -152,7 +152,6 @@ extension UserState { let scaleWidth = maxWidth == nil ? nil : UIScreen.main.scale(maxWidth!) let parameters = Paths.GetUserImageParameters( - tag: data.primaryImageTag, maxWidth: scaleWidth ) let request = Paths.getUserImage( diff --git a/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift b/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift index 316a47768..3a878ca48 100644 --- a/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift +++ b/Shared/SwiftfinStore/V1Schema/SwiftfinStore+V1.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift b/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift index de961b8c4..d89c90d33 100644 --- a/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift +++ b/Shared/SwiftfinStore/V1Schema/V1ServerModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V1Schema/V1UserModel.swift b/Shared/SwiftfinStore/V1Schema/V1UserModel.swift index 2618a689e..787497144 100644 --- a/Shared/SwiftfinStore/V1Schema/V1UserModel.swift +++ b/Shared/SwiftfinStore/V1Schema/V1UserModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift b/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift index 7d269ac98..d68438324 100644 --- a/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift +++ b/Shared/SwiftfinStore/V2Schema/SwiftfinStore+V2.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V2Schema/V2AnyData.swift b/Shared/SwiftfinStore/V2Schema/V2AnyData.swift index c31142f08..9145639bb 100644 --- a/Shared/SwiftfinStore/V2Schema/V2AnyData.swift +++ b/Shared/SwiftfinStore/V2Schema/V2AnyData.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift b/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift index c416f12d1..e57ba0d63 100644 --- a/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift +++ b/Shared/SwiftfinStore/V2Schema/V2ServerModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/SwiftfinStore/V2Schema/V2UserModel.swift b/Shared/SwiftfinStore/V2Schema/V2UserModel.swift index 931f143ab..698db5d2a 100644 --- a/Shared/SwiftfinStore/V2Schema/V2UserModel.swift +++ b/Shared/SwiftfinStore/V2Schema/V2UserModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift b/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift index f191e291b..ee4f304ee 100644 --- a/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/APIKeysViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift b/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift index d2dae1244..00c930408 100644 --- a/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ActiveSessionsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift b/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift index 2ad50f1b5..79ce618e3 100644 --- a/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/AddServerUserViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift b/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift index f1b41a337..ac4f35a0a 100644 --- a/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/DeviceDetailViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift b/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift index 91878612f..3917b997f 100644 --- a/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/DevicesViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift b/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift index 4a0e1a0e2..3205ca85c 100644 --- a/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift +++ b/Shared/ViewModels/AdminDashboard/ServerTaskObserver.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift index e371a3fa4..5f2fb8f41 100644 --- a/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerTasksViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift index f3eba6f18..3d90edc28 100644 --- a/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerUserAdminViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -24,7 +24,7 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl enum Action: Equatable { case cancel - case loadDetails + case refresh case loadLibraries(isHidden: Bool? = false) case updatePolicy(UserPolicy) case updateConfiguration(UserConfiguration) @@ -68,10 +68,22 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl .eraseToAnyPublisher() } - // MARK: - Initialize + // MARK: - Initializer init(user: UserDto) { self.user = user + super.init() + + Notifications[.didChangeUserProfile] + .publisher + .sink { userID in + guard userID == self.user.id else { return } + + Task { + await self.send(.refresh) + } + } + .store(in: &cancellables) } // MARK: - Respond @@ -81,7 +93,7 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl case .cancel: return .initial - case .loadDetails: + case .refresh: userTaskCancellable?.cancel() userTaskCancellable = Task { @@ -280,6 +292,7 @@ final class ServerUserAdminViewModel: ViewModel, Eventful, Stateful, Identifiabl await MainActor.run { self.user.name = username + Notifications[.didChangeUserProfile].post(userID) } } } diff --git a/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift b/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift index 7dab67a8a..bc193b5f3 100644 --- a/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift +++ b/Shared/ViewModels/AdminDashboard/ServerUsersViewModel.swift @@ -3,11 +3,12 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine import Foundation +import IdentifiedCollections import JellyfinAPI import OrderedCollections import SwiftUI @@ -24,6 +25,7 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { // MARK: Actions enum Action: Equatable { + case refreshUser(String) case getUsers(isHidden: Bool = false, isDisabled: Bool = false) case deleteUsers([String]) case appendUser(UserDto) @@ -49,8 +51,10 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { @Published final var backgroundStates: OrderedSet = [] + @Published - final var users: [UserDto] = [] + final var users: IdentifiedArrayOf = [] + @Published final var state: State = .initial @@ -63,10 +67,51 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { private var userTask: AnyCancellable? private var eventSubject: PassthroughSubject = .init() + // MARK: - Initializer + + override init() { + super.init() + + Notifications[.didChangeUserProfile] + .publisher + .sink { userID in + Task { + await self.send(.refreshUser(userID)) + } + } + .store(in: &cancellables) + } + // MARK: - Respond to Action func respond(to action: Action) -> State { switch action { + case let .refreshUser(userID): + userTask?.cancel() + backgroundStates.append(.gettingUsers) + + userTask = Task { + do { + try await refreshUser(userID) + + await MainActor.run { + state = .content + } + } catch { + await MainActor.run { + self.state = .error(.init(error.localizedDescription)) + self.eventSubject.send(.error(.init(error.localizedDescription))) + } + } + + await MainActor.run { + _ = self.backgroundStates.remove(.gettingUsers) + } + } + .asAnyCancellable() + + return state + case let .getUsers(isHidden, isDisabled): userTask?.cancel() backgroundStates.append(.gettingUsers) @@ -144,6 +189,21 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { } } + // MARK: - Refresh User + + private func refreshUser(_ userID: String) async throws { + let request = Paths.getUserByID(userID: userID) + let response = try await userSession.client.send(request) + + let newUser = response.value + + await MainActor.run { + if let index = self.users.firstIndex(where: { $0.id == userID }) { + self.users[index] = newUser + } + } + } + // MARK: - Load Users private func loadUsers(isHidden: Bool, isDisabled: Bool) async throws { @@ -154,7 +214,7 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { .sorted(using: \.name) await MainActor.run { - self.users = newUsers + self.users = IdentifiedArray(uniqueElements: newUsers) } } @@ -179,9 +239,7 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { } await MainActor.run { - self.users = self.users.filter { - !userIdsToDelete.contains($0.id ?? "") - } + self.users.removeAll(where: { userIdsToDelete.contains($0.id ?? "") }) } } @@ -197,7 +255,7 @@ final class ServerUsersViewModel: ViewModel, Eventful, Stateful, Identifiable { private func appendUser(user: UserDto) async { await MainActor.run { users.append(user) - users = users.sorted(using: \.name) + users.sort(by: { $0.name ?? "" < $1.name ?? "" }) } } } diff --git a/Shared/ViewModels/ChannelLibraryViewModel.swift b/Shared/ViewModels/ChannelLibraryViewModel.swift index 754d4e4b7..98cb6ad32 100644 --- a/Shared/ViewModels/ChannelLibraryViewModel.swift +++ b/Shared/ViewModels/ChannelLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 480e095a6..8b2f1db09 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/DownloadListViewModel.swift b/Shared/ViewModels/DownloadListViewModel.swift index f68a85e1c..c9fb3a709 100644 --- a/Shared/ViewModels/DownloadListViewModel.swift +++ b/Shared/ViewModels/DownloadListViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Shared/ViewModels/FilterViewModel.swift b/Shared/ViewModels/FilterViewModel.swift index 3f2f40ba4..b400e1554 100644 --- a/Shared/ViewModels/FilterViewModel.swift +++ b/Shared/ViewModels/FilterViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -19,12 +19,24 @@ final class FilterViewModel: ViewModel { var allFilters: ItemFilterCollection = .all private let parent: (any LibraryParent)? + private let itemTypes: [BaseItemKind]? init( parent: (any LibraryParent)? = nil, currentFilters: ItemFilterCollection = .default ) { self.parent = parent + self.itemTypes = nil + self.currentFilters = currentFilters + super.init() + } + + init( + itemTypes: [BaseItemKind], + currentFilters: ItemFilterCollection = .default + ) { + self.parent = nil + self.itemTypes = itemTypes self.currentFilters = currentFilters super.init() } @@ -43,7 +55,8 @@ final class FilterViewModel: ViewModel { private func getQueryFilters() async -> (genres: [ItemGenre], tags: [ItemTag], years: [ItemYear]) { let parameters = Paths.GetQueryFiltersLegacyParameters( userID: userSession.user.id, - parentID: parent?.id as? String + parentID: parent?.id as? String, + includeItemTypes: itemTypes ) let request = Paths.getQueryFiltersLegacy(parameters: parameters) diff --git a/Shared/ViewModels/HomeViewModel.swift b/Shared/ViewModels/HomeViewModel.swift index 8454357e6..008c0b75f 100644 --- a/Shared/ViewModels/HomeViewModel.swift +++ b/Shared/ViewModels/HomeViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift b/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift index a8138b038..1ffa5d44a 100644 --- a/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/DeleteItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift b/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift new file mode 100644 index 000000000..9be23a4b4 --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/IdentifyItemViewModel.swift @@ -0,0 +1,224 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import Get +import JellyfinAPI +import OrderedCollections + +class IdentifyItemViewModel: ViewModel, Stateful, Eventful { + + // MARK: - Events + + enum Event: Equatable { + case updated + case cancelled + case error(JellyfinAPIError) + } + + // MARK: - Actions + + enum Action: Equatable { + case cancel + case search(name: String? = nil, originalTitle: String? = nil, year: Int? = nil) + case update(RemoteSearchResult) + } + + // MARK: - State + + enum State: Hashable { + case content + case searching + case updating + } + + @Published + var item: BaseItemDto + @Published + var searchResults: [RemoteSearchResult] = [] + @Published + var state: State = .content + + private var updateTask: AnyCancellable? + private var searchTask: AnyCancellable? + + private let eventSubject = PassthroughSubject() + + var events: AnyPublisher { + eventSubject + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + } + + // MARK: - Initializer + + init(item: BaseItemDto) { + self.item = item + super.init() + } + + // MARK: - Respond to Actions + + func respond(to action: Action) -> State { + switch action { + + case .cancel: + updateTask?.cancel() + searchTask?.cancel() + + return .content + + case let .search(name, originalTitle, year): + searchTask?.cancel() + + searchTask = Task { + do { + let newResults = try await self.searchItem( + name: name, + originalTitle: originalTitle, + year: year + ) + + await MainActor.run { + self.searchResults = newResults + self.state = .content + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .content + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + return .searching + + case let .update(searchResult): + updateTask?.cancel() + + updateTask = Task { + do { + try await updateItem(searchResult) + + await MainActor.run { + self.state = .content + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .content + self.eventSubject.send(.error(apiError)) + } + } + }.asAnyCancellable() + + return .updating + } + } + + // MARK: - Return Matching Elements (To Be Overridden) + + private func searchItem( + name: String?, + originalTitle: String?, + year: Int? + ) async throws -> [RemoteSearchResult] { + + guard let itemID = item.id, let itemType = item.type else { + return [] + } + + switch itemType { + case .boxSet: + let parameters = BoxSetInfoRemoteSearchQuery( + itemID: itemID, + searchInfo: BoxSetInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getBoxSetRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + case .movie: + let parameters = MovieInfoRemoteSearchQuery( + itemID: itemID, + searchInfo: MovieInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getMovieRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + case .person: + let parameters = PersonLookupInfoRemoteSearchQuery( + itemID: itemID, + searchInfo: PersonLookupInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getPersonRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + case .series: + let parameters = SeriesInfoRemoteSearchQuery( + itemID: itemID, + searchInfo: SeriesInfo( + name: name, + originalTitle: originalTitle, + year: year + ) + ) + let request = Paths.getSeriesRemoteSearchResults(parameters) + let response = try await userSession.client.send(request) + + return response.value + + default: + return [] + } + } + + // MARK: - Save Updated Item to Server + + private func updateItem(_ match: RemoteSearchResult) async throws { + guard let itemID = item.id else { return } + + let request = Paths.applySearchCriteria(itemID: itemID, match) + _ = try await userSession.client.send(request) + + try await refreshItem() + } + + // MARK: - Refresh Item + + private func refreshItem() async throws { + guard let itemID = item.id else { return } + + 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 + Notifications[.itemShouldRefreshMetadata].post(itemID) + } + } +} diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift index 6edac7055..c4848ab36 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/GenreEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift index baa76dfa9..64c47eabd 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/ItemEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -279,6 +279,8 @@ class ItemEditorViewModel: ViewModel, Stateful, Eventful { // MARK: - Reorder Elements (To Be Overridden) + // TODO: should instead move to an index-based self insertion + // instead of replacement func reorderComponents(_ tags: [Element]) async throws { fatalError("This method should be overridden in subclasses") } diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift index 3386c7db6..fd8a1eadf 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/PeopleEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift index 2c91995c2..129696a53 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/StudioEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift index 1e67c904b..8403963da 100644 --- a/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/ItemEditorViewModel/TagEditorViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift new file mode 100644 index 000000000..a133b30fd --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/ItemImagesViewModel.swift @@ -0,0 +1,403 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Foundation +import JellyfinAPI +import OrderedCollections +import SwiftUI + +class ItemImagesViewModel: ViewModel, Stateful, Eventful { + + enum Event: Equatable { + case updated + case error(JellyfinAPIError) + } + + enum Action: Equatable { + case cancel + case refresh + case setImage(RemoteImageInfo) + case uploadImage(image: UIImage, type: ImageType) + case uploadFile(file: URL, type: ImageType) + case deleteImage(ImageInfo) + } + + enum BackgroundState: Hashable { + case updating + } + + enum State: Hashable { + case initial + case content + case error(JellyfinAPIError) + } + + // MARK: - Published Variables + + @Published + var item: BaseItemDto + @Published + var images: [ImageType: [ImageInfo]] = [:] + + // MARK: - State Management + + @Published + var state: State = .initial + @Published + var backgroundStates: OrderedSet = [] + + private var task: AnyCancellable? + private let eventSubject = PassthroughSubject() + + // MARK: - Eventful + + var events: AnyPublisher { + eventSubject + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + } + + // MARK: - Init + + init(item: BaseItemDto) { + self.item = item + } + + // MARK: - Respond to Actions + + func respond(to action: Action) -> State { + switch action { + + case .cancel: + task?.cancel() + return .initial + + case .refresh: + task?.cancel() + + task = Task { [weak self] in + guard let self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + self.images.removeAll() + } + + try await self.getAllImages() + + await MainActor.run { + self.state = .content + _ = self.backgroundStates.remove(.updating) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.state = .error(apiError) + self.eventSubject.send(.error(apiError)) + self.backgroundStates.remove(.updating) + } + } + }.asAnyCancellable() + + return .initial + + case let .setImage(remoteImageInfo): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await self.setImage(remoteImageInfo) + try await self.getAllImages() + + await MainActor.run { + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.eventSubject.send(.error(apiError)) + } + } + + await MainActor.run { + _ = self.backgroundStates.remove(.updating) + } + }.asAnyCancellable() + + return .content + + case let .uploadImage(image, type): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await self.uploadPhoto(image, type: type) + try await self.getAllImages() + + await MainActor.run { + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.eventSubject.send(.error(apiError)) + } + } + + await MainActor.run { + _ = self.backgroundStates.remove(.updating) + } + }.asAnyCancellable() + + return .content + + case let .uploadFile(url, type): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await self.uploadFile(url, type: type) + try await self.getAllImages() + + await MainActor.run { + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.eventSubject.send(.error(apiError)) + } + } + + await MainActor.run { + _ = self.backgroundStates.remove(.updating) + } + }.asAnyCancellable() + + return .content + + case let .deleteImage(imageInfo): + task?.cancel() + + task = Task { [weak self] in + guard let self = self else { return } + do { + await MainActor.run { + _ = self.backgroundStates.append(.updating) + } + + try await deleteImage(imageInfo) + try await refreshItem() + + await MainActor.run { + self.eventSubject.send(.updated) + } + } catch { + let apiError = JellyfinAPIError(error.localizedDescription) + await MainActor.run { + self.eventSubject.send(.error(apiError)) + } + } + + await MainActor.run { + _ = self.backgroundStates.remove(.updating) + } + }.asAnyCancellable() + + return .content + } + } + + // MARK: - Get All Item Images + + private func getAllImages() async throws { + guard let itemID = item.id else { return } + + let request = Paths.getItemImageInfos(itemID: itemID) + let response = try await self.userSession.client.send(request) + + let newImages: [ImageType: [ImageInfo]] = response.value.grouped(by: \.imageType) + .mapValues { $0.sorted(using: \.imageIndex) } + .reduce(into: [:]) { partialResult, kv in + guard let k = kv.key else { return } + partialResult[k] = kv.value + } + + await MainActor.run { + self.images = newImages + } + } + + // MARK: - Set Image From URL + + private func setImage(_ remoteImageInfo: RemoteImageInfo) async throws { + guard let itemID = item.id, + let type = remoteImageInfo.type, + let imageURL = remoteImageInfo.url else { return } + + let parameters = Paths.DownloadRemoteImageParameters(type: type, imageURL: imageURL) + let imageRequest = Paths.downloadRemoteImage(itemID: itemID, parameters: parameters) + try await userSession.client.send(imageRequest) + } + + // MARK: - Upload Image/File + + private func upload(imageData: Data, imageType: ImageType, contentType: String) async throws { + guard let itemID = item.id else { return } + + let uploadLimit: Int = 30_000_000 + + guard imageData.count <= uploadLimit else { + throw JellyfinAPIError( + "This image (\(imageData.count.formatted(.byteCount(style: .file)))) exceeds the maximum allowed size for upload (\(uploadLimit.formatted(.byteCount(style: .file)))." + ) + } + + var request = Paths.setItemImage( + itemID: itemID, + imageType: imageType.rawValue, + imageData.base64EncodedData() + ) + request.headers = ["Content-Type": contentType] + + _ = try await userSession.client.send(request) + } + + // MARK: - Prepare Photo for Upload + + private func uploadPhoto(_ image: UIImage, type: ImageType) async throws { + let contentType: String + let imageData: Data + + if let pngData = image.pngData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1) { + contentType = "image/jpeg" + imageData = jpgData + } else { + logger.error("Unable to convert given profile image to png/jpg") + throw JellyfinAPIError("An internal error occurred") + } + + try await upload( + imageData: imageData, + imageType: type, + contentType: contentType + ) + } + + // MARK: - Prepare Image for Upload + + private func uploadFile(_ url: URL, type: ImageType) async throws { + guard url.startAccessingSecurityScopedResource() else { + logger.error("Unable to access file at \(url)") + throw JellyfinAPIError("An internal error occurred.") + } + defer { url.stopAccessingSecurityScopedResource() } + + let contentType: String + let imageData: Data + + switch url.pathExtension.lowercased() { + case "png": + contentType = "image/png" + imageData = try Data(contentsOf: url) + case "jpeg", "jpg": + contentType = "image/jpeg" + imageData = try Data(contentsOf: url) + default: + guard let image = try UIImage(data: Data(contentsOf: url)) else { + logger.error("Unable to load image from file") + throw JellyfinAPIError("An internal error occurred.") + } + + if let pngData = image.pngData() { + contentType = "image/png" + imageData = pngData + } else if let jpgData = image.jpegData(compressionQuality: 1) { + contentType = "image/jpeg" + imageData = jpgData + } else { + logger.error("Failed to convert image to png/jpg") + throw JellyfinAPIError("An internal error occurred.") + } + } + + try await upload( + imageData: imageData, + imageType: type, + contentType: contentType + ) + } + + // MARK: - Delete Image + + private func deleteImage(_ imageInfo: ImageInfo) async throws { + guard let itemID = item.id, + let imageType = imageInfo.imageType else { return } + + if let imageIndex = imageInfo.imageIndex { + let request = Paths.deleteItemImageByIndex( + itemID: itemID, + imageType: imageType.rawValue, + imageIndex: imageIndex + ) + + try await userSession.client.send(request) + } else { + let request = Paths.deleteItemImage( + itemID: itemID, + imageType: imageType.rawValue + ) + + try await userSession.client.send(request) + } + + try await getAllImages() + } + + // MARK: - Refresh Item + + private func refreshItem() async throws { + guard let itemID = item.id else { return } + + await MainActor.run { + _ = backgroundStates.append(.updating) + } + + 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(.updating) + Notifications[.itemMetadataDidChange].post(item) + } + } +} diff --git a/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift b/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift index bbd20192c..04844c1a1 100644 --- a/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift +++ b/Shared/ViewModels/ItemAdministration/RefreshMetadataViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift new file mode 100644 index 000000000..22e380a58 --- /dev/null +++ b/Shared/ViewModels/ItemAdministration/RemoteImageInfoViewModel.swift @@ -0,0 +1,64 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import JellyfinAPI + +class RemoteImageInfoViewModel: PagingLibraryViewModel { + + // Image providers come from the paging call + @Published + private(set) var providers: [String] = [] + + @Published + var includeAllLanguages: Bool = false { + didSet { + DispatchQueue.main.async { + self.send(.refresh) + } + } + } + + @Published + var provider: String? = nil { + didSet { + DispatchQueue.main.async { + self.send(.refresh) + } + } + } + + let imageType: ImageType + + init(imageType: ImageType, parent: BaseItemDto) { + + self.imageType = imageType + + super.init(parent: parent) + } + + override func get(page: Int) async throws -> [RemoteImageInfo] { + guard let itemID = parent?.id else { return [] } + + var parameters = Paths.GetRemoteImagesParameters() + parameters.isIncludeAllLanguages = includeAllLanguages + parameters.limit = pageSize + parameters.providerName = provider + parameters.startIndex = page * pageSize + parameters.type = imageType + + let request = Paths.getRemoteImages(itemID: itemID, parameters: parameters) + let response = try await userSession.client.send(request) + + await MainActor.run { + providers = response.value.providers ?? [] + } + + return response.value.images ?? [] + } +} diff --git a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift index 368af421c..d82c539b0 100644 --- a/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/CollectionItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift index 39454f150..c3384427d 100644 --- a/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/EpisodeItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift index cebab7706..a62ae0991 100644 --- a/Shared/ViewModels/ItemViewModel/ItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/ItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -14,6 +14,8 @@ import JellyfinAPI import OrderedCollections import UIKit +// TODO: come up with a cleaner, more defined way for item update notifications + class ItemViewModel: ViewModel, Stateful { // MARK: Action @@ -89,10 +91,10 @@ class ItemViewModel: ViewModel, Stateful { self.item = item super.init() - Notifications[.itemShouldRefresh] + Notifications[.itemShouldRefreshMetadata] .publisher - .sink { itemID, parentID in - guard itemID == self.item.id || parentID == self.item.id else { return } + .sink { itemID in + guard itemID == self.item.id else { return } Task { await self.send(.backgroundRefresh) @@ -141,9 +143,16 @@ class ItemViewModel: ViewModel, Stateful { await MainActor.run { self.backgroundStates.remove(.refresh) - self.item = results.fullItem + + // see TODO, as the item will be set in + // itemMetadataDidChange notification but + // is a bit redundant +// self.item = results.fullItem + self.similarItems = results.similarItems self.specialFeatures = results.specialFeatures + + Notifications[.itemMetadataDidChange].post(results.fullItem) } } catch { guard !Task.isCancelled else { return } @@ -332,7 +341,7 @@ class ItemViewModel: ViewModel, Stateful { } let _ = try await userSession.client.send(request) - Notifications[.itemShouldRefresh].post((itemID, nil)) + Notifications[.itemShouldRefreshMetadata].post(itemID) } private func setIsFavorite(_ isFavorite: Bool) async throws { diff --git a/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift index 4c8f340af..c2bf4c3e5 100644 --- a/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/MovieItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift index 0f9cb0bed..527992ccb 100644 --- a/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeasonItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift index bfed1f2cf..f275d6405 100644 --- a/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift +++ b/Shared/ViewModels/ItemViewModel/SeriesItemViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift index f4c422099..2ac76e410 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -56,9 +56,9 @@ final class ItemLibraryViewModel: PagingLibraryViewModel { var includeItemTypes: [BaseItemKind] = [.movie, .series, .boxSet] var isRecursive: Bool? = true - // TODO: determine `includeItemTypes` better - // - look at parent collection type if necessary - // - condense supported values + // TODO: this logic should be moved to a `LibraryParent` function + // that transforms a `GetItemsByUserIDParameters` struct, instead + // of having to do this case-by-case. if let libraryType = parent?.libraryType, let id = parent?.id { switch libraryType { diff --git a/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift index 0bf9b6c50..739544883 100644 --- a/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/ItemTypeLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -11,17 +11,28 @@ import Foundation import Get import JellyfinAPI -// TODO: atow, this is only really used for tvOS tabs +// TODO: filtering on `itemTypes` should be moved to `ItemFilterCollection`, +// but there is additional logic based on the parent type, mainly `.folder`. final class ItemTypeLibraryViewModel: PagingLibraryViewModel { let itemTypes: [BaseItemKind] - init(itemTypes: [BaseItemKind]) { + // MARK: Initializer + + init( + itemTypes: [BaseItemKind], + filters: ItemFilterCollection? = nil + ) { self.itemTypes = itemTypes - super.init() + super.init( + itemTypes: itemTypes, + filters: filters + ) } + // MARK: Get Page + override func get(page: Int) async throws -> [BaseItemDto] { let parameters = itemParameters(for: page) @@ -31,6 +42,8 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { return response.value.items ?? [] } + // MARK: Item Parameters + func itemParameters(for page: Int?) -> Paths.GetItemsByUserIDParameters { var parameters = Paths.GetItemsByUserIDParameters() @@ -38,8 +51,6 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { parameters.fields = .MinimumFields parameters.includeItemTypes = itemTypes parameters.isRecursive = true - parameters.sortBy = [ItemSortBy.name.rawValue] - parameters.sortOrder = [.ascending] // Page size if let page { @@ -47,6 +58,48 @@ final class ItemTypeLibraryViewModel: PagingLibraryViewModel { parameters.startIndex = page * pageSize } + // Filters + if let filterViewModel { + let filters = filterViewModel.currentFilters + parameters.filters = filters.traits + parameters.genres = filters.genres.map(\.value) + parameters.sortBy = filters.sortBy.map(\.rawValue) + parameters.sortOrder = filters.sortOrder + parameters.tags = filters.tags.map(\.value) + parameters.years = filters.years.compactMap { Int($0.value) } + + if filters.letter.first?.value == "#" { + parameters.nameLessThan = "A" + } else { + parameters.nameStartsWith = filters.letter + .map(\.value) + .filter { $0 != "#" } + .first + } + + // Random sort won't take into account previous items, so + // manual exclusion is necessary. This could possibly be + // a performance issue for loading pages after already loading + // many items, but there's nothing we can do about that. + if filters.sortBy.first == ItemSortBy.random { + parameters.excludeItemIDs = elements.compactMap(\.id) + } + } + return parameters } + + // MARK: Get Random Item + + override func getRandomItem() async -> BaseItemDto? { + + var parameters = itemParameters(for: nil) + parameters.limit = 1 + parameters.sortBy = [ItemSortBy.random.rawValue] + + let request = Paths.getItemsByUserID(userID: userSession.user.id, parameters: parameters) + let response = try? await userSession.client.send(request) + + return response?.value.items?.first + } } diff --git a/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift index dd7231950..d873709d5 100644 --- a/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/LatestInLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift index 4edcb87f3..e3320520e 100644 --- a/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/NextUpLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift index 1c6cdfc45..11c370288 100644 --- a/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/PagingLibraryViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -49,6 +49,8 @@ protocol LibraryIdentifiable: Identifiable { // on refresh. Should make bidirectional/offset index start? // - use startIndex/index ranges instead of pages // - source of data doesn't guarantee that all items in 0 ..< startIndex exist +// TODO: have `filterViewModel` be private to the parent and the `get_` overrides recieve the +// current filters as a parameter /* Note: if `rememberSort == true`, then will override given filters with stored sorts @@ -218,6 +220,52 @@ class PagingLibraryViewModel: ViewModel, Eventful, Stateful { } } + // paging item type + init( + itemTypes: [BaseItemKind], + filters: ItemFilterCollection? = nil, + pageSize: Int = DefaultPageSize + ) { + self.elements = IdentifiedArray([], id: \.unwrappedIDHashOrZero, uniquingIDsWith: { x, _ in x }) + self.isStatic = false + self.pageSize = pageSize + + self.parent = nil + + if let filters { + self.filterViewModel = .init( + itemTypes: itemTypes, + currentFilters: filters + ) + } else { + self.filterViewModel = nil + } + + super.init() + + Notifications[.didDeleteItem] + .publisher + .sink { id in + self.elements.remove(id: id.hashValue) + } + .store(in: &cancellables) + + if let filterViewModel { + filterViewModel.$currentFilters + .dropFirst() + .debounce(for: 1, scheduler: RunLoop.main) + .removeDuplicates() + .sink { [weak self] _ in + guard let self else { return } + + Task { @MainActor in + self.send(.refresh) + } + } + .store(in: &cancellables) + } + } + convenience init( title: String, id: String?, diff --git a/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift b/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift index 59347b782..6cf3d1573 100644 --- a/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift +++ b/Shared/ViewModels/LibraryViewModel/RecentlyAddedViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/LiveVideoPlayerManager.swift b/Shared/ViewModels/LiveVideoPlayerManager.swift index 1f2307706..5a0fce2d5 100644 --- a/Shared/ViewModels/LiveVideoPlayerManager.swift +++ b/Shared/ViewModels/LiveVideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/MediaViewModel/MediaType.swift b/Shared/ViewModels/MediaViewModel/MediaType.swift index b25bd02fe..98ffb78e3 100644 --- a/Shared/ViewModels/MediaViewModel/MediaType.swift +++ b/Shared/ViewModels/MediaViewModel/MediaType.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/MediaViewModel/MediaViewModel.swift b/Shared/ViewModels/MediaViewModel/MediaViewModel.swift index 6b9f188f6..903d0469c 100644 --- a/Shared/ViewModels/MediaViewModel/MediaViewModel.swift +++ b/Shared/ViewModels/MediaViewModel/MediaViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -154,6 +154,6 @@ final class MediaViewModel: ViewModel, Stateful { let response = try await userSession.client.send(request) return (response.value.items ?? []) - .map { $0.imageSource(.backdrop, maxWidth: 500) } + .map { $0.imageSource(.backdrop, maxWidth: 200) } } } diff --git a/Shared/ViewModels/ParentalRatingsViewModel.swift b/Shared/ViewModels/ParentalRatingsViewModel.swift index 4dd943c79..0c4e9b015 100644 --- a/Shared/ViewModels/ParentalRatingsViewModel.swift +++ b/Shared/ViewModels/ParentalRatingsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ProgramsViewModel.swift b/Shared/ViewModels/ProgramsViewModel.swift index 67c90d95c..64e661e0b 100644 --- a/Shared/ViewModels/ProgramsViewModel.swift +++ b/Shared/ViewModels/ProgramsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift b/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift index 293258fbe..9228842fb 100644 --- a/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift +++ b/Shared/ViewModels/QuickConnectAuthorizeViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ResetUserPasswordViewModel.swift b/Shared/ViewModels/ResetUserPasswordViewModel.swift index dd028605d..ce15b455f 100644 --- a/Shared/ViewModels/ResetUserPasswordViewModel.swift +++ b/Shared/ViewModels/ResetUserPasswordViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/SearchViewModel.swift b/Shared/ViewModels/SearchViewModel.swift index 3de164f9b..88f474e21 100644 --- a/Shared/ViewModels/SearchViewModel.swift +++ b/Shared/ViewModels/SearchViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/SelectUserViewModel.swift b/Shared/ViewModels/SelectUserViewModel.swift index 9997ba4ff..6fc803326 100644 --- a/Shared/ViewModels/SelectUserViewModel.swift +++ b/Shared/ViewModels/SelectUserViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/ServerCheckViewModel.swift b/Shared/ViewModels/ServerCheckViewModel.swift index a25963900..047e5e0c7 100644 --- a/Shared/ViewModels/ServerCheckViewModel.swift +++ b/Shared/ViewModels/ServerCheckViewModel.swift @@ -3,10 +3,11 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine +import Factory import Foundation import JellyfinAPI @@ -36,12 +37,15 @@ class ServerCheckViewModel: ViewModel, Stateful { // TODO: also server stuff connectCancellable = Task { do { + try await userSession.server.updateServerInfo() + let request = Paths.getCurrentUser let response = try await userSession.client.send(request) await MainActor.run { userSession.user.data = response.value self.state = .connected + Container.shared.currentUserSession.reset() } } catch { await MainActor.run { diff --git a/Shared/ViewModels/ServerConnectionViewModel.swift b/Shared/ViewModels/ServerConnectionViewModel.swift index 778e7e2f2..53977928b 100644 --- a/Shared/ViewModels/ServerConnectionViewModel.swift +++ b/Shared/ViewModels/ServerConnectionViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore diff --git a/Shared/ViewModels/ServerLogsViewModel.swift b/Shared/ViewModels/ServerLogsViewModel.swift index 86ef22f65..e63f837fc 100644 --- a/Shared/ViewModels/ServerLogsViewModel.swift +++ b/Shared/ViewModels/ServerLogsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 96295ee30..820f07516 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore @@ -15,7 +15,6 @@ import JellyfinAPI import UIKit // TODO: should probably break out into a `Settings` and `AppSettings` view models -// - don't need delete user profile image from app settings // - could clean up all settings view models final class SettingsViewModel: ViewModel { @@ -62,25 +61,6 @@ final class SettingsViewModel: ViewModel { } } - func deleteCurrentUserProfileImage() { - Task { - let request = Paths.deleteUserImage( - userID: userSession.user.id, - imageType: "Primary" - ) - let _ = try await userSession.client.send(request) - - let currentUserRequest = Paths.getCurrentUser - let response = try await userSession.client.send(currentUserRequest) - - await MainActor.run { - userSession.user.data = response.value - - Notifications[.didChangeUserProfileImage].post() - } - } - } - func select(icon: any AppIcon) { let previousAppIcon = currentAppIcon currentAppIcon = icon diff --git a/Shared/ViewModels/UserLocalSecurityViewModel.swift b/Shared/ViewModels/UserLocalSecurityViewModel.swift index 5de3f2387..422027579 100644 --- a/Shared/ViewModels/UserLocalSecurityViewModel.swift +++ b/Shared/ViewModels/UserLocalSecurityViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/UserProfileImageViewModel.swift b/Shared/ViewModels/UserProfileImageViewModel.swift index 5e3cdf50f..bcdff63e1 100644 --- a/Shared/ViewModels/UserProfileImageViewModel.swift +++ b/Shared/ViewModels/UserProfileImageViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -14,51 +14,77 @@ import UIKit class UserProfileImageViewModel: ViewModel, Eventful, Stateful { + // MARK: - Action + enum Action: Equatable { case cancel + case delete case upload(UIImage) } + // MARK: - Event + enum Event: Hashable { case error(JellyfinAPIError) + case deleted case uploaded } + var events: AnyPublisher { + eventSubject + .receive(on: RunLoop.main) + .eraseToAnyPublisher() + } + + // MARK: - State + enum State: Hashable { case initial + case deleting case uploading } @Published var state: State = .initial - var events: AnyPublisher { - eventSubject - .receive(on: RunLoop.main) - .eraseToAnyPublisher() - } + // MARK: - Published Values + + let user: UserDto + + // MARK: - Task Variables private var eventSubject: PassthroughSubject = .init() private var uploadCancellable: AnyCancellable? + // MARK: - Initializer + + init(user: UserDto) { + self.user = user + } + + // MARK: - Respond to Action + func respond(to action: Action) -> State { switch action { case .cancel: uploadCancellable?.cancel() - return .initial - case let .upload(image): + case let .upload(image): uploadCancellable = Task { do { - try await upload(image: image) + await MainActor.run { + self.state = .uploading + } + + try await upload(image) await MainActor.run { self.eventSubject.send(.uploaded) self.state = .initial } } catch is CancellationError { - // cancel doesn't matter + // Cancel doesn't matter } catch { await MainActor.run { self.eventSubject.send(.error(.init(error.localizedDescription))) @@ -68,11 +94,41 @@ class UserProfileImageViewModel: ViewModel, Eventful, Stateful { } .asAnyCancellable() - return .uploading + return state + + case .delete: + uploadCancellable = Task { + do { + await MainActor.run { + self.state = .deleting + } + + try await delete() + + await MainActor.run { + self.eventSubject.send(.deleted) + self.state = .initial + } + } catch is CancellationError { + // Cancel doesn't matter + } catch { + await MainActor.run { + self.eventSubject.send(.error(.init(error.localizedDescription))) + self.state = .initial + } + } + } + .asAnyCancellable() + + return state } } - private func upload(image: UIImage) async throws { + // MARK: - Upload Image + + private func upload(_ image: UIImage) async throws { + + guard let userID = user.id else { return } let contentType: String let imageData: Data @@ -89,21 +145,60 @@ class UserProfileImageViewModel: ViewModel, Eventful, Stateful { } var request = Paths.postUserImage( - userID: userSession.user.id, + userID: userID, imageType: "Primary", imageData ) request.headers = ["Content-Type": contentType] + guard imageData.count <= 30_000_000 else { + throw JellyfinAPIError( + "This profile image is too large (\(imageData.count.formatted(.byteCount(style: .file)))). The upload limit for images is 30 MB." + ) + } + let _ = try await userSession.client.send(request) - let currentUserRequest = Paths.getCurrentUser - let response = try await userSession.client.send(currentUserRequest) + sweepProfileImageCache() await MainActor.run { - userSession.user.data = response.value + Notifications[.didChangeUserProfile].post(userID) + } + } + + // MARK: - Delete Image + + private func delete() async throws { + + guard let userID = user.id else { return } + + let request = Paths.deleteUserImage( + userID: userID, + imageType: "Primary" + ) + let _ = try await userSession.client.send(request) + + sweepProfileImageCache() + + await MainActor.run { + Notifications[.didChangeUserProfile].post(userID) + } + } + + private func sweepProfileImageCache() { + if let userImageURL = user.profileImageSource(client: userSession.client, maxWidth: 60).url { + ImagePipeline.Swiftfin.local.removeItem(for: userImageURL) + ImagePipeline.Swiftfin.posters.removeItem(for: userImageURL) + } + + if let userImageURL = user.profileImageSource(client: userSession.client, maxWidth: 120).url { + ImagePipeline.Swiftfin.local.removeItem(for: userImageURL) + ImagePipeline.Swiftfin.posters.removeItem(for: userImageURL) + } - Notifications[.didChangeUserProfileImage].post() + if let userImageURL = user.profileImageSource(client: userSession.client, maxWidth: 150).url { + ImagePipeline.Swiftfin.local.removeItem(for: userImageURL) + ImagePipeline.Swiftfin.posters.removeItem(for: userImageURL) } } } diff --git a/Shared/ViewModels/UserSignInViewModel.swift b/Shared/ViewModels/UserSignInViewModel.swift index 63138fba5..61903be2b 100644 --- a/Shared/ViewModels/UserSignInViewModel.swift +++ b/Shared/ViewModels/UserSignInViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift index 82eab8b8f..d1fdd7588 100644 --- a/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/DownloadVideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift index a50eb0d99..89c3f22f5 100644 --- a/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/OnlineVideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift b/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift index 006bc0e9d..9f0482256 100644 --- a/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift +++ b/Shared/ViewModels/VideoPlayerManager/VideoPlayerManager.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVFoundation diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index e3636104f..af9957b91 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Shared/ViewModels/ViewModel.swift b/Shared/ViewModels/ViewModel.swift index c00c3e451..fc614814c 100644 --- a/Shared/ViewModels/ViewModel.swift +++ b/Shared/ViewModels/ViewModel.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift index c459d3d58..00c64838e 100644 --- a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift +++ b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingController.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // TODO: IMPLEMENT BUTTON OVERRIDES IN `PreferencesView` PACKAGE diff --git a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift index f4231d8b2..feafce530 100644 --- a/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift +++ b/Swiftfin tvOS/App/PreferenceUIHosting/PreferenceUIHostingSwizzling.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // TODO: IMPLEMENT BUTTON OVERRIDES IN `PreferencesView` PACKAGE diff --git a/Swiftfin tvOS/App/SwiftfinApp.swift b/Swiftfin tvOS/App/SwiftfinApp.swift index 26eb28c2d..361cd2f07 100644 --- a/Swiftfin tvOS/App/SwiftfinApp.swift +++ b/Swiftfin tvOS/App/SwiftfinApp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore @@ -47,7 +47,7 @@ struct SwiftfinApp: App { return mimeType.contains("svg") ? ImageDecoders.Empty() : nil } - ImagePipeline.shared = .Swiftfin.default + ImagePipeline.shared = .Swiftfin.posters // UIKit diff --git a/Swiftfin tvOS/Components/CinematicBackgroundView.swift b/Swiftfin tvOS/Components/CinematicBackgroundView.swift index 23606d345..a224c9758 100644 --- a/Swiftfin tvOS/Components/CinematicBackgroundView.swift +++ b/Swiftfin tvOS/Components/CinematicBackgroundView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/Components/CinematicItemSelector.swift b/Swiftfin tvOS/Components/CinematicItemSelector.swift index 83ad76709..31c1bb222 100644 --- a/Swiftfin tvOS/Components/CinematicItemSelector.swift +++ b/Swiftfin tvOS/Components/CinematicItemSelector.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin tvOS/Components/DotHStack.swift b/Swiftfin tvOS/Components/DotHStack.swift index 191955931..6cb39feb3 100644 --- a/Swiftfin tvOS/Components/DotHStack.swift +++ b/Swiftfin tvOS/Components/DotHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/EnumPickerView.swift b/Swiftfin tvOS/Components/EnumPickerView.swift index 5ea8fcea3..87640f8ab 100644 --- a/Swiftfin tvOS/Components/EnumPickerView.swift +++ b/Swiftfin tvOS/Components/EnumPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/FullScreenMenu.swift b/Swiftfin tvOS/Components/FullScreenMenu.swift deleted file mode 100644 index d035fc5f8..000000000 --- a/Swiftfin tvOS/Components/FullScreenMenu.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import SwiftUI - -struct FullScreenMenu: View { - - private let content: () -> Content - private let title: String - - init(_ title: String, @ViewBuilder content: @escaping () -> Content) { - self.title = title - self.content = content - } - - var body: some View { - ZStack { - Color.black - .opacity(0.5) - - HStack { - Spacer() - - VStack { - Text(title) - .font(.title2) - .fontWeight(.bold) - - ScrollView { - VStack { - content() - } - .padding(.horizontal, 20) - } - .frame(width: 580) - } - .padding(.top, 20) - .background(Material.regular, in: RoundedRectangle(cornerRadius: 30)) - .frame(width: 620) - .padding(100) - .shadow(radius: 50) - } - } - .ignoresSafeArea() - } -} diff --git a/Swiftfin tvOS/Components/InlineEnumToggle.swift b/Swiftfin tvOS/Components/InlineEnumToggle.swift index 18f24c7a7..c38fbd473 100644 --- a/Swiftfin tvOS/Components/InlineEnumToggle.swift +++ b/Swiftfin tvOS/Components/InlineEnumToggle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift b/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift index be5e9e0c2..c66ca9504 100644 --- a/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift +++ b/Swiftfin tvOS/Components/LandscapePosterProgressBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/ListRowButton.swift b/Swiftfin tvOS/Components/ListRowButton.swift index 0c96b5b4a..7b964e3d8 100644 --- a/Swiftfin tvOS/Components/ListRowButton.swift +++ b/Swiftfin tvOS/Components/ListRowButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -11,10 +11,12 @@ import SwiftUI struct ListRowButton: View { let title: String + let role: ButtonRole? let action: () -> Void - init(_ title: String, action: @escaping () -> Void) { + init(_ title: String, role: ButtonRole? = nil, action: @escaping () -> Void) { self.title = title + self.role = role self.action = action } @@ -23,15 +25,33 @@ struct ListRowButton: View { action() } label: { ZStack { - Rectangle() - .foregroundStyle(.secondary) + RoundedRectangle(cornerRadius: 10) + .fill(secondaryStyle) Text(title) + .foregroundStyle(primaryStyle) .font(.body.weight(.bold)) - .foregroundStyle(.primary) } } .buttonStyle(.card) .frame(height: 75) } + + // MARK: - Styles + + private var primaryStyle: some ShapeStyle { + if role == .destructive { + return AnyShapeStyle(Color.red) + } else { + return AnyShapeStyle(.primary) + } + } + + private var secondaryStyle: some ShapeStyle { + if role == .destructive { + return AnyShapeStyle(Color.red.opacity(0.2)) + } else { + return AnyShapeStyle(.secondary) + } + } } diff --git a/Swiftfin tvOS/Components/NonePosterButton.swift b/Swiftfin tvOS/Components/NonePosterButton.swift index 33629f022..e533275ad 100644 --- a/Swiftfin tvOS/Components/NonePosterButton.swift +++ b/Swiftfin tvOS/Components/NonePosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift b/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift index cc9effe64..9b958c649 100644 --- a/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift +++ b/Swiftfin tvOS/Components/OrderedSectionSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory @@ -64,13 +64,13 @@ struct OrderedSectionSelectorView: View { Text(L10n.enabled) Spacer() if editMode?.wrappedValue.isEditing ?? false { - Button("Done") { + Button(L10n.done) { withAnimation { editMode?.wrappedValue = .inactive } } } else { - Button("Edit") { + Button(L10n.edit) { withAnimation { editMode?.wrappedValue = .active } diff --git a/Swiftfin tvOS/Components/PosterButton.swift b/Swiftfin tvOS/Components/PosterButton.swift index 1b977d7db..6ad58f255 100644 --- a/Swiftfin tvOS/Components/PosterButton.swift +++ b/Swiftfin tvOS/Components/PosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Components/PosterHStack.swift b/Swiftfin tvOS/Components/PosterHStack.swift index 10af0e780..244b1c48a 100644 --- a/Swiftfin tvOS/Components/PosterHStack.swift +++ b/Swiftfin tvOS/Components/PosterHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin tvOS/Components/SFSymbolButton.swift b/Swiftfin tvOS/Components/SFSymbolButton.swift index 40b47a4ac..523307a29 100644 --- a/Swiftfin tvOS/Components/SFSymbolButton.swift +++ b/Swiftfin tvOS/Components/SFSymbolButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/SeeAllPosterButton.swift b/Swiftfin tvOS/Components/SeeAllPosterButton.swift index 947de6e82..11617446e 100644 --- a/Swiftfin tvOS/Components/SeeAllPosterButton.swift +++ b/Swiftfin tvOS/Components/SeeAllPosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/ServerButton.swift b/Swiftfin tvOS/Components/ServerButton.swift index ef7f7653f..4565e0227 100644 --- a/Swiftfin tvOS/Components/ServerButton.swift +++ b/Swiftfin tvOS/Components/ServerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/SplitFormWindowView.swift b/Swiftfin tvOS/Components/SplitFormWindowView.swift index 20fb319c9..3d44c38cc 100644 --- a/Swiftfin tvOS/Components/SplitFormWindowView.swift +++ b/Swiftfin tvOS/Components/SplitFormWindowView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Components/SplitLoginWindowView.swift b/Swiftfin tvOS/Components/SplitLoginWindowView.swift new file mode 100644 index 000000000..dfdc7e915 --- /dev/null +++ b/Swiftfin tvOS/Components/SplitLoginWindowView.swift @@ -0,0 +1,94 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Foundation +import SwiftUI + +struct SplitLoginWindowView: View { + + // MARK: - Loading State + + private let isLoading: Bool + + // MARK: - Content Variables + + private let leadingTitle: String + private let leadingContentView: () -> Leading + private let trailingTitle: String + private let trailingContentView: () -> Trailing + + // MARK: - Background Variable + + private let backgroundImageSource: ImageSource? + + // MARK: - Body + + var body: some View { + HStack(alignment: .top) { + VStack(alignment: .leading) { + Section(leadingTitle) { + VStack(alignment: .leading) { + leadingContentView() + .eraseToAnyView() + } + .frame(maxWidth: .infinity) + .padding(.vertical) + } + } + + Divider() + .padding(.vertical, 100) + + VStack(alignment: .leading) { + Section(trailingTitle) { + VStack(alignment: .leading) { + trailingContentView() + .eraseToAnyView() + } + .frame(maxWidth: .infinity) + .padding(.vertical) + } + } + } + .navigationBarBranding(isLoading: isLoading) + .background { + if let backgroundImageSource { + ZStack { + ImageView(backgroundImageSource) + .aspectRatio(contentMode: .fill) + .id(backgroundImageSource) + .transition(.opacity) + .animation(.linear, value: backgroundImageSource) + + Color.black + .opacity(0.9) + } + .ignoresSafeArea() + } + } + } +} + +extension SplitLoginWindowView { + + init( + isLoading: Bool = false, + leadingTitle: String, + trailingTitle: String, + backgroundImageSource: ImageSource? = nil, + @ViewBuilder leadingContentView: @escaping () -> Leading, + @ViewBuilder trailingContentView: @escaping () -> Trailing + ) { + self.isLoading = isLoading + self.leadingTitle = leadingTitle + self.trailingTitle = trailingTitle + self.leadingContentView = leadingContentView + self.trailingContentView = trailingContentView + self.backgroundImageSource = backgroundImageSource + } +} diff --git a/Swiftfin tvOS/Components/StepperView.swift b/Swiftfin tvOS/Components/StepperView.swift index 945b38921..3c7492d26 100644 --- a/Swiftfin tvOS/Components/StepperView.swift +++ b/Swiftfin tvOS/Components/StepperView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift b/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.swift new file mode 100644 index 000000000..75318c5f4 --- /dev/null +++ b/Swiftfin tvOS/Extensions/View/Modifiers/NavigationBarMenuButton.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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct NavigationBarBrandingModifier: ViewModifier { + + let isLoading: Bool + + func body(content: Content) -> some View { + content + .toolbar { + ToolbarItem(placement: .principal) { + Image(.jellyfinBlobBlue) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 100) + .padding(.bottom, 25) + } + + if isLoading { + ToolbarItem(placement: .topBarTrailing) { + ProgressView() + } + } + } + } +} diff --git a/Swiftfin tvOS/Extensions/View/View-tvOS.swift b/Swiftfin tvOS/Extensions/View/View-tvOS.swift new file mode 100644 index 000000000..233da0c3b --- /dev/null +++ b/Swiftfin tvOS/Extensions/View/View-tvOS.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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI +import SwiftUIIntrospect + +extension View { + + @ViewBuilder + func navigationBarBranding( + isLoading: Bool = false + ) -> some View { + modifier( + NavigationBarBrandingModifier( + isLoading: isLoading + ) + ) + } +} diff --git a/Swiftfin tvOS/ImageButtonStyle.swift b/Swiftfin tvOS/ImageButtonStyle.swift index 1b9d16eba..202564441 100644 --- a/Swiftfin tvOS/ImageButtonStyle.swift +++ b/Swiftfin tvOS/ImageButtonStyle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // struct ImageButtonStyle: ButtonStyle { diff --git a/Swiftfin tvOS/Objects/FocusGuide.swift b/Swiftfin tvOS/Objects/FocusGuide.swift index 027554911..00c9014ba 100644 --- a/Swiftfin tvOS/Objects/FocusGuide.swift +++ b/Swiftfin tvOS/Objects/FocusGuide.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/AppLoadingView.swift b/Swiftfin tvOS/Views/AppLoadingView.swift index 008752333..1fcdc22ce 100644 --- a/Swiftfin tvOS/Views/AppLoadingView.swift +++ b/Swiftfin tvOS/Views/AppLoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/BasicAppSettingsView.swift b/Swiftfin tvOS/Views/BasicAppSettingsView.swift index 2ea0642a8..3cd265ab5 100644 --- a/Swiftfin tvOS/Views/BasicAppSettingsView.swift +++ b/Swiftfin tvOS/Views/BasicAppSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -51,7 +51,7 @@ struct AppSettingsView: View { // ) // } // -// ChevronButton("Logs") +// ChevronButton(L10n.logs) // .onSelect { // router.route(to: \.log) // } @@ -68,7 +68,7 @@ struct AppSettingsView: View { // Button { // removeAllServersSelected = true // } label: { -// Text("Remove All Servers") +// Text(L10n.removeAllServers) // } // } // } @@ -79,9 +79,9 @@ struct AppSettingsView: View { //// viewModel.resetUserSettings() // } // } message: { -// Text("Reset all settings back to defaults.") +// Text(L10n.resetAllSettings) // } -// .alert("Remove All Servers", isPresented: $removeAllServersSelected) { +// .alert(L10n.removeAllServers, isPresented: $removeAllServersSelected) { // Button(L10n.reset, role: .destructive) { //// viewModel.removeAllServers() // } diff --git a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift index f8cae264c..8a2ad4c48 100644 --- a/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin tvOS/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift b/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift index eb0741e33..997b58a3e 100644 --- a/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift +++ b/Swiftfin tvOS/Views/ChannelLibraryView/Components/WideChannelGridItem.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift b/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift new file mode 100644 index 000000000..11c08d6ae --- /dev/null +++ b/Swiftfin tvOS/Views/ConnectToServerView/Components/LocalServerButton.swift @@ -0,0 +1,62 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import SwiftUI + +extension ConnectToServerView { + + struct LocalServerButton: View { + + // MARK: - Environment Variables + + @Environment(\.isEnabled) + private var isEnabled: Bool + + // MARK: - Local Server Variables + + private let server: ServerState + private let action: () -> Void + + // MARK: - Initializer + + init(server: ServerState, action: @escaping () -> Void) { + self.server = server + self.action = action + } + + // MARK: - Local Server Button + + var body: some View { + Button(action: action) { + HStack { + VStack(alignment: .leading) { + Text(server.name) + .font(.headline) + .fontWeight(.semibold) + + Text(server.currentURL.absoluteString) + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.body.weight(.regular)) + .foregroundStyle(.secondary) + } + .padding() + } + .disabled(!isEnabled) + .buttonStyle(.card) + .foregroundStyle(.primary) + } + } +} diff --git a/Swiftfin tvOS/Views/ConnectToServerView.swift b/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift similarity index 52% rename from Swiftfin tvOS/Views/ConnectToServerView.swift rename to Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift index d9c1793e5..da2f10e81 100644 --- a/Swiftfin tvOS/Views/ConnectToServerView.swift +++ b/Swiftfin tvOS/Views/ConnectToServerView/ConnectToServerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -55,82 +55,56 @@ struct ConnectToServerView: View { @ViewBuilder private var connectSection: some View { - Section(L10n.connectToServer) { - TextField(L10n.serverURL, text: $url) - .disableAutocorrection(true) - .textInputAutocapitalization(.never) - .keyboardType(.URL) - .focused($isURLFocused) - } + TextField(L10n.serverURL, text: $url) + .disableAutocorrection(true) + .textInputAutocapitalization(.never) + .keyboardType(.URL) + .focused($isURLFocused) if viewModel.state == .connecting { -// ListRowButton(L10n.cancel) { -// viewModel.send(.cancel) -// } - Button(L10n.cancel) { + ListRowButton(L10n.cancel) { viewModel.send(.cancel) } - .foregroundStyle(.red, .red.opacity(0.2)) + .foregroundStyle(.red, accentColor) + .padding(.vertical) } else { -// ListRowButton(L10n.connect) { -// isURLFocused = false -// viewModel.send(.connect(url)) -// } - Button(L10n.connect) { + ListRowButton(L10n.connect) { isURLFocused = false viewModel.send(.connect(url)) } .disabled(url.isEmpty) .foregroundStyle( accentColor.overlayColor, - accentColor + url.isEmpty ? Color.white.opacity(0.5) : accentColor ) .opacity(url.isEmpty ? 0.5 : 1) + .padding(.vertical) } } - // MARK: - Local Server Button - - private func localServerButton(for server: ServerState) -> some View { - Button { - url = server.currentURL.absoluteString - viewModel.send(.connect(server.currentURL.absoluteString)) - } label: { - HStack { - VStack(alignment: .leading) { - Text(server.name) - .font(.headline) - .fontWeight(.semibold) - - Text(server.currentURL.absoluteString) - .font(.subheadline) - .foregroundColor(.secondary) - } - - Spacer() - - Image(systemName: "chevron.right") - .font(.body.weight(.regular)) - .foregroundColor(.secondary) - } - } - .disabled(viewModel.state == .connecting) - .buttonStyle(.plain) - } - // MARK: - Local Servers Section @ViewBuilder private var localServersSection: some View { - Section(L10n.localServers) { - if viewModel.localServers.isEmpty { - L10n.noLocalServersFound.text - .font(.callout) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - } else { - ForEach(viewModel.localServers) { server in - localServerButton(for: server) + if viewModel.localServers.isEmpty { + L10n.noLocalServersFound.text + .font(.callout) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity) + } else { + LazyVGrid( + columns: Array(repeating: GridItem(.flexible()), count: 1), + spacing: 30 + ) { + ForEach(viewModel.localServers, id: \.id) { server in + LocalServerButton(server: server) { + url = server.currentURL.absoluteString + viewModel.send(.connect(server.currentURL.absoluteString)) + } + .environment( + \.isEnabled, + viewModel.state != .connecting && server.currentURL.absoluteString != url + ) } } } @@ -139,34 +113,14 @@ struct ConnectToServerView: View { // MARK: - Body var body: some View { - VStack { - HStack { - Spacer() - - if viewModel.state == .connecting { - ProgressView() - } - } - .frame(height: 100) - .overlay { - Image(.jellyfinBlobBlue) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 100) - .edgePadding() - } - - HStack(alignment: .top) { - VStack(alignment: .leading) { - connectSection - } - - VStack(alignment: .leading) { - localServersSection - } - } - - Spacer() + SplitLoginWindowView( + isLoading: viewModel.state == .connecting, + leadingTitle: L10n.connectToServer, + trailingTitle: L10n.localServers + ) { + connectSection + } trailingContentView: { + localServersSection } .onFirstAppear { isURLFocused = true diff --git a/Swiftfin tvOS/Views/FontPickerView.swift b/Swiftfin tvOS/Views/FontPickerView.swift index 8b76365cc..c82ed374f 100644 --- a/Swiftfin tvOS/Views/FontPickerView.swift +++ b/Swiftfin tvOS/Views/FontPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift index 5bd3a6f56..7b5036343 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/CinematicRecentlyAddedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift b/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift index 5609c8b1a..a8fc2b2c5 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/CinematicResumeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift index 84b2cd167..a4b36ce3c 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/LatestInLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift index 5e14053b8..b8a8209da 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/NextUpView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift index 3d627b332..cdd2d1502 100644 --- a/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin tvOS/Views/HomeView/Components/RecentlyAddedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift index 428f3cdc1..b7d62961a 100644 --- a/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift +++ b/Swiftfin tvOS/Views/HomeView/HomeErrorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/HomeView/HomeView.swift b/Swiftfin tvOS/Views/HomeView/HomeView.swift index 16e4919c3..5cfb0b65b 100644 --- a/Swiftfin tvOS/Views/HomeView/HomeView.swift +++ b/Swiftfin tvOS/Views/HomeView/HomeView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemOverviewView.swift b/Swiftfin tvOS/Views/ItemOverviewView.swift index 7b918f2c2..8cd80faf2 100644 --- a/Swiftfin tvOS/Views/ItemOverviewView.swift +++ b/Swiftfin tvOS/Views/ItemOverviewView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift index 37f06a662..04cf0ec8f 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicCollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift index f2074d012..1101fd285 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicEpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift index 439a15d4d..cfec802e3 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicItemAboutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift b/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift index ab1489204..8d01a060f 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicItemViewTopRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift b/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift index 192279eb6..1bc83e6af 100644 --- a/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CinematicSeasonItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift index 5209c5bed..2652597ac 100644 --- a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift index 15fcaf241..cfe932b8a 100644 --- a/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/CollectionItemView/CollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift index be9192504..b84a0fe26 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/AboutView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift index 8c1cb7e2f..ed2b25612 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/AboutViewCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift index 24434ce5e..326dd52c1 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift index 36433dee9..f5d259256 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/OverviewCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift index 86ae5460d..e7ea09a3c 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AboutView/Components/RatingsCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift index dafee4615..0660a6f34 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift index 4a31af6a9..26a35e408 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionButtonHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift index ff66b787b..1d939f635 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/ActionMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift index 6f82251ca..a8328b88d 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/ActionButtons/RefreshMetadataButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift index 891707aa6..d553901bd 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/AttributeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift index c957ca372..e976b233b 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/CastAndCrewHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift index 80c08508c..7ad6e88c5 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift index ec4ee0d77..4352bcc87 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index 2cf9f967c..d5302d846 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift index 4b3083866..30b57b911 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift index a9be264d6..6a9072697 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index f0071d025..f027096bf 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack @@ -50,7 +50,7 @@ struct SeriesEpisodeSelector: View { selection = viewModel.seasons.first?.id } } - .onChange(of: selection) { _ in + .onChange(of: selection) { _, _ in guard let selectionViewModel else { return } if selectionViewModel.state == .initial { diff --git a/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift b/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift index e727bfc77..c71d05384 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/PlayButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift index 992985e5c..f6e49a756 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/SimilarItemsHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift b/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift index d3abbf7d0..595a7b0c2 100644 --- a/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift +++ b/Swiftfin tvOS/Views/ItemView/Components/SpecialFeaturesHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -20,7 +20,7 @@ extension ItemView { var body: some View { PosterHStack( - title: "Special Features", + title: L10n.specialFeatures, type: .landscape, items: items ) diff --git a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift index f9adec4c0..4bfdf9331 100644 --- a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift index ace4a254c..e541bc39e 100644 --- a/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/EpisodeItemView/EpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/ItemView.swift b/Swiftfin tvOS/Views/ItemView/ItemView.swift index 2a736e5bd..cce801081 100644 --- a/Swiftfin tvOS/Views/ItemView/ItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/ItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift index ca265576b..772216600 100644 --- a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift index 98bfa64e6..820a4ba6f 100644 --- a/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/MovieItemView/MovieItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift index d1ea1e9a4..33384541f 100644 --- a/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin tvOS/Views/ItemView/ScrollViews/CinematicScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift index 8a31864df..324ca87bc 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -37,6 +37,10 @@ extension SeriesItemView { ItemView.CastAndCrewHStack(people: castAndCrew) } + if viewModel.specialFeatures.isNotEmpty { + ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) + } + if viewModel.similarItems.isNotEmpty { ItemView.SimilarItemsHStack(items: viewModel.similarItems) } diff --git a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift index c1eee1093..745c4c396 100644 --- a/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift +++ b/Swiftfin tvOS/Views/ItemView/SeriesItemView/SeriesItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/LearnMoreModal.swift b/Swiftfin tvOS/Views/LearnMoreModal.swift index ae551b31a..6639e4c08 100644 --- a/Swiftfin tvOS/Views/LearnMoreModal.swift +++ b/Swiftfin tvOS/Views/LearnMoreModal.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/MediaSourceInfoView.swift b/Swiftfin tvOS/Views/MediaSourceInfoView.swift index d449c996a..00cc32b10 100644 --- a/Swiftfin tvOS/Views/MediaSourceInfoView.swift +++ b/Swiftfin tvOS/Views/MediaSourceInfoView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift b/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift index a5534e5d9..a8457adf1 100644 --- a/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift +++ b/Swiftfin tvOS/Views/MediaView/Components/MediaItem.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/MediaView/MediaView.swift b/Swiftfin tvOS/Views/MediaView/MediaView.swift index 6142869a8..961f95b3a 100644 --- a/Swiftfin tvOS/Views/MediaView/MediaView.swift +++ b/Swiftfin tvOS/Views/MediaView/MediaView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift b/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift index 34939e5a4..f7236fd43 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/Components/LibraryRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift b/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift index 75348c656..8c8882d0b 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/Components/ListRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift index d1a8d1531..b319408a1 100644 --- a/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin tvOS/Views/PagingLibraryView/PagingLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid @@ -12,32 +12,45 @@ import JellyfinAPI import SwiftUI // TODO: Figure out proper tab bar handling with the collection offset -// TODO: list columns -// TODO: list row view (LibraryRow) // TODO: fix paging for next item focusing the tab struct PagingLibraryView: View { @Default(.Customization.Library.cinematicBackground) private var cinematicBackground - @Default(.Customization.Library.posterType) - private var posterType - @Default(.Customization.Library.displayType) - private var viewType - @Default(.Customization.showPosterLabels) - private var showPosterLabels + @Default(.Customization.Library.enabledDrawerFilters) + private var enabledDrawerFilters + @Default(.Customization.Library.rememberLayout) + private var rememberLayout + + @Default + private var defaultDisplayType: LibraryDisplayType + @Default + private var defaultListColumnCount: Int + @Default + private var defaultPosterType: PosterDisplayType @EnvironmentObject private var router: LibraryCoordinator.Router @State private var focusedItem: Element? - @State private var presentBackground = false @State private var layout: CollectionVGridLayout + @State + private var safeArea: EdgeInsets = .zero + @StoredValue + private var displayType: LibraryDisplayType + @StoredValue + private var listColumnCount: Int + @StoredValue + private var posterType: PosterDisplayType + + @StateObject + private var collectionVGridProxy: CollectionVGridProxy = .init() @StateObject private var viewModel: PagingLibraryViewModel @@ -45,22 +58,33 @@ struct PagingLibraryView: View { private var cinematicBackgroundViewModel: CinematicBackgroundView.ViewModel = .init() init(viewModel: PagingLibraryViewModel) { + + self._defaultDisplayType = Default(.Customization.Library.displayType) + self._defaultListColumnCount = Default(.Customization.Library.listColumnCount) + self._defaultPosterType = Default(.Customization.Library.posterType) + + self._displayType = StoredValue(.User.libraryDisplayType(parentID: viewModel.parent?.id)) + self._listColumnCount = StoredValue(.User.libraryListColumnCount(parentID: viewModel.parent?.id)) + self._posterType = StoredValue(.User.libraryPosterType(parentID: viewModel.parent?.id)) + self._viewModel = StateObject(wrappedValue: viewModel) - let initialPosterType = Defaults[.Customization.Library.posterType] - let initialViewType = Defaults[.Customization.Library.displayType] - let listColumnCount = Defaults[.Customization.Library.listColumnCount] + let initialDisplayType = Defaults[.Customization.Library.rememberLayout] ? _displayType.wrappedValue : _defaultDisplayType + .wrappedValue + let initialListColumnCount = Defaults[.Customization.Library.rememberLayout] ? _listColumnCount + .wrappedValue : _defaultListColumnCount.wrappedValue + let initialPosterType = Defaults[.Customization.Library.rememberLayout] ? _posterType.wrappedValue : _defaultPosterType.wrappedValue self._layout = State( initialValue: Self.makeLayout( posterType: initialPosterType, - displayType: initialViewType, - listColumnCount: listColumnCount + viewType: initialDisplayType, + listColumnCount: initialListColumnCount ) ) } - // MARK: onSelect + // MARK: On Select private func onSelect(_ element: Element) { switch element { @@ -73,29 +97,36 @@ struct PagingLibraryView: View { } } + // MARK: Select Item + private func select(item: BaseItemDto) { switch item.type { case .collectionFolder, .folder: let viewModel = ItemLibraryViewModel(parent: item) router.route(to: \.library, viewModel) + case .person: + let viewModel = ItemLibraryViewModel(parent: item) + router.route(to: \.library, viewModel) default: router.route(to: \.item, item) } } + // MARK: Select Person + private func select(person: BaseItemPerson) { let viewModel = ItemLibraryViewModel(parent: person) router.route(to: \.library, viewModel) } - // MARK: layout + // MARK: Make Layout private static func makeLayout( posterType: PosterDisplayType, - displayType: LibraryDisplayType, + viewType: LibraryDisplayType, listColumnCount: Int ) -> CollectionVGridLayout { - switch (posterType, displayType) { + switch (posterType, viewType) { case (.landscape, .grid): return .columns(5, insets: .init(50), itemSpacing: 50, lineSpacing: 50) case (.portrait, .grid): @@ -105,6 +136,47 @@ struct PagingLibraryView: View { } } + // MARK: Set Default Layout + + private func setDefaultLayout() { + layout = Self.makeLayout( + posterType: defaultPosterType, + viewType: defaultDisplayType, + listColumnCount: defaultListColumnCount + ) + } + + // MARK: Set Custom Layout + + private func setCustomLayout() { + layout = Self.makeLayout( + posterType: posterType, + viewType: displayType, + listColumnCount: listColumnCount + ) + } + + // MARK: Set Cinematic Background + + private func setCinematicBackground() { + guard let focusedItem else { + withAnimation { + presentBackground = false + } + return + } + + cinematicBackgroundViewModel.select(item: focusedItem) + + if !presentBackground { + withAnimation { + presentBackground = true + } + } + } + + // MARK: Landscape Grid Item View + private func landscapeGridItemView(item: Element) -> some View { PosterButton(item: item, type: .landscape) .content { @@ -112,6 +184,11 @@ struct PagingLibraryView: View { PosterButton.TitleContentView(item: item) .backport .lineLimit(1, reservesSpace: true) + } else if viewModel.parent?.libraryType == .folder { + PosterButton.TitleContentView(item: item) + .backport + .lineLimit(1, reservesSpace: true) + .hidden() } } .onFocusChanged { newValue in @@ -124,6 +201,9 @@ struct PagingLibraryView: View { } } + // MARK: Portrait Grid Item View + + @ViewBuilder private func portraitGridItemView(item: Element) -> some View { PosterButton(item: item, type: .portrait) .content { @@ -131,6 +211,11 @@ struct PagingLibraryView: View { PosterButton.TitleContentView(item: item) .backport .lineLimit(1, reservesSpace: true) + } else if viewModel.parent?.libraryType == .folder { + PosterButton.TitleContentView(item: item) + .backport + .lineLimit(1, reservesSpace: true) + .hidden() } } .onFocusChanged { newValue in @@ -143,6 +228,8 @@ struct PagingLibraryView: View { } } + // MARK: List Item View + @ViewBuilder private func listItemView(item: Element, posterType: PosterDisplayType) -> some View { LibraryRow(item: item, posterType: posterType) @@ -156,13 +243,31 @@ struct PagingLibraryView: View { } } + // MARK: Error View + @ViewBuilder - private var contentView: some View { + private func errorView(with error: some Error) -> some View { + Text(error.localizedDescription) + /* ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } */ + } + + // MARK: Grid View + + @ViewBuilder + private var gridView: some View { CollectionVGrid( uniqueElements: viewModel.elements, layout: layout ) { item in - switch (posterType, viewType) { + + let displayType = Defaults[.Customization.Library.rememberLayout] ? _displayType.wrappedValue : _defaultDisplayType + .wrappedValue + let posterType = Defaults[.Customization.Library.rememberLayout] ? _posterType.wrappedValue : _defaultPosterType.wrappedValue + + switch (posterType, displayType) { case (.landscape, .grid): landscapeGridItemView(item: item) case (.portrait, .grid): @@ -174,55 +279,146 @@ struct PagingLibraryView: View { .onReachedBottomEdge(offset: .rows(3)) { viewModel.send(.getNextPage) } + .proxy(collectionVGridProxy) + .scrollIndicatorsVisible(false) + } + + // MARK: Inner Content View + + @ViewBuilder + private var innerContent: some View { + switch viewModel.state { + case .content: + if viewModel.elements.isEmpty { + L10n.noResults.text + } else { + gridView + } + case .initial, .refreshing: + ProgressView() + default: + AssertionFailureView("Expected view for unexpected state") + } + } + + // MARK: Content View + + @ViewBuilder + private var contentView: some View { + + innerContent + // These exist here to alleviate type-checker issues + .onChange(of: posterType) { + setCustomLayout() + } + .onChange(of: displayType) { + setCustomLayout() + } + .onChange(of: listColumnCount) { + setCustomLayout() + } + + // Logic for LetterPicker. Enable when ready + + /* if letterPickerEnabled, let filterViewModel = viewModel.filterViewModel { + ZStack(alignment: letterPickerOrientation.alignment) { + innerContent + .padding(letterPickerOrientation.edge, LetterPickerBar.size + 10) + .frame(maxWidth: .infinity) + + LetterPickerBar(viewModel: filterViewModel) + .padding(.top, safeArea.top) + .padding(.bottom, safeArea.bottom) + .padding(letterPickerOrientation.edge, 10) + } + } else { + innerContent + } + // These exist here to alleviate type-checker issues + .onChange(of: posterType) { + setCustomLayout() + } + .onChange(of: displayType) { + setCustomLayout() + } + .onChange(of: listColumnCount) { + setCustomLayout() + }*/ } + // MARK: Body + var body: some View { ZStack { + Color.clear + if cinematicBackground { CinematicBackgroundView(viewModel: cinematicBackgroundViewModel) .visible(presentBackground) .blurred() } - WrappedView { - Group { - switch viewModel.state { - case let .error(error): - Text(error.localizedDescription) - case .initial, .refreshing: - ProgressView() - case .content: - if viewModel.elements.isEmpty { - L10n.noResults.text - } else { - contentView - } - } - } + switch viewModel.state { + case .content, .initial, .refreshing: + contentView + case let .error(error): + errorView(with: error) } } + .animation(.linear(duration: 0.1), value: viewModel.state) .ignoresSafeArea() .navigationTitle(viewModel.parent?.displayTitle ?? "") - .onFirstAppear { - if viewModel.state == .initial { - viewModel.send(.refresh) - } + .onChange(of: focusedItem) { + setCinematicBackground() } - .onChange(of: focusedItem) { _, newValue in - guard let newValue else { - withAnimation { - presentBackground = false - } - return + .onChange(of: rememberLayout) { + if rememberLayout { + setCustomLayout() + } else { + setDefaultLayout() } + } + .onChange(of: defaultPosterType) { + guard !Defaults[.Customization.Library.rememberLayout] else { return } + setDefaultLayout() + } + .onChange(of: defaultDisplayType) { + guard !Defaults[.Customization.Library.rememberLayout] else { return } + setDefaultLayout() + } + .onChange(of: defaultListColumnCount) { + guard !Defaults[.Customization.Library.rememberLayout] else { return } + setDefaultLayout() + } + .onChange(of: viewModel.filterViewModel?.currentFilters) { _, newValue in + guard let newValue, let id = viewModel.parent?.id else { return } - cinematicBackgroundViewModel.select(item: newValue) + if Defaults[.Customization.Library.rememberSort] { + let newStoredFilters = StoredValues[.User.libraryFilters(parentID: id)] + .mutating(\.sortBy, with: newValue.sortBy) + .mutating(\.sortOrder, with: newValue.sortOrder) - if !presentBackground { - withAnimation { - presentBackground = true + StoredValues[.User.libraryFilters(parentID: id)] = newStoredFilters + } + } + .onReceive(viewModel.events) { event in + switch event { + case let .gotRandomItem(item): + switch item { + case let item as BaseItemDto: + router.route(to: \.item, item) + case let item as BaseItemPerson: + let viewModel = ItemLibraryViewModel(parent: item, filters: .default) + router.route(to: \.library, viewModel) + default: + assertionFailure("Used an unexpected type within a `PagingLibaryView`?") } } } + .onFirstAppear { + if viewModel.state == .initial { + viewModel.send(.refresh) + } + } } } diff --git a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift index f0d9bf967..5d65d1029 100644 --- a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift +++ b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramButtonContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift index 793b809a5..7c5ad24af 100644 --- a/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift +++ b/Swiftfin tvOS/Views/ProgramsView/Components/ProgramProgressOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift b/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift index fc621108d..686854cf0 100644 --- a/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift +++ b/Swiftfin tvOS/Views/ProgramsView/ProgramsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/QuickConnectView.swift b/Swiftfin tvOS/Views/QuickConnectView.swift index c2d609079..ad5e68939 100644 --- a/Swiftfin tvOS/Views/QuickConnectView.swift +++ b/Swiftfin tvOS/Views/QuickConnectView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin tvOS/Views/SearchView.swift b/Swiftfin tvOS/Views/SearchView.swift index 615532918..ce6b6ec97 100644 --- a/Swiftfin tvOS/Views/SearchView.swift +++ b/Swiftfin tvOS/Views/SearchView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SelectServerView.swift b/Swiftfin tvOS/Views/SelectServerView.swift deleted file mode 100644 index d2283656c..000000000 --- a/Swiftfin tvOS/Views/SelectServerView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import CollectionVGrid -import SwiftUI - -struct SelectServerView: View { - - @EnvironmentObject - private var router: SelectUserCoordinator.Router - - @Binding - private var serverSelection: SelectUserServerSelection - - @ObservedObject - private var viewModel: SelectUserViewModel - - private var selectedServer: ServerState? { - if case let SelectUserServerSelection.server(id: id) = serverSelection, - let server = viewModel.servers.keys.first(where: { server in server.id == id }) - { - return server - } - - return nil - } - - init( - selection: Binding, - viewModel: SelectUserViewModel - ) { - self._serverSelection = selection - self.viewModel = viewModel - } - - var body: some View { - FullScreenMenu(L10n.servers) { - Section { - Button { - router.popLast { - router.route(to: \.connectToServer) - } - } label: { - HStack { - L10n.addServer.text - - Spacer() - - Image(systemName: "plus") - } - } - - if let selectedServer { - Button { - router.popLast { - router.route(to: \.editServer, selectedServer) - } - } label: { - HStack { - L10n.editServer.text - - Spacer() - - Image(systemName: "server.rack") - } - } - } - } - - Section { - - if viewModel.servers.keys.count > 1 { - Button { - serverSelection = .all - router.popLast() - } label: { - HStack { - L10n.allServers.text - - Spacer() - - if serverSelection == .all { - Image(systemName: "checkmark.circle.fill") - } - } - } - } - - ForEach(viewModel.servers.keys.reversed()) { server in - Button { - serverSelection = .server(id: server.id) - router.popLast() - } label: { - HStack { - VStack(alignment: .leading) { - Text(server.name) - .font(.headline) - .fontWeight(.semibold) - - Text(server.currentURL.absoluteString) - .font(.subheadline) - .foregroundColor(.primary) - } - - Spacer() - - if selectedServer == server { - Image(systemName: "checkmark.circle.fill") - } - } - .padding() - } - .buttonStyle(.card) - .padding(.horizontal) - } - } header: { - Text(L10n.servers) - } - .headerProminence(.increased) - } - } -} diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift b/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift index 1391e34ed..48878ba4c 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/AddUserButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections @@ -42,36 +42,59 @@ extension SelectUserView { self.servers = servers } + @ViewBuilder + private var content: some View { + ZStack { + Color.secondarySystemFill + + RelativeSystemImageView(systemName: "plus") + .foregroundStyle(.secondary) + } + .clipShape(.circle) + .aspectRatio(1, contentMode: .fill) + .hoverEffect(.highlight) + + Text(L10n.addUser) + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(isEnabled ? .primary : .secondary) + + if serverSelection == .all { + // For layout, not to be localized + Text("Hidden") + .font(.footnote) + .hidden() + } + } + var body: some View { - VStack { + if serverSelection == .all { + Menu { + Text(L10n.selectServer) + + ForEach(servers) { server in + Button { + action(server) + } label: { + Text(server.name) + Text(server.currentURL.absoluteString) + } + } + } label: { + content + } + .buttonStyle(.borderless) + .buttonBorderShape(.circle) + } else { Button { if let selectedServer { action(selectedServer) } } label: { - ZStack { - Color.secondarySystemFill - - RelativeSystemImageView(systemName: "plus") - .foregroundStyle(.secondary) - } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - } - .buttonStyle(.card) - .buttonBorderShape(.circleBackport) - .disabled(!isEnabled) - - Text("Add User") - .font(.title3) - .fontWeight(.semibold) - .foregroundStyle(isEnabled ? .primary : .secondary) - - if serverSelection == .all { - Text("Hidden") - .font(.footnote) - .hidden() + content } + .buttonStyle(.borderless) + .buttonBorderShape(.circle) } } } diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift b/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift new file mode 100644 index 000000000..ece7b9b16 --- /dev/null +++ b/Swiftfin tvOS/Views/SelectUserView/Components/SelectUserBottomBar.swift @@ -0,0 +1,150 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +extension SelectUserView { + + struct SelectUserBottomBar: View { + + @Binding + private var isEditing: Bool + + @Binding + private var serverSelection: SelectUserServerSelection + + @ObservedObject + private var viewModel: SelectUserViewModel + + private let areUsersSelected: Bool + private let userCount: Int + + private let onDelete: () -> Void + private let toggleAllUsersSelected: () -> Void + + // MARK: - Advanced Menu + + @ViewBuilder + private var advancedMenu: some View { + Menu { + Button(L10n.editUsers, systemImage: "person.crop.circle") { + isEditing.toggle() + } + // TODO: Advanced settings on tvOS? + // + // Divider() + // + // Button(L10n.advanced, systemImage: "gearshape.fill") { + // router.route(to: \.advancedSettings) + // } + } label: { + Label(L10n.advanced, systemImage: "gearshape.fill") + .font(.body.weight(.semibold)) + .foregroundStyle(Color.primary) + .labelStyle(.iconOnly) + .frame(width: 50, height: 50) + } + + // TODO: Do we want to support a grid view and list view like iOS? +// if !viewModel.servers.isEmpty { +// Picker(selection: $userListDisplayType) { +// ForEach(LibraryDisplayType.allCases, id: \.hashValue) { +// Label($0.displayTitle, systemImage: $0.systemImage) +// .tag($0) +// } +// } label: { +// Text(L10n.layout) +// Text(userListDisplayType.displayTitle) +// Image(systemName: userListDisplayType.systemImage) +// } +// .pickerStyle(.menu) +// } + } + + // MARK: - Delete User Button + + private var deleteUsersButton: some View { + ListRowButton(L10n.delete, role: .destructive) { + onDelete() + } + .frame(width: 400, height: 50) + .disabled(!areUsersSelected) + } + + // MARK: - Initializer + + init( + isEditing: Binding, + serverSelection: Binding, + areUsersSelected: Bool, + viewModel: SelectUserViewModel, + userCount: Int, + onDelete: @escaping () -> Void, + toggleAllUsersSelected: @escaping () -> Void + ) { + self._isEditing = isEditing + self._serverSelection = serverSelection + self.viewModel = viewModel + self.areUsersSelected = areUsersSelected + self.userCount = userCount + self.onDelete = onDelete + self.toggleAllUsersSelected = toggleAllUsersSelected + } + + // MARK: - Content View + + @ViewBuilder + private var contentView: some View { + HStack(alignment: .center) { + if isEditing { + deleteUsersButton + + Button { + toggleAllUsersSelected() + } label: { + Text(areUsersSelected ? L10n.removeAll : L10n.selectAll) + .foregroundStyle(Color.primary) + .font(.body.weight(.semibold)) + .frame(width: 200, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + Button { + isEditing = false + } label: { + Text(L10n.cancel) + .foregroundStyle(Color.primary) + .font(.body.weight(.semibold)) + .frame(width: 200, height: 50) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } else { + ServerSelectionMenu( + selection: $serverSelection, + viewModel: viewModel + ) + + advancedMenu + } + } + } + + // MARK: - Body + + var body: some View { + // `Menu` with custom label has some weird additional + // frame/padding that differs from default label style + AlternateLayoutView(alignment: .top) { + Color.clear + .frame(height: 100) + } content: { + contentView + } + } + } +} diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift b/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift index 91cc45a91..d0fe4a7a8 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/ServerSelectionMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -12,17 +12,18 @@ extension SelectUserView { struct ServerSelectionMenu: View { + // MARK: - Observed & Environment Objects + @EnvironmentObject private var router: SelectUserCoordinator.Router - @Binding - private var serverSelection: SelectUserServerSelection - @ObservedObject private var viewModel: SelectUserViewModel - @State - private var isPresentingServers: Bool = false + // MARK: - Server Selection + + @Binding + private var serverSelection: SelectUserServerSelection private var selectedServer: ServerState? { if case let SelectUserServerSelection.server(id: id) = serverSelection, @@ -34,6 +35,8 @@ extension SelectUserView { return nil } + // MARK: - Initializer + init( selection: Binding, viewModel: SelectUserViewModel @@ -42,34 +45,54 @@ extension SelectUserView { self.viewModel = viewModel } - var body: some View { - Button { - let parameters = SelectUserCoordinator.SelectServerParameters( - selection: _serverSelection, - viewModel: viewModel - ) + // MARK: - Body - router.route(to: \.selectServer, parameters) + var body: some View { + Menu { + Picker(L10n.servers, selection: _serverSelection) { + ForEach(viewModel.servers.keys) { server in + Button { + Text(server.name) + Text(server.currentURL.absoluteString) + } + .tag(SelectUserServerSelection.server(id: server.id)) + } + if viewModel.servers.keys.count > 1 { + Label(L10n.allServers, systemImage: "person.2.fill") + .tag(SelectUserServerSelection.all) + } + } + Section { + if let selectedServer { + Button(L10n.editServer, systemImage: "server.rack") { + router.route(to: \.editServer, selectedServer) + } + } + Button(L10n.addServer, systemImage: "plus") { + router.route(to: \.connectToServer) + } + } } label: { - ZStack { - - Group { - switch serverSelection { - case .all: - Label("All Servers", systemImage: "person.2.fill") - case let .server(id): - if let server = viewModel.servers.keys.first(where: { $0.id == id }) { - Label(server.name, systemImage: "server.rack") - } + HStack(spacing: 16) { + switch serverSelection { + case .all: + Image(systemName: "person.2.fill") + Text(L10n.allServers) + case let .server(id): + if let server = viewModel.servers.keys.first(where: { $0.id == id }) { + Image(systemName: "server.rack") + Text(server.name) } } - .font(.body.weight(.semibold)) - .foregroundStyle(Color.primary) + Image(systemName: "chevron.up.chevron.down") + .foregroundStyle(.secondary) + .font(.subheadline.weight(.semibold)) } - .frame(height: 50) - .frame(maxWidth: 400) - .clipShape(RoundedRectangle(cornerRadius: 10)) + .font(.body.weight(.semibold)) + .foregroundStyle(Color.primary) + .frame(width: 400, height: 50) } + .menuOrder(.fixed) } } } diff --git a/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift b/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift index ff961e558..a9e0a715f 100644 --- a/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift +++ b/Swiftfin tvOS/Views/SelectUserView/Components/UserGridButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -28,6 +28,8 @@ extension SelectUserView { private let action: () -> Void private let onDelete: () -> Void + // MARK: - Initializer + init( user: UserState, server: ServerState, @@ -42,80 +44,36 @@ extension SelectUserView { self.onDelete = onDelete } + // MARK: - Label Foreground Style + private var labelForegroundStyle: some ShapeStyle { guard isEditing else { return .primary } return isSelected ? .primary : .secondary } - @ViewBuilder - private var personView: some View { - ZStack { - Color.secondarySystemFill - - RelativeSystemImageView(systemName: "person.fill", ratio: 0.5) - .foregroundStyle(.secondary) - } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - } - - var body: some View { - VStack { - Button { - action() - } label: { - VStack(alignment: .center) { - ZStack { - Color.clear - - ImageView(user.profileImageSource(client: server.client, maxWidth: 120)) - .image { image in - image - .posterBorder(ratio: 1 / 2, of: \.width) - } - .placeholder { _ in - personView - } - .failure { - personView - } - } - .aspectRatio(1, contentMode: .fill) - } - } - .buttonStyle(.card) - .buttonBorderShape(.circleBackport) - // .contextMenu { - // Button("Delete", role: .destructive) { - // onDelete() - // } - // } - - Text(user.username) - .font(.title3) - .fontWeight(.semibold) - .foregroundStyle(labelForegroundStyle) - .lineLimit(1) - - if showServer { - Text(server.name) - .font(.footnote) - .foregroundStyle(.secondary) - } - } + // MARK: - User Portrait + + private var userPortrait: some View { + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: server.client, + maxWidth: 120 + ), + pipeline: .Swiftfin.local + ) .overlay { if isEditing { - ZStack(alignment: .bottomTrailing) { + ZStack(alignment: .bottom) { Color.black .opacity(isSelected ? 0 : 0.5) - .clipShape(.circle) if isSelected { Image(systemName: "checkmark.circle.fill") .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 40, height: 40, alignment: .bottomTrailing) + .aspectRatio(contentMode: .fill) + .frame(width: 75, height: 75) .symbolRenderingMode(.palette) .foregroundStyle(accentColor.overlayColor, accentColor) } @@ -123,5 +81,37 @@ extension SelectUserView { } } } + + // MARK: - Body + + var body: some View { + VStack { + Button { + action() + } label: { + userPortrait + .hoverEffect(.highlight) + + Text(user.username) + .font(.title3) + .fontWeight(.semibold) + .foregroundStyle(labelForegroundStyle) + .lineLimit(1) + + if showServer { + Text(server.name) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .buttonStyle(.borderless) + .buttonBorderShape(.circle) + .contextMenu { + Button(L10n.delete, role: .destructive) { + onDelete() + } + } + } + } } } diff --git a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift index da61e0c86..c96a54a74 100644 --- a/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin tvOS/Views/SelectUserView/SelectUserView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid @@ -17,11 +17,6 @@ import SwiftUI struct SelectUserView: View { - // MARK: - Defaults - - @Default(.selectUserServerSelection) - private var serverSelection - // MARK: - User Grid Item Enum private enum UserGridItem: Hashable { @@ -29,6 +24,16 @@ struct SelectUserView: View { case addUser } + // MARK: - Defaults + + @Default(.selectUserServerSelection) + private var serverSelection + + // MARK: - Environment Variable + + @Environment(\.colorScheme) + private var colorScheme + // MARK: - State & Environment Objects @EnvironmentObject @@ -46,16 +51,37 @@ struct SelectUserView: View { @State private var gridItemSize: CGSize = .zero @State + private var isEditingUsers: Bool = false + @State private var padGridItemColumnCount: Int = 1 @State + private var pin: String = "" + @State private var scrollViewOffset: CGFloat = 0 @State + private var selectedUsers: Set = [] + @State private var splashScreenImageSource: ImageSource? = nil + private var users: [UserState] { + gridItems.compactMap { item in + switch item { + case let .user(user, _): + return user + default: + return nil + } + } + } + // MARK: - Dialog States + @State + private var isPresentingConfirmDeleteUsers = false @State private var isPresentingServers: Bool = false + @State + private var isPresentingLocalPin: Bool = false // MARK: - Error State @@ -141,6 +167,28 @@ struct SelectUserView: View { return CGFloat(lastRowMissing) * (gridItemSize.width + EdgeInsets.edgePadding) / 2 } + // MARK: - Select User(s) + + private func select(user: UserState, needsPin: Bool = true) { + Task { @MainActor in + selectedUsers.insert(user) + + switch user.accessPolicy { + case .requireDeviceAuthentication: + // Do nothing, no device authentication on tvOS + break + case .requirePin: + if needsPin { + isPresentingLocalPin = true + return + } + case .none: () + } + + viewModel.send(.signIn(user, pin: pin)) + } + } + // MARK: - Grid Content View @ViewBuilder @@ -175,17 +223,17 @@ struct SelectUserView: View { server: server, showServer: serverSelection == .all ) { -// if isEditingUsers { -// selectedUsers.toggle(value: user) -// } else { - viewModel.send(.signIn(user, pin: "")) -// } + if isEditingUsers { + selectedUsers.toggle(value: user) + } else { + select(user: user) + } } onDelete: { -// selectedUsers.insert(user) -// isPresentingConfirmDeleteUsers = true + selectedUsers.insert(user) + isPresentingConfirmDeleteUsers = true } -// .environment(\.isEditing, isEditingUsers) -// .environment(\.isSelected, selectedUsers.contains(user)) + .environment(\.isEditing, isEditingUsers) + .environment(\.isSelected, selectedUsers.contains(user)) case .addUser: AddUserButton( serverSelection: $serverSelection, @@ -193,6 +241,7 @@ struct SelectUserView: View { ) { server in router.route(to: \.userSignIn, server) } + .environment(\.isEnabled, !isEditingUsers) } } @@ -211,24 +260,34 @@ struct SelectUserView: View { .frame(height: 100) gridContentView + .focusSection() } .scrollIfLargerThanContainer(padding: 100) .scrollViewOffset($scrollViewOffset) } - HStack { - ServerSelectionMenu( - selection: $serverSelection, - viewModel: viewModel - ) + SelectUserBottomBar( + isEditing: $isEditingUsers, + serverSelection: $serverSelection, + areUsersSelected: selectedUsers.isNotEmpty, + viewModel: viewModel, + userCount: gridItems.count, + onDelete: { + isPresentingConfirmDeleteUsers = true + } + ) { + if selectedUsers.count == users.count { + selectedUsers.removeAll() + } else { + selectedUsers.insert(contentsOf: users) + } } + .focusSection() } .animation(.linear(duration: 0.1), value: scrollViewOffset) .background { if let splashScreenImageSource { ZStack { - Color.clear - ImageView(splashScreenImageSource) .aspectRatio(contentMode: .fill) .id(splashScreenImageSource) @@ -247,35 +306,56 @@ struct SelectUserView: View { @ViewBuilder private var emptyView: some View { - ZStack { - VStack { - Image(.jellyfinBlobBlue) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 100) - .edgePadding() - - Color.clear + VStack(spacing: 50) { + L10n.connectToJellyfinServerStart.text + .font(.body) + .frame(minWidth: 50, maxWidth: 500) + .multilineTextAlignment(.center) + + Button { + router.route(to: \.connectToServer) + } label: { + L10n.connect.text + .font(.callout) + .fontWeight(.bold) + .frame(width: 400, height: 75) + .background(Color.jellyfinPurple) } + .buttonStyle(.card) + } + } - VStack(spacing: 50) { - L10n.connectToJellyfinServerStart.text - .font(.body) - .frame(minWidth: 50, maxWidth: 500) - .multilineTextAlignment(.center) - - Button { - router.route(to: \.connectToServer) - } label: { - L10n.connect.text - .font(.callout) - .fontWeight(.bold) - .frame(width: 400, height: 75) - .background(Color.jellyfinPurple) - } - .buttonStyle(.card) + // MARK: - Functions + + private func didDelete(_ server: ServerState) { + viewModel.send(.getServers) + + if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id { + if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first { + serverSelection = .server(id: first.id) + } else { + serverSelection = .all } } + + setSplashScreenImageSource() + } + + // MARK: - Did Appear + + private func didAppear() { + viewModel.send(.getServers) + + setSplashScreenImageSource() + } + + // MARK: - Set Splash Screen Image Source + + private func setSplashScreenImageSource() { + splashScreenImageSource = makeSplashScreenImageSource( + serverSelection: serverSelection, + allServersSelection: .all + ) } // MARK: - Body @@ -289,31 +369,25 @@ struct SelectUserView: View { } } .ignoresSafeArea() + .navigationBarBranding() .onAppear { - viewModel.send(.getServers) - - splashScreenImageSource = makeSplashScreenImageSource( - serverSelection: serverSelection, - allServersSelection: .all - ) - -// gridItems = OrderedSet( -// (0 ..< 20) -// .map { i in -// UserState(accessToken: "", id: "\(i)", serverID: "", username: "\(i)") -// } -// .map { u in -// UserGridItem.user(u, server: .init(urls: [], currentURL: URL(string: "/")!, name: "Test", id: "", usersIDs: [])) -// } -// ) + didAppear() + } + .onChange(of: isEditingUsers) { _, newValue in + guard !newValue else { return } + selectedUsers.removeAll() } .onChange(of: serverSelection) { _, newValue in gridItems = makeGridItems(for: newValue) - splashScreenImageSource = makeSplashScreenImageSource( - serverSelection: newValue, - allServersSelection: .all - ) + setSplashScreenImageSource() + } + .onChange(of: isPresentingLocalPin) { _, newValue in + if newValue { + pin = "" + } else { + selectedUsers.removeAll() + } } .onChange(of: viewModel.servers) { _, _ in gridItems = makeGridItems(for: serverSelection) @@ -342,18 +416,49 @@ struct SelectUserView: View { serverSelection = .server(id: server.id) } .onNotification(.didDeleteServer) { server in - viewModel.send(.getServers) - - if case let SelectUserServerSelection.server(id: id) = serverSelection, server.id == id { - if viewModel.servers.keys.count == 1, let first = viewModel.servers.keys.first { - serverSelection = .server(id: first.id) - } else { - serverSelection = .all + didDelete(server) + } + .confirmationDialog( + Text(L10n.deleteUser), + isPresented: $isPresentingConfirmDeleteUsers, + presenting: selectedUsers + ) { selectedUsers in + Button(L10n.delete, role: .destructive) { + viewModel.send(.deleteUsers(Array(selectedUsers))) + isEditingUsers = false + } + } message: { selectedUsers in + if selectedUsers.count == 1, let first = selectedUsers.first { + Text(L10n.deleteUserSingleConfirmation(first.username)) + } else { + Text(L10n.deleteUserMultipleConfirmation(selectedUsers.count)) + } + } + .alert(L10n.signIn, isPresented: $isPresentingLocalPin) { + + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // TextField(L10n.pin, text: $pin) + TextField(text: $pin) {} + .keyboardType(.numberPad) + + Button(L10n.signIn) { + guard let user = selectedUsers.first else { + assertionFailure("User not selected") + return } + select(user: user, needsPin: false) } - // change splash screen selection if necessary -// selectUserAllServersSplashscreen = serverSelection + Button(L10n.cancel, role: .cancel) {} + } message: { + if let user = selectedUsers.first, user.pinHint.isNotEmpty { + Text(user.pinHint) + } else { + let username = selectedUsers.first?.username ?? .emptyDash + + Text(L10n.enterPinForUser(username)) + } } .errorMessage($error) } diff --git a/Swiftfin tvOS/Views/ServerDetailView.swift b/Swiftfin tvOS/Views/ServerDetailView.swift index 52e2d1f9e..d6af52244 100644 --- a/Swiftfin tvOS/Views/ServerDetailView.swift +++ b/Swiftfin tvOS/Views/ServerDetailView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -75,14 +75,17 @@ struct EditServerView: View { .foregroundColor(.secondary) } } + .listRowBackground(Color.clear) + .listRowInsets(.zero) } if isEditing { Section { - ListRowButton(L10n.delete) { + ListRowButton(L10n.delete, role: .destructive) { isPresentingConfirmDeletion = true } - .foregroundStyle(.primary, .red.opacity(0.5)) + .listRowBackground(Color.clear) + .listRowInsets(.zero) } } } diff --git a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift index 6d3284a2c..53bdc87c6 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -48,7 +48,7 @@ extension CustomDeviceProfileSettingsView { profileDetailsView( title: L10n.useAsTranscodingProfile, - detail: profile.useAsTranscodingProfile ? "Yes" : "No" + detail: profile.useAsTranscodingProfile ? L10n.yes : L10n.no ) } diff --git a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift index 2f0ead287..96d67b7d5 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -95,7 +95,7 @@ extension CustomDeviceProfileSettingsView { HStack { Text(L10n.customProfile) Spacer() - Button("Save") { + Button(L10n.save) { if createProfile { customDeviceProfiles.append(profile.value) } @@ -135,7 +135,7 @@ extension CustomDeviceProfileSettingsView { } .navigationTitle(L10n.customProfile) .alert("Profile not saved", isPresented: $isPresentingNotSaved) { - Button("Close", role: .destructive) { + Button(L10n.close, role: .destructive) { router.dismissCoordinator() } } diff --git a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift index f9295912c..9af28401c 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -82,7 +82,7 @@ struct CustomDeviceProfileSettingsView: View { Button(role: .destructive) { deleteProfile(profile) } label: { - Label("Delete", systemImage: "trash") + Label(L10n.delete, systemImage: "trash") } } } @@ -93,7 +93,7 @@ struct CustomDeviceProfileSettingsView: View { Text(L10n.profiles) Spacer() if customProfiles.isNotEmpty { - Button("Add") { + Button(L10n.add) { router.route(to: \.createCustomDeviceProfile) } } diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift index 9d6caa0fd..0b0277bf8 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/ListColumnsPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -16,7 +16,7 @@ struct ListColumnsPickerView: View { var body: some View { StepperView( - title: "Columns", + title: L10n.columns, value: $selection, range: 1 ... 3, step: 1 diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index f3db9a586..c29a80679 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift index dadfba623..5ea483ddf 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/LibrarySection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/LibrarySection.swift new file mode 100644 index 000000000..6b37aaeb9 --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/LibrarySection.swift @@ -0,0 +1,80 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +extension CustomizeViewsSettings { + + struct LibrarySection: View { + + @Default(.Customization.Library.randomImage) + private var libraryRandomImage + @Default(.Customization.Library.showFavorites) + private var showFavorites + + @Default(.Customization.Library.cinematicBackground) + private var cinematicBackground + @Default(.Customization.Library.displayType) + private var libraryDisplayType + @Default(.Customization.Library.posterType) + private var libraryPosterType + @Default(.Customization.Library.listColumnCount) + private var listColumnCount + + @Default(.Customization.Library.rememberLayout) + private var rememberLibraryLayout + @Default(.Customization.Library.rememberSort) + private var rememberLibrarySort + + @EnvironmentObject + private var router: CustomizeSettingsCoordinator.Router + + @State + private var isPresentingNextUpDays = false + + var body: some View { + Section(L10n.media) { + + Toggle(L10n.randomImage, isOn: $libraryRandomImage) + + Toggle(L10n.showFavorites, isOn: $showFavorites) + } + + Section(L10n.library) { + Toggle(L10n.cinematicBackground, isOn: $cinematicBackground) + + InlineEnumToggle(title: L10n.posters, selection: $libraryPosterType) + + InlineEnumToggle(title: L10n.library, selection: $libraryDisplayType) + + if libraryDisplayType == .list { + ChevronButton( + L10n.columns, + subtitle: listColumnCount.description + ) + .onSelect { + router.route(to: \.listColumnSettings, $listColumnCount) + } + } + } + + Section { + Toggle(L10n.rememberLayout, isOn: $rememberLibraryLayout) + } footer: { + Text(L10n.rememberLayoutFooter) + } + + Section { + Toggle(L10n.rememberSorting, isOn: $rememberLibrarySort) + } footer: { + Text(L10n.rememberSortingFooter) + } + } + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index ec4f48117..049ad5916 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -31,19 +31,6 @@ struct CustomizeViewsSettings: View { @Default(.Customization.Library.displayType) private var libraryViewType - @Default(.Customization.Library.cinematicBackground) - private var cinematicBackground - @Default(.Customization.Library.randomImage) - private var libraryRandomImage - @Default(.Customization.Library.showFavorites) - private var showFavorites - @Default(.Customization.Library.displayType) - private var libraryDisplayType - @Default(.Customization.Library.posterType) - private var libraryPosterType - @Default(.Customization.Library.listColumnCount) - private var listColumnCount - @EnvironmentObject private var router: CustomizeSettingsCoordinator.Router @@ -84,26 +71,7 @@ struct CustomizeViewsSettings: View { InlineEnumToggle(title: L10n.search, selection: $searchPosterType) } - Section(L10n.library) { - - Toggle(L10n.cinematicBackground, isOn: $cinematicBackground) - - Toggle(L10n.randomImage, isOn: $libraryRandomImage) - - Toggle(L10n.showFavorites, isOn: $showFavorites) - - InlineEnumToggle(title: L10n.posters, selection: $libraryPosterType) - InlineEnumToggle(title: L10n.library, selection: $libraryDisplayType) - if libraryDisplayType == .list { - ChevronButton( - L10n.columns, - subtitle: listColumnCount.description - ) - .onSelect { - router.route(to: \.listColumnSettings, $listColumnCount) - } - } - } + LibrarySection() ItemSection() diff --git a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift index 6b6e51077..6af5a3e6e 100644 --- a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift index 742c28f8a..565301c88 100644 --- a/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/IndicatorSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift b/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift index 9bb4369e0..fec284862 100644 --- a/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/PlaybackQualitySettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index 7ec282758..69be51e15 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -33,11 +33,8 @@ struct SettingsView: View { .contentView { Section(L10n.jellyfin) { - Button {} label: { - TextPairView( - leading: L10n.user, - trailing: viewModel.userSession.user.username - ) + UserProfileRow(user: viewModel.userSession.user.data) { + router.route(to: \.userProfile, viewModel) } ChevronButton( diff --git a/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift new file mode 100644 index 000000000..55c1f9717 --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift @@ -0,0 +1,278 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import KeychainSwift +import SwiftUI + +// TODO: present toast when authentication successfully changed +// TODO: pop is just a workaround to get change published from usersession. +// find fix and don't pop when successfully changed +// TODO: could cleanup/refactor greatly + +struct UserLocalSecurityView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - State & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @StateObject + private var viewModel = UserLocalSecurityViewModel() + + // MARK: - Local Security Variables + + @State + private var listSize: CGSize = .zero + @State + private var onPinCompletion: (() -> Void)? + @State + private var pin: String = "" + @State + private var pinHint: String = "" + @State + private var signInPolicy: UserAccessPolicy = .none + + // MARK: - Dialog States + + @State + private var isPresentingOldPinPrompt: Bool = false + @State + private var isPresentingNewPinPrompt: Bool = false + + // MARK: - Error State + + @State + private var error: Error? = nil + + // MARK: - Focus Management + + @FocusState + private var focusedItem: FocusableItem? + + private enum FocusableItem: Hashable { + case security + } + + // MARK: - Check Old Policy + + private func checkOldPolicy() { + do { + try viewModel.checkForOldPolicy() + } catch { + return + } + + checkNewPolicy() + } + + // MARK: - Check New Policy + + private func checkNewPolicy() { + do { + try viewModel.checkFor(newPolicy: signInPolicy) + } catch { + return + } + + viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: pinHint) + } + + // MARK: - Event Handler + + private func onReceive(_ event: UserLocalSecurityViewModel.Event) { + switch event { + case let .error(eventError): + error = eventError + case .promptForOldPin: + onPinCompletion = { + Task { + try viewModel.check(oldPin: pin) + + checkNewPolicy() + } + } + + pin = "" + isPresentingOldPinPrompt = true + case .promptForNewPin: + onPinCompletion = { + viewModel.set(newPolicy: signInPolicy, newPin: pin, newPinHint: pinHint) + router.popLast() + } + + pin = "" + isPresentingNewPinPrompt = true + case .promptForOldDeviceAuth, .promptForNewDeviceAuth: + break + } + } + + // MARK: - Body + + var body: some View { + SplitFormWindowView() + .descriptionView { + descriptionView + } + .contentView { + Section { + Toggle( + L10n.pin, + isOn: Binding( + get: { signInPolicy == .requirePin }, + set: { signInPolicy = $0 ? .requirePin : .none } + ) + ) + .focused($focusedItem, equals: .security) + /* Picker(L10n.security, selection: $signInPolicy) { + ForEach(UserAccessPolicy.allCases.filter { $0 != .requireDeviceAuthentication }, id: \.self) { policy in + Text(policy.displayTitle) + } + } */ + } + + if signInPolicy == .requirePin { + Section { + ChevronAlertButton( + L10n.hint, + subtitle: pinHint, + description: L10n.setPinHintDescription + ) { + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // TextField(L10n.hint, text: $pinHint) + TextField(text: $pinHint) {} + } + } header: { + Text(L10n.hint) + } footer: { + Text(L10n.setPinHintDescription) + } + } + } + .animation(.linear, value: signInPolicy) + .navigationTitle(L10n.security) + .onFirstAppear { + pinHint = viewModel.userSession.user.pinHint + signInPolicy = viewModel.userSession.user.accessPolicy + } + .onReceive(viewModel.events) { event in + onReceive(event) + } + .topBarTrailing { + Button { + checkOldPolicy() + } label: { + Group { + if signInPolicy == .requirePin, signInPolicy == viewModel.userSession.user.accessPolicy { + Text(L10n.changePin) + } else { + Text(L10n.save) + } + } + .foregroundStyle(accentColor.overlayColor) + .font(.headline) + .padding(.vertical, 5) + .padding(.horizontal, 10) + .background { + accentColor + } + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + .trackingSize($listSize) + .alert( + L10n.enterPin, + isPresented: $isPresentingOldPinPrompt, + presenting: onPinCompletion + ) { completion in + + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // SecureField(L10n.pin, text: $pin) + SecureField(text: $pin) {} + .keyboardType(.numberPad) + + Button(L10n.continue) { + completion() + } + + Button(L10n.cancel, role: .cancel) {} + } message: { _ in + Text(L10n.enterPinForUser(viewModel.userSession.user.username)) + } + .alert( + L10n.setPin, + isPresented: $isPresentingNewPinPrompt, + presenting: onPinCompletion + ) { completion in + + // TODO: Verify on tvOS 18 + // https://forums.developer.apple.com/forums/thread/739545 + // SecureField(L10n.pin, text: $pin) + SecureField(text: $pin) {} + .keyboardType(.numberPad) + + Button(L10n.set) { + completion() + } + + Button(L10n.cancel, role: .cancel) {} + } message: { _ in + Text(L10n.createPinForUser(viewModel.userSession.user.username)) + } + .errorMessage($error) + } + + // MARK: - Description View Icon + + private var descriptionView: some View { + ZStack { + Image(systemName: "lock.fill") + .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 .security: + LearnMoreModal { + TextPair( + title: L10n.security, + subtitle: L10n.additionalSecurityAccessDescription + ) + TextPair( + title: UserAccessPolicy.requirePin.displayTitle, + subtitle: L10n.requirePinDescription + ) + TextPair( + title: UserAccessPolicy.none.displayTitle, + subtitle: L10n.saveUserWithoutAuthDescription + ) + } + + case nil: + EmptyView() + } + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift new file mode 100644 index 000000000..043b35095 --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift @@ -0,0 +1,92 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Factory +import JellyfinAPI +import SwiftUI + +struct UserProfileSettingsView: View { + + @EnvironmentObject + private var router: UserProfileSettingsCoordinator.Router + + @ObservedObject + private var viewModel: SettingsViewModel + @StateObject + private var profileImageViewModel: UserProfileImageViewModel + + @State + private var isPresentingConfirmReset: Bool = false + + init(viewModel: SettingsViewModel) { + self.viewModel = viewModel + self._profileImageViewModel = StateObject(wrappedValue: UserProfileImageViewModel(user: viewModel.userSession.user.data)) + } + + var body: some View { + SplitFormWindowView() + .descriptionView { + UserProfileImage( + userID: viewModel.userSession.user.id, + source: viewModel.userSession.user.profileImageSource( + client: viewModel.userSession.client, + maxWidth: 400 + ) + ) + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + } + .contentView { + + // TODO: bring reset password to tvOS +// Section { +// ChevronButton(L10n.password) +// .onSelect { +// router.route(to: \.resetUserPassword, viewModel.userSession.user.id) +// } +// } + + Section { + ChevronButton(L10n.security) + .onSelect { + router.route(to: \.localSecurity) + } + } + + // TODO: Do we want this option on tvOS? +// Section { +// // TODO: move under future "Storage" tab +// // when downloads implemented +// Button(L10n.resetSettings) { +// isPresentingConfirmReset = true +// } +// .foregroundStyle(.red) +// } footer: { +// Text(L10n.resetSettingsDescription) +// } + } + .withDescriptionTopPadding() + .navigationTitle(L10n.user) + .confirmationDialog( + L10n.resetSettings, + isPresented: $isPresentingConfirmReset, + titleVisibility: .visible + ) { + Button(L10n.reset, role: .destructive) { + do { + try viewModel.userSession.user.deleteSettings() + } catch { + viewModel.logger.error("Unable to reset user settings: \(error.localizedDescription)") + } + } + } message: { + Text(L10n.resetSettingsMessage) + } + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift index 264409779..3e84a39a4 100644 --- a/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/VideoPlayerSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift new file mode 100644 index 000000000..b592881ed --- /dev/null +++ b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserButton.swift @@ -0,0 +1,79 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension UserSignInView { + + struct PublicUserButton: View { + + // MARK: - Environment Variables + + @Environment(\.isEnabled) + private var isEnabled: Bool + + // MARK: - Public User Variables + + private let user: UserDto + private let client: JellyfinClient + private let action: () -> Void + + // MARK: - Initializer + + init( + user: UserDto, + client: JellyfinClient, + action: @escaping () -> Void + ) { + self.user = user + self.client = client + self.action = action + } + + // MARK: - Person View + + @ViewBuilder + private var personView: some View { + ZStack { + Color.clear + + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: client, + maxWidth: 120 + ) + ) + } + } + + // MARK: - Body + + var body: some View { + Button(action: action) { + personView + .aspectRatio(1, contentMode: .fill) + .posterShadow() + .clipShape(.circle) + .frame(width: 150, height: 150) + .hoverEffect(.highlight) + + Text(user.name ?? .emptyDash) + .fontWeight(.semibold) + .foregroundStyle(.primary) + .lineLimit(1) + .padding(.bottom) + } + .buttonBorderShape(.circle) + .buttonStyle(.borderless) + .disabled(!isEnabled) + .foregroundStyle(.primary) + } + } +} diff --git a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift b/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift deleted file mode 100644 index 7843b6219..000000000 --- a/Swiftfin tvOS/Views/UserSignInView/Components/PublicUserRow.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import JellyfinAPI -import SwiftUI - -// TODO: change from list to grid button - -extension UserSignInView { - - struct PublicUserRow: View { - - private let user: UserDto - private let client: JellyfinClient - private let action: () -> Void - - init( - user: UserDto, - client: JellyfinClient, - action: @escaping () -> Void - ) { - self.user = user - self.client = client - self.action = action - } - - @ViewBuilder - private var personView: some View { - ZStack { - Color.secondarySystemFill - - RelativeSystemImageView(systemName: "person.fill", ratio: 0.5) - .foregroundStyle(.secondary) - } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - } - - var body: some View { - Button { - action() - } label: { - HStack { - ZStack { - Color.clear - - ImageView(user.profileImageSource(client: client, maxWidth: 120)) - .image { image in - image - .posterBorder(ratio: 0.5, of: \.width) - } - .placeholder { _ in - personView - } - .failure { - personView - } - } - .aspectRatio(1, contentMode: .fill) - .posterShadow() - .clipShape(.circle) - .frame(width: 50, height: 50) - - Text(user.name ?? .emptyDash) - .fontWeight(.semibold) - .foregroundStyle(.primary) - .lineLimit(1) - - Spacer() - } - } - .buttonStyle(.card) - .foregroundStyle(.primary) - } - } -} diff --git a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift index 790e9f63a..45ffd2725 100644 --- a/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin tvOS/Views/UserSignInView/UserSignInView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid @@ -13,8 +13,6 @@ import JellyfinAPI import Stinsen import SwiftUI -// TODO: change public users from list to grid - struct UserSignInView: View { // MARK: - Defaults @@ -30,13 +28,16 @@ struct UserSignInView: View { } @FocusState - private var focusedTextField: FocusField? + private var focusedField: FocusField? // MARK: - State & Environment Objects @EnvironmentObject private var router: UserSignInCoordinator.Router + @StateObject + private var focusGuide: FocusGuide = .init() + @StateObject private var viewModel: UserSignInViewModel @@ -69,39 +70,37 @@ struct UserSignInView: View { @ViewBuilder private var signInSection: some View { - Section { - TextField(L10n.username, text: $username) - .autocorrectionDisabled() - .textInputAutocapitalization(.never) - .focused($focusedTextField, equals: .username) - - SecureField(L10n.password, text: $password) - .focused($focusedTextField, equals: .password) - .onSubmit { - guard username.isNotEmpty else { - return - } - viewModel.send(.signIn(username: username, password: password, policy: .none)) + TextField(L10n.username, text: $username) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .focused($focusedField, equals: .username) + + SecureField(L10n.password, text: $password) + .focused($focusedField, equals: .password) + .onSubmit { + guard username.isNotEmpty else { + return } - } header: { - Text(L10n.signInToServer(viewModel.server.name)) - } + viewModel.send(.signIn(username: username, password: password, policy: .none)) + } if case .signingIn = viewModel.state { - Button(L10n.cancel) { + ListRowButton(L10n.cancel) { viewModel.send(.cancel) } - .foregroundStyle(.red, .red.opacity(0.2)) + .foregroundStyle(.red, accentColor) + .padding(.vertical) } else { - Button(L10n.signIn) { + ListRowButton(L10n.signIn) { viewModel.send(.signIn(username: username, password: password, policy: .none)) } .disabled(username.isEmpty) .foregroundStyle( accentColor.overlayColor, - accentColor + username.isEmpty ? Color.white.opacity(0.5) : accentColor ) .opacity(username.isEmpty ? 0.5 : 1) + .padding(.vertical) } if viewModel.isQuickConnectEnabled { @@ -114,14 +113,17 @@ struct UserSignInView: View { accentColor.overlayColor, accentColor ) + .padding(.bottom) } } if let disclaimer = viewModel.serverDisclaimer { Section(L10n.disclaimer) { Text(disclaimer) + .foregroundStyle(.secondary) .font(.callout) } + .padding(.top) } } @@ -129,22 +131,30 @@ struct UserSignInView: View { @ViewBuilder private var publicUsersSection: some View { - Section(L10n.publicUsers) { - if viewModel.publicUsers.isEmpty { - L10n.noPublicUsers.text - .font(.callout) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - } else { + if viewModel.publicUsers.isEmpty { + L10n.noPublicUsers.text + .font(.callout) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + .frame(maxHeight: .infinity, alignment: .center) + } else { + LazyVGrid( + columns: Array(repeating: GridItem(.flexible()), count: 4), + spacing: 30 + ) { ForEach(viewModel.publicUsers, id: \.id) { user in - PublicUserRow( + PublicUserButton( user: user, client: viewModel.server.client ) { username = user.name ?? "" password = "" - focusedTextField = .password + focusedField = .password } + .environment( + \.isEnabled, + viewModel.state != .signingIn + ) } } } @@ -153,34 +163,15 @@ struct UserSignInView: View { // MARK: - Body var body: some View { - VStack { - HStack { - Spacer() - - if viewModel.state == .signingIn { - ProgressView() - } - } - .frame(height: 100) - .overlay { - Image(.jellyfinBlobBlue) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 100) - .edgePadding() - } - - HStack(alignment: .top) { - VStack(alignment: .leading) { - signInSection - } - - VStack(alignment: .leading) { - publicUsersSection - } - } - - Spacer() + SplitLoginWindowView( + isLoading: viewModel.state == .signingIn, + leadingTitle: L10n.signInToServer(viewModel.server.name), + trailingTitle: L10n.publicUsers, + backgroundImageSource: viewModel.server.splashScreenImageSource() + ) { + signInSection + } trailingContentView: { + publicUsersSection } .onReceive(viewModel.events) { event in switch event { @@ -198,7 +189,7 @@ struct UserSignInView: View { } } .onFirstAppear { - focusedTextField = .username + focusedField = .username viewModel.send(.getPublicData) } .alert( @@ -209,11 +200,11 @@ struct UserSignInView: View { // TODO: uncomment when duplicate user fixed // Button(L10n.signIn) { -// signInUplicate(user: user, replace: false) +// signInDuplicate(user: user, replace: false) // } // Button("Replace") { -// signInUplicate(user: user, replace: true) +// signInDuplicate(user: user, replace: true) // } Button(L10n.dismiss, role: .cancel) diff --git a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift index a578a88e2..74391c0a9 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Components/LoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen @@ -22,7 +22,7 @@ extension VideoPlayer { VStack(spacing: 10) { - Text("Retrieving media information") + Text(L10n.retrievingMediaInformation) .foregroundColor(.white) ProgressView() @@ -30,7 +30,7 @@ extension VideoPlayer { Button { router.dismissCoordinator() } label: { - Text("Cancel") + Text(L10n.cancel) .foregroundColor(.red) .padding() .overlay { diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift index 1f56fa192..b3da98b07 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift index 90554ba65..ccb836d76 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift index 014d5cf9e..20fe278a3 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveLoadingOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift index 86843a5a0..a1e919037 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift index 35cf99343..79eca923a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift index b56257247..f3ef9b2db 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/LiveVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift index be8947b85..f54e94fa6 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/NativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 76a036fad..e77fd32ab 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift index 3d81ae007..b235e2a92 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift index 83d56c57f..b182b25b6 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift index cdbfe9e16..4c7099b3b 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift index ce094353e..92594014c 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift index 9ddd284b8..b8f05750f 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift index 250291993..c43576b61 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift index 808a039ef..00885428b 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift index 3844d8bd4..5e82a2364 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/BottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift index a2e7e6c95..fb9551698 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/SliderView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift index 9929c015f..e2faadaac 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Components/tvOSSLider/tvOSSlider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // Modification of https://github.com/zattoo/TvOSSlider diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift index fb52e37af..b5851700a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift index 007277e20..4f867d41c 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/MainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift index 2d692b2b2..10360ec4d 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/Overlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import PreferencesView diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift index 8b109f537..b5a611ab0 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -20,11 +20,11 @@ extension VideoPlayer { var displayTitle: String { switch self { case .audio: - return "Audio" + return L10n.audio case .playbackSpeed: - return "Playback Speed" + return L10n.playbackSpeed case .subtitles: - return "Subtitles" + return L10n.subtitles } } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift index 48fe3e0b9..0c41f574a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -87,7 +87,7 @@ struct VideoPlayer: View { @ViewBuilder private var loadingView: some View { - Text("Retrieving media information") + Text(L10n.retrievingMediaInformation) } var body: some View { diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index d22bfc395..8930039ce 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 4E10C81D2CC046610012CC9F /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E10C81C2CC0465F0012CC9F /* UserSection.swift */; }; 4E11805F2CBF52380077A588 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; 4E12F9172CBE9619006C217E /* DeviceType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E12F9152CBE9615006C217E /* DeviceType.swift */; }; + 4E13FAD82D18D5AF007785F6 /* ImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */; }; + 4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */; }; 4E14DC032CD43DD2001B621B /* AdminDashboardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E14DC022CD43DCB001B621B /* AdminDashboardCoordinator.swift */; }; 4E16FD512C0183DB00110147 /* LetterPickerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */; }; 4E16FD532C01840C00110147 /* LetterPickerBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD522C01840C00110147 /* LetterPickerBar.swift */; }; @@ -32,6 +34,8 @@ 4E17498F2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */; }; 4E182C9C2C94993200FBEFD5 /* ServerTasksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */; }; 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */; }; + 4E1AA0042D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; }; + 4E1AA0052D0640AA00524970 /* RemoteImageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */; }; 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */; }; 4E2182E52CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; }; 4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2182E42CAF67EF0094806B /* PlayMethod.swift */; }; @@ -68,8 +72,12 @@ 4E35CE6C2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E35CE6D2CBEDB7600DBD886 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */; }; 4E36395C2CC4DF0E00110EBC /* APIKeysViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */; }; + 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */; }; 4E3A24DA2CFE34A00083A72C /* SearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */; }; 4E3A24DC2CFE35D50083A72C /* NameInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */; }; + 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */; }; + 4E4593A32D04E2B500E277E1 /* ItemImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */; }; + 4E4593A62D04E4E300E277E1 /* AddItemImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */; }; 4E49DECB2CE54AA200352DCD /* SessionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */; }; 4E49DECD2CE54C7A00352DCD /* PermissionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */; }; 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */; }; @@ -79,24 +87,21 @@ 4E49DED52CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */; }; 4E49DED62CE54D9D00352DCD /* LoginFailurePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */; }; 4E49DED82CE5509300352DCD /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DED72CE5509000352DCD /* StatusSection.swift */; }; - 4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */; }; - 4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */; }; 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; 4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; }; - 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */; }; + 4E49DEE62CE5616800352DCD /* UserProfileImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */; }; 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; }; + 4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */; }; + 4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */; }; 4E4E9C672CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; }; - 4E4E9C682CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; }; 4E4E9C6A2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */; }; - 4E4E9C6B2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */; }; 4E5071D72CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */; }; - 4E5071D82CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */; }; 4E5071DA2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; }; - 4E5071DB2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071D92CFCEC0E003FA2AD /* GenreEditorViewModel.swift */; }; 4E5071E42CFCEFD3003FA2AD /* AddItemElementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5071E32CFCEFD1003FA2AD /* AddItemElementView.swift */; }; 4E5334A22CD1A28700D59FA8 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */; }; 4E537A842D03D11200659A1A /* ServerUserDeviceAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */; }; 4E537A8D2D04410E00659A1A /* ServerUserLiveTVAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */; }; + 4E5508732D13AFED002A5345 /* UserProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5508722D13AFE3002A5345 /* UserProfileImage.swift */; }; 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E556AB12D036F6900733377 /* UserPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E556AAF2D036F5E00733377 /* UserPermissions.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; @@ -104,7 +109,6 @@ 4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; }; 4E656C302D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.swift */; }; 4E656C312D0798AA00F993F3 /* ParentalRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E656C2F2D0798A900F993F3 /* ParentalRating.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 */; }; @@ -135,12 +139,16 @@ 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 */; }; + 4E6C27082C8BD0AD00FD2185 /* ServerSessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C27072C8BD0AD00FD2185 /* ServerSessionDetailView.swift */; }; 4E71D6892C80910900A0174D /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */; }; + 4E7315742D14772700EA2A95 /* UserProfileHeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7315732D14770E00EA2A95 /* UserProfileHeroImage.swift */; }; + 4E7315752D1485C900EA2A95 /* UserProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5508722D13AFE3002A5345 /* UserProfileImage.swift */; }; + 4E7315762D1485CC00EA2A95 /* UserProfileHeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7315732D14770E00EA2A95 /* UserProfileHeroImage.swift */; }; 4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; 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 */; }; + 4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.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 */; }; @@ -159,6 +167,8 @@ 4E90F76A2CC72B1F00417C31 /* DetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E90F7592CC72B1F00417C31 /* DetailsSection.swift */; }; 4E97D1832D064748004B89AD /* ItemSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1822D064748004B89AD /* ItemSection.swift */; }; 4E97D1852D064B43004B89AD /* RefreshMetadataButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */; }; + 4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */; }; + 4E98F7D32D123AD4001E7518 /* View-tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E98F7C92D123AD4001E7518 /* View-tvOS.swift */; }; 4E9A24E62C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */; }; 4E9A24E82C82B6190023DA83 /* CustomProfileButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */; }; 4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC1C8572C80332500E2879E /* EditCustomDeviceProfileCoordinator.swift */; }; @@ -167,6 +177,14 @@ 4EA09DE12CC4E4F100CB27E4 /* APIKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */; }; 4EA09DE42CC4E85C00CB27E4 /* APIKeysRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */; }; 4EA397472CD31CC000904C25 /* AddServerUserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */; }; + 4EA78B132D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */; }; + 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */; }; + 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */; }; + 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */; }; + 4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */; }; + 4EAE340C2D42B857006FBAD3 /* LibrarySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */; }; + 4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; + 4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */; }; 4EB1404C2C8E45B1008691F3 /* StreamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */; }; 4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; }; 4EB1A8CC2C9B1BA200F43898 /* DestructiveServerTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */; }; @@ -205,9 +223,28 @@ 4EDBDCD22CBDD6590033D347 /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */; }; 4EE11E1A2CC6A513004BF852 /* ActivityBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE11E192CC6A50D004BF852 /* ActivityBadge.swift */; }; 4EE141692C8BABDF0045B661 /* ProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ProgressSection.swift */; }; + 4ECF5D882D0A3D0200F066B1 /* AddAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */; }; + 4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */; }; + 4ECF5D8B2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */; }; + 4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */; }; + 4ED25CA42D07E4990010333C /* EditAccessScheduleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */; }; 4EE07CBB2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE07CBC2D08B19700B0B636 /* ErrorMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */; }; 4EE141692C8BABDF0045B661 /* ActiveSessionProgressSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */; }; + 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */; }; + 4EE766F72D132054009658F0 /* IdentifyItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */; }; + 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; + 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */; }; + 4EE767082D13403F009658F0 /* RemoteSearchResultRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */; }; + 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */; }; + 4EECA4E32D2C7D530080A863 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */; }; + 4EECA4E62D2C7D650080A863 /* PhotoCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */; }; + 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */; }; + 4EECA4EF2D2C9B310080A863 /* ItemImageDetailsHeaderSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4EE2D2C9B260080A863 /* ItemImageDetailsHeaderSection.swift */; }; + 4EECA4F12D2C9E860080A863 /* ItemImageDetailsDetailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F02D2C9E7B0080A863 /* ItemImageDetailsDetailsSection.swift */; }; + 4EECA4F32D2CA5A10080A863 /* ItemImageDetailsDeleteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F22D2CA59B0080A863 /* ItemImageDetailsDeleteButton.swift */; }; + 4EECA4F52D2CAA380080A863 /* RatingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F42D2CAA350080A863 /* RatingType.swift */; }; + 4EECA4F62D2CAA380080A863 /* RatingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EECA4F42D2CAA350080A863 /* RatingType.swift */; }; 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 */; }; @@ -217,6 +254,11 @@ 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF18B292CB993AD00343666 /* ListRow.swift */; }; 4EF3D80B2CF7D6670081AD20 /* ServerUserAccessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */; }; 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */; }; + 4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */; }; + 4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */; }; + 4EFAC1332D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */; }; + 4EFAC1362D1FB1A100E40880 /* AccessTagSearchResultsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC1352D1FB1A100E40880 /* AccessTagSearchResultsSection.swift */; }; + 4EFAC1382D1FB26600E40880 /* TagInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFAC1372D1FB26600E40880 /* TagInput.swift */; }; 4EFD172E2CE4182200A4BAC5 /* LearnMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */; }; 4EFE0C7D2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; }; 4EFE0C7E2D0156A900D4834D /* PersonKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */; }; @@ -358,6 +400,10 @@ BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3957782C113EC40078CEF8 /* SubtitleSection.swift */; }; BD39577C2C113FAA0078CEF8 /* TimestampSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */; }; BD39577E2C1140810078CEF8 /* TransitionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD39577D2C1140810078CEF8 /* TransitionSection.swift */; }; + BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */; }; + BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */; }; + BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */; }; + BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */; }; C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; }; C44FA6E02AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */; }; C44FA6E12AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */; }; @@ -497,7 +543,6 @@ E11BDF972B865F550045C54A /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF962B865F550045C54A /* ItemTag.swift */; }; E11BDF982B865F550045C54A /* ItemTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11BDF962B865F550045C54A /* ItemTag.swift */; }; E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */; }; - E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */; }; E11CEB8928998549003E74C7 /* BottomEdgeGradientModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E19E551E2897326C003CE330 /* BottomEdgeGradientModifier.swift */; }; E11CEB8B28998552003E74C7 /* View-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8A28998552003E74C7 /* View-iOS.swift */; }; E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CEB8C28999B4A003E74C7 /* Font.swift */; }; @@ -758,8 +803,7 @@ E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A282BF3046A004DF6AB /* AddUserButton.swift */; }; E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */; }; E1763A642BF3C9AA004DF6AB /* ListRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */; }; - E1763A662BF3CA83004DF6AB /* FullScreenMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */; }; - E1763A6A2BF3D177004DF6AB /* PublicUserRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserRow.swift */; }; + E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A692BF3D177004DF6AB /* PublicUserButton.swift */; }; E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */; }; E1763A742BF3FA4C004DF6AB /* AppLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */; }; @@ -786,8 +830,6 @@ E17FB55728C1256400311DFE /* CastAndCrewHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */; }; E17FB55928C125E900311DFE /* StudiosHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55828C125E900311DFE /* StudiosHStack.swift */; }; E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55A28C1266400311DFE /* GenresHStack.swift */; }; - E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1803EA02BFBD6CF0039F90E /* Hashable.swift */; }; - E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1803EA02BFBD6CF0039F90E /* Hashable.swift */; }; E18121062CBE428000682985 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528728FD229500600579 /* ChevronButton.swift */; }; E18295E429CAC6F100F91ED0 /* BasicNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */; }; E18443CB2A037773002DDDC8 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E18443CA2A037773002DDDC8 /* UDPBroadcast */; }; @@ -877,7 +919,6 @@ E193D5432719407E00900D82 /* tvOSMainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */; }; E193D547271941C500900D82 /* SelectUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D546271941C500900D82 /* SelectUserView.swift */; }; E193D549271941CC00900D82 /* UserSignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D548271941CC00900D82 /* UserSignInView.swift */; }; - E193D54B271941D300900D82 /* SelectServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54A271941D300900D82 /* SelectServerView.swift */; }; E193D5502719430400900D82 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D54F2719430400900D82 /* ServerDetailView.swift */; }; E193D5512719432400900D82 /* ServerConnectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerConnectionViewModel.swift */; }; E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */; }; @@ -921,6 +962,8 @@ E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */; }; E1A42E4F28CBD3E100A14DCB /* HomeErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */; }; E1A42E5128CBE44500A14DCB /* LandscapePosterProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */; }; + E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; }; + E1A5056B2D0B733F007EE305 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A505692D0B733F007EE305 /* Optional.swift */; }; E1A7B1652B9A9F7800152546 /* PreferencesView in Frameworks */ = {isa = PBXBuildFile; productRef = E1A7B1642B9A9F7800152546 /* PreferencesView */; }; E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */; }; E1A7F0DF2BD4EC7400620DDD /* Dictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */; }; @@ -1181,6 +1224,7 @@ 4E10C8182CC045690012CC9F /* CustomDeviceNameSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceNameSection.swift; sourceTree = ""; }; 4E10C81C2CC0465F0012CC9F /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; }; 4E12F9152CBE9615006C217E /* DeviceType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceType.swift; sourceTree = ""; }; + 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageInfo.swift; sourceTree = ""; }; 4E14DC022CD43DCB001B621B /* AdminDashboardCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDashboardCoordinator.swift; sourceTree = ""; }; 4E16FD502C0183DB00110147 /* LetterPickerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerButton.swift; sourceTree = ""; }; 4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = ""; }; @@ -1188,6 +1232,7 @@ 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; 4E182C9B2C94993200FBEFD5 /* ServerTasksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksView.swift; sourceTree = ""; }; 4E182C9E2C94A1E000FBEFD5 /* ServerTaskRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskRow.swift; sourceTree = ""; }; + 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfo.swift; sourceTree = ""; }; 4E204E582C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeSettingsCoordinator.swift; sourceTree = ""; }; 4E2182E42CAF67EF0094806B /* PlayMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayMethod.swift; sourceTree = ""; }; 4E2470062D078DD7009139D8 /* ServerUserParentalRatingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserParentalRatingView.swift; sourceTree = ""; }; @@ -1214,18 +1259,22 @@ 4E35CE682CBED95F00DBD886 /* DayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayOfWeek.swift; sourceTree = ""; }; 4E35CE6B2CBEDB7300DBD886 /* TaskState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = ""; }; 4E36395A2CC4DF0900110EBC /* APIKeysViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysViewModel.swift; sourceTree = ""; }; + 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImageInfoViewModel.swift; sourceTree = ""; }; 4E3A24D92CFE349A0083A72C /* SearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsSection.swift; sourceTree = ""; }; 4E3A24DB2CFE35CC0083A72C /* NameInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameInput.swift; sourceTree = ""; }; + 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesViewModel.swift; sourceTree = ""; }; + 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesView.swift; sourceTree = ""; }; + 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemImageView.swift; sourceTree = ""; }; 4E49DECA2CE54A9200352DCD /* SessionsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsSection.swift; sourceTree = ""; }; 4E49DECC2CE54C7200352DCD /* PermissionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionSection.swift; sourceTree = ""; }; 4E49DECE2CE54D2700352DCD /* MaxBitratePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaxBitratePolicy.swift; sourceTree = ""; }; 4E49DED12CE54D6900352DCD /* ActiveSessionsPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsPolicy.swift; sourceTree = ""; }; 4E49DED42CE54D9C00352DCD /* LoginFailurePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFailurePolicy.swift; sourceTree = ""; }; 4E49DED72CE5509000352DCD /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; - 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPicker.swift; sourceTree = ""; }; - 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareImageCropView.swift; sourceTree = ""; }; 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlayUserAccessType.swift; sourceTree = ""; }; - 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePicker.swift; sourceTree = ""; }; + 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImagePickerView.swift; sourceTree = ""; }; + 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitLoginWindowView.swift; sourceTree = ""; }; + 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalServerButton.swift; sourceTree = ""; }; 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudioEditorViewModel.swift; sourceTree = ""; }; 4E4E9C692CFEDC9D00A6946F /* PeopleEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleEditorViewModel.swift; sourceTree = ""; }; 4E5071D62CFCEB6F003FA2AD /* TagEditorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagEditorViewModel.swift; sourceTree = ""; }; @@ -1234,6 +1283,7 @@ 4E5334A12CD1A28400D59FA8 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; 4E537A832D03D10B00659A1A /* ServerUserDeviceAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDeviceAccessView.swift; sourceTree = ""; }; 4E537A8B2D04410E00659A1A /* ServerUserLiveTVAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserLiveTVAccessView.swift; sourceTree = ""; }; + 4E5508722D13AFE3002A5345 /* UserProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImage.swift; sourceTree = ""; }; 4E556AAF2D036F5E00733377 /* UserPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPermissions.swift; sourceTree = ""; }; 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 = ""; }; @@ -1263,10 +1313,13 @@ 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 = ""; }; + 4E6C27072C8BD0AD00FD2185 /* ServerSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSessionDetailView.swift; sourceTree = ""; }; 4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = ""; }; + 4E7315732D14770E00EA2A95 /* UserProfileHeroImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileHeroImage.swift; sourceTree = ""; }; 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = ""; }; + 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurgeUnusedStrings.swift; sourceTree = ""; }; 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = ""; }; + 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsCoordinator.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 = ""; }; @@ -1282,6 +1335,8 @@ 4E90F7612CC72B1F00417C31 /* EditServerTaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerTaskView.swift; sourceTree = ""; }; 4E97D1822D064748004B89AD /* ItemSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSection.swift; sourceTree = ""; }; 4E97D1842D064B43004B89AD /* RefreshMetadataButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshMetadataButton.swift; sourceTree = ""; }; + 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarMenuButton.swift; sourceTree = ""; }; + 4E98F7C92D123AD4001E7518 /* View-tvOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View-tvOS.swift"; sourceTree = ""; }; 4E9A24E52C82B5A50023DA83 /* CustomDeviceProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileSettingsView.swift; sourceTree = ""; }; 4E9A24E72C82B6190023DA83 /* CustomProfileButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomProfileButton.swift; sourceTree = ""; }; 4E9A24EA2C82B9ED0023DA83 /* CustomDeviceProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDeviceProfileCoordinator.swift; sourceTree = ""; }; @@ -1289,6 +1344,13 @@ 4EA09DE02CC4E4F000CB27E4 /* APIKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysView.swift; sourceTree = ""; }; 4EA09DE32CC4E85700CB27E4 /* APIKeysRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeysRow.swift; sourceTree = ""; }; 4EA397452CD31CB900904C25 /* AddServerUserViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserViewModel.swift; sourceTree = ""; }; + 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagesCoordinator.swift; sourceTree = ""; }; + 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsView.swift; sourceTree = ""; }; + 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoPickerView.swift; sourceTree = ""; }; + 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemPhotoCropView.swift; sourceTree = ""; }; + 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImagePickerCoordinator.swift; sourceTree = ""; }; + 4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySection.swift; sourceTree = ""; }; + 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = ""; }; 4EB1404B2C8E45B1008691F3 /* StreamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamSection.swift; sourceTree = ""; }; 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = ""; }; 4EB1A8CB2C9B1B9700F43898 /* DestructiveServerTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveServerTask.swift; sourceTree = ""; }; @@ -1316,12 +1378,29 @@ 4EC2B1A82CC97C0400D866BE /* ServerUserDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserDetailsView.swift; sourceTree = ""; }; 4EC50D602C934B3A00FC3D0E /* ServerTasksViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTasksViewModel.swift; sourceTree = ""; }; 4EC6C16A2C92999800FC904B /* TranscodeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeSection.swift; sourceTree = ""; }; + 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphabetizeStrings.swift; sourceTree = ""; }; 4ECDAA9D2C920A8E0030F2F5 /* TranscodeReason.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscodeReason.swift; sourceTree = ""; }; + 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccessScheduleView.swift; sourceTree = ""; }; + 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicDayOfWeek.swift; sourceTree = ""; }; + 4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleView.swift; sourceTree = ""; }; + 4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessScheduleRow.swift; sourceTree = ""; }; 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = ""; }; 4EE11E192CC6A50D004BF852 /* ActivityBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityBadge.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressSection.swift; sourceTree = ""; }; 4EE07CBA2D08B19100B0B636 /* ErrorMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMessage.swift; sourceTree = ""; }; 4EE141682C8BABDF0045B661 /* ActiveSessionProgressSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionProgressSection.swift; sourceTree = ""; }; + 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemView.swift; sourceTree = ""; }; + 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifyItemViewModel.swift; sourceTree = ""; }; + 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResult.swift; sourceTree = ""; }; + 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResultRow.swift; sourceTree = ""; }; + 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSearchResultView.swift; sourceTree = ""; }; + 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = ""; }; + 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCropView.swift; sourceTree = ""; }; + 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileImageCropView.swift; sourceTree = ""; }; + 4EECA4EE2D2C9B260080A863 /* ItemImageDetailsHeaderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsHeaderSection.swift; sourceTree = ""; }; + 4EECA4F02D2C9E7B0080A863 /* ItemImageDetailsDetailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsDetailsSection.swift; sourceTree = ""; }; + 4EECA4F22D2CA59B0080A863 /* ItemImageDetailsDeleteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemImageDetailsDeleteButton.swift; sourceTree = ""; }; + 4EECA4F42D2CAA350080A863 /* RatingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingType.swift; sourceTree = ""; }; 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 = ""; }; @@ -1331,6 +1410,11 @@ 4EF18B292CB993AD00343666 /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; 4EF3D8092CF7D6670081AD20 /* ServerUserAccessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserAccessView.swift; sourceTree = ""; }; 4EF659E22CDD270B00E0BE5D /* ActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionMenu.swift; sourceTree = ""; }; + 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditServerUserAccessTagsView.swift; sourceTree = ""; }; + 4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccessTagRow.swift; sourceTree = ""; }; + 4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserAccessTagsView.swift; sourceTree = ""; }; + 4EFAC1352D1FB1A100E40880 /* AccessTagSearchResultsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessTagSearchResultsSection.swift; sourceTree = ""; }; + 4EFAC1372D1FB26600E40880 /* TagInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagInput.swift; sourceTree = ""; }; 4EFD172D2CE4181F00A4BAC5 /* LearnMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LearnMoreButton.swift; sourceTree = ""; }; 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonKind.swift; sourceTree = ""; }; 4EFE0C7F2D02054300D4834D /* ItemArrayElements.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemArrayElements.swift; sourceTree = ""; }; @@ -1456,6 +1540,9 @@ BD3957782C113EC40078CEF8 /* SubtitleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSection.swift; sourceTree = ""; }; BD39577B2C113FAA0078CEF8 /* TimestampSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimestampSection.swift; sourceTree = ""; }; BD39577D2C1140810078CEF8 /* TransitionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionSection.swift; sourceTree = ""; }; + BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserBottomBar.swift; sourceTree = ""; }; + BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityView.swift; sourceTree = ""; }; + BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileSettingsView.swift; sourceTree = ""; }; C40CD924271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTypeLibraryViewModel.swift; sourceTree = ""; }; C44FA6DE2AACD19C00EDEB56 /* LiveSmallPlaybackButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveSmallPlaybackButton.swift; sourceTree = ""; }; C44FA6DF2AACD19C00EDEB56 /* LiveLargePlaybackButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLargePlaybackButtons.swift; sourceTree = ""; }; @@ -1685,8 +1772,7 @@ E1763A282BF3046A004DF6AB /* AddUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddUserButton.swift; sourceTree = ""; }; E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserGridButton.swift; sourceTree = ""; }; E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowButton.swift; sourceTree = ""; }; - E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMenu.swift; sourceTree = ""; }; - E1763A692BF3D177004DF6AB /* PublicUserRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserRow.swift; sourceTree = ""; }; + E1763A692BF3D177004DF6AB /* PublicUserButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicUserButton.swift; sourceTree = ""; }; E1763A702BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftfinStore+Mappings.swift"; sourceTree = ""; }; E1763A732BF3FA4C004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = ""; }; E1763A752BF3FF01004DF6AB /* AppLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLoadingView.swift; sourceTree = ""; }; @@ -1706,7 +1792,6 @@ E17FB55628C1256400311DFE /* CastAndCrewHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewHStack.swift; sourceTree = ""; }; E17FB55828C125E900311DFE /* StudiosHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudiosHStack.swift; sourceTree = ""; }; E17FB55A28C1266400311DFE /* GenresHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenresHStack.swift; sourceTree = ""; }; - E1803EA02BFBD6CF0039F90E /* Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashable.swift; sourceTree = ""; }; E185920528CDAA6400326F80 /* CastAndCrewHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastAndCrewHStack.swift; sourceTree = ""; }; E185920728CDAAA200326F80 /* SimilarItemsHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarItemsHStack.swift; sourceTree = ""; }; E185920928CEF23A00326F80 /* FocusGuide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusGuide.swift; sourceTree = ""; }; @@ -1763,7 +1848,6 @@ E193D5422719407E00900D82 /* tvOSMainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainCoordinator.swift; sourceTree = ""; }; E193D546271941C500900D82 /* SelectUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectUserView.swift; sourceTree = ""; }; E193D548271941CC00900D82 /* UserSignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSignInView.swift; sourceTree = ""; }; - E193D54A271941D300900D82 /* SelectServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectServerView.swift; sourceTree = ""; }; E193D54F2719430400900D82 /* ServerDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailView.swift; sourceTree = ""; }; E193D552271943D500900D82 /* tvOSMainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSMainTabCoordinator.swift; sourceTree = ""; }; E19D41A62BEEDC450082B8B2 /* UserLocalSecurityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocalSecurityViewModel.swift; sourceTree = ""; }; @@ -1790,6 +1874,7 @@ E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicItemSelector.swift; sourceTree = ""; }; E1A42E4E28CBD3E100A14DCB /* HomeErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeErrorView.swift; sourceTree = ""; }; E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandscapePosterProgressBar.swift; sourceTree = ""; }; + E1A505692D0B733F007EE305 /* Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Optional.swift; sourceTree = ""; }; E1A7F0DE2BD4EC7400620DDD /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = ""; }; E1A8FDEB2C0574A800D0A51C /* ListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRow.swift; sourceTree = ""; }; E1AA331C2782541500F6439C /* PrimaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryButton.swift; sourceTree = ""; }; @@ -2229,6 +2314,27 @@ path = ServerLogsView; sourceTree = ""; }; + 4E37F6182D17EB220022AADD /* ItemImages */ = { + isa = PBXGroup; + children = ( + 4E4593A52D04E4DE00E277E1 /* AddItemImageView.swift */, + 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */, + 4E4593A22D04E2AF00E277E1 /* ItemImagesView.swift */, + 4EA78B1E2D2B5A960093BFCE /* ItemPhotoPickerView */, + ); + path = ItemImages; + sourceTree = ""; + }; + 4E37F6192D17EB3C0022AADD /* ItemMetadata */ = { + isa = PBXGroup; + children = ( + 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, + 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, + 4E6619FF2CEFE39000025C99 /* EditMetadataView */, + ); + path = ItemMetadata; + sourceTree = ""; + }; 4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */ = { isa = PBXGroup; children = ( @@ -2241,8 +2347,7 @@ 4E49DEDE2CE55F7F00352DCD /* Components */ = { isa = PBXGroup; children = ( - 4E49DEDC2CE55F7F00352DCD /* PhotoPicker.swift */, - 4E49DEDD2CE55F7F00352DCD /* SquareImageCropView.swift */, + 4EECA4EC2D2C89D20080A863 /* UserProfileImageCropView.swift */, ); path = Components; sourceTree = ""; @@ -2251,11 +2356,28 @@ isa = PBXGroup; children = ( 4E49DEDE2CE55F7F00352DCD /* Components */, - 4E49DEE52CE5616800352DCD /* UserProfileImagePicker.swift */, + 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */, ); path = UserProfileImagePicker; sourceTree = ""; }; + 4E4DAC3A2D11F54300E13FF9 /* ConnectToServerView */ = { + isa = PBXGroup; + children = ( + 4E4DAC3B2D11F69000E13FF9 /* Components */, + 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, + ); + path = ConnectToServerView; + sourceTree = ""; + }; + 4E4DAC3B2D11F69000E13FF9 /* Components */ = { + isa = PBXGroup; + children = ( + 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */, + ); + path = Components; + sourceTree = ""; + }; 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */ = { isa = PBXGroup; children = ( @@ -2307,25 +2429,14 @@ 4E63B9F52C8A5BEF00C25378 /* AdminDashboardView */ = { isa = PBXGroup; children = ( - 4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */, - 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */, - 4EB7C8D32CCED318000CC011 /* AddServerUserView */, - 4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */, + 4EFE80842D3EF80E0029CCB6 /* ActiveSessions */, 4E63B9F42C8A5BEF00C25378 /* AdminDashboardView.swift */, 4EA09DDF2CC4E4D000CB27E4 /* APIKeyView */, E1DE64902CC6F06C00E423B6 /* Components */, - 4E10C80F2CC030B20012CC9F /* DeviceDetailsView */, - 4EED87492CBF824B002354D2 /* DevicesView */, - 4E90F7622CC72B1F00417C31 /* EditServerTaskView */, + 4EFE80852D3EF8270029CCB6 /* ServerDevices */, 4E35CE622CBED3FF00DBD886 /* ServerLogsView */, - 4E182C9A2C94991800FBEFD5 /* ServerTasksView */, - 4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */, - 4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */, - 4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */, - 4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */, - 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */, - 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */, - 4EC2B1992CC96E5E00D866BE /* ServerUsersView */, + 4ECF5D8C2D0A780F00F066B1 /* ServerTasks */, + 4EFE80862D3EF8640029CCB6 /* ServerUsers */, ); path = AdminDashboardView; sourceTree = ""; @@ -2423,6 +2534,7 @@ children = ( 4E699BBF2CB34775007CBD5D /* HomeSection.swift */, 4E97D1822D064748004B89AD /* ItemSection.swift */, + 4EAE340B2D42B852006FBAD3 /* LibrarySection.swift */, ); path = Sections; sourceTree = ""; @@ -2430,19 +2542,37 @@ 4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */ = { isa = PBXGroup; children = ( - 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */, + 4E6C27072C8BD0AD00FD2185 /* ServerSessionDetailView.swift */, 4EB1A8D32C9B91A200F43898 /* Components */, ); path = ActiveSessionDetailView; sourceTree = ""; }; + 4E7315722D14752400EA2A95 /* UserProfileImage */ = { + isa = PBXGroup; + children = ( + 4E7315732D14770E00EA2A95 /* UserProfileHeroImage.swift */, + 4E5508722D13AFE3002A5345 /* UserProfileImage.swift */, + ); + path = UserProfileImage; + sourceTree = ""; + }; + 4E75B34D2D16583900D16531 /* Translations */ = { + isa = PBXGroup; + children = ( + 4EC71FBB2D161FE300D0B3A8 /* AlphabetizeStrings.swift */, + 4E75B34A2D164AC100D16531 /* PurgeUnusedStrings.swift */, + ); + path = Translations; + sourceTree = ""; + }; 4E8F74A32CE03D3100CC8969 /* ItemEditorView */ = { isa = PBXGroup; children = ( - 4E5071E22CFCEFC3003FA2AD /* AddItemElementView */, 4E8F74A62CE03D4C00CC8969 /* Components */, - 4E31EFA22CFFFB410053DFE7 /* EditItemElementView */, - 4E6619FF2CEFE39000025C99 /* EditMetadataView */, + 4E37F6182D17EB220022AADD /* ItemImages */, + 4E37F6192D17EB3C0022AADD /* ItemMetadata */, + 4EE766F32D131F6E009658F0 /* IdentifyItemView */, 4E8F74A42CE03D3800CC8969 /* ItemEditorView.swift */, ); path = ItemEditorView; @@ -2461,7 +2591,10 @@ children = ( 4E8F74AA2CE03DC600CC8969 /* DeleteItemViewModel.swift */, 4E5071D52CFCEB03003FA2AD /* ItemEditorViewModel */, + 4E45939D2D04E1E600E277E1 /* ItemImagesViewModel.swift */, + 4EE766F62D132043009658F0 /* IdentifyItemViewModel.swift */, 4E8F74B02CE03EAF00CC8969 /* RefreshMetadataViewModel.swift */, + 4E37F6152D17C1710022AADD /* RemoteImageInfoViewModel.swift */, ); path = ItemAdministration; sourceTree = ""; @@ -2496,6 +2629,31 @@ path = EditServerTaskView; sourceTree = ""; }; + 4E98F7C82D123AD4001E7518 /* Modifiers */ = { + isa = PBXGroup; + children = ( + 4E98F7C12D123AD4001E7518 /* NavigationBarMenuButton.swift */, + ); + path = Modifiers; + sourceTree = ""; + }; + 4E98F7CA2D123AD4001E7518 /* View */ = { + isa = PBXGroup; + children = ( + 4E98F7C82D123AD4001E7518 /* Modifiers */, + 4E98F7C92D123AD4001E7518 /* View-tvOS.swift */, + ); + path = View; + sourceTree = ""; + }; + 4E98F7CB2D123AD4001E7518 /* Extensions */ = { + isa = PBXGroup; + children = ( + 4E98F7CA2D123AD4001E7518 /* View */, + ); + path = Extensions; + sourceTree = ""; + }; 4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */ = { isa = PBXGroup; children = ( @@ -2531,6 +2689,42 @@ path = Components; sourceTree = ""; }; + 4EA78B152D2A0C4A0093BFCE /* ItemImageDetailsView */ = { + isa = PBXGroup; + children = ( + 4EA78B1B2D2A266A0093BFCE /* Components */, + 4EA78B142D2A0C4A0093BFCE /* ItemImageDetailsView.swift */, + ); + path = ItemImageDetailsView; + sourceTree = ""; + }; + 4EA78B1B2D2A266A0093BFCE /* Components */ = { + isa = PBXGroup; + children = ( + 4EECA4F22D2CA59B0080A863 /* ItemImageDetailsDeleteButton.swift */, + 4EECA4F02D2C9E7B0080A863 /* ItemImageDetailsDetailsSection.swift */, + 4EECA4EE2D2C9B260080A863 /* ItemImageDetailsHeaderSection.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4EA78B1E2D2B5A960093BFCE /* ItemPhotoPickerView */ = { + isa = PBXGroup; + children = ( + 4EA78B212D2B5CDD0093BFCE /* Components */, + 4EA78B1F2D2B5A9E0093BFCE /* ItemPhotoPickerView.swift */, + ); + path = ItemPhotoPickerView; + sourceTree = ""; + }; + 4EA78B212D2B5CDD0093BFCE /* Components */ = { + isa = PBXGroup; + children = ( + 4EA78B222D2B5CEF0093BFCE /* ItemPhotoCropView.swift */, + ); + path = Components; + sourceTree = ""; + }; 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */ = { isa = PBXGroup; children = ( @@ -2641,6 +2835,93 @@ path = ServerUserDetailsView; sourceTree = ""; }; + 4EC71FBA2D161FD800D0B3A8 /* Scripts */ = { + isa = PBXGroup; + children = ( + 4E75B34D2D16583900D16531 /* Translations */, + ); + path = Scripts; + sourceTree = ""; + }; + 4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */ = { + isa = PBXGroup; + children = ( + 4ECF5D812D0A3D0200F066B1 /* AddAccessScheduleView.swift */, + ); + path = AddAccessScheduleView; + sourceTree = ""; + }; + 4ECF5D8C2D0A780F00F066B1 /* ServerTasks */ = { + isa = PBXGroup; + children = ( + 4E35CE5B2CBED3F300DBD886 /* AddTaskTriggerView */, + 4E90F7622CC72B1F00417C31 /* EditServerTaskView */, + 4E182C9A2C94991800FBEFD5 /* ServerTasksView */, + ); + path = ServerTasks; + sourceTree = ""; + }; + 4ED25C9F2D07E20C0010333C /* ServerUserAccessSchedule */ = { + isa = PBXGroup; + children = ( + 4ECF5D822D0A3D0200F066B1 /* AddAccessScheduleView */, + 4ED25CA52D07E64F0010333C /* EditAccessScheduleView */, + ); + path = ServerUserAccessSchedule; + sourceTree = ""; + }; + 4ED25CA32D07E4990010333C /* Components */ = { + isa = PBXGroup; + children = ( + 4ED25CA22D07E4990010333C /* EditAccessScheduleRow.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4ED25CA52D07E64F0010333C /* EditAccessScheduleView */ = { + isa = PBXGroup; + children = ( + 4ED25CA32D07E4990010333C /* Components */, + 4ED25CA02D07E3520010333C /* EditAccessScheduleView.swift */, + ); + path = EditAccessScheduleView; + sourceTree = ""; + }; + 4EE766F32D131F6E009658F0 /* IdentifyItemView */ = { + isa = PBXGroup; + children = ( + 4EE767062D13401C009658F0 /* Components */, + 4EE766F42D131FB7009658F0 /* IdentifyItemView.swift */, + ); + path = IdentifyItemView; + sourceTree = ""; + }; + 4EE767062D13401C009658F0 /* Components */ = { + isa = PBXGroup; + children = ( + 4EE767092D135CAC009658F0 /* RemoteSearchResultView.swift */, + 4EE767072D134020009658F0 /* RemoteSearchResultRow.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4EECA4E12D2C7D450080A863 /* PhotoPickerView */ = { + isa = PBXGroup; + children = ( + 4EECA4E42D2C7D570080A863 /* Components */, + 4EECA4E22D2C7D530080A863 /* PhotoPickerView.swift */, + ); + path = PhotoPickerView; + sourceTree = ""; + }; + 4EECA4E42D2C7D570080A863 /* Components */ = { + isa = PBXGroup; + children = ( + 4EECA4E52D2C7D650080A863 /* PhotoCropView.swift */, + ); + path = Components; + sourceTree = ""; + }; 4EED87472CBF824B002354D2 /* Components */ = { isa = PBXGroup; children = ( @@ -2692,6 +2973,93 @@ path = ServerUserAccessView; sourceTree = ""; }; + 4EFAC12A2D1E253300E40880 /* ServerUserAccessTags */ = { + isa = PBXGroup; + children = ( + 4EFAC1312D1E373B00E40880 /* AddServerUserAccessTagsView */, + 4EFAC12D2D1E2C4700E40880 /* EditServerUserAccessTagsView */, + ); + path = ServerUserAccessTags; + sourceTree = ""; + }; + 4EFAC12D2D1E2C4700E40880 /* EditServerUserAccessTagsView */ = { + isa = PBXGroup; + children = ( + 4EFAC12F2D1E2EB900E40880 /* Components */, + 4EFAC12B2D1E255600E40880 /* EditServerUserAccessTagsView.swift */, + ); + path = EditServerUserAccessTagsView; + sourceTree = ""; + }; + 4EFAC12F2D1E2EB900E40880 /* Components */ = { + isa = PBXGroup; + children = ( + 4EFAC12E2D1E2EB900E40880 /* EditAccessTagRow.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4EFAC1312D1E373B00E40880 /* AddServerUserAccessTagsView */ = { + isa = PBXGroup; + children = ( + 4EFAC1342D1FB19700E40880 /* Components */, + 4EFAC1322D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift */, + ); + path = AddServerUserAccessTagsView; + sourceTree = ""; + }; + 4EFAC1342D1FB19700E40880 /* Components */ = { + isa = PBXGroup; + children = ( + 4EFAC1352D1FB1A100E40880 /* AccessTagSearchResultsSection.swift */, + 4EFAC1372D1FB26600E40880 /* TagInput.swift */, + ); + path = Components; + sourceTree = ""; + }; + 4EFE80842D3EF80E0029CCB6 /* ActiveSessions */ = { + isa = PBXGroup; + children = ( + 4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */, + 4EB1A8CF2C9B2FA200F43898 /* ActiveSessionsView */, + ); + path = ActiveSessions; + sourceTree = ""; + }; + 4EFE80852D3EF8270029CCB6 /* ServerDevices */ = { + isa = PBXGroup; + children = ( + 4E10C80F2CC030B20012CC9F /* DeviceDetailsView */, + 4EED87492CBF824B002354D2 /* DevicesView */, + ); + path = ServerDevices; + sourceTree = ""; + }; + 4EFE80862D3EF8640029CCB6 /* ServerUsers */ = { + isa = PBXGroup; + children = ( + 4EB7C8D32CCED318000CC011 /* AddServerUserView */, + 4EC2B1A72CC9725400D866BE /* ServerUserDetailsView */, + 4EFE80882D3EF9150029CCB6 /* ServerUserSettings */, + 4EC2B1992CC96E5E00D866BE /* ServerUsersView */, + ); + path = ServerUsers; + sourceTree = ""; + }; + 4EFE80882D3EF9150029CCB6 /* ServerUserSettings */ = { + isa = PBXGroup; + children = ( + 4EFAC12A2D1E253300E40880 /* ServerUserAccessTags */, + 4E2470072D078DD7009139D8 /* ServerUserParentalRatingView */, + 4E537A822D03D0FA00659A1A /* ServerUserDeviceAccessView */, + 4E537A8C2D04410E00659A1A /* ServerUserLiveTVAccessView */, + 4ED25C9F2D07E20C0010333C /* ServerUserAccessSchedule */, + 4EF3D80A2CF7D6670081AD20 /* ServerUserAccessView */, + 4EB538B32CE3C75900EB72D5 /* ServerUserPermissionsView */, + ); + path = ServerUserSettings; + sourceTree = ""; + }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { isa = PBXGroup; children = ( @@ -2792,6 +3160,7 @@ children = ( E12186DF2718F2030010884C /* App */, 536D3D77267BB9650004248C /* Components */, + 4E98F7CB2D123AD4001E7518 /* Extensions */, E185920B28CEF23F00326F80 /* Objects */, E1DABAD62A26E28E008AC34A /* Resources */, E12186E02718F23B0010884C /* Views */, @@ -2882,7 +3251,6 @@ E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */, E1C92618288756BD002A7A66 /* DotHStack.swift */, E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */, - E1763A652BF3CA83004DF6AB /* FullScreenMenu.swift */, E1549677296CB22B00C4EF88 /* InlineEnumToggle.swift */, E1A42E5028CBE44500A14DCB /* LandscapePosterProgressBar.swift */, E1763A632BF3C9AA004DF6AB /* ListRowButton.swift */, @@ -2894,6 +3262,7 @@ E1E9EFE928C6B96400CC1F8B /* ServerButton.swift */, E17885A3278105170094FBCF /* SFSymbolButton.swift */, E12E30F0296383810022FAC9 /* SplitFormWindowView.swift */, + 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */, E187A60429AD2E25008387E6 /* StepperView.swift */, ); path = Components; @@ -2908,6 +3277,7 @@ 534D4FE126A7D7CC000A7A48 /* Translations */, 5377CBF2263B596A003A4E83 /* Products */, 53D5E3DB264B47EE00BADDC8 /* Frameworks */, + 4EC71FBA2D161FD800D0B3A8 /* Scripts */, ); sourceTree = ""; }; @@ -3166,14 +3536,15 @@ E133328729538D8D00EE76AB /* Files.swift */, E11CEB8C28999B4A003E74C7 /* Font.swift */, E10432F52BE4426F006FF9DD /* FormatStyle.swift */, - E1803EA02BFBD6CF0039F90E /* Hashable.swift */, E1E6C44A29AED2B70064123F /* HorizontalAlignment.swift */, E139CC1E28EC83E400688DE2 /* Int.swift */, E1AD105226D96D5F003E4A08 /* JellyfinAPI */, E174120E29AE9D94003EF3B5 /* NavigationCoordinatable.swift */, E150C0B82BFD44E900944FFA /* Nuke */, + E1A505692D0B733F007EE305 /* Optional.swift */, E1B4E4362CA7795200DC49DE /* OrderedDictionary.swift */, E1B490432967E26300D3EDCE /* PersistentLogHandler.swift */, + 4EECA4F42D2CAA350080A863 /* RatingType.swift */, E1B5861129E32EEF00E45D6E /* Sequence.swift */, E145EB442BE0AD4E003BF6F3 /* Set.swift */, 621338922660107500A81A2A /* String.swift */, @@ -3218,6 +3589,8 @@ 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */, 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */, 4E8F74A02CE03C8B00CC8969 /* ItemEditorCoordinator.swift */, + 4EA78B242D2B5DB20093BFCE /* ItemImagePickerCoordinator.swift */, + 4EA78B112D29F6240093BFCE /* ItemImagesCoordinator.swift */, 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */, E102312B2BCF8A08009D71FC /* LiveTVCoordinator */, C46DD8D12A8DC1F60046A504 /* LiveVideoPlayerCoordinator.swift */, @@ -3229,6 +3602,7 @@ 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */, E13DD4012717EE79009D4DAF /* SelectUserCoordinator.swift */, 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */, + 4E8274F32D2ECF0200F5E610 /* UserProfileSettingsCoordinator.swift */, E11C15342BF7C505006BC9B6 /* UserProfileImageCoordinator.swift */, E13DD3F127179378009D4DAF /* UserSignInCoordinator.swift */, E18A8E8428D60D0000333B9A /* VideoPlayerCoordinator.swift */, @@ -3260,6 +3634,15 @@ path = Sections; sourceTree = ""; }; + BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */ = { + isa = PBXGroup; + children = ( + BDFF67AD2D2CA59A009A9A3A /* UserLocalSecurityView.swift */, + BDFF67AE2D2CA59A009A9A3A /* UserProfileSettingsView.swift */, + ); + path = UserProfileSettingsView; + sourceTree = ""; + }; C44FA6DD2AACD15300EDEB56 /* PlaybackButtons */ = { isa = PBXGroup; children = ( @@ -3627,7 +4010,7 @@ E1763A752BF3FF01004DF6AB /* AppLoadingView.swift */, E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */, E10231522BCF8AF8009D71FC /* ChannelLibraryView */, - 53ABFDEA2679753200886593 /* ConnectToServerView.swift */, + 4E4DAC3A2D11F54300E13FF9 /* ConnectToServerView */, E154967B296CBB1A00C4EF88 /* FontPickerView.swift */, E1A42E4D28CBD3B200A14DCB /* HomeView */, E12376B22A33DFAC001F5B44 /* ItemOverviewView.swift */, @@ -3639,7 +4022,6 @@ E10B1E8C2BD7708900A92EAF /* QuickConnectView.swift */, E1E1643928BAC2EF00323B0A /* SearchView.swift */, 4EF18B232CB9932F00343666 /* PagingLibraryView */, - E193D54A271941D300900D82 /* SelectServerView.swift */, E164A8122BE4995200A54B18 /* SelectUserView */, E193D54F2719430400900D82 /* ServerDetailView.swift */, E1E5D54D2783E66600692DFE /* SettingsView */, @@ -3712,6 +4094,7 @@ E19F6C5C28F5189300C5197E /* MediaStreamInfoView.swift */, E103DF922BCF2F23000229B2 /* MediaView */, E1EDA8D62B92C9D700F9A57E /* PagingLibraryView */, + 4EECA4E12D2C7D450080A863 /* PhotoPickerView */, E10231342BCF8A3C009D71FC /* ProgramsView */, E1171A1828A2212600FA1AF5 /* QuickConnectView.swift */, 4EF10D4C2CE2EC5A000ED5F5 /* ResetUserPasswordView */, @@ -3719,6 +4102,7 @@ E10B1EAF2BD9769500A92EAF /* SelectUserView */, E19D41AB2BF288110082B8B2 /* ServerCheckView.swift */, E1E5D54A2783E26100692DFE /* SettingsView */, + 4E49DEDF2CE55F7F00352DCD /* UserProfileImagePicker */, E1171A1A28A2215800FA1AF5 /* UserSignInView */, E193D5452719418B00900D82 /* VideoPlayer */, ); @@ -3789,7 +4173,6 @@ isa = PBXGroup; children = ( 6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */, - 4E49DEDF2CE55F7F00352DCD /* UserProfileImagePicker */, E1EA09872BEE9CF3004CDE76 /* UserLocalSecurityView.swift */, E1BE1CEF2BDB6C97008176A9 /* UserProfileSettingsView.swift */, ); @@ -3859,6 +4242,7 @@ children = ( E1763A282BF3046A004DF6AB /* AddUserButton.swift */, E1763A262BF303C9004DF6AB /* ServerSelectionMenu.swift */, + BDA623522D0D0854009A157F /* SelectUserBottomBar.swift */, E1763A2A2BF3046E004DF6AB /* UserGridButton.swift */, ); path = Components; @@ -3955,7 +4339,7 @@ E1763A682BF3D16E004DF6AB /* Components */ = { isa = PBXGroup; children = ( - E1763A692BF3D177004DF6AB /* PublicUserRow.swift */, + E1763A692BF3D177004DF6AB /* PublicUserButton.swift */, ); path = Components; sourceTree = ""; @@ -4245,9 +4629,11 @@ 4E17498D2CC00A2E00DD07D1 /* DeviceInfo.swift */, 4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */, 4E12F9152CBE9615006C217E /* DeviceType.swift */, - 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */, E1CB75712C80E71800217C76 /* DirectPlayProfile.swift */, + 4ECF5D892D0A57EF00F066B1 /* DynamicDayOfWeek.swift */, E1722DB029491C3900CC0239 /* ImageBlurHashes.swift */, + 4E13FAD72D18D5AD007785F6 /* ImageInfo.swift */, + 4EB132EE2D2CF6D300B5A8E5 /* ImageType.swift */, E1D842902933F87500D1041A /* ItemFields.swift */, E148128728C154BF003B8787 /* ItemFilter+ItemTrait.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, @@ -4262,7 +4648,10 @@ 4EFE0C7C2D0156A500D4834D /* PersonKind.swift */, E1ED7FDA2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift */, 4E2182E42CAF67EF0094806B /* PlayMethod.swift */, + 4E1AA0032D0640A400524970 /* RemoteImageInfo.swift */, + 4EE766F92D13294F009658F0 /* RemoteSearchResult.swift */, 4E35CE652CBED8B300DBD886 /* ServerTicks.swift */, + 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */, 4EDBDCD02CBDD6510033D347 /* SessionInfo.swift */, E148128428C15472003B8787 /* SortOrder+ItemSortOrder.swift */, E1DA654B28E69B0500592A73 /* SpecialFeatureType.swift */, @@ -4304,6 +4693,8 @@ E1047E2227E5880000CB0D4A /* SystemImageContentView.swift */, E1A1528928FD22F600600579 /* TextPairView.swift */, E1EBCB41278BD174009FE6E9 /* TruncatedText.swift */, + 4E7315722D14752400EA2A95 /* UserProfileImage */, + E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */, E1B5784028F8AFCB00D42911 /* WrappedView.swift */, ); path = Components; @@ -4368,7 +4759,6 @@ E1BE1CEC2BDB68C4008176A9 /* Components */ = { isa = PBXGroup; children = ( - E1BE1CED2BDB68CD008176A9 /* UserProfileRow.swift */, ); path = Components; sourceTree = ""; @@ -4568,6 +4958,7 @@ E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */, 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */, 5398514426B64DA100101B49 /* SettingsView.swift */, + BDFF67AF2D2CA59A009A9A3A /* UserProfileSettingsView */, E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */, ); path = SettingsView; @@ -4658,6 +5049,7 @@ isa = PBXNativeTarget; buildConfigurationList = 535870712669D21700D05A09 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS" */; buildPhases = ( + 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */, 6286F0A3271C0ABA00C40ED5 /* Run Swiftgen.swift */, BD83D7852B55EEB600652C24 /* Run SwiftFormat */, 5358705C2669D21600D05A09 /* Sources */, @@ -4705,6 +5097,7 @@ isa = PBXNativeTarget; buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "Swiftfin iOS" */; buildPhases = ( + 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */, 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */, BD0BA2282AD64BB200306A8D /* Run SwiftFormat */, 5377CBED263B596A003A4E83 /* Sources */, @@ -4894,6 +5287,46 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 4EC71FBC2D16201C00D0B3A8 /* Alphabetize Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Translations/en.lproj/Localizable.strings", + ); + name = "Alphabetize Strings"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/alphabetizeStrings.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n"; + }; + 4EC71FBD2D1620AF00D0B3A8 /* Alphabetize Strings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Translations/en.lproj/Localizable.strings", + ); + name = "Alphabetize Strings"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/alphabetizeStrings.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "xcrun --sdk macosx swift \"${SRCROOT}/Scripts/Translations/AlphabetizeStrings.swift\"\n"; + }; 6286F09E271C093000C40ED5 /* Run Swiftgen.swift */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -4996,11 +5429,15 @@ E1575E99293E7B1E001665B1 /* UIColor.swift in Sources */, E1575E92293E7B1E001665B1 /* CGSize.swift in Sources */, E1575E7E293E77B5001665B1 /* ItemFilterCollection.swift in Sources */, + 4E98F7D22D123AD4001E7518 /* NavigationBarMenuButton.swift in Sources */, + 4E98F7D32D123AD4001E7518 /* View-tvOS.swift in Sources */, C46DD8EF2A8FB56E0046A504 /* LiveBottomBarView.swift in Sources */, + 4EE766FB2D132954009658F0 /* RemoteSearchResult.swift in Sources */, C46DD8EA2A8FB45C0046A504 /* LiveOverlay.swift in Sources */, E11E376D293E9CC1009EF240 /* VideoPlayerCoordinator.swift in Sources */, E1575E6F293E77B5001665B1 /* GestureAction.swift in Sources */, E10B1ED12BD9AFF200A92EAF /* V2UserModel.swift in Sources */, + 4E7315762D1485CC00EA2A95 /* UserProfileHeroImage.swift in Sources */, E18A8E7E28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, E1E6C43B29AECBD30064123F /* BottomBarView.swift in Sources */, E190704C2C858CEB0004600E /* VideoPlayerType+Shared.swift in Sources */, @@ -5009,6 +5446,7 @@ 4EF18B2A2CB993BD00343666 /* ListRow.swift in Sources */, 4EF659E32CDD270D00E0BE5D /* ActionMenu.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, + 4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */, 4E97D1832D064748004B89AD /* ItemSection.swift in Sources */, E145EB232BDCCA43003BF6F3 /* BulletedList.swift in Sources */, E104DC972B9E7E29008F506D /* AssertionFailureView.swift in Sources */, @@ -5019,11 +5457,11 @@ E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */, E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */, E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */, + 4E13FAD82D18D5AF007785F6 /* ImageInfo.swift in Sources */, E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */, C40CD926271F8D1E000FB198 /* ItemTypeLibraryViewModel.swift in Sources */, E13D98EE2D0664C1005FE96D /* NotificationSet.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 */, @@ -5068,7 +5506,6 @@ E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, E1575E95293E7B1E001665B1 /* Font.swift in Sources */, 4E49DED02CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */, - E11C15362BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, E172D3AE2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, 4EC1C8532C7FDFA300E2879E /* PlaybackDeviceProfile.swift in Sources */, 4E24ECFC2D076F6200A473A9 /* ListRowCheckbox.swift in Sources */, @@ -5114,7 +5551,6 @@ E1C9261A288756BD002A7A66 /* PosterButton.swift in Sources */, E1CB75782C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */, E1575E5F293E77B5001665B1 /* StreamType.swift in Sources */, - E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */, 4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */, E1388A42293F0AAD009721B1 /* PreferenceUIHostingSwizzling.swift in Sources */, E1575E93293E7B1E001665B1 /* Double.swift in Sources */, @@ -5125,13 +5561,14 @@ E1763A2B2BF3046E004DF6AB /* UserGridButton.swift in Sources */, E1EF473A289A0F610034046B /* TruncatedText.swift in Sources */, E1C926112887565C002A7A66 /* ActionButtonHStack.swift in Sources */, + 4E8274F52D2ECF1900F5E610 /* UserProfileSettingsCoordinator.swift in Sources */, E178859B2780F1F40094FBCF /* tvOSSlider.swift in Sources */, E103DF952BCF31CD000229B2 /* MediaItem.swift in Sources */, E1ED91192B95993300802036 /* TitledLibraryParent.swift in Sources */, 62E632E1267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */, + 4ECF5D8B2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */, 62E632ED267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 5398514526B64DA100101B49 /* SettingsView.swift in Sources */, - E193D54B271941D300900D82 /* SelectServerView.swift in Sources */, E1575E91293E7B1E001665B1 /* URL.swift in Sources */, 53ABFDE6267974EF00886593 /* SettingsViewModel.swift in Sources */, E10B1EC22BD9AD6100A92EAF /* V1UserModel.swift in Sources */, @@ -5149,6 +5586,7 @@ E1E2F8462B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */, E1575E80293E77CF001665B1 /* VideoPlayerViewModel.swift in Sources */, E1C926122887565C002A7A66 /* SeriesItemContentView.swift in Sources */, + 4EECA4F62D2CAA380080A863 /* RatingType.swift in Sources */, E193D53727193F8700900D82 /* MediaCoordinator.swift in Sources */, E12CC1CB28D1333400678D5D /* CinematicResumeItemView.swift in Sources */, 4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */, @@ -5171,7 +5609,6 @@ E1E2F8402B757DFA00B75998 /* OnFinalDisappearModifier.swift in Sources */, E17DC74B2BE740D900B42379 /* StoredValues+Server.swift in Sources */, 4E0253BD2CBF0C06007EB9CD /* DeviceType.swift in Sources */, - 4E5071D82CFCEB75003FA2AD /* TagEditorViewModel.swift in Sources */, E10E842A29A587110064EA49 /* LoadingView.swift in Sources */, E193D53927193F8E00900D82 /* SearchCoordinator.swift in Sources */, E13316FF2ADE42B6009BF865 /* OnSizeChangedModifier.swift in Sources */, @@ -5199,8 +5636,8 @@ E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */, E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */, E18A17F0298C68B700C22F62 /* Overlay.swift in Sources */, - 4E5071DB2CFCEC1D003FA2AD /* GenreEditorViewModel.swift in Sources */, 4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */, + 4E7315752D1485C900EA2A95 /* UserProfileImage.swift in Sources */, E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */, 4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */, E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */, @@ -5214,7 +5651,9 @@ 4E884C652CEBB301004CF6AD /* LearnMoreModal.swift in Sources */, E1B4E4372CA7795200DC49DE /* OrderedDictionary.swift in Sources */, E1AD104E26D96CE3003E4A08 /* BaseItemDto.swift in Sources */, + 4EAE340C2D42B857006FBAD3 /* LibrarySection.swift in Sources */, E118959E289312020042947B /* BaseItemPerson+Poster.swift in Sources */, + 4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */, 62E632DD267D2E130063E547 /* SearchViewModel.swift in Sources */, BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, E1575EA2293E7B1E001665B1 /* Color.swift in Sources */, @@ -5234,11 +5673,12 @@ E11042762B8013DF00821020 /* Stateful.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, E1575E66293E77B5001665B1 /* Poster.swift in Sources */, - 4E4E9C682CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */, E18E021F2887492B0022598C /* SystemImageContentView.swift in Sources */, E19D41B42BF2C0020082B8B2 /* StoredValues+Temp.swift in Sources */, 4EF18B282CB9936D00343666 /* ListColumnsPickerView.swift in Sources */, E11BDF7B2B85529D0045C54A /* SupportedCaseIterable.swift in Sources */, + BDFF67B02D2CA59A009A9A3A /* UserLocalSecurityView.swift in Sources */, + BDFF67B22D2CA59A009A9A3A /* UserProfileSettingsView.swift in Sources */, 4E204E592C574FD9004D22A2 /* CustomizeSettingsCoordinator.swift in Sources */, E1575E8C293E7B1E001665B1 /* UIScreen.swift in Sources */, C46DD8EC2A8FB49A0046A504 /* LiveMainOverlay.swift in Sources */, @@ -5254,6 +5694,7 @@ 4E661A1B2CEFE54800025C99 /* BoxSetDisplayOrder.swift in Sources */, E129428628F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, + BDFF67B32D2CA99D009A9A3A /* UserProfileRow.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, E149CCAE2BE6ECC8008B9331 /* Storable.swift in Sources */, C46DD8D92A8DC2990046A504 /* LiveNativeVideoPlayer.swift in Sources */, @@ -5265,7 +5706,7 @@ E1575E7D293E77B5001665B1 /* PosterDisplayType.swift in Sources */, E1E5D553278419D900692DFE /* ConfirmCloseOverlay.swift in Sources */, E18A17F2298C68BB00C22F62 /* MainOverlay.swift in Sources */, - E1763A6A2BF3D177004DF6AB /* PublicUserRow.swift in Sources */, + E1763A6A2BF3D177004DF6AB /* PublicUserButton.swift in Sources */, E1E6C44B29AED2B70064123F /* HorizontalAlignment.swift in Sources */, 4E35CE672CBED8B600DBD886 /* ServerTicks.swift in Sources */, E193D549271941CC00900D82 /* UserSignInView.swift in Sources */, @@ -5273,7 +5714,6 @@ E148128628C15475003B8787 /* SortOrder+ItemSortOrder.swift in Sources */, E1CB75722C80E71800217C76 /* DirectPlayProfile.swift in Sources */, E1E1E24E28DF8A2E000DF5FD /* PreferenceKeys.swift in Sources */, - 4E4E9C6B2CFEDCA400A6946F /* PeopleEditorViewModel.swift in Sources */, E1575E9B293E7B1E001665B1 /* EnvironmentValue+Keys.swift in Sources */, E133328929538D8D00EE76AB /* Files.swift in Sources */, E154967A296CB4B000C4EF88 /* VideoPlayerSettingsView.swift in Sources */, @@ -5343,6 +5783,8 @@ E18E021C2887492B0022598C /* BlurView.swift in Sources */, E187F7682B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, E1E6C44729AECD5D0064123F /* PlayPreviousItemActionButton.swift in Sources */, + 4E1AA0052D0640AA00524970 /* RemoteImageInfo.swift in Sources */, + E1A5056B2D0B733F007EE305 /* Optional.swift in Sources */, E1E6C44E29AEE9DC0064123F /* SmallMenuOverlay.swift in Sources */, E1CB75832C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */, E10B1ECB2BD9AF8200A92EAF /* SwiftfinStore+V1.swift in Sources */, @@ -5351,6 +5793,7 @@ E1D90D772C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */, E10231582BCF8AF8009D71FC /* WideChannelGridItem.swift in Sources */, E15D4F082B1B12C300442DB8 /* Backport.swift in Sources */, + BDA623532D0D0854009A157F /* SelectUserBottomBar.swift in Sources */, E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */, E1575E9A293E7B1E001665B1 /* Array.swift in Sources */, E1575E8D293E7B1E001665B1 /* URLComponents.swift in Sources */, @@ -5368,6 +5811,7 @@ E1A1528E28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */, E18E021D2887492B0022598C /* AttributeStyleModifier.swift in Sources */, E1CAF6632BA363840087D991 /* UIHostingController.swift in Sources */, + 4EB132F02D2CF6D600B5A8E5 /* ImageType.swift in Sources */, E10231492BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */, E1CB75702C80E66700217C76 /* CommaStringBuilder.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */, @@ -5380,7 +5824,6 @@ E193D553271943D500900D82 /* tvOSMainTabCoordinator.swift in Sources */, 4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */, E174121029AE9D94003EF3B5 /* NavigationCoordinatable.swift in Sources */, - E1763A662BF3CA83004DF6AB /* FullScreenMenu.swift in Sources */, E14EDECD2B8FB709000F00A4 /* ItemYear.swift in Sources */, E154965F296CA2EF00C4EF88 /* DownloadTask.swift in Sources */, E154967E296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift in Sources */, @@ -5408,10 +5851,12 @@ E1B33ECF28EB6EA90073B0FD /* OverlayMenu.swift in Sources */, E146A9D82BE6E9830034DA1E /* StoredValue.swift in Sources */, 6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */, + 4EFAC1362D1FB1A100E40880 /* AccessTagSearchResultsSection.swift in Sources */, E17AC96D2954E9CA003D2BC2 /* DownloadListView.swift in Sources */, 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */, 4E2470082D078DD7009139D8 /* ServerUserParentalRatingView.swift in Sources */, E1A1528828FD229500600579 /* ChevronButton.swift in Sources */, + 4ECF5D882D0A3D0200F066B1 /* AddAccessScheduleView.swift in Sources */, E1CB75732C80E71800217C76 /* DirectPlayProfile.swift in Sources */, E1B490472967E2E500D3EDCE /* CoreStore.swift in Sources */, 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, @@ -5453,7 +5898,7 @@ 4EB7C8D52CCED6E7000CC011 /* AddServerUserView.swift in Sources */, E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */, E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */, - E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */, + 4E4593A32D04E2B500E277E1 /* ItemImagesView.swift in Sources */, 4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */, 62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */, E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */, @@ -5499,6 +5944,7 @@ 4E661A102CEFE46300025C99 /* TitleSection.swift in Sources */, 4E661A112CEFE46300025C99 /* LockMetadataSection.swift in Sources */, 4E661A122CEFE46300025C99 /* MediaFormatSection.swift in Sources */, + 4EE766F52D131FBC009658F0 /* IdentifyItemView.swift in Sources */, 4E661A132CEFE46300025C99 /* EpisodeSection.swift in Sources */, 4E661A142CEFE46300025C99 /* DisplayOrderSection.swift in Sources */, 4E661A152CEFE46300025C99 /* LocalizationSection.swift in Sources */, @@ -5509,6 +5955,7 @@ E1A3E4CD2BB7D8C8005C59F8 /* Label-iOS.swift in Sources */, E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E12CC1BE28D11F4500678D5D /* RecentlyAddedView.swift in Sources */, + 4EE767082D13403F009658F0 /* RemoteSearchResultRow.swift in Sources */, E1ED7FD92CA8AF7400ACB6E3 /* ServerTaskObserver.swift in Sources */, E17AC96A2954D00E003D2BC2 /* URLResponse.swift in Sources */, 4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */, @@ -5531,7 +5978,9 @@ C46DD8E02A8DC7790046A504 /* LiveOverlay.swift in Sources */, E111D8F828D03BF900400001 /* PagingLibraryView.swift in Sources */, E187F7672B8E6A1C005400FE /* EnvironmentValue+Values.swift in Sources */, + 4E37F6162D17C1860022AADD /* RemoteImageInfoViewModel.swift in Sources */, 4E10C8172CC0455A0012CC9F /* CompatibilitiesSection.swift in Sources */, + 4EE7670A2D135CBA009658F0 /* RemoteSearchResultView.swift in Sources */, E1FA891B289A302300176FEB /* iPadOSCollectionItemView.swift in Sources */, E14E9DF12BCF7A99004E3371 /* ItemLetter.swift in Sources */, E1B5861229E32EEF00E45D6E /* Sequence.swift in Sources */, @@ -5539,11 +5988,15 @@ 4E2AC4C82C6C493C00DD600D /* SubtitleFormat.swift in Sources */, E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */, 4E661A2E2CEFE77700025C99 /* MetadataField.swift in Sources */, + 4EFAC1332D1E8C6B00E40880 /* AddServerUserAccessTagsView.swift in Sources */, E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, + 4E1AA0042D0640AA00524970 /* RemoteImageInfo.swift in Sources */, 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */, + E1A5056A2D0B733F007EE305 /* Optional.swift in Sources */, + 4EFAC1382D1FB26600E40880 /* TagInput.swift in Sources */, E102314D2BCF8A7E009D71FC /* AlternateLayoutView.swift in Sources */, E12CC1BB28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */, E1BE1CEE2BDB68CD008176A9 /* UserProfileRow.swift in Sources */, @@ -5563,12 +6016,14 @@ 4EBE064F2C7ECE8D004A6C03 /* InlineEnumToggle.swift in Sources */, E14EDEC82B8FB65F000F00A4 /* ItemFilterType.swift in Sources */, E1EBCB42278BD174009FE6E9 /* TruncatedText.swift in Sources */, + 4ED25CA12D07E3590010333C /* EditAccessScheduleView.swift in Sources */, 62133890265F83A900A81A2A /* MediaView.swift in Sources */, E13332942953BAA100EE76AB /* DownloadTaskContentView.swift in Sources */, 4E14DC032CD43DD2001B621B /* AdminDashboardCoordinator.swift in Sources */, E18E01E1288747230022598C /* EpisodeItemContentView.swift in Sources */, 4E8F74B22CE03EB000CC8969 /* RefreshMetadataViewModel.swift in Sources */, E129429B28F4A5E300796AC6 /* PlaybackSettingsView.swift in Sources */, + 4E4593A62D04E4E300E277E1 /* AddItemImageView.swift in Sources */, E1E9017B28DAAE4D001B1594 /* RoundedCorner.swift in Sources */, E18E01F2288747230022598C /* ActionButtonHStack.swift in Sources */, 4E2AC4C52C6C492700DD600D /* MediaContainer.swift in Sources */, @@ -5580,6 +6035,7 @@ E1CB75752C80EAFA00217C76 /* ArrayBuilder.swift in Sources */, E1047E2327E5880000CB0D4A /* SystemImageContentView.swift in Sources */, E1C8CE5B28FE512400DF5D7B /* CGPoint.swift in Sources */, + 4EECA4F52D2CAA380080A863 /* RatingType.swift in Sources */, 4E49DECF2CE54D3000352DCD /* MaxBitratePolicy.swift in Sources */, E18ACA922A15A32F00BB4F35 /* (null) in Sources */, E1A3E4C92BB74EA3005C59F8 /* LoadingCard.swift in Sources */, @@ -5606,6 +6062,7 @@ E1921B7428E61914003A5238 /* SpecialFeatureHStack.swift in Sources */, E118959D289312020042947B /* BaseItemPerson+Poster.swift in Sources */, E1D90D762C051D44000EA787 /* BackPort+ScrollIndicatorVisibility.swift in Sources */, + 4EECA4E62D2C7D650080A863 /* PhotoCropView.swift in Sources */, 6264E88C273850380081A12A /* Strings.swift in Sources */, E145EB252BE055AD003BF6F3 /* ServerResponse.swift in Sources */, E1BDF31729525F0400CC0294 /* AdvancedActionButton.swift in Sources */, @@ -5613,6 +6070,7 @@ 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, E10E67B72CF515130095365B /* Binding.swift in Sources */, E119696A2CC99EA9001A58BE /* ServerTaskProgressSection.swift in Sources */, + 4EA78B132D29F62E0093BFCE /* ItemImagesCoordinator.swift in Sources */, E1BAFE102BE921270069C4D7 /* SwiftfinApp+ValueObservation.swift in Sources */, E1ED7FDE2CAA641F00ACB6E3 /* ListTitleSection.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, @@ -5627,10 +6085,10 @@ 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, E129428528F080B500796AC6 /* OnReceiveNotificationModifier.swift in Sources */, E11E0E8C2BF7E76F007676DD /* DataCache.swift in Sources */, + 4E5508732D13AFED002A5345 /* UserProfileImage.swift in Sources */, E10231482BCF8A6D009D71FC /* ChannelLibraryViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, - 4E49DEE02CE55F7F00352DCD /* PhotoPicker.swift in Sources */, - 4E49DEE12CE55F7F00352DCD /* SquareImageCropView.swift in Sources */, + 4ED25CA42D07E4990010333C /* EditAccessScheduleRow.swift in Sources */, 4E90F7642CC72B1F00417C31 /* LastRunSection.swift in Sources */, 4E90F7652CC72B1F00417C31 /* EditServerTaskView.swift in Sources */, 4E90F7662CC72B1F00417C31 /* LastErrorSection.swift in Sources */, @@ -5640,6 +6098,7 @@ E129428828F0831F00796AC6 /* SplitTimestamp.swift in Sources */, C46DD8E72A8FA77F0046A504 /* LiveBottomBarView.swift in Sources */, 4EFE0C7E2D0156A900D4834D /* PersonKind.swift in Sources */, + 4E7315742D14772700EA2A95 /* UserProfileHeroImage.swift in Sources */, E11CEB8D28999B4A003E74C7 /* Font.swift in Sources */, E139CC1F28EC83E400688DE2 /* Int.swift in Sources */, E11895A9289383BC0042947B /* ScrollViewOffsetModifier.swift in Sources */, @@ -5664,6 +6123,7 @@ E1BE1CEA2BDB5AFE008176A9 /* UserGridButton.swift in Sources */, E1401CB129386C9200E8B599 /* UIColor.swift in Sources */, E1E2F8452B757E3400B75998 /* SinceLastDisappearModifier.swift in Sources */, + 4EECA4F32D2CA5A10080A863 /* ItemImageDetailsDeleteButton.swift in Sources */, E18E01AB288746AF0022598C /* PillHStack.swift in Sources */, E19070492C84F2BB0004600E /* ButtonStyle-iOS.swift in Sources */, E1401CAB2938140A00E8B599 /* LightAppIcon.swift in Sources */, @@ -5673,12 +6133,14 @@ E18E0207288749200022598C /* AttributeStyleModifier.swift in Sources */, E1002B642793CEE800E47059 /* ChapterInfo.swift in Sources */, 4E661A012CEFE39D00025C99 /* EditMetadataView.swift in Sources */, + 4E45939E2D04E20000E277E1 /* ItemImagesViewModel.swift in Sources */, C46DD8E52A8FA6510046A504 /* LiveTopBarView.swift in Sources */, E18E01AD288746AF0022598C /* DotHStack.swift in Sources */, E170D107294D23BA0017224C /* MediaSourceInfoCoordinator.swift in Sources */, E102313B2BCF8A3C009D71FC /* ProgramProgressOverlay.swift in Sources */, 4E01446D2D0292E200193038 /* Trie.swift in Sources */, E1937A61288F32DB00CB80AA /* Poster.swift in Sources */, + 4EA78B162D2A0C4A0093BFCE /* ItemImageDetailsView.swift in Sources */, 4E2182E62CAF67F50094806B /* PlayMethod.swift in Sources */, E145EB482BE0C136003BF6F3 /* ScrollIfLargerThanContainerModifier.swift in Sources */, E1CB757C2C80F00D00217C76 /* TranscodingProfile.swift in Sources */, @@ -5697,6 +6159,7 @@ E1D3044428D1991900587289 /* LibraryViewTypeToggle.swift in Sources */, C45C36542A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */, E1CB75822C80F66900217C76 /* VideoPlayerType+Swiftfin.swift in Sources */, + 4ECF5D8A2D0A57EF00F066B1 /* DynamicDayOfWeek.swift in Sources */, E148128B28C15526003B8787 /* ItemSortBy.swift in Sources */, E10231412BCF8A3C009D71FC /* ChannelLibraryView.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, @@ -5725,6 +6188,7 @@ C46DD8E22A8DC7FB0046A504 /* LiveMainOverlay.swift in Sources */, 4E17498E2CC00A3100DD07D1 /* DeviceInfo.swift in Sources */, 4EC1C86D2C80903A00E2879E /* CustomProfileButton.swift in Sources */, + 4E13FAD92D18D5AF007785F6 /* ImageInfo.swift in Sources */, 4EED87512CBF84AD002354D2 /* DevicesViewModel.swift in Sources */, E1FE69A728C29B720021BC93 /* ProgressBar.swift in Sources */, E13332912953B91000EE76AB /* DownloadTaskCoordinator.swift in Sources */, @@ -5742,6 +6206,7 @@ E1CAF6622BA363840087D991 /* UIHostingController.swift in Sources */, E11895AC289383EE0042947B /* NavigationBarOffsetModifier.swift in Sources */, E1CD13EF28EF364100CB46CA /* DetectOrientationModifier.swift in Sources */, + 4EFAC12C2D1E255900E40880 /* EditServerUserAccessTagsView.swift in Sources */, E157563029355B7900976E1F /* UpdateView.swift in Sources */, E1D8424F2932F7C400D1041A /* OverviewView.swift in Sources */, E113133628BE98AA00930F75 /* FilterDrawerButton.swift in Sources */, @@ -5808,6 +6273,7 @@ E1A1528D28FD23AC00600579 /* VideoPlayerSettingsCoordinator.swift in Sources */, 4E35CE642CBED69600DBD886 /* TaskTriggerType.swift in Sources */, E18E01EE288747230022598C /* AboutView.swift in Sources */, + 4EB132EF2D2CF6D600B5A8E5 /* ImageType.swift in Sources */, 62E632E0267D30CA0063E547 /* ItemLibraryViewModel.swift in Sources */, E1B33EB028EA890D0073B0FD /* Equatable.swift in Sources */, E1549662296CA2EF00C4EF88 /* UserSession.swift in Sources */, @@ -5816,13 +6282,15 @@ E10B1ECD2BD9AFD800A92EAF /* SwiftfinStore+V2.swift in Sources */, E1401CA92938140700E8B599 /* DarkAppIcon.swift in Sources */, E1A1529028FD23D600600579 /* PlaybackSettingsCoordinator.swift in Sources */, + 4EA78B252D2B5DBD0093BFCE /* ItemImagePickerCoordinator.swift in Sources */, E11042752B8013DF00821020 /* Stateful.swift in Sources */, E1AA331F2782639D00F6439C /* OverlayType.swift in Sources */, E12376AE2A33D680001F5B44 /* AboutView+Card.swift in Sources */, E1A2C154279A7D5A005EC829 /* UIApplication.swift in Sources */, - 4E49DEE62CE5616800352DCD /* UserProfileImagePicker.swift in Sources */, - 4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */, + 4E49DEE62CE5616800352DCD /* UserProfileImagePickerView.swift in Sources */, + 4E6C27082C8BD0AD00FD2185 /* ServerSessionDetailView.swift in Sources */, E11C15352BF7C505006BC9B6 /* UserProfileImageCoordinator.swift in Sources */, + 4EA78B232D2B5CFC0093BFCE /* ItemPhotoCropView.swift in Sources */, E1D8428F2933F2D900D1041A /* MediaSourceInfo.swift in Sources */, E1BDF2EC2952290200CC0294 /* AspectFillActionButton.swift in Sources */, BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */, @@ -5858,8 +6326,10 @@ E113133828BEADBA00930F75 /* LibraryParent.swift in Sources */, E17DC74D2BE7601E00B42379 /* SettingsBarButton.swift in Sources */, E190704F2C8592B40004600E /* PlaybackCompatibility+Video.swift in Sources */, + 4EFAC1302D1E2EB900E40880 /* EditAccessTagRow.swift in Sources */, E104DC962B9E7E29008F506D /* AssertionFailureView.swift in Sources */, E102312C2BCF8A08009D71FC /* iOSLiveTVCoordinator.swift in Sources */, + 4EECA4E32D2C7D530080A863 /* PhotoPickerView.swift in Sources */, E1ED7FDC2CAA4B6D00ACB6E3 /* PlayerStateInfo.swift in Sources */, E149CCAD2BE6ECC8008B9331 /* Storable.swift in Sources */, E1CB75792C80ECF100217C76 /* VideoPlayerType+Native.swift in Sources */, @@ -5905,12 +6375,14 @@ E1AD105F26D9ADDD003E4A08 /* NameGuidPair.swift in Sources */, 4E556AB02D036F6900733377 /* UserPermissions.swift in Sources */, E18A8E7D28D606BE00333B9A /* BaseItemDto+VideoPlayerViewModel.swift in Sources */, + 4EE766F72D132054009658F0 /* IdentifyItemViewModel.swift in Sources */, 4EC2B19B2CC96E7400D866BE /* ServerUsersView.swift in Sources */, E18E01F1288747230022598C /* PlayButton.swift in Sources */, E129429028F0BDC300796AC6 /* TimeStampType.swift in Sources */, E1F5CF092CB0A04500607465 /* Text.swift in Sources */, 4E182C9F2C94A1E000FBEFD5 /* ServerTaskRow.swift in Sources */, E1B490442967E26300D3EDCE /* PersistentLogHandler.swift in Sources */, + 4EA78B202D2B5AA30093BFCE /* ItemPhotoPickerView.swift in Sources */, E1CB756F2C80E66700217C76 /* CommaStringBuilder.swift in Sources */, E19D41AC2BF288110082B8B2 /* ServerCheckView.swift in Sources */, E1D5C39928DF914700CDBEFB /* CapsuleSlider.swift in Sources */, @@ -5925,10 +6397,12 @@ E1E5D54C2783E27200692DFE /* ExperimentalSettingsView.swift in Sources */, E111D8F528D03B7500400001 /* PagingLibraryViewModel.swift in Sources */, E16AF11C292C98A7001422A8 /* GestureSettingsView.swift in Sources */, + 4EE766FA2D132954009658F0 /* RemoteSearchResult.swift in Sources */, E1581E27291EF59800D6C640 /* SplitContentView.swift in Sources */, C46DD8DC2A8DC3420046A504 /* LiveVideoPlayer.swift in Sources */, E11BDF972B865F550045C54A /* ItemTag.swift in Sources */, E1D4BF8A2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */, + 4EECA4F12D2C9E860080A863 /* ItemImageDetailsDetailsSection.swift in Sources */, E1D37F482B9C648E00343D2B /* MaxHeightText.swift in Sources */, BD3957752C112A330078CEF8 /* ButtonSection.swift in Sources */, E1ED7FE32CAA6BAF00ACB6E3 /* ServerLogsViewModel.swift in Sources */, @@ -5949,6 +6423,7 @@ E1763A712BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, E1D8429529346C6400D1041A /* BasicStepper.swift in Sources */, + 4EECA4ED2D2C89D70080A863 /* UserProfileImageCropView.swift in Sources */, E18E01EA288747230022598C /* MovieItemView.swift in Sources */, 6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */, E164A7F42BE4736300A54B18 /* SignOutIntervalSection.swift in Sources */, @@ -5977,6 +6452,7 @@ E13DD4022717EE79009D4DAF /* SelectUserCoordinator.swift in Sources */, E11245B128D919CD00D8A977 /* Overlay.swift in Sources */, E145EB4D2BE1688E003BF6F3 /* SwiftinStore+UserState.swift in Sources */, + 4EECA4EF2D2C9B310080A863 /* ItemImageDetailsHeaderSection.swift in Sources */, 53EE24E6265060780068F029 /* SearchView.swift in Sources */, E164A8152BE58C2F00A54B18 /* V2AnyData.swift in Sources */, E1DC9841296DEBD800982F06 /* WatchedIndicator.swift in Sources */, diff --git a/Swiftfin/App/AppDelegate.swift b/Swiftfin/App/AppDelegate.swift index 285954fc4..f3fd39149 100644 --- a/Swiftfin/App/AppDelegate.swift +++ b/Swiftfin/App/AppDelegate.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVFAudio diff --git a/Swiftfin/App/SwiftfinApp+ValueObservation.swift b/Swiftfin/App/SwiftfinApp+ValueObservation.swift index 03eeaf941..fc8098d79 100644 --- a/Swiftfin/App/SwiftfinApp+ValueObservation.swift +++ b/Swiftfin/App/SwiftfinApp+ValueObservation.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/App/SwiftfinApp.swift b/Swiftfin/App/SwiftfinApp.swift index 66a0e5fd4..3636706a8 100644 --- a/Swiftfin/App/SwiftfinApp.swift +++ b/Swiftfin/App/SwiftfinApp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CoreStore @@ -54,7 +54,7 @@ struct SwiftfinApp: App { return mimeType.contains("svg") ? ImageDecoders.Empty() : nil } - ImagePipeline.shared = .Swiftfin.default + ImagePipeline.shared = .Swiftfin.posters // UIKit diff --git a/Swiftfin/Components/BasicStepper.swift b/Swiftfin/Components/BasicStepper.swift index bf7e6daef..5c48123fa 100644 --- a/Swiftfin/Components/BasicStepper.swift +++ b/Swiftfin/Components/BasicStepper.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/CircularProgressView.swift b/Swiftfin/Components/CircularProgressView.swift index 6099f821a..1ec7a6de2 100644 --- a/Swiftfin/Components/CircularProgressView.swift +++ b/Swiftfin/Components/CircularProgressView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/CountryPicker.swift b/Swiftfin/Components/CountryPicker.swift index 2c903a559..f8049df4d 100644 --- a/Swiftfin/Components/CountryPicker.swift +++ b/Swiftfin/Components/CountryPicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/DelayedProgressView.swift b/Swiftfin/Components/DelayedProgressView.swift index 1314ec659..a55e738f4 100644 --- a/Swiftfin/Components/DelayedProgressView.swift +++ b/Swiftfin/Components/DelayedProgressView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Components/DotHStack.swift b/Swiftfin/Components/DotHStack.swift index 6c496c19a..0a3f68222 100644 --- a/Swiftfin/Components/DotHStack.swift +++ b/Swiftfin/Components/DotHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/ErrorView.swift b/Swiftfin/Components/ErrorView.swift index e5f55bd3f..ba68f514d 100644 --- a/Swiftfin/Components/ErrorView.swift +++ b/Swiftfin/Components/ErrorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/GestureView.swift b/Swiftfin/Components/GestureView.swift index 409b193ed..9a48541ea 100644 --- a/Swiftfin/Components/GestureView.swift +++ b/Swiftfin/Components/GestureView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Components/HourMinutePicker.swift b/Swiftfin/Components/HourMinutePicker.swift index b5573d349..1ad1b7b67 100644 --- a/Swiftfin/Components/HourMinutePicker.swift +++ b/Swiftfin/Components/HourMinutePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/LandscapePosterProgressBar.swift b/Swiftfin/Components/LandscapePosterProgressBar.swift index 01f4ec918..e5bd4f12e 100644 --- a/Swiftfin/Components/LandscapePosterProgressBar.swift +++ b/Swiftfin/Components/LandscapePosterProgressBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/LanguagePicker.swift b/Swiftfin/Components/LanguagePicker.swift index 796855dca..e48816211 100644 --- a/Swiftfin/Components/LanguagePicker.swift +++ b/Swiftfin/Components/LanguagePicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/LearnMoreButton.swift b/Swiftfin/Components/LearnMoreButton.swift index c969b3d88..d4b9e4fea 100644 --- a/Swiftfin/Components/LearnMoreButton.swift +++ b/Swiftfin/Components/LearnMoreButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift index c0f33ac65..0960f7bea 100644 --- a/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift +++ b/Swiftfin/Components/LetterPickerBar/Components/LetterPickerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift index e4d37f902..f9275cb13 100644 --- a/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift +++ b/Swiftfin/Components/LetterPickerBar/LetterPickerBar.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/ListRow.swift b/Swiftfin/Components/ListRow.swift index 625485f69..260b81800 100644 --- a/Swiftfin/Components/ListRow.swift +++ b/Swiftfin/Components/ListRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/ListRowButton.swift b/Swiftfin/Components/ListRowButton.swift index da110b833..dc17bb2d8 100644 --- a/Swiftfin/Components/ListRowButton.swift +++ b/Swiftfin/Components/ListRowButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -13,36 +13,53 @@ import SwiftUI // Meant to be used within `List` or `Form` struct ListRowButton: View { - let title: String - let action: () -> Void + private let title: String + private let role: ButtonRole? + private let action: () -> Void - init(_ title: String, action: @escaping () -> Void) { + init(_ title: String, role: ButtonRole? = nil, action: @escaping () -> Void) { self.title = title + self.role = role self.action = action } var body: some View { - Button(title) { - action() - } - .font(.body.weight(.bold)) - .buttonStyle(ListRowButtonStyle()) - .listRowInsets(.init(.zero)) + Button(title, role: role, action: action) + .buttonStyle(ListRowButtonStyle()) + .listRowInsets(.zero) } } private struct ListRowButtonStyle: ButtonStyle { + @Environment(\.isEnabled) + private var isEnabled + + private func primaryStyle(configuration: Configuration) -> some ShapeStyle { + if configuration.role == .destructive || configuration.role == .cancel { + return AnyShapeStyle(Color.red) + } else { + return AnyShapeStyle(HierarchicalShapeStyle.primary) + } + } + + private func secondaryStyle(configuration: Configuration) -> some ShapeStyle { + if configuration.role == .destructive { + return AnyShapeStyle(Color.red.opacity(0.2)) + } else { + return isEnabled ? AnyShapeStyle(HierarchicalShapeStyle.secondary) : AnyShapeStyle(Color.gray) + } + } + func makeBody(configuration: Configuration) -> some View { ZStack { Rectangle() - .foregroundStyle(.secondary) + .fill(secondaryStyle(configuration: configuration)) configuration.label - .foregroundStyle(.primary) + .foregroundStyle(primaryStyle(configuration: configuration)) } .opacity(configuration.isPressed ? 0.75 : 1) - .frame(maxWidth: .infinity) - .listRowInsets(.zero) + .font(.body.weight(.bold)) } } diff --git a/Swiftfin/Components/ListTitleSection.swift b/Swiftfin/Components/ListTitleSection.swift index 752f0d81f..c9d22a467 100644 --- a/Swiftfin/Components/ListTitleSection.swift +++ b/Swiftfin/Components/ListTitleSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -25,6 +25,7 @@ struct ListTitleSection: View { Text(title) .font(.title3) .fontWeight(.semibold) + .multilineTextAlignment(.center) if let description { Text(description) diff --git a/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift b/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift index 71e5167ad..75244ac28 100644 --- a/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift +++ b/Swiftfin/Components/NavigationBarFilterDrawer/FilterDrawerButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift b/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift index ba3462ca2..ad624b573 100644 --- a/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift +++ b/Swiftfin/Components/NavigationBarFilterDrawer/NavigationBarFilterDrawer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/OrderedSectionSelectorView.swift b/Swiftfin/Components/OrderedSectionSelectorView.swift index 36b58a5dc..6f0b45777 100644 --- a/Swiftfin/Components/OrderedSectionSelectorView.swift +++ b/Swiftfin/Components/OrderedSectionSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/PillHStack.swift b/Swiftfin/Components/PillHStack.swift index 88cae310e..ce8707a7a 100644 --- a/Swiftfin/Components/PillHStack.swift +++ b/Swiftfin/Components/PillHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/PosterButton.swift b/Swiftfin/Components/PosterButton.swift index f282721c0..9010f6490 100644 --- a/Swiftfin/Components/PosterButton.swift +++ b/Swiftfin/Components/PosterButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -13,10 +13,14 @@ import SwiftUI // TODO: expose `ImageView.image` modifier for image aspect fill/fit // TODO: allow `content` to trigger `onSelect`? // - not in button label to avoid context menu visual oddities -// TODO: get width/height for images from layout size? // TODO: why don't shadows work with failure image views? // - due to `Color`? +/// Retrieving images by exact pixel dimensions is a bit +/// intense for normal usage and eases cache usage and modifications. +private let landscapeMaxWidth: CGFloat = 200 +private let portraitMaxWidth: CGFloat = 200 + struct PosterButton: View { private var item: Item @@ -29,9 +33,9 @@ struct PosterButton: View { private func imageView(from item: Item) -> ImageView { switch type { case .landscape: - ImageView(item.landscapeImageSources(maxWidth: 500)) + ImageView(item.landscapeImageSources(maxWidth: landscapeMaxWidth)) case .portrait: - ImageView(item.portraitImageSources(maxWidth: 200)) + ImageView(item.portraitImageSources(maxWidth: portraitMaxWidth)) } } diff --git a/Swiftfin/Components/PosterHStack.swift b/Swiftfin/Components/PosterHStack.swift index b8cd9d9d7..e0fd6d7b7 100644 --- a/Swiftfin/Components/PosterHStack.swift +++ b/Swiftfin/Components/PosterHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack @@ -26,7 +26,7 @@ struct PosterHStack: View wher private var padHStack: some View { CollectionHStack( uniqueElements: data, - columns: type == .portrait ? 140 : 220 + minWidth: type == .portrait ? 140 : 220 ) { item in PosterButton( item: item, diff --git a/Swiftfin/Components/PrimaryButton.swift b/Swiftfin/Components/PrimaryButton.swift index b65fe36a8..87cee22aa 100644 --- a/Swiftfin/Components/PrimaryButton.swift +++ b/Swiftfin/Components/PrimaryButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/SeeAllButton.swift b/Swiftfin/Components/SeeAllButton.swift index 8a47e193e..d50f94b46 100644 --- a/Swiftfin/Components/SeeAllButton.swift +++ b/Swiftfin/Components/SeeAllButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/SettingsBarButton.swift b/Swiftfin/Components/SettingsBarButton.swift index 08f1aef65..1d1828d93 100644 --- a/Swiftfin/Components/SettingsBarButton.swift +++ b/Swiftfin/Components/SettingsBarButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory @@ -31,29 +31,17 @@ struct SettingsBarButton: View { ZStack { Color.clear - RedrawOnNotificationView(.didChangeUserProfileImage) { - ImageView(user.profileImageSource( + UserProfileImage( + userID: user.id, + source: user.profileImageSource( client: server.client, maxWidth: 120 - )) - .pipeline(.Swiftfin.branding) - .image { image in - image - .posterBorder(ratio: 1 / 2, of: \.width) - .onAppear { - isUserImage = true - } - } - .placeholder { _ in - Color.clear - } - .onDisappear { - isUserImage = false - } + ), + pipeline: .Swiftfin.local + ) { + Color.clear } } - .aspectRatio(contentMode: .fill) - .clipShape(.circle) } } .accessibilityLabel(L10n.settings) diff --git a/Swiftfin/Components/Slider/CapsuleSlider.swift b/Swiftfin/Components/Slider/CapsuleSlider.swift index 0d64eb449..4bfd80623 100644 --- a/Swiftfin/Components/Slider/CapsuleSlider.swift +++ b/Swiftfin/Components/Slider/CapsuleSlider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/Slider/Slider.swift b/Swiftfin/Components/Slider/Slider.swift index af1077948..dd0a956b0 100644 --- a/Swiftfin/Components/Slider/Slider.swift +++ b/Swiftfin/Components/Slider/Slider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/Slider/ThumbSlider.swift b/Swiftfin/Components/Slider/ThumbSlider.swift index bd30a36f3..3d573ef92 100644 --- a/Swiftfin/Components/Slider/ThumbSlider.swift +++ b/Swiftfin/Components/Slider/ThumbSlider.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Components/SplitContentView.swift b/Swiftfin/Components/SplitContentView.swift index 3cda99250..51c134e93 100644 --- a/Swiftfin/Components/SplitContentView.swift +++ b/Swiftfin/Components/SplitContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/UnmaskSecureField.swift b/Swiftfin/Components/UnmaskSecureField.swift index a1b8015bd..d46860cec 100644 --- a/Swiftfin/Components/UnmaskSecureField.swift +++ b/Swiftfin/Components/UnmaskSecureField.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/UpdateView.swift b/Swiftfin/Components/UpdateView.swift index 8ea47ebfb..8f93a180f 100644 --- a/Swiftfin/Components/UpdateView.swift +++ b/Swiftfin/Components/UpdateView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Components/Video3DFormatPicker.swift b/Swiftfin/Components/Video3DFormatPicker.swift index cc783d720..f5916cdee 100644 --- a/Swiftfin/Components/Video3DFormatPicker.swift +++ b/Swiftfin/Components/Video3DFormatPicker.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Components/iOS15View.swift b/Swiftfin/Components/iOS15View.swift index 88cd13ec1..1c2373225 100644 --- a/Swiftfin/Components/iOS15View.swift +++ b/Swiftfin/Components/iOS15View.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/ButtonStyle-iOS.swift b/Swiftfin/Extensions/ButtonStyle-iOS.swift index b582c4634..5e3da819b 100644 --- a/Swiftfin/Extensions/ButtonStyle-iOS.swift +++ b/Swiftfin/Extensions/ButtonStyle-iOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -20,6 +20,7 @@ extension ButtonStyle where Self == ToolbarPillButtonStyle { } } +// TODO: don't take `Color`, take generic `ShapeStyle` struct ToolbarPillButtonStyle: ButtonStyle { @Environment(\.isEnabled) diff --git a/Swiftfin/Extensions/Label-iOS.swift b/Swiftfin/Extensions/Label-iOS.swift index 89251c698..02f82ad0d 100644 --- a/Swiftfin/Extensions/Label-iOS.swift +++ b/Swiftfin/Extensions/Label-iOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift b/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift index c32f81623..fb24906bf 100644 --- a/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift +++ b/Swiftfin/Extensions/View/Modifiers/DetectOrientationModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift index 57e651f96..6ab0cfc7c 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarCloseButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift index 52b1186dd..c6b363bf4 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift index 32606fafe..fe58581b2 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarDrawerButtons/NavigationBarDrawerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift index 6eed9adf1..0eaa8089f 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarMenuButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift index 5795cea91..2c5469c63 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetModifier.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift index da0797940..24753167b 100644 --- a/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift +++ b/Swiftfin/Extensions/View/Modifiers/NavigationBarOffset/NavigationBarOffsetView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Extensions/View/View-iOS.swift b/Swiftfin/Extensions/View/View-iOS.swift index d4d22ad4c..2a64f9936 100644 --- a/Swiftfin/Extensions/View/View-iOS.swift +++ b/Swiftfin/Extensions/View/View-iOS.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Objects/AppURLHandler.swift b/Swiftfin/Objects/AppURLHandler.swift index e66f41c0a..f6ad987da 100644 --- a/Swiftfin/Objects/AppURLHandler.swift +++ b/Swiftfin/Objects/AppURLHandler.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Objects/DeepLink.swift b/Swiftfin/Objects/DeepLink.swift index 526f7b03a..20d226b79 100644 --- a/Swiftfin/Objects/DeepLink.swift +++ b/Swiftfin/Objects/DeepLink.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/AboutAppView.swift b/Swiftfin/Views/AboutAppView.swift index 5ca359acf..fa9b1c2a2 100644 --- a/Swiftfin/Views/AboutAppView.swift +++ b/Swiftfin/Views/AboutAppView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift b/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift index a84054f4f..0c89dd38f 100644 --- a/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift +++ b/Swiftfin/Views/AdminDashboardView/APIKeyView/APIKeysView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift b/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift index 44549e1a1..f0d313d3e 100644 --- a/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift +++ b/Swiftfin/Views/AdminDashboardView/APIKeyView/Components/APIKeysRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -15,11 +15,11 @@ extension APIKeysView { struct APIKeysRow: View { - // MARK: - Actions + // MARK: - API Key Variables let apiKey: AuthenticationInfo - // MARK: - Actions + // MARK: - API Key Actions let onSelect: () -> Void let onDelete: () -> Void diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/Components/StreamSection.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift rename to Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/Components/StreamSection.swift index 023fb53d4..0e7327abe 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/StreamSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/Components/StreamSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/Components/TranscodeSection.swift similarity index 95% rename from Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift rename to Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/Components/TranscodeSection.swift index e1cfdf1eb..3701a48b6 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/Components/TranscodeSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/Components/TranscodeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/ServerSessionDetailView.swift similarity index 99% rename from Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift rename to Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/ServerSessionDetailView.swift index 4a24205fe..671fa73fd 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionDetailView/ActiveSessionDetailView.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionDetailView/ServerSessionDetailView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/ActiveSessionsView.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift rename to Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/ActiveSessionsView.swift index 90735f3ef..e49fdbcae 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/ActiveSessionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/ActiveSessionsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/Components/ActiveSessionProgressSection.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift rename to Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/Components/ActiveSessionProgressSection.swift index e736c298b..f05f61bf4 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionProgressSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/Components/ActiveSessionProgressSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/Components/ActiveSessionRow.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift rename to Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/Components/ActiveSessionRow.swift index 19c97f080..65cfaead1 100644 --- a/Swiftfin/Views/AdminDashboardView/ActiveSessionsView/Components/ActiveSessionRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ActiveSessions/ActiveSessionsView/Components/ActiveSessionRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift b/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift index 93d67c701..61a9b4094 100644 --- a/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift +++ b/Swiftfin/Views/AdminDashboardView/AdminDashboardView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift b/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift index 9b992e997..a0ced98a8 100644 --- a/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift +++ b/Swiftfin/Views/AdminDashboardView/Components/DeviceSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift b/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift index 4cf92d50f..ec524a418 100644 --- a/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift +++ b/Swiftfin/Views/AdminDashboardView/Components/UserSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift b/Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift index b24bebd17..71a6000a0 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/Components/Sections/CompatibilitiesSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift b/Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift similarity index 91% rename from Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift index 3e1661b0b..3016443fb 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/Components/Sections/CustomDeviceNameSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/DeviceDetailsView.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift rename to Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/DeviceDetailsView.swift index 538bf2f57..a708ab7d0 100644 --- a/Swiftfin/Views/AdminDashboardView/DeviceDetailsView/DeviceDetailsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerDevices/DeviceDetailsView/DeviceDetailsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift b/Swiftfin/Views/AdminDashboardView/ServerDevices/DevicesView/Components/DeviceRow.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerDevices/DevicesView/Components/DeviceRow.swift index 2b40e7151..46c79ef8b 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/Components/DeviceRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerDevices/DevicesView/Components/DeviceRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift b/Swiftfin/Views/AdminDashboardView/ServerDevices/DevicesView/DevicesView.swift similarity index 99% rename from Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift rename to Swiftfin/Views/AdminDashboardView/ServerDevices/DevicesView/DevicesView.swift index b420e7c0b..202d6b17f 100644 --- a/Swiftfin/Views/AdminDashboardView/DevicesView/DevicesView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerDevices/DevicesView/DevicesView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift b/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift index 180a72cb0..e24e050c5 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerLogsView/ServerLogsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -25,7 +25,7 @@ struct ServerLogsView: View { private var contentView: some View { List { ListTitleSection( - L10n.logs, + L10n.serverLogs, description: L10n.logsDescription ) { UIApplication.shared.open(URL(string: "https://jellyfin.org/docs/general/administration/troubleshooting")!) diff --git a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/AddTaskTriggerView.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/AddTaskTriggerView.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift index de366c1de..b56799f55 100644 --- a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/AddTaskTriggerView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/AddTaskTriggerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/DayOfWeekRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift similarity index 94% rename from Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/DayOfWeekRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift index 0a78f3001..f4b296b85 100644 --- a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/DayOfWeekRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/DayOfWeekRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/IntervalRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/IntervalRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift index bb59c35f0..b1d11169f 100644 --- a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/IntervalRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/IntervalRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TimeLimitSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TimeLimitSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift index 5d73bf487..63073ef93 100644 --- a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TimeLimitSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeLimitSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TimeRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift similarity index 94% rename from Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TimeRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift index 54b6c5b42..5d630bf50 100644 --- a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TimeRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TimeRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TriggerTypeRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TriggerTypeRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift index b196ed6e0..2f1ed6139 100644 --- a/Swiftfin/Views/AdminDashboardView/AddTaskTriggerView/Components/TriggerTypeRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/AddTaskTriggerView/Components/TriggerTypeRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/DetailsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift similarity index 89% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/DetailsSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift index 40d868f40..7cfafd7c9 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/DetailsSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/DetailsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/LastErrorSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift similarity index 89% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/LastErrorSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift index 60ccd9357..59086c4b2 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/LastErrorSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastErrorSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/LastRunSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift similarity index 94% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/LastRunSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift index c1c3a32d9..25ad2daab 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/LastRunSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/LastRunSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift index 25669db7c..bf9de7f6a 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/ServerTaskProgressSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // // diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/TriggersSection.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/TriggersSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift index ed7eb14b5..cc240750e 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/Sections/TriggersSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/Sections/TriggersSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/TriggerRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/TriggerRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift index 433404f7b..538e1e84c 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/Components/TriggerRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/Components/TriggerRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/EditServerTaskView.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift similarity index 81% rename from Swiftfin/Views/AdminDashboardView/EditServerTaskView/EditServerTaskView.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift index 9e64012cf..1e4c11028 100644 --- a/Swiftfin/Views/AdminDashboardView/EditServerTaskView/EditServerTaskView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/EditServerTaskView/EditServerTaskView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -12,23 +12,24 @@ import SwiftUI struct EditServerTaskView: View { + // MARK: - Observed & Environment Objects + @EnvironmentObject private var router: AdminDashboardCoordinator.Router @ObservedObject var observer: ServerTaskObserver - // MARK: - State Variables + // MARK: - Trigger Variables - @State - private var isPresentingDeleteConfirmation = false - @State - private var isPresentingEventAlert = false - @State - private var error: JellyfinAPIError? @State private var selectedTrigger: TaskTriggerInfo? + // MARK: - Error State + + @State + private var error: Error? + // MARK: - Body var body: some View { @@ -78,17 +79,8 @@ struct EditServerTaskView: View { switch event { case let .error(eventError): error = eventError - isPresentingEventAlert = true } } - .alert( - L10n.error, - isPresented: $isPresentingEventAlert, - presenting: error - ) { _ in - - } message: { error in - Text(error.localizedDescription) - } + .errorMessage($error) } } diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasksView/Components/DestructiveServerTask.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift similarity index 95% rename from Swiftfin/Views/AdminDashboardView/ServerTasksView/Components/DestructiveServerTask.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift index 2e35376ef..ced1c6a87 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasksView/Components/DestructiveServerTask.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/DestructiveServerTask.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasksView/Components/ServerTaskRow.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/ServerTasksView/Components/ServerTaskRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift index db926e33b..11f752732 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasksView/Components/ServerTaskRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/Components/ServerTaskRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerTasksView/ServerTasksView.swift b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/ServerTasksView/ServerTasksView.swift rename to Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift index 02e99ab16..61ff42279 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerTasksView/ServerTasksView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerTasks/ServerTasksView/ServerTasksView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift deleted file mode 100644 index 587fa4174..000000000 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDetailsView/ServerUserDetailsView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Defaults -import JellyfinAPI -import SwiftUI - -struct ServerUserDetailsView: View { - @EnvironmentObject - private var router: AdminDashboardCoordinator.Router - - @CurrentDate - private var currentDate: Date - - @StateObject - private var viewModel: ServerUserAdminViewModel - - // MARK: - Initializer - - init(user: UserDto) { - _viewModel = StateObject(wrappedValue: ServerUserAdminViewModel(user: user)) - } - - // MARK: - Body - - var body: some View { - List { - // TODO: Replace with Update Profile Picture & Username - AdminDashboardView.UserSection( - user: viewModel.user, - lastActivityDate: viewModel.user.lastActivityDate - ) - - Section { - if let userId = viewModel.user.id { - ChevronButton(L10n.password) - .onSelect { - router.route(to: \.resetUserPassword, userId) - } - } - } - - Section(L10n.advanced) { - ChevronButton(L10n.permissions) - .onSelect { - router.route(to: \.userPermissions, viewModel) - } - } - - Section(L10n.access) { - ChevronButton(L10n.devices) - .onSelect { - router.route(to: \.userDeviceAccess, viewModel) - } - ChevronButton(L10n.liveTV) - .onSelect { - router.route(to: \.userLiveTVAccess, viewModel) - } - ChevronButton(L10n.media) - .onSelect { - router.route(to: \.userMediaAccess, viewModel) - } - } - - Section(L10n.parentalControls) { - // TODO: Access Schedules - accessSchedules - /* ChevronButton("Access schedule") - .onSelect { - router.route(to: \.userAccessSchedules, viewModel) - } - // TODO: Allow items SDK 10.10 - allowedTags - ChevronButton("Allow items") - .onSelect { - router.route(to: \.userAllowedTags, viewModel) - } - // TODO: Block items - blockedTags - ChevronButton("Block items") - .onSelect { - router.route(to: \.userBlockedTags, viewModel) - }*/ - ChevronButton(L10n.ratings) - .onSelect { - router.route(to: \.userParentalRatings, viewModel) - } - } - } - .navigationTitle(L10n.user) - .onAppear { - viewModel.send(.loadDetails) - } - } -} diff --git a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/AddServerUserView/AddServerUserView.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/AddServerUserView/AddServerUserView.swift index adc62b1ed..eb72520e5 100644 --- a/Swiftfin/Views/AdminDashboardView/AddServerUserView/AddServerUserView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/AddServerUserView/AddServerUserView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -46,11 +46,6 @@ struct AddServerUserView: View { @State private var confirmPassword: String = "" - // MARK: - Dialog State - - @State - private var isPresentingSuccess: Bool = false - // MARK: - Error State @State diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift new file mode 100644 index 000000000..dff381c69 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserDetailsView/ServerUserDetailsView.swift @@ -0,0 +1,141 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct ServerUserDetailsView: View { + + // MARK: - Current Date + + @CurrentDate + private var currentDate: Date + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: AdminDashboardCoordinator.Router + + @StateObject + private var viewModel: ServerUserAdminViewModel + + @StateObject + private var profileViewModel: UserProfileImageViewModel + + // MARK: - Dialog State + + @State + private var username: String + @State + private var isPresentingUsername = false + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Initializer + + init(user: UserDto) { + self._viewModel = StateObject(wrappedValue: ServerUserAdminViewModel(user: user)) + self._profileViewModel = StateObject(wrappedValue: UserProfileImageViewModel(user: user)) + self.username = user.name ?? "" + } + + // MARK: - Body + + var body: some View { + List { + UserProfileHeroImage( + user: viewModel.user, + source: viewModel.user.profileImageSource( + client: viewModel.userSession.client, + maxWidth: 150 + ) + ) { + router.route(to: \.userPhotoPicker, profileViewModel) + } onDelete: { + profileViewModel.send(.delete) + } + + Section { + ChevronAlertButton( + L10n.username, + subtitle: viewModel.user.name + ) { + TextField(L10n.username, text: $username) + HStack { + Button(L10n.cancel) { + username = viewModel.user.name ?? "" + isPresentingUsername = false + } + Button(L10n.save) { + viewModel.send(.updateUsername(username)) + isPresentingUsername = false + } + } + } + if let userId = viewModel.user.id { + ChevronButton(L10n.password) + .onSelect { + router.route(to: \.resetUserPassword, userId) + } + } + ChevronButton(L10n.permissions) + .onSelect { + router.route(to: \.userPermissions, viewModel) + } + } + + Section(L10n.access) { + ChevronButton(L10n.devices) + .onSelect { + router.route(to: \.userDeviceAccess, viewModel) + } + ChevronButton(L10n.liveTV) + .onSelect { + router.route(to: \.userLiveTVAccess, viewModel) + } + ChevronButton(L10n.media) + .onSelect { + router.route(to: \.userMediaAccess, viewModel) + } + } + + Section(L10n.parentalControls) { + ChevronButton(L10n.ratings) + .onSelect { + router.route(to: \.userParentalRatings, viewModel) + } + ChevronButton(L10n.accessSchedules) + .onSelect { + router.route(to: \.userEditAccessSchedules, viewModel) + } + ChevronButton(L10n.accessTags) + .onSelect { + router.route(to: \.userEditAccessTags, viewModel) + } + } + } + .navigationTitle(L10n.user) + .onAppear { + viewModel.send(.refresh) + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + username = viewModel.user.name ?? "" + case .updated: + break + } + } + .errorMessage($error) + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift new file mode 100644 index 000000000..61b8cf7e5 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/AddAccessScheduleView/AddAccessScheduleView.swift @@ -0,0 +1,188 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct AddAccessScheduleView: View { + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + private var viewModel: ServerUserAdminViewModel + + // MARK: - Access Schedule Variables + + @State + private var tempPolicy: UserPolicy + @State + private var selectedDay: DynamicDayOfWeek = .everyday + @State + private var startTime: Date = Calendar.current.startOfDay(for: Date()) + @State + private var endTime: Date = Calendar.current.startOfDay(for: Date()).addingTimeInterval(+3600) + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Initializer + + init(viewModel: ServerUserAdminViewModel) { + self.viewModel = viewModel + self.tempPolicy = viewModel.user.policy! + } + + private var isValidRange: Bool { + startTime < endTime + } + + private var newSchedule: AccessSchedule? { + guard isValidRange else { return nil } + + let calendar = Calendar.current + let startComponents = calendar.dateComponents([.hour, .minute], from: startTime) + let endComponents = calendar.dateComponents([.hour, .minute], from: endTime) + + guard let startHour = startComponents.hour, + let startMinute = startComponents.minute, + let endHour = endComponents.hour, + let endMinute = endComponents.minute + else { + return nil + } + + // AccessSchedule Hours are formatted as 23.5 == 11:30pm or 8.25 == 8:15am + let startDouble = Double(startHour) + Double(startMinute) / 60.0 + let endDouble = Double(endHour) + Double(endMinute) / 60.0 + + // AccessSchedule should have valid Start & End Hours + let newSchedule = AccessSchedule( + dayOfWeek: selectedDay, + endHour: endDouble, + startHour: startDouble, + userID: viewModel.user.id + ) + + return newSchedule + } + + private var isDuplicateSchedule: Bool { + guard let newSchedule, let existingSchedules = viewModel.user.policy?.accessSchedules else { + return false + } + + return existingSchedules.contains { other in + other.dayOfWeek == selectedDay && + other.startHour == newSchedule.startHour && + other.endHour == newSchedule.endHour + } + } + + // MARK: - Body + + var body: some View { + contentView + .navigationTitle(L10n.addAccessSchedule.localizedCapitalized) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.refreshing) { + ProgressView() + } + if viewModel.backgroundStates.contains(.updating) { + Button(L10n.cancel) { + viewModel.send(.cancel) + } + .buttonStyle(.toolbarPill(.red)) + } else { + Button(L10n.save) { + saveSchedule() + } + .buttonStyle(.toolbarPill) + .disabled(!isValidRange) + } + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + } + } + .errorMessage($error) + } + + // MARK: - Content View + + private var contentView: some View { + Form { + Section(L10n.dayOfWeek) { + Picker(L10n.dayOfWeek, selection: $selectedDay) { + ForEach(DynamicDayOfWeek.allCases, id: \.self) { day in + + if day == .everyday { + Divider() + } + + Text(day.displayTitle).tag(day) + } + } + } + + Section(L10n.startTime) { + DatePicker(L10n.startTime, selection: $startTime, displayedComponents: .hourAndMinute) + } + + Section { + DatePicker(L10n.endTime, selection: $endTime, displayedComponents: .hourAndMinute) + } header: { + Text(L10n.endTime) + } footer: { + if !isValidRange { + Label(L10n.accessScheduleInvalidTime, systemImage: "exclamationmark.circle.fill") + .labelStyle(.sectionFooterWithImage(imageStyle: .orange)) + } + + if isDuplicateSchedule { + Label(L10n.scheduleAlreadyExists, systemImage: "exclamationmark.circle.fill") + .labelStyle(.sectionFooterWithImage(imageStyle: .orange)) + } + } + } + } + + // MARK: - Save Schedule + + private func saveSchedule() { + + guard isValidRange, let newSchedule else { + error = JellyfinAPIError(L10n.accessScheduleInvalidTime) + return + } + + guard !isDuplicateSchedule else { + error = JellyfinAPIError(L10n.scheduleAlreadyExists) + return + } + + tempPolicy.accessSchedules = tempPolicy.accessSchedules + .appendedOrInit(newSchedule) + + viewModel.send(.updatePolicy(tempPolicy)) + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift new file mode 100644 index 000000000..f3ef7541d --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/EditAccessScheduleView/Components/EditAccessScheduleRow.swift @@ -0,0 +1,108 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension EditAccessScheduleView { + + struct EditAccessScheduleRow: View { + + // MARK: - Environment Variables + + @Environment(\.isEditing) + private var isEditing + @Environment(\.isSelected) + private var isSelected + + // MARK: - Schedule Variable + + let schedule: AccessSchedule + + // MARK: - Schedule Actions + + let onSelect: () -> Void + let onDelete: () -> Void + + // MARK: - Body + + var body: some View { + Button(action: onSelect) { + rowContent + } + .foregroundStyle(.primary, .secondary) + .swipeActions { + Button(L10n.delete, systemImage: "trash", action: onDelete) + .tint(.red) + } + } + + // MARK: - Row Content + + @ViewBuilder + private var rowContent: some View { + HStack { + VStack(alignment: .leading) { + if let dayOfWeek = schedule.dayOfWeek { + Text(dayOfWeek.rawValue) + .fontWeight(.semibold) + } + + Group { + if let startHour = schedule.startHour { + TextPairView( + leading: L10n.startTime, + trailing: doubleToTimeString(startHour) + ) + } + + if let endHour = schedule.endHour { + TextPairView( + leading: L10n.endTime, + trailing: doubleToTimeString(endHour) + ) + } + } + .font(.subheadline) + .foregroundStyle(.secondary) + } + .foregroundStyle( + isEditing ? (isSelected ? .primary : .secondary) : .primary, + .secondary + ) + + Spacer() + + ListRowCheckbox() + } + } + + // MARK: - Convert Double to Date + + private func doubleToTimeString(_ double: Double) -> String { + let startHours = Int(double) + let startMinutes = Int(double.truncatingRemainder(dividingBy: 1) * 60) + + var dateComponents = DateComponents() + dateComponents.hour = startHours + dateComponents.minute = startMinutes + + let calendar = Calendar.current + + guard let date = calendar.date(from: dateComponents) else { + return .emptyTime + } + + let formatter = DateFormatter() + formatter.timeStyle = .short + + return formatter.string(from: date) + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift new file mode 100644 index 000000000..d5c878553 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessSchedule/EditAccessScheduleView/EditAccessScheduleView.swift @@ -0,0 +1,227 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct EditAccessScheduleView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: AdminDashboardCoordinator.Router + + @ObservedObject + private var viewModel: ServerUserAdminViewModel + + // MARK: - Policy Variable + + @State + private var selectedSchedules: Set = [] + + // MARK: - Dialog States + + @State + private var isPresentingDeleteSelectionConfirmation = false + @State + private var isPresentingDeleteConfirmation = false + + // MARK: - Editing State + + @State + private var isEditing: Bool = false + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Initializer + + init(viewModel: ServerUserAdminViewModel) { + self.viewModel = viewModel + } + + // MARK: - Body + + var body: some View { + contentView + .navigationTitle(L10n.accessSchedules) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(isEditing) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if isEditing { + navigationBarSelectView + } + } + ToolbarItem(placement: .topBarTrailing) { + if isEditing { + Button(L10n.cancel) { + isEditing.toggle() + selectedSchedules.removeAll() + UIDevice.impact(.light) + } + .buttonStyle(.toolbarPill) + .foregroundStyle(accentColor) + } + } + ToolbarItem(placement: .bottomBar) { + if isEditing { + Button(L10n.delete) { + isPresentingDeleteSelectionConfirmation = true + } + .buttonStyle(.toolbarPill(.red)) + .disabled(selectedSchedules.isEmpty) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + } + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains(.refreshing), + isHidden: isEditing || viewModel.user.policy?.accessSchedules == [] + ) { + Button(L10n.add, systemImage: "plus") { + router.route(to: \.userAddAccessSchedule, viewModel) + } + + Button(L10n.edit, systemImage: "checkmark.circle") { + isEditing = true + } + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + case .updated: + UIDevice.feedback(.success) + } + } + .confirmationDialog( + L10n.deleteSelectedSchedules, + isPresented: $isPresentingDeleteSelectionConfirmation, + titleVisibility: .visible + ) { + deleteSelectedSchedulesConfirmationActions + } message: { + Text(L10n.deleteSelectionSchedulesWarning) + } + .confirmationDialog( + L10n.deleteSchedule, + isPresented: $isPresentingDeleteConfirmation, + titleVisibility: .visible + ) { + deleteScheduleConfirmationActions + } message: { + Text(L10n.deleteScheduleWarning) + } + .errorMessage($error) + } + + // MARK: - Content View + + @ViewBuilder + var contentView: some View { + List { + ListTitleSection( + L10n.accessSchedules.localizedCapitalized, + description: L10n.accessSchedulesDescription + ) { + UIApplication.shared.open(.jellyfinDocsManagingUsers) + } + + if viewModel.user.policy?.accessSchedules == [] { + Button(L10n.add) { + router.route(to: \.userAddAccessSchedule, viewModel) + } + } else { + ForEach(viewModel.user.policy?.accessSchedules ?? [], id: \.self) { schedule in + EditAccessScheduleRow(schedule: schedule) { + if isEditing { + selectedSchedules.toggle(value: schedule) + } + } onDelete: { + selectedSchedules = [schedule] + isPresentingDeleteConfirmation = true + } + .environment(\.isEditing, isEditing) + .environment(\.isSelected, selectedSchedules.contains(schedule)) + } + } + } + } + + // MARK: - Navigation Bar Select/Remove All Content + + @ViewBuilder + private var navigationBarSelectView: some View { + + let isAllSelected: Bool = selectedSchedules.count == viewModel.user.policy?.accessSchedules?.count + + Button(isAllSelected ? L10n.removeAll : L10n.selectAll) { + if isAllSelected { + selectedSchedules = [] + } else { + selectedSchedules = Set(viewModel.user.policy?.accessSchedules ?? []) + } + } + .buttonStyle(.toolbarPill) + .disabled(!isEditing) + .foregroundStyle(accentColor) + } + + // MARK: - Delete Selected Schedules Confirmation Actions + + @ViewBuilder + private var deleteSelectedSchedulesConfirmationActions: some View { + Button(L10n.cancel, role: .cancel) {} + + Button(L10n.confirm, role: .destructive) { + + var tempPolicy: UserPolicy = viewModel.user.policy! + + if selectedSchedules.isNotEmpty { + tempPolicy.accessSchedules = tempPolicy.accessSchedules?.filter { !selectedSchedules.contains($0) + } + viewModel.send(.updatePolicy(tempPolicy)) + isEditing = false + selectedSchedules.removeAll() + } + } + } + + // MARK: - Delete Schedule Confirmation Actions + + @ViewBuilder + private var deleteScheduleConfirmationActions: some View { + Button(L10n.cancel, role: .cancel) {} + + Button(L10n.delete, role: .destructive) { + + var tempPolicy: UserPolicy = viewModel.user.policy! + + if let scheduleToDelete = selectedSchedules.first, + selectedSchedules.count == 1 + { + tempPolicy.accessSchedules = tempPolicy.accessSchedules?.filter { + $0 != scheduleToDelete + } + viewModel.send(.updatePolicy(tempPolicy)) + isEditing = false + selectedSchedules.removeAll() + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift new file mode 100644 index 000000000..954155579 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/AddServerUserAccessTagsView.swift @@ -0,0 +1,151 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct AddServerUserAccessTagsView: View { + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + private var viewModel: ServerUserAdminViewModel + + @StateObject + private var tagViewModel: TagEditorViewModel + + // MARK: - Access Tag Variables + + @State + private var tempPolicy: UserPolicy + @State + private var tempTag: String = "" + @State + private var access: Bool = false + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Name is Valid + + private var isValid: Bool { + tempTag.isNotEmpty && !tagIsDuplicate + } + + // MARK: - Tag is Already Blocked/Allowed + + private var tagIsDuplicate: Bool { + viewModel.user.policy!.blockedTags!.contains(tempTag) // && + //! viewModel.user.policy!.allowedTags!.contains(tempTag) + } + + // MARK: - Tag Already Exists on Jellyfin + + private var tagAlreadyExists: Bool { + tagViewModel.trie.contains(key: tempTag.localizedLowercase) + } + + // MARK: - Initializer + + init(viewModel: ServerUserAdminViewModel) { + self.viewModel = viewModel + self.tempPolicy = viewModel.user.policy! + self._tagViewModel = StateObject(wrappedValue: TagEditorViewModel(item: .init())) + } + + // MARK: - Body + + var body: some View { + contentView + .navigationTitle(L10n.addAccessTag.localizedCapitalized) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.refreshing) { + ProgressView() + } + if viewModel.backgroundStates.contains(.updating) { + Button(L10n.cancel) { + viewModel.send(.cancel) + } + .buttonStyle(.toolbarPill(.red)) + } else { + Button(L10n.save) { + if access { + // TODO: Enable on 10.10 + /* tempPolicy.allowedTags = tempPolicy.allowedTags + .appendedOrInit(tempTag) */ + } else { + tempPolicy.blockedTags = tempPolicy.blockedTags + .appendedOrInit(tempTag) + } + + viewModel.send(.updatePolicy(tempPolicy)) + } + .buttonStyle(.toolbarPill) + .disabled(!isValid) + } + } + .onFirstAppear { + tagViewModel.send(.load) + } + .onChange(of: tempTag) { _ in + if !tagViewModel.backgroundStates.contains(.loading) { + tagViewModel.send(.search(tempTag)) + } + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + } + } + .onReceive(tagViewModel.events) { event in + switch event { + case .updated: + break + case .loaded: + tagViewModel.send(.search(tempTag)) + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + } + } + .errorMessage($error) + } + + // MARK: - Content View + + private var contentView: some View { + Form { + TagInput( + access: $access, + tag: $tempTag, + tagIsDuplicate: tagIsDuplicate, + tagAlreadyExists: tagAlreadyExists + ) + + SearchResultsSection( + tag: $tempTag, + tags: tagViewModel.matches, + isSearching: tagViewModel.backgroundStates.contains(.searching) + ) + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift new file mode 100644 index 000000000..80c5a0bc1 --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/Components/AccessTagSearchResultsSection.swift @@ -0,0 +1,72 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension AddServerUserAccessTagsView { + + struct SearchResultsSection: View { + + // MARK: - Element Variables + + @Binding + var tag: String + + // MARK: - Element Search Variables + + let tags: [String] + let isSearching: Bool + + // MARK: - Body + + var body: some View { + if tag.isNotEmpty { + Section { + if tags.isNotEmpty { + resultsView + } else if !isSearching { + noResultsView + } + } header: { + HStack { + Text(L10n.existingItems) + + if isSearching { + ProgressView() + } else { + Text("-") + + Text(tags.count, format: .number) + } + } + } + .animation(.linear(duration: 0.2), value: tags) + } + } + + // MARK: - No Results View + + private var noResultsView: some View { + Text(L10n.none) + .foregroundStyle(.secondary) + } + + // MARK: - Results View + + private var resultsView: some View { + ForEach(tags, id: \.self) { result in + Button(result) { + tag = result + } + .foregroundStyle(.primary) + .disabled(tag == result) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift new file mode 100644 index 000000000..76a820e1c --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/AddServerUserAccessTagsView/Components/TagInput.swift @@ -0,0 +1,92 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension AddServerUserAccessTagsView { + + struct TagInput: View { + + // MARK: - Element Variables + + @FocusState + private var isTagFocused: Bool + + @Binding + var access: Bool + @Binding + var tag: String + + let tagIsDuplicate: Bool + let tagAlreadyExists: Bool + + // MARK: - Body + + var body: some View { + // TODO: Enable on 10.10 +// Section { +// Picker(L10n.access, selection: $access) { +// Text(L10n.allowed).tag(true) +// Text(L10n.blocked).tag(false) +// } +// .disabled(true) +// } header: { +// Text(L10n.access) +// } footer: { +// LearnMoreButton(L10n.accessTags) { +// TextPair( +// title: L10n.allowed, +// subtitle: L10n.accessTagAllowDescription +// ) +// TextPair( +// title: L10n.blocked, +// subtitle: L10n.accessTagBlockDescription +// ) +// } +// } + + Section { + TextField(L10n.name, text: $tag) + .autocorrectionDisabled() + .focused($isTagFocused) + } footer: { + if tag.isEmpty { + Label( + L10n.required, + systemImage: "exclamationmark.circle.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .orange)) + } else if tagIsDuplicate { + Label( + L10n.accessTagAlreadyExists, + systemImage: "exclamationmark.circle.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .orange)) + } else { + if tagAlreadyExists { + Label( + L10n.existsOnServer, + systemImage: "checkmark.circle.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .green)) + } else { + Label( + L10n.willBeCreatedOnServer, + systemImage: "checkmark.seal.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .blue)) + } + } + } + .onFirstAppear { + isTagFocused = true + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.swift new file mode 100644 index 000000000..9f1820a3c --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/EditServerUserAccessTagsView/Components/EditAccessTagRow.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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension EditServerUserAccessTagsView { + + struct EditAccessTagRow: View { + + // MARK: - Metadata Variables + + let tag: String + + // MARK: - Row Actions + + let onSelect: () -> Void + let onDelete: () -> Void + + // MARK: - Body + + var body: some View { + Button(action: onSelect) { + HStack { + Text(tag) + .frame(maxWidth: .infinity, alignment: .leading) + + ListRowCheckbox() + } + } + .foregroundStyle(.primary) + .swipeActions { + Button( + L10n.delete, + systemImage: "trash", + action: onDelete + ) + .tint(.red) + } + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift new file mode 100644 index 000000000..b72dc3f4a --- /dev/null +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessTags/EditServerUserAccessTagsView/EditServerUserAccessTagsView.swift @@ -0,0 +1,231 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct EditServerUserAccessTagsView: View { + + private struct TagWithAccess: Hashable { + let tag: String + let access: Bool + } + + // MARK: - Observed, State, & Environment Objects + + @EnvironmentObject + private var router: AdminDashboardCoordinator.Router + + @StateObject + private var viewModel: ServerUserAdminViewModel + + // MARK: - Dialog States + + @State + private var isPresentingDeleteConfirmation = false + + // MARK: - Editing States + + @State + private var selectedTags: Set = [] + @State + private var isEditing: Bool = false + + // MARK: - Error State + + @State + private var error: Error? + + private var blockedTags: [TagWithAccess] { + viewModel.user.policy?.blockedTags? + .sorted() + .map { TagWithAccess(tag: $0, access: false) } ?? [] + } + +// private var allowedTags: [TagWithAccess] { +// viewModel.user.policy?.allowedTags? +// .sorted() +// .map { TagWithAccess(tag: $0, access: true) } ?? [] +// } + + // MARK: - Initializera + + init(viewModel: ServerUserAdminViewModel) { + self._viewModel = StateObject(wrappedValue: viewModel) + } + + // MARK: - Body + + var body: some View { + ZStack { + switch viewModel.state { + case .initial, .content: + contentView + case let .error(error): + errorView(with: error) + } + } + .navigationTitle(L10n.accessTags) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(isEditing) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if isEditing { + navigationBarSelectView + } + } + ToolbarItem(placement: .topBarTrailing) { + if isEditing { + Button(L10n.cancel) { + isEditing = false + UIDevice.impact(.light) + selectedTags.removeAll() + } + .buttonStyle(.toolbarPill) + } + } + ToolbarItem(placement: .bottomBar) { + if isEditing { + Button(L10n.delete) { + isPresentingDeleteConfirmation = true + } + .buttonStyle(.toolbarPill(.red)) + .disabled(selectedTags.isEmpty) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + } + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains(.refreshing), + isHidden: isEditing || ( + viewModel.user.policy?.blockedTags?.isEmpty == true + ) + ) { + Button(L10n.add, systemImage: "plus") { + router.route(to: \.userAddAccessTag, viewModel) + } + + if viewModel.user.policy?.blockedTags?.isNotEmpty == true { + Button(L10n.edit, systemImage: "checkmark.circle") { + isEditing = true + } + } + } + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + default: + break + } + } + .confirmationDialog( + L10n.delete, + isPresented: $isPresentingDeleteConfirmation, + titleVisibility: .visible + ) { + deleteSelectedConfirmationActions + } message: { + Text(L10n.deleteSelectedConfirmation) + } + .errorMessage($error) + } + + // MARK: - ErrorView + + @ViewBuilder + private func errorView(with error: some Error) -> some View { + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + } + + @ViewBuilder + private func makeRow(tag: TagWithAccess) -> some View { + EditAccessTagRow(tag: tag.tag) { + if isEditing { + selectedTags.toggle(value: tag) + } + } onDelete: { + selectedTags = [tag] + isPresentingDeleteConfirmation = true + } + .environment(\.isEditing, isEditing) + .environment(\.isSelected, selectedTags.contains(tag)) + } + + // MARK: - Content View + + @ViewBuilder + private var contentView: some View { + List { + ListTitleSection( + L10n.accessTags, + description: L10n.accessTagsDescription + ) { + UIApplication.shared.open(.jellyfinDocsManagingUsers) + } + + if blockedTags.isEmpty { + Button(L10n.add) { + router.route(to: \.userAddAccessTag, viewModel) + } + } else { + + // TODO: with allowed, use `DisclosureGroup` instead + Section(L10n.blocked) { + ForEach( + blockedTags, + id: \.self, + content: makeRow + ) + } + + // TODO: allowed with 10.10 + } + } + } + + // MARK: - Select/Remove All Button + + @ViewBuilder + private var navigationBarSelectView: some View { + let isAllSelected = selectedTags.count == blockedTags.count + + Button(isAllSelected ? L10n.removeAll : L10n.selectAll) { + selectedTags = isAllSelected ? [] : Set(blockedTags) + } + .buttonStyle(.toolbarPill) + .disabled(!isEditing) + } + + // MARK: - Delete Selected Confirmation Actions + + @ViewBuilder + private var deleteSelectedConfirmationActions: some View { + Button(L10n.cancel, role: .cancel) {} + + Button(L10n.delete, role: .destructive) { + var tempPolicy = viewModel.user.policy ?? UserPolicy() + + for tag in selectedTags { + if tag.access { + // tempPolicy.allowedTags?.removeAll { $0 == tag.tag } + } else { + tempPolicy.blockedTags?.removeAll { $0 == tag.tag } + } + } + + viewModel.send(.updatePolicy(tempPolicy)) + selectedTags.removeAll() + isEditing = false + } + } +} diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessView/ServerUserAccessView.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessView/ServerUserAccessView.swift index 8695886b8..c65718074 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserAccessView/ServerUserAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserAccessView/ServerUserAccessView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -58,6 +58,9 @@ struct ServerUserMediaAccessView: View { .buttonStyle(.toolbarPill) .disabled(viewModel.user.policy == tempPolicy) } + .onFirstAppear { + viewModel.send(.loadLibraries()) + } .onReceive(viewModel.events) { event in switch event { case let .error(eventError): diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift index 2781c9f7c..68f7e6010 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserDeviceAccessView/ServerUserDeviceAccessView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift index e8b0000c7..619cb3871 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserLiveTVAccessView/ServerUserLiveTVAccessView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -17,7 +17,7 @@ struct ServerUserLiveTVAccessView: View { @CurrentDate private var currentDate: Date - // MARK: - State & Environment Objects + // MARK: - Observed & Environment Objects @EnvironmentObject private var router: BasicNavigationViewCoordinator.Router diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserParentalRatingView/ServerUserParentalRatingView.swift similarity index 99% rename from Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserParentalRatingView/ServerUserParentalRatingView.swift index d1518b631..6b5403739 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserParentalRatingView/ServerUserParentalRatingView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserParentalRatingView/ServerUserParentalRatingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift similarity index 97% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift index 099da752f..2a3aef51c 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/ExternalAccessSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/ManagementSection.swift similarity index 96% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/ManagementSection.swift index dea82fd35..0b102915f 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/ManagementSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/ManagementSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift similarity index 95% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift index 4efc03ae3..c3b3b23e9 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/MediaPlaybackSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/PermissionSection.swift similarity index 93% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/PermissionSection.swift index 39fe395b1..a0383afc0 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/PermissionSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/PermissionSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift similarity index 93% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift index 99d75e221..e505b6618 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/RemoteControlSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/SessionsSection.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/SessionsSection.swift index 86760e46e..cc32d79df 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SessionsSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/SessionsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/StatusSection.swift similarity index 92% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/StatusSection.swift index 0e045ed33..e09354ed7 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/StatusSection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/StatusSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift similarity index 91% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift index 8db9c1e9e..e5b5283e9 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/Components/Sections/SyncPlaySection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/ServerUserPermissionsView.swift similarity index 98% rename from Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/ServerUserPermissionsView.swift index e08d51a1d..920f18cd5 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUserPermissionsView/ServerUserPermissionsView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUserSettings/ServerUserPermissionsView/ServerUserPermissionsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUsersView/Components/ServerUsersRow.swift similarity index 85% rename from Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUsersView/Components/ServerUsersRow.swift index fe006974b..1381ee697 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/Components/ServerUsersRow.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUsersView/Components/ServerUsersRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -75,24 +75,20 @@ extension ServerUsersView { @ViewBuilder private var userImage: some View { ZStack { - ImageView(user.profileImageSource(client: userSession!.client)) - .pipeline(.Swiftfin.branding) - .placeholder { _ in - SystemImageContentView(systemName: "person.fill", ratio: 0.5) - } - .failure { - SystemImageContentView(systemName: "person.fill", ratio: 0.5) - } - .grayscale(userActive ? 0.0 : 1.0) + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: userSession!.client, + maxWidth: 60 + ) + ) + .grayscale(userActive ? 0.0 : 1.0) if isEditing { Color.black .opacity(isSelected ? 0 : 0.5) } } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - .posterShadow() .frame(width: 60, height: 60) } diff --git a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUsersView/ServerUsersView.swift similarity index 99% rename from Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift rename to Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUsersView/ServerUsersView.swift index 5dccc1f59..374e72d03 100644 --- a/Swiftfin/Views/AdminDashboardView/ServerUsersView/ServerUsersView.swift +++ b/Swiftfin/Views/AdminDashboardView/ServerUsers/ServerUsersView/ServerUsersView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/AppIconSelectorView.swift b/Swiftfin/Views/AppIconSelectorView.swift index 228e4734a..1c28aa98c 100644 --- a/Swiftfin/Views/AppIconSelectorView.swift +++ b/Swiftfin/Views/AppIconSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/AppLoadingView.swift b/Swiftfin/Views/AppLoadingView.swift index 7103bea5b..c3338419c 100644 --- a/Swiftfin/Views/AppLoadingView.swift +++ b/Swiftfin/Views/AppLoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -27,7 +27,7 @@ struct AppLoadingView: View { } } .topBarTrailing { - Button("Advanced", systemImage: "gearshape.fill") {} + Button(L10n.advanced, systemImage: "gearshape.fill") {} .foregroundStyle(.secondary) .disabled(true) .opacity(didFailMigration ? 0 : 1) diff --git a/Swiftfin/Views/AppSettingsView/AppSettingsView.swift b/Swiftfin/Views/AppSettingsView/AppSettingsView.swift index c3ddc31a6..f6d3988d0 100644 --- a/Swiftfin/Views/AppSettingsView/AppSettingsView.swift +++ b/Swiftfin/Views/AppSettingsView/AppSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -62,10 +62,10 @@ struct AppSettingsView: View { Toggle("Use splashscreen", isOn: $selectUserUseSplashscreen) if selectUserUseSplashscreen { - Picker("Servers", selection: $selectUserAllServersSplashscreen) { + Picker(L10n.servers, selection: $selectUserAllServersSplashscreen) { Section { - Label("Random", systemImage: "dice.fill") + Label(L10n.random, systemImage: "dice.fill") .tag(SelectUserServerSelection.all) } diff --git a/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift b/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift index 0350a097c..4e94f2c3a 100644 --- a/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift +++ b/Swiftfin/Views/AppSettingsView/Components/SignOutIntervalSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift b/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift index 23a58bcb7..2df99f3b0 100644 --- a/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift +++ b/Swiftfin/Views/ChannelLibraryView/ChannelLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid @@ -165,7 +165,7 @@ struct ChannelLibraryView: View { // We repurposed `LibraryDisplayType` but want different labels Picker("Channel Display", selection: $channelDisplayType) { - Label("Compact", systemImage: LibraryDisplayType.grid.systemImage) + Label(L10n.compact, systemImage: LibraryDisplayType.grid.systemImage) .tag(LibraryDisplayType.grid) Label("Detailed", systemImage: LibraryDisplayType.list.systemImage) diff --git a/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift b/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift index 8bd058673..b43350563 100644 --- a/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift +++ b/Swiftfin/Views/ChannelLibraryView/Components/CompactChannelView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift b/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift index 93c6ed6f5..672a326c0 100644 --- a/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift +++ b/Swiftfin/Views/ChannelLibraryView/Components/DetailedChannelView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ConnectToServerView.swift b/Swiftfin/Views/ConnectToServerView.swift index 23ce660f3..1c9687c94 100644 --- a/Swiftfin/Views/ConnectToServerView.swift +++ b/Swiftfin/Views/ConnectToServerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/DownloadListView.swift b/Swiftfin/Views/DownloadListView.swift index 70f1d5217..7ec2c1268 100644 --- a/Swiftfin/Views/DownloadListView.swift +++ b/Swiftfin/Views/DownloadListView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift b/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift index 207b6b14d..263fe2b72 100644 --- a/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift +++ b/Swiftfin/Views/DownloadTaskView/DownloadTaskContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift b/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift index efafdedb8..3af9d7659 100644 --- a/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift +++ b/Swiftfin/Views/DownloadTaskView/DownloadTaskView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/EditServerView.swift b/Swiftfin/Views/EditServerView.swift index b6d35120e..2957db3b4 100644 --- a/Swiftfin/Views/EditServerView.swift +++ b/Swiftfin/Views/EditServerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory @@ -55,7 +55,7 @@ struct EditServerView: View { } if isEditing { - ListRowButton("Delete") { + ListRowButton(L10n.delete) { isPresentingConfirmDeletion = true } .foregroundStyle(.red, .red.opacity(0.2)) @@ -66,8 +66,8 @@ struct EditServerView: View { .onChange(of: currentServerURL) { newValue in viewModel.setCurrentURL(to: newValue) } - .alert("Delete Server", isPresented: $isPresentingConfirmDeletion) { - Button("Delete", role: .destructive) { + .alert(L10n.deleteServer, isPresented: $isPresentingConfirmDeletion) { + Button(L10n.delete, role: .destructive) { viewModel.delete() router.popLast() } diff --git a/Swiftfin/Views/FilterView.swift b/Swiftfin/Views/FilterView.swift index f8a937006..2968fb348 100644 --- a/Swiftfin/Views/FilterView.swift +++ b/Swiftfin/Views/FilterView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/FontPickerView.swift b/Swiftfin/Views/FontPickerView.swift index 73a51142d..a14069617 100644 --- a/Swiftfin/Views/FontPickerView.swift +++ b/Swiftfin/Views/FontPickerView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift index 6b3495b41..be52dcc63 100644 --- a/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift +++ b/Swiftfin/Views/HomeView/Components/ContinueWatchingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift index a3aa83aeb..0954b31eb 100644 --- a/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift +++ b/Swiftfin/Views/HomeView/Components/LatestInLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/HomeView/Components/NextUpView.swift b/Swiftfin/Views/HomeView/Components/NextUpView.swift index 1e33535f1..fd716f262 100644 --- a/Swiftfin/Views/HomeView/Components/NextUpView.swift +++ b/Swiftfin/Views/HomeView/Components/NextUpView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift index e8f3d0587..e0423efe9 100644 --- a/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift +++ b/Swiftfin/Views/HomeView/Components/RecentlyAddedView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/HomeView/HomeView.swift b/Swiftfin/Views/HomeView/HomeView.swift index 75af2cac9..a64b4b8e6 100644 --- a/Swiftfin/Views/HomeView/HomeView.swift +++ b/Swiftfin/Views/HomeView/HomeView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift b/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift index 3f3931e88..07ec23e9d 100644 --- a/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift +++ b/Swiftfin/Views/ItemEditorView/Components/RefreshMetadataButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift new file mode 100644 index 000000000..03223c91f --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultRow.swift @@ -0,0 +1,56 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import JellyfinAPI +import SwiftUI + +extension IdentifyItemView { + + struct RemoteSearchResultRow: View { + + // MARK: - Remote Search Result Variable + + let result: RemoteSearchResult + + // MARK: - Remote Search Result Action + + let onSelect: () -> Void + + // MARK: - Result Title + + private var resultTitle: String { + result.displayTitle + .appending(" (\(result.premiereDate!.formatted(.dateTime.year())))", if: result.premiereDate != nil) + } + + // MARK: - Body + + var body: some View { + ListRow { + IdentifyItemView.resultImage(URL(string: result.imageURL)) + .frame(width: 60) + } content: { + VStack(alignment: .leading) { + Text(resultTitle) + .font(.headline) + .foregroundStyle(.primary) + + if let overview = result.overview { + Text(overview) + .lineLimit(3) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + } + .onSelect(perform: onSelect) + .isSeparatorVisible(false) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift new file mode 100644 index 000000000..669d71687 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/Components/RemoteSearchResultView.swift @@ -0,0 +1,107 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension IdentifyItemView { + + struct RemoteSearchResultView: View { + + // MARK: - Item Info Variables + + let result: RemoteSearchResult + + // MARK: - Item Info Actions + + let onSave: () -> Void + let onClose: () -> Void + + // MARK: - Body + + @ViewBuilder + private var header: some View { + Section { + HStack(alignment: .bottom, spacing: 12) { + IdentifyItemView.resultImage(URL(string: result.imageURL)) + .frame(width: 100) + .accessibilityIgnoresInvertColors() + + Text(result.displayTitle) + .font(.title2) + .fontWeight(.semibold) + .foregroundStyle(.primary) + .lineLimit(2) + .padding(.bottom) + } + } + .listRowBackground(Color.clear) + .listRowCornerRadius(0) + .listRowInsets(.zero) + } + + @ViewBuilder + private var resultDetails: some View { + Section(L10n.details) { + + if let premiereDate = result.premiereDate { + TextPairView( + L10n.premiereDate, + value: Text(premiereDate.formatted(.dateTime.year().month().day())) + ) + } + + if let productionYear = result.productionYear { + TextPairView( + L10n.productionYear, + value: Text(productionYear, format: .number.grouping(.never)) + ) + } + + if let provider = result.searchProviderName { + TextPairView( + leading: L10n.provider, + trailing: provider + ) + } + + if let providerID = result.providerIDs?.values.first { + TextPairView( + leading: L10n.id, + trailing: providerID + ) + } + } + + if let overview = result.overview { + Section(L10n.overview) { + Text(overview) + } + } + } + + var body: some View { + NavigationView { + List { + header + + resultDetails + } + .navigationTitle(L10n.identify) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + onClose() + } + .topBarTrailing { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift new file mode 100644 index 000000000..ef42169dc --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/IdentifyItemView/IdentifyItemView.swift @@ -0,0 +1,197 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import JellyfinAPI +import SwiftUI + +struct IdentifyItemView: View { + + private struct SearchFields: Equatable { + var name: String? + var originalTitle: String? + var year: Int? + + var isEmpty: Bool { + name.isNilOrEmpty && + originalTitle.isNilOrEmpty && + year == nil + } + } + + @Default(.accentColor) + private var accentColor + + @FocusState + private var isTitleFocused: Bool + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: ItemEditorCoordinator.Router + + @StateObject + private var viewModel: IdentifyItemViewModel + + // MARK: - Identity Variables + + @State + private var selectedResult: RemoteSearchResult? + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Lookup States + + @State + private var search = SearchFields() + + // MARK: - Initializer + + init(item: BaseItemDto) { + self._viewModel = StateObject(wrappedValue: IdentifyItemViewModel(item: item)) + } + + // MARK: - Body + + var body: some View { + Group { + switch viewModel.state { + case .content, .searching: + contentView + case .updating: + ProgressView() + } + } + .navigationTitle(L10n.identify) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(viewModel.state == .updating) + .sheet(item: $selectedResult) { + selectedResult = nil + } content: { result in + RemoteSearchResultView( + result: result, + onSave: { + selectedResult = nil + viewModel.send(.update(result)) + }, + onClose: { + selectedResult = nil + } + ) + } + .onReceive(viewModel.events) { events in + switch events { + case let .error(eventError): + error = eventError + case .cancelled: + selectedResult = nil + case .updated: + router.pop() + } + } + .errorMessage($error) + .onFirstAppear { + isTitleFocused = true + } + } + + // MARK: - Content View + + @ViewBuilder + private var contentView: some View { + Form { + searchView + + resultsView + } + } + + // MARK: - Search View + + @ViewBuilder + private var searchView: some View { + Section(L10n.search) { + TextField( + L10n.title, + text: $search.name.coalesce("") + ) + .focused($isTitleFocused) + + TextField( + L10n.originalTitle, + text: $search.originalTitle.coalesce("") + ) + + TextField( + L10n.year, + text: $search.year + .map( + getter: { $0 == nil ? "" : "\($0!)" }, + setter: { Int($0) } + ) + ) + .keyboardType(.numberPad) + } + + if viewModel.state == .searching { + ListRowButton(L10n.cancel) { + viewModel.send(.cancel) + } + .foregroundStyle(.red, .red.opacity(0.2)) + } else { + ListRowButton(L10n.search) { + viewModel.send(.search( + name: search.name, + originalTitle: search.originalTitle, + year: search.year + )) + } + .disabled(search.isEmpty) + .foregroundStyle( + accentColor.overlayColor, + accentColor + ) + } + } + + // MARK: - Results View + + @ViewBuilder + private var resultsView: some View { + if viewModel.searchResults.isNotEmpty { + Section(L10n.items) { + ForEach(viewModel.searchResults) { result in + RemoteSearchResultRow(result: result) { + selectedResult = result + } + } + } + } + } + + // MARK: - Result Image + + @ViewBuilder + static func resultImage(_ url: URL?) -> some View { + ZStack { + Color.clear + + ImageView(url) + .failure { + Image(systemName: "questionmark") + .foregroundStyle(.primary) + } + } + .posterStyle(.portrait) + .posterShadow() + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift index f926dd576..b5e4d5f7f 100644 --- a/Swiftfin/Views/ItemEditorView/ItemEditorView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemEditorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory @@ -98,6 +98,16 @@ struct ItemEditorView: View { @ViewBuilder private var editView: some View { Section(L10n.edit) { + if [.boxSet, .movie, .person, .series].contains(viewModel.item.type) { + ChevronButton(L10n.identify) + .onSelect { + router.route(to: \.identifyItem, viewModel.item) + } + } + ChevronButton(L10n.images) + .onSelect { + router.route(to: \.editImages, ItemImagesViewModel(item: viewModel.item)) + } ChevronButton(L10n.metadata) .onSelect { router.route(to: \.editMetadata, viewModel.item) diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift similarity index 98% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift index 468fc05ff..77cf97067 100644 --- a/Swiftfin/Views/ItemEditorView/AddItemElementView/AddItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/AddItemElementView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift similarity index 97% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift index 4f9bc97ce..b4ab8fc28 100644 --- a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/NameInput.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/NameInput.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift similarity index 98% rename from Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift index 099f7a4a7..267359be0 100644 --- a/Swiftfin/Views/ItemEditorView/AddItemElementView/Components/SearchResultsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/AddItemElementView/Components/SearchResultsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift similarity index 98% rename from Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift index 730eb9d62..f1d5df9b8 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemElementView/Components/EditItemElementRow.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/Components/EditItemElementRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift similarity index 99% rename from Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift rename to Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift index cef53b599..ff93e5a0c 100644 --- a/Swiftfin/Views/ItemEditorView/EditItemElementView/EditItemElementView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemElements/EditItemElementView/EditItemElementView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift new file mode 100644 index 000000000..52ed0fb39 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/AddItemImageView.swift @@ -0,0 +1,214 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import BlurHashKit +import CollectionVGrid +import JellyfinAPI +import SwiftUI + +// TODO: different layouts per image type +// - also based on iOS vs iPadOS + +struct AddItemImageView: View { + + // MARK: - Observed, & Environment Objects + + @EnvironmentObject + private var router: ItemImagesCoordinator.Router + + @ObservedObject + private var viewModel: ItemImagesViewModel + + @StateObject + private var remoteImageInfoViewModel: RemoteImageInfoViewModel + + // MARK: - Dialog State + + @State + private var selectedImage: RemoteImageInfo? + @State + private var error: Error? + + // MARK: - Collection Layout + + @State + private var layout: CollectionVGridLayout = .minWidth(150) + + // MARK: - Initializer + + init(viewModel: ItemImagesViewModel, imageType: ImageType) { + self.viewModel = viewModel + self._remoteImageInfoViewModel = StateObject( + wrappedValue: RemoteImageInfoViewModel( + imageType: imageType, + parent: viewModel.item + ) + ) + } + + // MARK: - Body + + var body: some View { + ZStack { + switch remoteImageInfoViewModel.state { + case .initial, .refreshing: + DelayedProgressView() + case .content: + gridView + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + } + } + .animation(.linear(duration: 0.1), value: remoteImageInfoViewModel.state) + .navigationTitle(remoteImageInfoViewModel.imageType.displayTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(viewModel.backgroundStates.contains(.updating)) + .navigationBarMenuButton(isLoading: viewModel.backgroundStates.contains(.updating)) { + Button { + remoteImageInfoViewModel.includeAllLanguages.toggle() + } label: { + if remoteImageInfoViewModel.includeAllLanguages { + Label(L10n.allLanguages, systemImage: "checkmark") + } else { + Text(L10n.allLanguages) + } + } + + if remoteImageInfoViewModel.providers.isNotEmpty { + Menu { + Button { + remoteImageInfoViewModel.provider = nil + } label: { + if remoteImageInfoViewModel.provider == nil { + Label(L10n.all, systemImage: "checkmark") + } else { + Text(L10n.all) + } + } + + ForEach(remoteImageInfoViewModel.providers, id: \.self) { provider in + Button { + remoteImageInfoViewModel.provider = provider + } label: { + if remoteImageInfoViewModel.provider == provider { + Label(provider, systemImage: "checkmark") + } else { + Text(provider) + } + } + } + } label: { + Text(L10n.provider) + + Text(remoteImageInfoViewModel.provider ?? L10n.all) + } + } + } + .sheet(item: $selectedImage) { + selectedImage = nil + } content: { remoteImageInfo in + ItemImageDetailsView( + viewModel: viewModel, + imageSource: ImageSource(url: remoteImageInfo.url?.url), + width: remoteImageInfo.width, + height: remoteImageInfo.height, + language: remoteImageInfo.language, + provider: remoteImageInfo.providerName, + rating: remoteImageInfo.communityRating, + ratingVotes: remoteImageInfo.voteCount, + onClose: { + selectedImage = nil + }, + onSave: { + viewModel.send(.setImage(remoteImageInfo)) + selectedImage = nil + } + ) + } + .onFirstAppear { + remoteImageInfoViewModel.send(.refresh) + } + .onReceive(viewModel.events) { event in + switch event { + case .updated: + UIDevice.feedback(.success) + router.pop() + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + } + } + .errorMessage($error) + } + + // MARK: - Content Grid View + + @ViewBuilder + private var gridView: some View { + if remoteImageInfoViewModel.elements.isEmpty { + Text(L10n.none) + } else { + CollectionVGrid( + uniqueElements: remoteImageInfoViewModel.elements, + layout: layout + ) { image in + imageButton(image) + } + .onReachedBottomEdge(offset: .offset(300)) { + remoteImageInfoViewModel.send(.getNextPage) + } + } + } + + // MARK: - Poster Image Button + + @ViewBuilder + private func imageButton(_ image: RemoteImageInfo) -> some View { + Button { + selectedImage = image + } label: { + posterImage( + image, + posterStyle: (image.height ?? 0) > (image.width ?? 0) ? .portrait : .landscape + ) + } + } + + // MARK: - Poster Image + + @ViewBuilder + private func posterImage( + _ posterImageInfo: RemoteImageInfo?, + posterStyle: PosterDisplayType + ) -> some View { + ZStack { + Color.secondarySystemFill + .frame(maxWidth: .infinity, maxHeight: .infinity) + + ImageView(posterImageInfo?.url?.url) + .placeholder { source in + if let blurHash = source.blurHash { + BlurHashView(blurHash: blurHash) + .scaledToFit() + } else { + Image(systemName: "photo") + } + } + .failure { + Image(systemName: "photo") + } + .pipeline(.Swiftfin.other) + .foregroundStyle(.secondary) + .font(.headline) + } + .posterStyle(posterStyle) + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift new file mode 100644 index 000000000..f762d4e20 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDeleteButton.swift @@ -0,0 +1,50 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + + struct DeleteButton: View { + + // MARK: - Delete Action + + let onDelete: () -> Void + + // MARK: - Dialog State + + @State + private var isPresentingConfirmation: Bool = false + + // MARK: - Body + + var body: some View { + ListRowButton(L10n.delete, role: .destructive) { + isPresentingConfirmation = true + } + .confirmationDialog( + L10n.delete, + isPresented: $isPresentingConfirmation, + titleVisibility: .visible + ) { + Button( + L10n.delete, + role: .destructive, + action: onDelete + ) + + Button(L10n.cancel, role: .cancel) { + isPresentingConfirmation = false + } + } message: { + Text(L10n.deleteItemConfirmationMessage) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift new file mode 100644 index 000000000..7ea72a524 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsDetailsSection.swift @@ -0,0 +1,103 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + + struct DetailsSection: View { + + // MARK: - Image Details Variables + + private let index: Int? + private let language: String? + private let width: Int? + private let height: Int? + private let provider: String? + + // MARK: - Image Ratings Variables + + private let rating: Double? + private let ratingVotes: Int? + + // MARK: - Image Source Variable + + private let url: URL? + + // MARK: - Initializer + + init( + url: URL? = nil, + index: Int? = nil, + language: String? = nil, + width: Int? = nil, + height: Int? = nil, + provider: String? = nil, + rating: Double? = nil, + ratingType: RatingType? = nil, + ratingVotes: Int? = nil + ) { + self.url = url + self.index = index + self.language = language + self.width = width + self.height = height + self.provider = provider + self.rating = rating + self.ratingVotes = ratingVotes + } + + // MARK: - Body + + var body: some View { + Section(L10n.details) { + if let provider { + TextPairView(leading: L10n.provider, trailing: provider) + } + + if let language { + TextPairView(leading: L10n.language, trailing: language) + } + + if let width, let height { + TextPairView( + leading: L10n.dimensions, + trailing: "\(width) x \(height)" + ) + } + + if let index { + TextPairView(leading: L10n.index, trailing: index.description) + } + } + + if let rating { + Section(L10n.ratings) { + TextPairView(leading: L10n.rating, trailing: rating.formatted(.number.precision(.fractionLength(2)))) + + if let ratingVotes { + TextPairView(L10n.votes, value: Text(ratingVotes, format: .number)) + } + } + } + + if let url { + Section { + ChevronButton( + L10n.imageSource, + external: true + ) + .onSelect { + UIApplication.shared.open(url) + } + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.swift new file mode 100644 index 000000000..6fc1c89fc --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/Components/ItemImageDetailsHeaderSection.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) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension ItemImageDetailsView { + + struct HeaderSection: View { + + // MARK: - Image Info + + let imageSource: ImageSource + let posterType: PosterDisplayType + + // MARK: - Body + + var body: some View { + Section { + ImageView(imageSource) + .placeholder { _ in + Image(systemName: "photo") + } + .failure { + Image(systemName: "photo") + } + .pipeline(.Swiftfin.other) + } + .scaledToFit() + .frame(maxHeight: 300) + .posterStyle(posterType) + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + .listRowCornerRadius(0) + .listRowInsets(.zero) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift new file mode 100644 index 000000000..1cabcaf83 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImageDetailsView/ItemImageDetailsView.swift @@ -0,0 +1,126 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +struct ItemImageDetailsView: View { + + @Environment(\.isEditing) + private var isEditing + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + private var viewModel: ItemImagesViewModel + + // MARK: - Image Variable + + private let imageSource: ImageSource + + // MARK: - Description Variables + + private let index: Int? + private let width: Int? + private let height: Int? + private let language: String? + private let provider: String? + private let rating: Double? + private let ratingVotes: Int? + + // MARK: - Image Actions + + private let onClose: () -> Void + private let onSave: (() -> Void)? + private let onDelete: (() -> Void)? + + // MARK: - Initializer + + init( + viewModel: ItemImagesViewModel, + imageSource: ImageSource, + index: Int? = nil, + width: Int? = nil, + height: Int? = nil, + language: String? = nil, + provider: String? = nil, + rating: Double? = nil, + ratingVotes: Int? = nil, + onClose: @escaping () -> Void, + onSave: (() -> Void)? = nil, + onDelete: (() -> Void)? = nil + ) { + self.viewModel = viewModel + self.imageSource = imageSource + self.index = index + self.width = width + self.height = height + self.language = language + self.provider = provider + self.rating = rating + self.ratingVotes = ratingVotes + self.onClose = onClose + self.onSave = onSave + self.onDelete = onDelete + } + + // MARK: - Body + + var body: some View { + NavigationView { + contentView + .navigationTitle(L10n.image) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + onClose() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.updating) { + ProgressView() + } + + if let onSave { + Button(L10n.save, action: onSave) + .buttonStyle(.toolbarPill) + } + } + } + } + + // MARK: - Content View + + @ViewBuilder + private var contentView: some View { + List { + HeaderSection( + imageSource: imageSource, + posterType: height ?? 0 > width ?? 0 ? .portrait : .landscape + ) + + DetailsSection( + url: imageSource.url, + index: index, + language: language, + width: width, + height: height, + provider: provider, + rating: rating, + ratingVotes: ratingVotes + ) + + if isEditing, let onDelete { + DeleteButton { + onDelete() + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift new file mode 100644 index 000000000..bbe63e6d0 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemImagesView.swift @@ -0,0 +1,222 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +struct ItemImagesView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: ItemImagesCoordinator.Router + + @StateObject + var viewModel: ItemImagesViewModel + + // MARK: - Dialog State + + @State + private var selectedImage: ImageInfo? + @State + private var selectedType: ImageType? + @State + private var isFilePickerPresented = false + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Body + + var body: some View { + ZStack { + switch viewModel.state { + case .content: + imageView + case .initial: + DelayedProgressView() + case let .error(error): + ErrorView(error: error) + .onRetry { + viewModel.send(.refresh) + } + } + } + .navigationTitle(L10n.images) + .navigationBarTitleDisplayMode(.inline) + .onFirstAppear { + viewModel.send(.refresh) + } + .navigationBarCloseButton { + router.dismissCoordinator() + } + .sheet(item: $selectedImage) { + selectedImage = nil + } content: { imageInfo in + ItemImageDetailsView( + viewModel: viewModel, + imageSource: imageInfo.itemImageSource( + itemID: viewModel.item.id!, + client: viewModel.userSession.client + ), + index: imageInfo.imageIndex, + width: imageInfo.width, + height: imageInfo.height, + onClose: { + selectedImage = nil + }, + onDelete: { + viewModel.send(.deleteImage(imageInfo)) + selectedImage = nil + } + ) + .environment(\.isEditing, true) + } + .fileImporter( + isPresented: $isFilePickerPresented, + allowedContentTypes: [.png, .jpeg, .heic], + allowsMultipleSelection: false + ) { + switch $0 { + case let .success(urls): + if let file = urls.first, let type = selectedType { + viewModel.send(.uploadFile(file: file, type: type)) + selectedType = nil + } + case let .failure(fileError): + error = fileError + selectedType = nil + } + } + .onReceive(viewModel.events) { event in + switch event { + case .updated: () + case let .error(eventError): + self.error = eventError + } + } + .errorMessage($error) + } + + // MARK: - Image View + + @ViewBuilder + private var imageView: some View { + ScrollView { + ForEach(ImageType.allCases.sorted(using: \.rawValue), id: \.self) { imageType in + Section { + imageScrollView(for: imageType) + + RowDivider() + .padding(.vertical, 16) + } header: { + sectionHeader(for: imageType) + } + } + } + } + + // MARK: - Image Scroll View + + @ViewBuilder + private func imageScrollView(for imageType: ImageType) -> some View { + let images = viewModel.images[imageType] ?? [] + + if images.isNotEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(images, id: \.self) { imageInfo in + imageButton(imageInfo: imageInfo) { + selectedImage = imageInfo + } + } + } + .edgePadding(.horizontal) + } + } + } + + // MARK: - Section Header + + @ViewBuilder + private func sectionHeader(for imageType: ImageType) -> some View { + HStack { + Text(imageType.displayTitle) + .font(.headline) + + Spacer() + + Menu(L10n.options, systemImage: "plus") { + Button(L10n.search, systemImage: "magnifyingglass") { + router.route( + to: \.addImage, + imageType + ) + } + + Divider() + + Button(L10n.uploadFile, systemImage: "document.badge.plus") { + selectedType = imageType + isFilePickerPresented = true + } + + Button(L10n.uploadPhoto, systemImage: "photo.badge.plus") { + router.route(to: \.photoPicker, imageType) + } + } + .font(.body) + .labelStyle(.iconOnly) + .backport + .fontWeight(.semibold) + .foregroundStyle(accentColor) + } + .edgePadding(.horizontal) + } + + // MARK: - Image Button + + // TODO: instead of using `posterStyle`, should be sized based on + // the image type and just ignore and poster styling + @ViewBuilder + private func imageButton( + imageInfo: ImageInfo, + onSelect: @escaping () -> Void + ) -> some View { + Button(action: onSelect) { + ZStack { + Color.secondarySystemFill + + ImageView( + imageInfo.itemImageSource( + itemID: viewModel.item.id!, + client: viewModel.userSession.client + ) + ) + .placeholder { _ in + Image(systemName: "photo") + } + .failure { + Image(systemName: "photo") + } + .pipeline(.Swiftfin.other) + } + .posterStyle(imageInfo.height ?? 0 > imageInfo.width ?? 0 ? .portrait : .landscape) + .frame(maxHeight: 150) + .posterShadow() + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift new file mode 100644 index 000000000..72716c35b --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/Components/ItemPhotoCropView.swift @@ -0,0 +1,59 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import Mantis +import SwiftUI + +struct ItemPhotoCropView: View { + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: ItemImagePickerCoordinator.Router + + @ObservedObject + var viewModel: ItemImagesViewModel + + // MARK: - Image Variable + + let image: UIImage + let type: ImageType + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Body + + var body: some View { + PhotoCropView( + isSaving: viewModel.backgroundStates.contains(.updating), + image: image, + cropShape: .rect, + presetRatio: .canUseMultiplePresetFixedRatio() + ) { + viewModel.send(.uploadImage(image: $0, type: type)) + } onCancel: { + router.dismissCoordinator() + } + .animation(.linear(duration: 0.1), value: viewModel.state) + .interactiveDismissDisabled(viewModel.backgroundStates.contains(.updating)) + .navigationBarBackButtonHidden(viewModel.backgroundStates.contains(.updating)) + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + case .updated: + router.dismissCoordinator() + } + } + .errorMessage($error) + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.swift new file mode 100644 index 000000000..5fa611fa7 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemImages/ItemPhotoPickerView/ItemPhotoPickerView.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) 2025 Jellyfin & Jellyfin Contributors +// + +import SwiftUI + +struct ItemImagePicker: View { + + // MARK: - Observed, & Environment Objects + + @EnvironmentObject + private var router: ItemImagePickerCoordinator.Router + + // MARK: - Body + + var body: some View { + PhotoPickerView { + router.route(to: \.cropImage, $0) + } onCancel: { + router.dismissCoordinator() + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift new file mode 100644 index 000000000..77cf97067 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/AddItemElementView.swift @@ -0,0 +1,141 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import JellyfinAPI +import SwiftUI + +struct AddItemElementView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Environment & Observed Objects + + @EnvironmentObject + private var router: BasicNavigationViewCoordinator.Router + + @ObservedObject + var viewModel: ItemEditorViewModel + + // MARK: - Elements Variables + + let type: ItemArrayElements + + @State + private var id: String? + @State + private var name: String = "" + @State + private var personKind: PersonKind = .unknown + @State + private var personRole: String = "" + + // MARK: - Trie Data Loaded + + @State + private var loaded: Bool = false + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Name is Valid + + private var isValid: Bool { + name.isNotEmpty + } + + // MARK: - Name Already Exists + + private var itemAlreadyExists: Bool { + viewModel.trie.contains(key: name.localizedLowercase) + } + + // MARK: - Body + + var body: some View { + ZStack { + switch viewModel.state { + case .initial, .content, .updating: + contentView + case let .error(error): + ErrorView(error: error) + } + } + .navigationTitle(type.displayTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarCloseButton { + router.dismissCoordinator() + } + .topBarTrailing { + if viewModel.backgroundStates.contains(.loading) { + ProgressView() + } + + Button(L10n.save) { + viewModel.send(.add([type.createElement( + name: name, + id: id, + personRole: personRole.isEmpty ? (personKind == .unknown ? nil : personKind.rawValue) : personRole, + personKind: personKind.rawValue + )])) + } + .buttonStyle(.toolbarPill) + .disabled(!isValid) + } + .onFirstAppear { + viewModel.send(.load) + } + .onChange(of: name) { _ in + if !viewModel.backgroundStates.contains(.loading) { + viewModel.send(.search(name)) + } + } + .onReceive(viewModel.events) { event in + switch event { + case .updated: + UIDevice.feedback(.success) + router.dismissCoordinator() + case .loaded: + loaded = true + viewModel.send(.search(name)) + case let .error(eventError): + UIDevice.feedback(.error) + error = eventError + } + } + .errorMessage($error) + } + + // MARK: - Content View + + private var contentView: some View { + List { + NameInput( + name: $name, + personKind: $personKind, + personRole: $personRole, + type: type, + itemAlreadyExists: itemAlreadyExists + ) + + SearchResultsSection( + name: $name, + id: $id, + type: type, + population: viewModel.matches, + isSearching: viewModel.backgroundStates.contains(.searching) + ) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift new file mode 100644 index 000000000..b4ab8fc28 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/NameInput.swift @@ -0,0 +1,87 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension AddItemElementView { + + struct NameInput: View { + + // MARK: - Element Variables + + @Binding + var name: String + @Binding + var personKind: PersonKind + @Binding + var personRole: String + + let type: ItemArrayElements + let itemAlreadyExists: Bool + + // MARK: - Body + + var body: some View { + nameView + + if type == .people { + personView + } + } + + // MARK: - Name View + + private var nameView: some View { + Section { + TextField(L10n.name, text: $name) + .autocorrectionDisabled() + } header: { + Text(L10n.name) + } footer: { + if name.isEmpty || name == "" { + Label( + L10n.required, + systemImage: "exclamationmark.circle.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .orange)) + } else { + if itemAlreadyExists { + Label( + L10n.existsOnServer, + systemImage: "checkmark.circle.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .green)) + } else { + Label( + L10n.willBeCreatedOnServer, + systemImage: "checkmark.seal.fill" + ) + .labelStyle(.sectionFooterWithImage(imageStyle: .blue)) + } + } + } + } + + // MARK: - Person View + + var personView: some View { + Section { + Picker(L10n.type, selection: $personKind) { + ForEach(PersonKind.allCases, id: \.self) { kind in + Text(kind.displayTitle).tag(kind) + } + } + if personKind == PersonKind.actor { + TextField(L10n.role, text: $personRole) + .autocorrectionDisabled() + } + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift new file mode 100644 index 000000000..267359be0 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/AddItemElementView/Components/SearchResultsSection.swift @@ -0,0 +1,112 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import JellyfinAPI +import SwiftUI + +extension AddItemElementView { + + struct SearchResultsSection: View { + + // MARK: - Element Variables + + @Binding + var name: String + @Binding + var id: String? + + // MARK: - Element Search Variables + + let type: ItemArrayElements + let population: [Element] + + // TODO: Why doesn't environment(\.isSearching) work? + let isSearching: Bool + + // MARK: - Body + + var body: some View { + if name.isNotEmpty { + Section { + if population.isNotEmpty { + resultsView + .animation(.easeInOut, value: population.count) + } else if !isSearching { + noResultsView + .transition(.opacity) + .animation(.easeInOut, value: population.count) + } + } header: { + HStack { + Text(L10n.existingItems) + if isSearching { + DelayedProgressView() + } else { + Text("-") + Text(population.count.description) + } + } + .animation(.easeInOut, value: isSearching) + } + } + } + + // MARK: - No Results View + + private var noResultsView: some View { + Text(L10n.none) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .center) + } + + // MARK: - Results View + + private var resultsView: some View { + ForEach(population, id: \.self) { result in + Button { + name = type.getName(for: result) + id = type.getId(for: result) + } label: { + labelView(result) + } + .foregroundStyle(.primary) + .disabled(name == type.getName(for: result)) + .transition(.opacity.combined(with: .move(edge: .top))) + .animation(.easeInOut, value: population.count) + } + } + + // MARK: - Label View + + @ViewBuilder + private func labelView(_ match: Element) -> some View { + switch type { + case .people: + let person = match as! BaseItemPerson + HStack { + ZStack { + Color.clear + ImageView(person.portraitImageSources(maxWidth: 30)) + .failure { + SystemImageContentView(systemName: "person.fill") + } + } + .posterStyle(.portrait) + .frame(width: 30, height: 90) + .padding(.horizontal) + + Text(type.getName(for: match)) + .frame(maxWidth: .infinity, alignment: .leading) + } + default: + Text(type.getName(for: match)) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift new file mode 100644 index 000000000..f1d5df9b8 --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/Components/EditItemElementRow.swift @@ -0,0 +1,113 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import SwiftUI + +extension EditItemElementView { + + struct EditItemElementRow: View { + + // MARK: - Enviroment Variables + + @Environment(\.isEditing) + var isEditing + @Environment(\.isSelected) + var isSelected + + // MARK: - Metadata Variables + + let item: Element + let type: ItemArrayElements + + // MARK: - Row Actions + + let onSelect: () -> Void + let onDelete: () -> Void + + // MARK: - Body + + var body: some View { + ListRow { + if type == .people { + personImage + } + } content: { + rowContent + } + .onSelect(perform: onSelect) + .isSeparatorVisible(false) + .swipeActions { + Button(L10n.delete, systemImage: "trash", action: onDelete) + .tint(.red) + } + } + + // MARK: - Row Content + + @ViewBuilder + private var rowContent: some View { + HStack { + VStack(alignment: .leading) { + Text(type.getName(for: item)) + .foregroundStyle( + isEditing ? (isSelected ? .primary : .secondary) : .primary + ) + .font(.headline) + .lineLimit(1) + + if type == .people { + let person = (item as! BaseItemPerson) + + TextPairView( + leading: person.type ?? .emptyDash, + trailing: person.role ?? .emptyDash + ) + .foregroundStyle( + isEditing ? (isSelected ? .primary : .secondary) : .primary, + .secondary + ) + .font(.subheadline) + .lineLimit(1) + } + } + + if isEditing { + Spacer() + + Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(width: 24, height: 24) + .foregroundStyle(isSelected ? Color.accentColor : .secondary) + } + } + } + + // MARK: - Person Image + + @ViewBuilder + private var personImage: some View { + let person = (item as! BaseItemPerson) + + ZStack { + Color.clear + + ImageView(person.portraitImageSources(maxWidth: 30)) + .failure { + SystemImageContentView(systemName: "person.fill") + } + } + .posterStyle(.portrait) + .posterShadow() + .frame(width: 30, height: 90) + .padding(.horizontal) + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift new file mode 100644 index 000000000..ff93e5a0c --- /dev/null +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditItemElementView/EditItemElementView.swift @@ -0,0 +1,274 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Combine +import Defaults +import JellyfinAPI +import SwiftUI + +struct EditItemElementView: View { + + // MARK: - Defaults + + @Default(.accentColor) + private var accentColor + + // MARK: - Observed & Environment Objects + + @EnvironmentObject + private var router: ItemEditorCoordinator.Router + + @ObservedObject + var viewModel: ItemEditorViewModel + + // MARK: - Elements + + @State + private var elements: [Element] + + // MARK: - Type & Route + + private let type: ItemArrayElements + private let route: (ItemEditorCoordinator.Router, ItemEditorViewModel) -> Void + + // MARK: - Dialog States + + @State + private var isPresentingDeleteConfirmation = false + @State + private var isPresentingDeleteSelectionConfirmation = false + + // MARK: - Editing States + + @State + private var selectedElements: Set = [] + @State + private var isEditing: Bool = false + @State + private var isReordering: Bool = false + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Initializer + + init( + viewModel: ItemEditorViewModel, + type: ItemArrayElements, + route: @escaping (ItemEditorCoordinator.Router, ItemEditorViewModel) -> Void + ) { + self.viewModel = viewModel + self.type = type + self.route = route + self.elements = type.getElement(for: viewModel.item) + } + + // MARK: - Body + + var body: some View { + ZStack { + switch viewModel.state { + case .initial, .content, .updating: + contentView + case let .error(error): + errorView(with: error) + } + } + .navigationBarTitle(type.displayTitle) + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(isEditing || isReordering) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if isEditing { + navigationBarSelectView + } + } + ToolbarItem(placement: .topBarTrailing) { + if isEditing || isReordering { + Button(L10n.cancel) { + if isEditing { + isEditing.toggle() + } + if isReordering { + elements = type.getElement(for: viewModel.item) + isReordering.toggle() + } + UIDevice.impact(.light) + selectedElements.removeAll() + } + .buttonStyle(.toolbarPill) + .foregroundStyle(accentColor) + } + } + ToolbarItem(placement: .bottomBar) { + if isEditing { + Button(L10n.delete) { + isPresentingDeleteSelectionConfirmation = true + } + .buttonStyle(.toolbarPill(.red)) + .disabled(selectedElements.isEmpty) + .frame(maxWidth: .infinity, alignment: .trailing) + } + if isReordering { + Button(L10n.save) { + viewModel.send(.reorder(elements)) + isReordering = false + } + .buttonStyle(.toolbarPill) + .disabled(type.getElement(for: viewModel.item) == elements) + .frame(maxWidth: .infinity, alignment: .trailing) + } + } + } + .navigationBarMenuButton( + isLoading: viewModel.backgroundStates.contains(.refreshing), + isHidden: isEditing || isReordering + ) { + Button(L10n.add, systemImage: "plus") { + route(router, viewModel) + } + + if elements.isNotEmpty == true { + Button(L10n.edit, systemImage: "checkmark.circle") { + isEditing = true + } + + Button(L10n.reorder, systemImage: "arrow.up.arrow.down") { + isReordering = true + } + } + } + .onReceive(viewModel.events) { events in + switch events { + case let .error(eventError): + error = eventError + default: + break + } + } + .errorMessage($error) + .confirmationDialog( + L10n.delete, + isPresented: $isPresentingDeleteSelectionConfirmation, + titleVisibility: .visible + ) { + deleteSelectedConfirmationActions + } message: { + Text(L10n.deleteSelectedConfirmation) + } + .confirmationDialog( + L10n.delete, + isPresented: $isPresentingDeleteConfirmation, + titleVisibility: .visible + ) { + deleteConfirmationActions + } message: { + Text(L10n.deleteItemConfirmation) + } + .onNotification(.itemMetadataDidChange) { _ in + self.elements = type.getElement(for: self.viewModel.item) + } + } + + // MARK: - Select/Remove All Button + + @ViewBuilder + private var navigationBarSelectView: some View { + let isAllSelected = selectedElements.count == (elements.count) + Button(isAllSelected ? L10n.removeAll : L10n.selectAll) { + selectedElements = isAllSelected ? [] : Set(elements) + } + .buttonStyle(.toolbarPill) + .disabled(!isEditing) + .foregroundStyle(accentColor) + } + + // MARK: - ErrorView + + @ViewBuilder + private func errorView(with error: some Error) -> some View { + ErrorView(error: error) + .onRetry { + viewModel.send(.load) + } + } + + // MARK: - Content View + + private var contentView: some View { + List { + InsetGroupedListHeader(type.displayTitle, description: type.description) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .padding(.vertical, 24) + + if elements.isNotEmpty { + ForEach(elements, id: \.self) { element in + EditItemElementRow( + item: element, + type: type, + onSelect: { + if isEditing { + selectedElements.toggle(value: element) + } + }, + onDelete: { + selectedElements.toggle(value: element) + isPresentingDeleteConfirmation = true + } + ) + .environment(\.isEditing, isEditing) + .environment(\.isSelected, selectedElements.contains(element)) + .listRowInsets(.edgeInsets) + } + .onMove { source, destination in + guard isReordering else { return } + elements.move(fromOffsets: source, toOffset: destination) + } + } else { + Text(L10n.none) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .listRowSeparator(.hidden) + .listRowInsets(.zero) + } + } + .listStyle(.plain) + .environment(\.editMode, isReordering ? .constant(.active) : .constant(.inactive)) + } + + // MARK: - Delete Selected Confirmation Actions + + @ViewBuilder + private var deleteSelectedConfirmationActions: some View { + Button(L10n.cancel, role: .cancel) {} + + Button(L10n.confirm, role: .destructive) { + let elementsToRemove = elements.filter { selectedElements.contains($0) } + viewModel.send(.remove(elementsToRemove)) + selectedElements.removeAll() + isEditing = false + } + } + + // MARK: - Delete Single Confirmation Actions + + @ViewBuilder + private var deleteConfirmationActions: some View { + Button(L10n.cancel, role: .cancel) {} + + Button(L10n.delete, role: .destructive) { + if let elementToRemove = selectedElements.first, selectedElements.count == 1 { + viewModel.send(.remove([elementToRemove])) + selectedElements.removeAll() + isEditing = false + } + } + } +} diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift similarity index 96% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift index ac4f54f0d..d70a4e46f 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DateSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DateSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift similarity index 97% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift index d00c7bc23..2ea9b7204 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/DisplayOrderSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/DisplayOrderSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift similarity index 96% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift index e42c7b23d..ac73d0be4 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/EpisodeSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/EpisodeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift similarity index 93% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift index c789f0c69..854935c4b 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LocalizationSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LocalizationSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift similarity index 95% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift index 78b197fbb..675d2dfb6 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/LockMetadataSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/LockMetadataSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift similarity index 93% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift index 560d5412f..c9a580f86 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/MediaFormatSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/MediaFormatSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift similarity index 96% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift index 97998bacf..a8c8f27ec 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/OverviewSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/OverviewSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift similarity index 98% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift index 9b4be8488..ed5b324eb 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ParentialRatingsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ParentialRatingsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift similarity index 97% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift index 0cb298553..8dbefa43d 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/ReviewsSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/ReviewsSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift similarity index 98% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift index 5444f1aab..2789ee25f 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/SeriesSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/SeriesSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift similarity index 95% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift index aa994e0a4..42f2fc774 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/Components/Sections/TitleSection.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/Components/Sections/TitleSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift similarity index 98% rename from Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift rename to Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift index 39832ccb9..95c39836c 100644 --- a/Swiftfin/Views/ItemEditorView/EditMetadataView/EditMetadataView.swift +++ b/Swiftfin/Views/ItemEditorView/ItemMetadata/EditMetadataView/EditMetadataView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/ItemOverviewView.swift b/Swiftfin/Views/ItemOverviewView.swift index d6a60f5ce..aebaaafbe 100644 --- a/Swiftfin/Views/ItemOverviewView.swift +++ b/Swiftfin/Views/ItemOverviewView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift index f3bfcb06a..df20f6629 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/AboutView.swift @@ -3,13 +3,13 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack import Defaults +import IdentifiedCollections import JellyfinAPI -import OrderedCollections import SwiftUI // TODO: rename `AboutItemView` @@ -22,8 +22,7 @@ extension ItemView { struct AboutView: View { - private enum AboutViewItem: Hashable, Identifiable { - + private enum AboutViewItem: Identifiable { case image case overview case mediaSource(MediaSourceInfo) @@ -43,21 +42,14 @@ extension ItemView { } } - @Default(.accentColor) - private var accentColor - @ObservedObject var viewModel: ItemViewModel @State private var contentSize: CGSize = .zero - @State - private var items: OrderedSet - - init(viewModel: ItemViewModel) { - self.viewModel = viewModel - var items: OrderedSet = [ + private var items: [AboutViewItem] { + var items: [AboutViewItem] = [ .image, .overview, ] @@ -70,7 +62,11 @@ extension ItemView { items.append(.ratings) } - self._items = State(initialValue: items) + return items + } + + init(viewModel: ItemViewModel) { + self.viewModel = viewModel } // TODO: break out into a general solution for general use? @@ -161,6 +157,7 @@ extension ItemView { .scrollBehavior(.continuousLeadingEdge) } .trackingSize($contentSize) + .id(viewModel.item.hashValue) } } } diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift index 6829e37b7..710d80df5 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/AboutView+Card.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift index 6b5f454b7..27ddd5547 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/MediaSourcesCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift index 258e89653..d30c1cc34 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/OverviewCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift b/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift index 3891700a6..35935d934 100644 --- a/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift +++ b/Swiftfin/Views/ItemView/Components/AboutView/Components/RatingsCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift index c4b4852db..f08905431 100644 --- a/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift +++ b/Swiftfin/Views/ItemView/Components/ActionButtonHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/AttributeHStack.swift b/Swiftfin/Views/ItemView/Components/AttributeHStack.swift index dd019db0f..910fb32b9 100644 --- a/Swiftfin/Views/ItemView/Components/AttributeHStack.swift +++ b/Swiftfin/Views/ItemView/Components/AttributeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift index 9440fe1f0..559140bd4 100644 --- a/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift +++ b/Swiftfin/Views/ItemView/Components/CastAndCrewHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift b/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift index fecc6d3d3..5c0d37e43 100644 --- a/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift +++ b/Swiftfin/Views/ItemView/Components/DownloadTaskButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Factory diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift index 9db21ebd8..ddd8c62dd 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EmptyCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift index 3b0628598..fa60cc94b 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -59,7 +59,7 @@ extension SeriesEpisodeSelector { ZStack { Color.clear - ImageView(episode.imageSource(.primary, maxWidth: 500)) + ImageView(episode.imageSource(.primary, maxWidth: 250)) .failure { SystemImageContentView(systemName: episode.systemImage) } diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift index f0fb0c4f5..e9eba1035 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift index f0a92bf11..bdd29c5af 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/EpisodeHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift index 4b3083866..30b57b911 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/ErrorCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift index a9be264d6..6a9072697 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/Components/LoadingCard.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift b/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift index 54fa6b31c..73e6b00dd 100644 --- a/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift +++ b/Swiftfin/Views/ItemView/Components/EpisodeSelector/EpisodeSelector.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/ItemView/Components/GenresHStack.swift b/Swiftfin/Views/ItemView/Components/GenresHStack.swift index eda8c51b7..842d222b0 100644 --- a/Swiftfin/Views/ItemView/Components/GenresHStack.swift +++ b/Swiftfin/Views/ItemView/Components/GenresHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift b/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift index 8ed6be6a2..1855d8337 100644 --- a/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift +++ b/Swiftfin/Views/ItemView/Components/OffsetScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/Components/OverviewView.swift b/Swiftfin/Views/ItemView/Components/OverviewView.swift index aed7e679e..c8d6af630 100644 --- a/Swiftfin/Views/ItemView/Components/OverviewView.swift +++ b/Swiftfin/Views/ItemView/Components/OverviewView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/PlayButton.swift b/Swiftfin/Views/ItemView/Components/PlayButton.swift index 70c0dd05a..d23079c1a 100644 --- a/Swiftfin/Views/ItemView/Components/PlayButton.swift +++ b/Swiftfin/Views/ItemView/Components/PlayButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift index 3e99f5508..ec1803676 100644 --- a/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SimilarItemsHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift b/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift index fb4638144..7297f3919 100644 --- a/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift +++ b/Swiftfin/Views/ItemView/Components/SpecialFeatureHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift index dadee2d12..21794aa7c 100644 --- a/Swiftfin/Views/ItemView/Components/StudiosHStack.swift +++ b/Swiftfin/Views/ItemView/Components/StudiosHStack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/ItemView.swift b/Swiftfin/Views/ItemView/ItemView.swift index 78aaf8b4f..efb194022 100644 --- a/Swiftfin/Views/ItemView/ItemView.swift +++ b/Swiftfin/Views/ItemView/ItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -122,7 +122,7 @@ struct ItemView: View { } var body: some View { - WrappedView { + ZStack { switch viewModel.state { case .content: contentView diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift index e7b9d8cb6..78e4909fb 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift index f0a005dc0..ea873520e 100644 --- a/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/CollectionItemView/CollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift index 5b8960278..965c192b1 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift index 1df326d48..0b607492e 100644 --- a/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/EpisodeItemView/EpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift index 8234dee2f..47211ca3a 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift index 5b9fdfe9b..0e30eccd2 100644 --- a/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/MovieItemView/MovieItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift index aec4ab53e..c4ae16e34 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CinematicScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift index a7da5a00d..14bfaedcb 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactLogoScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift index 436d2c11c..93fb7c746 100644 --- a/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift +++ b/Swiftfin/Views/ItemView/iOS/ScrollViews/CompactPortraitScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import BlurHashKit diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift index 3def39a01..0fd2101ae 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -52,6 +52,14 @@ extension SeriesItemView { RowDivider() } + // MARK: Special Features + + if viewModel.specialFeatures.isNotEmpty { + ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) + + RowDivider() + } + // MARK: Similar if viewModel.similarItems.isNotEmpty { diff --git a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift index 436ef3c17..36c771c4f 100644 --- a/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift +++ b/Swiftfin/Views/ItemView/iOS/SeriesItemView/SeriesItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift index 91bba8efe..0ac38399f 100644 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift index 9d316e903..4b73b2a87 100644 --- a/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/CollectionItemView/iPadOSCollectionItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift index 613bcfc0b..215062090 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift index bb9914aba..251d74886 100644 --- a/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/EpisodeItemView/iPadOSEpisodeItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift index a935ff4cb..e5341a8a2 100644 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift index 3dd447847..04fd0ba54 100644 --- a/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/MovieItemView/iPadOSMovieItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift index 668ea432c..fe188ebb2 100644 --- a/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/ScrollViews/iPadOSCinematicScrollView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift index 55113b1b0..d08781bd5 100644 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemContentView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -49,6 +49,14 @@ extension iPadOSSeriesItemView { RowDivider() } + // MARK: Special Features + + if viewModel.specialFeatures.isNotEmpty { + ItemView.SpecialFeaturesHStack(items: viewModel.specialFeatures) + + RowDivider() + } + // MARK: Similar if viewModel.similarItems.isNotEmpty { diff --git a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift index e67e4a400..0e73a1dcc 100644 --- a/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift +++ b/Swiftfin/Views/ItemView/iPadOS/SeriesItemView/iPadOSSeriesItemView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/MediaSourceInfoView.swift b/Swiftfin/Views/MediaSourceInfoView.swift index d3fd17b03..6f753b684 100644 --- a/Swiftfin/Views/MediaSourceInfoView.swift +++ b/Swiftfin/Views/MediaSourceInfoView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/MediaStreamInfoView.swift b/Swiftfin/Views/MediaStreamInfoView.swift index 4bf560058..ac0f5d27b 100644 --- a/Swiftfin/Views/MediaStreamInfoView.swift +++ b/Swiftfin/Views/MediaStreamInfoView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/MediaView/Components/MediaItem.swift b/Swiftfin/Views/MediaView/Components/MediaItem.swift index a40b80f85..ef336d38f 100644 --- a/Swiftfin/Views/MediaView/Components/MediaItem.swift +++ b/Swiftfin/Views/MediaView/Components/MediaItem.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -55,9 +55,9 @@ extension MediaView { } if case let MediaViewModel.MediaType.collectionFolder(item) = mediaType { - self.imageSources = [item.imageSource(.primary, maxWidth: 500)] + self.imageSources = [item.imageSource(.primary, maxWidth: 200)] } else if case let MediaViewModel.MediaType.liveTV(item) = mediaType { - self.imageSources = [item.imageSource(.primary, maxWidth: 500)] + self.imageSources = [item.imageSource(.primary, maxWidth: 200)] } } } diff --git a/Swiftfin/Views/MediaView/MediaView.swift b/Swiftfin/Views/MediaView/MediaView.swift index 929a910cc..45b192acc 100644 --- a/Swiftfin/Views/MediaView/MediaView.swift +++ b/Swiftfin/Views/MediaView/MediaView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift b/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift index 8042568b9..05fa1b2f9 100644 --- a/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift +++ b/Swiftfin/Views/PagingLibraryView/Components/LibraryRow.swift @@ -3,13 +3,16 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults import JellyfinAPI import SwiftUI +private let landscapeMaxWidth: CGFloat = 110 +private let portraitMaxWidth: CGFloat = 60 + extension PagingLibraryView { struct LibraryRow: View { @@ -21,9 +24,9 @@ extension PagingLibraryView { private func imageView(from element: Element) -> ImageView { switch posterType { case .landscape: - ImageView(element.landscapeImageSources(maxWidth: 110)) + ImageView(element.landscapeImageSources(maxWidth: landscapeMaxWidth)) case .portrait: - ImageView(element.portraitImageSources(maxWidth: 60)) + ImageView(element.portraitImageSources(maxWidth: portraitMaxWidth)) } } @@ -96,7 +99,7 @@ extension PagingLibraryView { } } .posterStyle(posterType) - .frame(width: posterType == .landscape ? 110 : 60) + .frame(width: posterType == .landscape ? landscapeMaxWidth : portraitMaxWidth) .posterShadow() .padding(.vertical, 8) } diff --git a/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift b/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift index d3ca3efd2..e0a5999b6 100644 --- a/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift +++ b/Swiftfin/Views/PagingLibraryView/Components/LibraryViewTypeToggle.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -59,7 +59,7 @@ extension PagingLibraryView { } } - Section("Layout") { + Section(L10n.layout) { Button { viewType = .grid } label: { @@ -87,9 +87,9 @@ extension PagingLibraryView { } label: { switch viewType { case .grid: - Label("Layout", systemImage: "square.grid.2x2.fill") + Label(L10n.layout, systemImage: "square.grid.2x2.fill") case .list: - Label("Layout", systemImage: "square.fill.text.grid.1x2") + Label(L10n.layout, systemImage: "square.fill.text.grid.1x2") } } } diff --git a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift index b2e63a52b..1198d1cfd 100644 --- a/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift +++ b/Swiftfin/Views/PagingLibraryView/PagingLibraryView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid diff --git a/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift new file mode 100644 index 000000000..611245c30 --- /dev/null +++ b/Swiftfin/Views/PhotoPickerView/Components/PhotoCropView.swift @@ -0,0 +1,161 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import Mantis +import SwiftUI + +struct PhotoCropView: View { + + // MARK: - State, Observed, & Environment Objects + + @StateObject + private var proxy: _PhotoCropView.Proxy = .init() + + // MARK: - Image Variable + + let isSaving: Bool + let image: UIImage + let cropShape: Mantis.CropShapeType + let presetRatio: Mantis.PresetFixedRatioType + let onSave: (UIImage) -> Void + let onCancel: () -> Void + + // MARK: - Body + + var body: some View { + _PhotoCropView( + initialImage: image, + cropShape: cropShape, + presetRatio: presetRatio, + proxy: proxy, + onImageCropped: onSave + ) + .topBarTrailing { + + Button(L10n.rotate, systemImage: "rotate.right") { + proxy.rotate() + } + + if isSaving { + Button(L10n.cancel, action: onCancel) + .buttonStyle(.toolbarPill(.red)) + } else { + Button(L10n.save) { + proxy.crop() + } + .buttonStyle(.toolbarPill) + } + } + .toolbar { + ToolbarItem(placement: .principal) { + if isSaving { + ProgressView() + } else { + Button(L10n.reset) { + proxy.reset() + } + .foregroundStyle(.yellow) + .disabled(isSaving) + } + } + } + .ignoresSafeArea() + .background { + Color.black + } + } +} + +// MARK: - Photo Crop View + +private struct _PhotoCropView: UIViewControllerRepresentable { + + class Proxy: ObservableObject { + + weak var cropViewController: CropViewController? + + func crop() { + cropViewController?.crop() + } + + func reset() { + cropViewController?.didSelectReset() + } + + func rotate() { + cropViewController?.didSelectClockwiseRotate() + } + } + + let initialImage: UIImage + let cropShape: Mantis.CropShapeType + let presetRatio: Mantis.PresetFixedRatioType + let proxy: Proxy + let onImageCropped: (UIImage) -> Void + + func makeUIViewController(context: Context) -> some UIViewController { + var config = Mantis.Config() + + config.cropViewConfig.backgroundColor = .black.withAlphaComponent(0.9) + config.cropViewConfig.cropShapeType = cropShape + config.presetFixedRatioType = presetRatio + config.showAttachedCropToolbar = false + + let cropViewController = Mantis.cropViewController( + image: initialImage, + config: config + ) + + cropViewController.delegate = context.coordinator + context.coordinator.onImageCropped = onImageCropped + + proxy.cropViewController = cropViewController + + return cropViewController + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator: CropViewControllerDelegate { + + var onImageCropped: ((UIImage) -> Void)? + + func cropViewControllerDidCrop( + _ cropViewController: CropViewController, + cropped: UIImage, + transformation: Transformation, + cropInfo: CropInfo + ) { + onImageCropped?(cropped) + } + + func cropViewControllerDidCancel( + _ cropViewController: CropViewController, + original: UIImage + ) {} + + func cropViewControllerDidFailToCrop( + _ cropViewController: CropViewController, + original: UIImage + ) {} + + func cropViewControllerDidBeginResize( + _ cropViewController: CropViewController + ) {} + + func cropViewControllerDidEndResize( + _ cropViewController: Mantis.CropViewController, + original: UIImage, + cropInfo: Mantis.CropInfo + ) {} + } +} diff --git a/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift new file mode 100644 index 000000000..a95e01081 --- /dev/null +++ b/Swiftfin/Views/PhotoPickerView/PhotoPickerView.swift @@ -0,0 +1,86 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors +// + +import PhotosUI +import SwiftUI + +// TODO: polish: find way to deselect image on appear +// - from popping from cropping +// TODO: polish: when image is picked, instead of loading it here +// which takes ~1-2s, show some kind of loading indicator +// on this view or push to another view that will go to crop + +struct PhotoPickerView: UIViewControllerRepresentable { + + // MARK: - Photo Picker Actions + + var onSelect: (UIImage) -> Void + var onCancel: () -> Void + + // MARK: - Initializer + + init(onSelect: @escaping (UIImage) -> Void, onCancel: @escaping () -> Void) { + self.onSelect = onSelect + self.onCancel = onCancel + } + + // MARK: - UIView Controller + + func makeUIViewController(context: Context) -> PHPickerViewController { + + var configuration = PHPickerConfiguration(photoLibrary: .shared()) + + configuration.filter = .all(of: [.images, .not(.livePhotos)]) + configuration.preferredAssetRepresentationMode = .current + configuration.selection = .default + configuration.selectionLimit = 1 + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = context.coordinator + + context.coordinator.onSelect = onSelect + context.coordinator.onCancel = onCancel + + return picker + } + + // MARK: - Update UIView Controller + + func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} + + // MARK: - Make Coordinator + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + // MARK: - Coordinator + + class Coordinator: PHPickerViewControllerDelegate { + + var onSelect: ((UIImage) -> Void)? + var onCancel: (() -> Void)? + + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + + guard let image = results.first else { + onCancel?() + return + } + + let itemProvider = image.itemProvider + + guard itemProvider.canLoadObject(ofClass: UIImage.self) else { return } + + itemProvider.loadObject(ofClass: UIImage.self) { image, _ in + guard let image = image as? UIImage else { return } + self.onSelect?(image) + } + } + } +} diff --git a/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift b/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift index f0d9bf967..5d65d1029 100644 --- a/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift +++ b/Swiftfin/Views/ProgramsView/Components/ProgramButtonContent.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift b/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift index 4208ea66f..a1adf0eed 100644 --- a/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift +++ b/Swiftfin/Views/ProgramsView/Components/ProgramProgressOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ProgramsView/ProgramsView.swift b/Swiftfin/Views/ProgramsView/ProgramsView.swift index acb86de55..f4c44ca95 100644 --- a/Swiftfin/Views/ProgramsView/ProgramsView.swift +++ b/Swiftfin/Views/ProgramsView/ProgramsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/QuickConnectView.swift b/Swiftfin/Views/QuickConnectView.swift index 0260de765..3ceb286a5 100644 --- a/Swiftfin/Views/QuickConnectView.swift +++ b/Swiftfin/Views/QuickConnectView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift b/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift index 9a5c0d296..2aeca1f1a 100644 --- a/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift +++ b/Swiftfin/Views/ResetUserPasswordView/ResetUserPasswordView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine diff --git a/Swiftfin/Views/SearchView.swift b/Swiftfin/Views/SearchView.swift index c2f97b196..a5d047d3b 100644 --- a/Swiftfin/Views/SearchView.swift +++ b/Swiftfin/Views/SearchView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift b/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift index 4b345bed5..5ebfa2145 100644 --- a/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift +++ b/Swiftfin/Views/SelectUserView/Components/AddUserButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections @@ -63,13 +63,13 @@ extension SelectUserView { .clipShape(.circle) .aspectRatio(1, contentMode: .fill) - Text("Add User") + Text(L10n.addUser) .font(.title3) .fontWeight(.semibold) .foregroundStyle(isEnabled ? .primary : .secondary) if serverSelection == .all { - Text("Hidden") + Text(L10n.hidden) .font(.footnote) .hidden() } @@ -80,7 +80,7 @@ extension SelectUserView { if serverSelection == .all { Menu { - Text("Select Server") + Text(L10n.selectServer) ForEach(servers) { server in Button { diff --git a/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift b/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift index 89da093a9..c1826586d 100644 --- a/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift +++ b/Swiftfin/Views/SelectUserView/Components/AddUserRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import OrderedCollections @@ -48,7 +48,7 @@ extension SelectUserView { private var rowContent: some View { HStack { - Text("Add User") + Text(L10n.addUser) .font(.title3) .fontWeight(.semibold) .foregroundStyle(isEnabled ? .primary : .secondary) @@ -99,7 +99,7 @@ extension SelectUserView { if serverSelection == .all { Menu { - Text("Select Server") + Text(L10n.selectServer) ForEach(servers) { server in Button { diff --git a/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift b/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift index a14f754b7..38b648341 100644 --- a/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift +++ b/Swiftfin/Views/SelectUserView/Components/ServerSelectionMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -45,21 +45,21 @@ extension SelectUserView { var body: some View { Menu { Section { - Button("Add Server", systemImage: "plus") { + Button(L10n.addServer, systemImage: "plus") { router.route(to: \.connectToServer) } if let selectedServer { - Button("Edit Server", systemImage: "server.rack") { + Button(L10n.editServer, systemImage: "server.rack") { router.route(to: \.editServer, selectedServer) } } } - Picker("Servers", selection: _serverSelection) { + Picker(L10n.servers, selection: _serverSelection) { if viewModel.servers.keys.count > 1 { - Label("All Servers", systemImage: "person.2.fill") + Label(L10n.allServers, systemImage: "person.2.fill") .tag(SelectUserServerSelection.all) } @@ -83,7 +83,7 @@ extension SelectUserView { HStack { switch serverSelection { case .all: - Label("All Servers", systemImage: "person.2.fill") + Label(L10n.allServers, systemImage: "person.2.fill") case let .server(id): if let server = viewModel.servers.keys.first(where: { $0.id == id }) { Label(server.name, systemImage: "server.rack") diff --git a/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift b/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift index 1c0761b52..19485597a 100644 --- a/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift +++ b/Swiftfin/Views/SelectUserView/Components/UserGridButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -50,25 +50,6 @@ extension SelectUserView { return isSelected ? .primary : .secondary } - @ViewBuilder - private var personView: some View { - ZStack { - Group { - if colorScheme == .light { - Color.secondarySystemFill - } else { - Color.tertiarySystemBackground - } - } - .posterShadow() - - RelativeSystemImageView(systemName: "person.fill", ratio: 0.5) - .foregroundStyle(.secondary) - } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - } - var body: some View { Button { action() @@ -77,21 +58,15 @@ extension SelectUserView { ZStack { Color.clear - ImageView(user.profileImageSource(client: server.client, maxWidth: 120)) - .pipeline(.Swiftfin.branding) - .image { image in - image - .posterBorder(ratio: 1 / 2, of: \.width) - } - .placeholder { _ in - personView - } - .failure { - personView - } + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: server.client, + maxWidth: 120 + ), + pipeline: .Swiftfin.local + ) } - .aspectRatio(contentMode: .fill) - .clipShape(.circle) .overlay { if isEditing { ZStack(alignment: .bottomTrailing) { @@ -126,7 +101,7 @@ extension SelectUserView { } .buttonStyle(.plain) .contextMenu { - Button("Delete", role: .destructive) { + Button(L10n.delete, role: .destructive) { onDelete() } } diff --git a/Swiftfin/Views/SelectUserView/Components/UserRow.swift b/Swiftfin/Views/SelectUserView/Components/UserRow.swift index 75c696fc3..c5bc65d92 100644 --- a/Swiftfin/Views/SelectUserView/Components/UserRow.swift +++ b/Swiftfin/Views/SelectUserView/Components/UserRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -74,18 +74,14 @@ extension SelectUserView { ZStack { Color.clear - ImageView(user.profileImageSource(client: server.client, maxWidth: 120)) - .pipeline(.Swiftfin.branding) - .image { image in - image - .posterBorder(ratio: 1 / 2, of: \.width) - } - .placeholder { _ in - personView - } - .failure { - personView - } + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: server.client, + maxWidth: 120 + ), + pipeline: .Swiftfin.local + ) if isEditing { Color.black @@ -131,7 +127,7 @@ extension SelectUserView { } .onSelect(perform: action) .contextMenu { - Button("Delete", role: .destructive) { + Button(L10n.delete, role: .destructive) { onDelete() } } diff --git a/Swiftfin/Views/SelectUserView/SelectUserView.swift b/Swiftfin/Views/SelectUserView/SelectUserView.swift index c36b2cc8f..19a2b3669 100644 --- a/Swiftfin/Views/SelectUserView/SelectUserView.swift +++ b/Swiftfin/Views/SelectUserView/SelectUserView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionVGrid @@ -20,6 +20,9 @@ import SwiftUI // TODO: user ordering // - name // - last signed in date +// TODO: between the server selection menu and delete toolbar, +// figure out a way to make the grid/list and splash screen +// not jump when size is changed struct SelectUserView: View { @@ -85,6 +88,17 @@ struct SelectUserView: View { @State private var error: Error? = nil + private var users: [UserState] { + gridItems.compactMap { item in + switch item { + case let .user(user, _): + return user + default: + return nil + } + } + } + // MARK: - Select Server private var selectedServer: ServerState? { @@ -380,33 +394,6 @@ struct SelectUserView: View { } } - // MARK: - Delete Users Button - - @ViewBuilder - private var deleteUsersButton: some View { - Button { - isPresentingConfirmDeleteUsers = true - } label: { - ZStack { - Color.red - - Text(L10n.delete) - .font(.body.weight(.semibold)) - .foregroundStyle(selectedUsers.isNotEmpty ? .primary : .secondary) - - if selectedUsers.isEmpty { - Color.black - .opacity(0.5) - } - } - .clipShape(RoundedRectangle(cornerRadius: 10)) - .frame(height: 50) - .frame(maxWidth: 400) - } - .disabled(selectedUsers.isEmpty) - .buttonStyle(.plain) - } - // MARK: - User View @ViewBuilder @@ -453,11 +440,6 @@ struct SelectUserView: View { ) .edgePadding([.bottom, .horizontal]) } - - if isEditingUsers { - deleteUsersButton - .edgePadding([.bottom, .horizontal]) - } } .background { if selectUserUseSplashscreen, splashScreenImageSources.isNotEmpty { @@ -465,7 +447,7 @@ struct SelectUserView: View { Color.clear ImageView(splashScreenImageSources) - .pipeline(.Swiftfin.branding) + .pipeline(.Swiftfin.local) .aspectRatio(contentMode: .fill) .id(splashScreenImageSources) .transition(.opacity) @@ -516,28 +498,49 @@ struct SelectUserView: View { .aspectRatio(contentMode: .fit) .frame(width: 30) } - } - .topBarTrailing { - if isEditingUsers { - Button { - isEditingUsers = false - } label: { - L10n.cancel.text - .font(.headline) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .background { - if colorScheme == .light { - Color.secondarySystemFill - } else { - Color.tertiarySystemBackground - } + + ToolbarItem(placement: .topBarLeading) { + if isEditingUsers { + if selectedUsers.count == users.count { + Button(L10n.removeAll) { + selectedUsers.removeAll() + } + .buttonStyle(.toolbarPill) + } else { + Button(L10n.selectAll) { + selectedUsers.insert(contentsOf: users) } - .clipShape(RoundedRectangle(cornerRadius: 10)) + .buttonStyle(.toolbarPill) + } + } + } + + ToolbarItemGroup(placement: .topBarTrailing) { + if isEditingUsers { + Button(isEditingUsers ? L10n.cancel : L10n.edit) { + isEditingUsers.toggle() + + UIDevice.impact(.light) + + if !isEditingUsers { + selectedUsers.removeAll() + } + } + .buttonStyle(.toolbarPill) + } else { + advancedMenu + } + } + + ToolbarItem(placement: .bottomBar) { + if isEditingUsers { + Button(L10n.delete) { + isPresentingConfirmDeleteUsers = true + } + .buttonStyle(.toolbarPill(.red)) + .disabled(selectedUsers.isEmpty) + .frame(maxWidth: .infinity, alignment: .trailing) } - .buttonStyle(.plain) - } else { - advancedMenu } } .onAppear { diff --git a/Swiftfin/Views/ServerCheckView.swift b/Swiftfin/Views/ServerCheckView.swift index d924c9ace..3ece182f5 100644 --- a/Swiftfin/Views/ServerCheckView.swift +++ b/Swiftfin/Views/ServerCheckView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift index f6fc5b672..0060044a2 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/CustomProfileButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation @@ -50,7 +50,7 @@ extension CustomDeviceProfileSettingsView { profileDetailsView( title: L10n.useAsTranscodingProfile, - detail: profile.useAsTranscodingProfile ? "Yes" : "No" + detail: profile.useAsTranscodingProfile ? L10n.yes : L10n.no ) } diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift index 0a9a3bc50..b66cf2bf8 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/Components/EditCustomDeviceProfileView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -123,7 +123,7 @@ extension CustomDeviceProfileSettingsView { } .navigationTitle(L10n.customProfile) .topBarTrailing { - Button("Save") { + Button(L10n.save) { if createProfile { customDeviceProfiles.append(profile) } else { @@ -137,7 +137,7 @@ extension CustomDeviceProfileSettingsView { .disabled(!isValid) } .alert("Profile not saved", isPresented: $isPresentingNotSaved) { - Button("Close", role: .destructive) { + Button(L10n.close, role: .destructive) { router.dismissCoordinator() } } diff --git a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift index 45e5c2962..b7490a771 100644 --- a/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/CustomDeviceProfileSettingsView/CustomDeviceProfileSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index 9a4688d46..ae3252895 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift index 0e0f730f9..0e08a81c5 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/ItemSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift index a4c8d1552..4a1e664d9 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/CustomizeViewsSettings.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -167,15 +167,15 @@ struct CustomizeViewsSettings: View { HomeSection() Section { - Toggle("Remember layout", isOn: $rememberLibraryLayout) + Toggle(L10n.rememberLayout, isOn: $rememberLibraryLayout) } footer: { - Text("Remember layout for individual libraries") + Text(L10n.rememberLayoutFooter) } Section { - Toggle("Remember sorting", isOn: $rememberLibrarySort) + Toggle(L10n.rememberSorting, isOn: $rememberLibrarySort) } footer: { - Text("Remember sorting for individual libraries") + Text(L10n.rememberSortingFooter) } Section { diff --git a/Swiftfin/Views/SettingsView/DebugSettingsView.swift b/Swiftfin/Views/SettingsView/DebugSettingsView.swift index 4abdf4acf..ddf2c1316 100644 --- a/Swiftfin/Views/SettingsView/DebugSettingsView.swift +++ b/Swiftfin/Views/SettingsView/DebugSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift index 162f984e4..9101f4d3b 100644 --- a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/GestureSettingsView.swift b/Swiftfin/Views/SettingsView/GestureSettingsView.swift index c062ef818..a84e816a0 100644 --- a/Swiftfin/Views/SettingsView/GestureSettingsView.swift +++ b/Swiftfin/Views/SettingsView/GestureSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -57,6 +57,6 @@ struct GestureSettingsView: View { CaseIterablePicker("Right Vertical Pan", selection: $verticalPanGestureRight) } } - .navigationTitle("Gestures") + .navigationTitle(L10n.gestures) } } diff --git a/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift b/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift index b4c74f89d..5cbea284e 100644 --- a/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift +++ b/Swiftfin/Views/SettingsView/IndicatorSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift b/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift index 0f64516c2..12ff5e561 100644 --- a/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift +++ b/Swiftfin/Views/SettingsView/NativeVideoPlayerSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -20,7 +20,7 @@ struct NativeVideoPlayerSettingsView: View { Section { BasicStepper( - title: "Resume Offset", + title: L10n.resumeOffset, value: $resumeOffset, range: 0 ... 30, step: 1 @@ -29,9 +29,9 @@ struct NativeVideoPlayerSettingsView: View { $0.secondLabel } } footer: { - Text("Resume content seconds before the recorded resume time") + Text(L10n.resumeOffsetDescription) } } - .navigationTitle("Native Player") + .navigationTitle(L10n.nativePlayer) } } diff --git a/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift b/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift index 1c003daf4..3beaed2dd 100644 --- a/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift +++ b/Swiftfin/Views/SettingsView/PlaybackQualitySettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift index 1d95a2429..d87d0b8ed 100644 --- a/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -102,7 +102,7 @@ struct SettingsView: View { Section { ColorPicker(L10n.accentColor, selection: $accentColor, supportsOpacity: false) } footer: { - Text(L10n.accentColorDescription) + Text(L10n.viewsMayRequireRestart) } ChevronButton(L10n.logs) diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift index a7af01cc0..b15a5e0a0 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/QuickConnectAuthorizeView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift index 7c33f5313..b341ab0fb 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserLocalSecurityView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Combine @@ -37,7 +37,7 @@ struct UserLocalSecurityView: View { @State private var listSize: CGSize = .zero @State - private var onPinCompletion: (() -> Void)? = nil + private var onPinCompletion: (() -> Void)? @State private var pin: String = "" @State diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/PhotoPicker.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/PhotoPicker.swift deleted file mode 100644 index f497d8b54..000000000 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/PhotoPicker.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import PhotosUI -import SwiftUI - -// TODO: polish: find way to deselect image on appear -// - from popping from cropping -// TODO: polish: when image is picked, instead of loading it here -// which takes ~1-2s, show some kind of loading indicator -// on this view or push to another view that will go to crop - -extension UserProfileImagePicker { - - struct PhotoPicker: UIViewControllerRepresentable { - - var onCancel: () -> Void - var onSelectedImage: (UIImage) -> Void - - init(onCancel: @escaping () -> Void, onSelectedImage: @escaping (UIImage) -> Void) { - self.onCancel = onCancel - self.onSelectedImage = onSelectedImage - } - - func makeUIViewController(context: Context) -> PHPickerViewController { - - var configuration = PHPickerConfiguration(photoLibrary: .shared()) - - configuration.filter = .all(of: [.images, .not(.livePhotos)]) - configuration.preferredAssetRepresentationMode = .current - configuration.selection = .ordered - configuration.selectionLimit = 1 - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = context.coordinator - - context.coordinator.onCancel = onCancel - context.coordinator.onSelectedImage = onSelectedImage - - return picker - } - - func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - class Coordinator: PHPickerViewControllerDelegate { - - var onCancel: (() -> Void)? - var onSelectedImage: ((UIImage) -> Void)? - - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - - guard let image = results.first else { - onCancel?() - return - } - - let itemProvider = image.itemProvider - - if itemProvider.canLoadObject(ofClass: UIImage.self) { - itemProvider.loadObject(ofClass: UIImage.self) { image, _ in - if let image = image as? UIImage { - self.onSelectedImage?(image) - } - } - } - } - } - } -} diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift deleted file mode 100644 index 57726e1c0..000000000 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/Components/SquareImageCropView.swift +++ /dev/null @@ -1,195 +0,0 @@ -// -// Swiftfin is subject to the terms of the Mozilla Public -// License, v2.0. If a copy of the MPL was not distributed with this -// file, you can obtain one at https://mozilla.org/MPL/2.0/. -// -// Copyright (c) 2024 Jellyfin & Jellyfin Contributors -// - -import Defaults -import Mantis -import SwiftUI - -extension UserProfileImagePicker { - - struct SquareImageCropView: View { - - // MARK: - Defaults - - @Default(.accentColor) - private var accentColor - - // MARK: - State & Environment Objects - - @EnvironmentObject - private var router: UserProfileImageCoordinator.Router - - @StateObject - private var proxy: _SquareImageCropView.Proxy = .init() - @StateObject - private var viewModel = UserProfileImageViewModel() - - // MARK: - Image Variable - - let image: UIImage - - // MARK: - Error State - - @State - private var error: Error? = nil - - // MARK: - Body - - var body: some View { - _SquareImageCropView(initialImage: image, proxy: proxy) { - viewModel.send(.upload($0)) - } - .animation(.linear(duration: 0.1), value: viewModel.state) - .interactiveDismissDisabled(viewModel.state == .uploading) - .navigationBarBackButtonHidden(viewModel.state == .uploading) - .topBarTrailing { - - if viewModel.state == .initial { - Button(L10n.rotate, systemImage: "rotate.right") { - proxy.rotate() - } - .foregroundStyle(.gray) - } - - if viewModel.state == .uploading { - Button(L10n.cancel) { - viewModel.send(.cancel) - } - .foregroundStyle(.red) - } else { - Button { - proxy.crop() - } label: { - Text(L10n.save) - .foregroundStyle(accentColor.overlayColor) - .font(.headline) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .background { - accentColor - } - .clipShape(RoundedRectangle(cornerRadius: 10)) - } - } - } - .toolbar { - ToolbarItem(placement: .principal) { - if viewModel.state == .uploading { - ProgressView() - } else { - Button(L10n.reset) { - proxy.reset() - } - .foregroundStyle(.yellow) - .disabled(viewModel.state == .uploading) - } - } - } - .ignoresSafeArea() - .background { - Color.black - } - .onReceive(viewModel.events) { event in - switch event { - case let .error(eventError): - error = eventError - case .uploaded: - router.dismissCoordinator() - } - } - .errorMessage($error) - } - } - - // MARK: - Square Image Crop View - - struct _SquareImageCropView: UIViewControllerRepresentable { - - class Proxy: ObservableObject { - - weak var cropViewController: CropViewController? - - func crop() { - cropViewController?.crop() - } - - func reset() { - cropViewController?.didSelectReset() - } - - func rotate() { - cropViewController?.didSelectClockwiseRotate() - } - } - - let initialImage: UIImage - let proxy: Proxy - let onImageCropped: (UIImage) -> Void - - func makeUIViewController(context: Context) -> some UIViewController { - var config = Mantis.Config() - - config.cropViewConfig.backgroundColor = .black.withAlphaComponent(0.9) - config.cropViewConfig.cropShapeType = .square - config.presetFixedRatioType = .alwaysUsingOnePresetFixedRatio(ratio: 1) - config.showAttachedCropToolbar = false - - let cropViewController = Mantis.cropViewController( - image: initialImage, - config: config - ) - - cropViewController.delegate = context.coordinator - context.coordinator.onImageCropped = onImageCropped - - proxy.cropViewController = cropViewController - - return cropViewController - } - - func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - class Coordinator: CropViewControllerDelegate { - - var onImageCropped: ((UIImage) -> Void)? - - func cropViewControllerDidCrop( - _ cropViewController: CropViewController, - cropped: UIImage, - transformation: Transformation, - cropInfo: CropInfo - ) { - onImageCropped?(cropped) - } - - func cropViewControllerDidCancel( - _ cropViewController: CropViewController, - original: UIImage - ) {} - - func cropViewControllerDidFailToCrop( - _ cropViewController: CropViewController, - original: UIImage - ) {} - - func cropViewControllerDidBeginResize( - _ cropViewController: CropViewController - ) {} - - func cropViewControllerDidEndResize( - _ cropViewController: Mantis.CropViewController, - original: UIImage, - cropInfo: Mantis.CropInfo - ) {} - } - } -} diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift index afebdcd6b..f7bf18b9b 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift +++ b/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileSettingsView.swift @@ -3,84 +3,44 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults import Factory +import JellyfinAPI import SwiftUI struct UserProfileSettingsView: View { - @Default(.accentColor) - private var accentColor - @EnvironmentObject private var router: SettingsCoordinator.Router @ObservedObject - var viewModel: SettingsViewModel + private var viewModel: SettingsViewModel + @StateObject + private var profileImageViewModel: UserProfileImageViewModel @State private var isPresentingConfirmReset: Bool = false - @State - private var isPresentingProfileImageOptions: Bool = false - @ViewBuilder - private var imageView: some View { - RedrawOnNotificationView(.didChangeUserProfileImage) { - ImageView( - viewModel.userSession.user.profileImageSource( - client: viewModel.userSession.client, - maxWidth: 120 - ) - ) - .pipeline(.Swiftfin.branding) - .image { image in - image.posterBorder(ratio: 1 / 2, of: \.width) - } - .placeholder { _ in - SystemImageContentView(systemName: "person.fill", ratio: 0.5) - } - .failure { - SystemImageContentView(systemName: "person.fill", ratio: 0.5) - } - } + init(viewModel: SettingsViewModel) { + self.viewModel = viewModel + self._profileImageViewModel = StateObject(wrappedValue: UserProfileImageViewModel(user: viewModel.userSession.user.data)) } var body: some View { List { - Section { - VStack(alignment: .center) { - Button { - isPresentingProfileImageOptions = true - } label: { - ZStack(alignment: .bottomTrailing) { - // `.aspectRatio(contentMode: .fill)` on `imageView` alone - // causes a crash on some iOS versions - ZStack { - imageView - } - .aspectRatio(1, contentMode: .fill) - .clipShape(.circle) - .frame(width: 150, height: 150) - .shadow(radius: 5) - - Image(systemName: "pencil.circle.fill") - .resizable() - .frame(width: 30, height: 30) - .shadow(radius: 10) - .symbolRenderingMode(.palette) - .foregroundStyle(accentColor.overlayColor, accentColor) - } - } - - Text(viewModel.userSession.user.username) - .fontWeight(.semibold) - .font(.title2) - } - .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) + UserProfileHeroImage( + user: profileImageViewModel.user, + source: viewModel.userSession.user.profileImageSource( + client: viewModel.userSession.client, + maxWidth: 150 + ) + ) { + router.route(to: \.photoPicker, profileImageViewModel) + } onDelete: { + profileImageViewModel.send(.delete) } Section { @@ -89,14 +49,14 @@ struct UserProfileSettingsView: View { router.route(to: \.quickConnect) } - ChevronButton("Password") + ChevronButton(L10n.password) .onSelect { router.route(to: \.resetUserPassword, viewModel.userSession.user.id) } } Section { - ChevronButton("Security") + ChevronButton(L10n.security) .onSelect { router.route(to: \.localSecurity) } @@ -105,16 +65,20 @@ struct UserProfileSettingsView: View { Section { // TODO: move under future "Storage" tab // when downloads implemented - Button("Reset Settings") { + Button(L10n.resetSettings) { isPresentingConfirmReset = true } .foregroundStyle(.red) } footer: { - Text("Reset Swiftfin user settings") + Text(L10n.resetSettingsDescription) } } - .alert("Reset Settings", isPresented: $isPresentingConfirmReset) { - Button("Reset", role: .destructive) { + .confirmationDialog( + L10n.resetSettings, + isPresented: $isPresentingConfirmReset, + titleVisibility: .visible + ) { + Button(L10n.reset, role: .destructive) { do { try viewModel.userSession.user.deleteSettings() } catch { @@ -122,21 +86,7 @@ struct UserProfileSettingsView: View { } } } message: { - Text("Are you sure you want to reset all user settings?") - } - .confirmationDialog( - "Profile Image", - isPresented: $isPresentingProfileImageOptions, - titleVisibility: .visible - ) { - - Button("Select Image") { - router.route(to: \.photoPicker, viewModel) - } - - Button("Delete", role: .destructive) { - viewModel.deleteCurrentUserProfileImage() - } + Text(L10n.resetSettingsMessage) } } } diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift index fc92ba9ef..27f84cda2 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/ActionButtonSelectorView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift index 05e2f6ee1..31dcb1913 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/ButtonSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift index e70365ad8..cfe89cc40 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SliderSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift index 146439874..2239aa189 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/SubtitleSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults @@ -42,7 +42,7 @@ extension VideoPlayerSettingsView { Text(L10n.subtitle) } footer: { // TODO: better wording - Text("Settings only affect some subtitle types") + Text(L10n.subtitlesDisclaimer) } } } diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift index 3ad3a5479..4597464be 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TimestampSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift index 3c307a527..e4ba36c75 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/Components/Sections/TransitionSection.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift index 885f046a8..2ab198055 100644 --- a/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift +++ b/Swiftfin/Views/SettingsView/VideoPlayerSettingsView/VideoPlayerSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.swift new file mode 100644 index 000000000..6deca814b --- /dev/null +++ b/Swiftfin/Views/UserProfileImagePicker/Components/UserProfileImageCropView.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) 2025 Jellyfin & Jellyfin Contributors +// + +import Defaults +import JellyfinAPI +import Mantis +import SwiftUI + +struct UserProfileImageCropView: View { + + // MARK: - State, Observed, & Environment Objects + + @EnvironmentObject + private var router: UserProfileImageCoordinator.Router + + @ObservedObject + var viewModel: UserProfileImageViewModel + + // MARK: - Image Variable + + let image: UIImage + + // MARK: - Error State + + @State + private var error: Error? + + // MARK: - Body + + var body: some View { + PhotoCropView( + isSaving: viewModel.state == .uploading, + image: image, + cropShape: .square, + presetRatio: .alwaysUsingOnePresetFixedRatio(ratio: 1) + ) { + viewModel.send(.upload($0)) + } onCancel: { + router.dismissCoordinator() + } + .animation(.linear(duration: 0.1), value: viewModel.state) + .interactiveDismissDisabled(viewModel.state == .uploading) + .navigationBarBackButtonHidden(viewModel.state == .uploading) + .onReceive(viewModel.events) { event in + switch event { + case let .error(eventError): + error = eventError + case .deleted: + break + case .uploaded: + router.dismissCoordinator() + } + } + .errorMessage($error) + } +} diff --git a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/UserProfileImagePicker.swift b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift similarity index 59% rename from Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/UserProfileImagePicker.swift rename to Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift index cefe33dda..4d4860204 100644 --- a/Swiftfin/Views/SettingsView/UserProfileSettingsView/UserProfileImagePicker/UserProfileImagePicker.swift +++ b/Swiftfin/Views/UserProfileImagePicker/UserProfileImagePickerView.swift @@ -3,21 +3,25 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI -struct UserProfileImagePicker: View { +struct UserProfileImagePickerView: View { + + // MARK: - Observed, & Environment Objects @EnvironmentObject private var router: UserProfileImageCoordinator.Router + // MARK: - Body + var body: some View { - PhotoPicker { + PhotoPickerView { + router.route(to: \.cropImage, $0) + } onCancel: { router.dismissCoordinator() - } onSelectedImage: { image in - router.route(to: \.cropImage, image) } } } diff --git a/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift b/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift index 16960b13d..9e41443a5 100644 --- a/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift +++ b/Swiftfin/Views/UserSignInView/Components/PublicUserRow.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI @@ -30,25 +30,6 @@ extension UserSignInView { self.action = action } - @ViewBuilder - private var personView: some View { - ZStack { - Group { - if colorScheme == .light { - Color.secondarySystemFill - } else { - Color.tertiarySystemBackground - } - } - .posterShadow() - - RelativeSystemImageView(systemName: "person.fill", ratio: 0.5) - .foregroundStyle(.secondary) - } - .clipShape(.circle) - .aspectRatio(1, contentMode: .fill) - } - var body: some View { Button { action() @@ -57,22 +38,14 @@ extension UserSignInView { ZStack { Color.clear - ImageView(user.profileImageSource(client: client, maxWidth: 120)) - .pipeline(.Swiftfin.branding) - .image { image in - image - .posterBorder(ratio: 0.5, of: \.width) - } - .placeholder { _ in - personView - } - .failure { - personView - } + UserProfileImage( + userID: user.id, + source: user.profileImageSource( + client: client, + maxWidth: 120 + ) + ) } - .aspectRatio(1, contentMode: .fill) - .posterShadow() - .clipShape(.circle) .frame(width: 50, height: 50) Text(user.name ?? .emptyDash) diff --git a/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift b/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift index df1c7fd5e..a431ea77e 100644 --- a/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift +++ b/Swiftfin/Views/UserSignInView/Components/UserSignInSecurityView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI @@ -43,7 +43,7 @@ extension UserSignInView { List { Section { - CaseIterablePicker("Security", selection: $updateSignInPolicy) + CaseIterablePicker(L10n.security, selection: $updateSignInPolicy) } footer: { // TODO: descriptions of each section @@ -59,7 +59,7 @@ extension UserSignInView { Text(UserAccessPolicy.requireDeviceAuthentication.displayTitle) .fontWeight(.semibold) - Text("Require device authentication when signing in to the user.") + Text(L10n.requireDeviceAuthDescription) } .padding(.bottom, 15) @@ -67,7 +67,7 @@ extension UserSignInView { Text(UserAccessPolicy.requirePin.displayTitle) .fontWeight(.semibold) - Text("Require a local pin when signing in to the user. This pin is unrecoverable.") + Text(L10n.requirePinDescription) } .padding(.bottom, 15) @@ -75,7 +75,7 @@ extension UserSignInView { Text(UserAccessPolicy.none.displayTitle) .fontWeight(.semibold) - Text("Save the user to this device without any local authentication.") + Text(L10n.saveUserWithoutAuthDescription) } } .frame(width: max(10, listSize.width - 50)) @@ -84,16 +84,16 @@ extension UserSignInView { if accessPolicy == .requirePin { Section { - TextField("Hint", text: $updatePinHint) + TextField(L10n.hint, text: $updatePinHint) } header: { - Text("Hint") + Text(L10n.hint) } footer: { - Text("Set a hint when prompting for the pin.") + Text(L10n.setPinHintDescription) } } } .animation(.linear, value: accessPolicy) - .navigationTitle("Security") + .navigationTitle(L10n.security) .navigationBarTitleDisplayMode(.inline) .navigationBarCloseButton { router.popLast() diff --git a/Swiftfin/Views/UserSignInView/UserSignInView.swift b/Swiftfin/Views/UserSignInView/UserSignInView.swift index 7be62cadd..aa8372379 100644 --- a/Swiftfin/Views/UserSignInView/UserSignInView.swift +++ b/Swiftfin/Views/UserSignInView/UserSignInView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift index 90cbfb6be..74391c0a9 100644 --- a/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift +++ b/Swiftfin/Views/VideoPlayer/Components/LoadingView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Stinsen diff --git a/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift b/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift index 33ad6e9f6..89194f41a 100644 --- a/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift +++ b/Swiftfin/Views/VideoPlayer/Components/PlaybackSettingsView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift b/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift index b3a0e85a4..4edd47a46 100644 --- a/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/LiveNativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift index 080e8d610..d7c572d84 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveBottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift index 9e46aa653..8659991e7 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/LiveTopBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift index d7861d6d5..0c2b5aaa0 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveLargePlaybackButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift index d7f01388f..3c619c6bc 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/Components/PlaybackButtons/LiveSmallPlaybackButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift index ca54557f2..a6b425034 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveMainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift index a013ec8de..2cd469308 100644 --- a/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/LiveOverlays/LiveOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift b/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift index 1f505d744..32a6be53b 100644 --- a/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/LiveVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift b/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift index b64449e30..d6f940e7d 100644 --- a/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/NativeVideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import AVKit diff --git a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift index 31b0969c7..b53bfb308 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/ChapterOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import CollectionHStack diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift index 3d81ae007..b235e2a92 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Foundation diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift index 0be7b9388..d5a40d569 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AdvancedActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift index 77eb23539..ad9ac8ccc 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AspectFillActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift index 1f878bdc2..3bb217e7a 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AudioActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift index 7c0cb3e8e..308383de8 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/AutoPlayActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift index e3543f3db..c842ad643 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/ChaptersActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift index cf1034606..cb8d30b0f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayNextItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift index 2e2515c92..3816c4a1b 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlayPreviousItemActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift index 1956fd033..673be6674 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/PlaybackSpeedActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift index fbfb98f3f..7829c9cd6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ActionButtons/SubtitleActionButton.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift index d3d44d35d..1fe8861ff 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/BarActionButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift index b333ee41b..d98c86e2f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/BottomBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift index 8f89e3156..d34cf044d 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/ChapterTrack.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import JellyfinAPI diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift index 03faba09d..73cdee522 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/OverlayMenu.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift index 99d4eda86..8a7f69335 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/LargePlaybackButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift index f56baccf3..9bb3ac8d6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/PlaybackButtons/SmallPlaybackButtons.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift index 649c0a20c..f84baf46f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/CompactTimeStamp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift index fd76d5cf6..0d99aaa12 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/Timestamp/SplitTimestamp.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift b/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift index 96518f99a..6e6c825db 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Components/TopBarView.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift index 585d24c06..985767409 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/MainOverlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift b/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift index b5315fb8e..a6089fe3f 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/Overlay.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift b/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift index 466917b02..cbb020194 100644 --- a/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift +++ b/Swiftfin/Views/VideoPlayer/VideoPlayer+Actions.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import SwiftUI diff --git a/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift b/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift index 19a58df96..5d6a0d4ce 100644 --- a/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift +++ b/Swiftfin/Views/VideoPlayer/VideoPlayer+KeyCommands.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Swiftfin/Views/VideoPlayer/VideoPlayer.swift b/Swiftfin/Views/VideoPlayer/VideoPlayer.swift index bf3e46cd1..800966ef3 100644 --- a/Swiftfin/Views/VideoPlayer/VideoPlayer.swift +++ b/Swiftfin/Views/VideoPlayer/VideoPlayer.swift @@ -3,7 +3,7 @@ // 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 +// Copyright (c) 2025 Jellyfin & Jellyfin Contributors // import Defaults diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index ec45e168a..45394677a 100644 --- a/Translations/en.lproj/Localizable.strings +++ b/Translations/en.lproj/Localizable.strings @@ -1,2259 +1,2091 @@ +/// About +"about" = "About"; + +/// Absolute +"absolute" = "Absolute"; + +/// Accent Color +"accentColor" = "Accent Color"; + +/// Access +"access" = "Access"; + +/// Accessibility "accessibility" = "Accessibility"; -"allGenres" = "All Genres"; + +/// The End Time must come after the Start Time. +"accessScheduleInvalidTime" = "The End Time must come after the Start Time."; + +/// Access Schedules +"accessSchedules" = "Access Schedules"; + +/// Define the allowed hours for usage and restrict access outside those times. +"accessSchedulesDescription" = "Define the allowed hours for usage and restrict access outside those times."; + +/// User will have access to no media unless it contains at least one allowed tag. +"accessTagAllowDescription" = "User will have access to no media unless it contains at least one allowed tag."; + +/// Access tag already exists +"accessTagAlreadyExists" = "Access tag already exists"; + +/// User will have access to all media except when it contains any blocked tag. +"accessTagBlockDescription" = "User will have access to all media except when it contains any blocked tag."; + +/// Access Tags +"accessTags" = "Access Tags"; + +/// Use tags to grant or restrict this user's access to media. +"accessTagsDescription" = "Use tags to grant or restrict this user's access to media."; + +/// Active +"active" = "Active"; + +/// Activity +"activity" = "Activity"; + +/// Actor +"actor" = "Actor"; + +/// Add +"add" = "Add"; + +/// Add access schedule +"addAccessSchedule" = "Add access schedule"; + +/// Add access tag +"addAccessTag" = "Add access tag"; + +/// Add API key +"addAPIKey" = "Add API key"; + +/// Additional security access for users signed in to this device. This does not change any Jellyfin server user settings. +"additionalSecurityAccessDescription" = "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings."; + +/// Add Server +"addServer" = "Add Server"; + +/// Add trigger +"addTrigger" = "Add trigger"; + +/// Add URL +"addURL" = "Add URL"; + +/// Add User +"addUser" = "Add User"; + +/// Administrator +"administrator" = "Administrator"; + +/// Advanced +"advanced" = "Advanced"; + +/// Age %@ +"agesGroup" = "Age %@"; + +/// Aired +"aired" = "Aired"; + +/// Air Time +"airTime" = "Air Time"; + +/// Airs %s +"airWithDate" = "Airs %s"; + +/// Album Artist +"albumArtist" = "Album Artist"; + +/// All +"all" = "All"; + +/// All Audiences +"allAudiences" = "All Audiences"; + +/// View all past and present devices that have connected. +"allDevicesDescription" = "View all past and present devices that have connected."; + +/// All languages +"allLanguages" = "All languages"; + +/// All Media "allMedia" = "All Media"; -"apply" = "Apply"; -"audioAndCaptions" = "Audio & Captions"; -"audioTrack" = "Audio Track"; -"back" = "Back"; -"cast" = "CAST"; -"changeServer" = "Change Server"; -"closedCaptions" = "Closed Captions"; -"comingSoon" = "Coming soon"; -"connect" = "Connect"; -"connectManually" = "Connect Manually"; -"connectToJellyfin" = "Connect to Jellyfin"; -"connectToServer" = "Connect to Server"; -"continueWatching" = "Continue Watching"; -"discoveredServers" = "Discovered Servers"; -"displayOrder" = "Display order"; -"emptyNextUp" = "Empty Next Up"; -"episodes" = "Episodes"; -"error" = "Error"; -"filterResults" = "Filter Results"; -"filters" = "Filters"; -"genres" = "Genres"; -"home" = "Home"; -"latestWithString" = "Latest %@"; -"library" = "Library"; -"loading" = "Loading"; -"localServers" = "Local Servers"; -"login" = "Login"; -"loginToWithString" = "Login to %@"; -"moreLikeThis" = "More Like This"; -"nextUp" = "Next Up"; -"noCastdevicesfound" = "No Cast devices found.."; -"noResults" = "No results."; -"ok" = "Ok"; -"otherUser" = "Other User"; -"pageOfWithNumbers" = "Page %1$@ of %2$@"; -"playNext" = "Play Next"; -"play" = "Play"; -"playback" = "Playback"; -"playbackSettings" = "Playback settings"; -"playbackSpeed" = "Playback Speed"; -"reload" = "Reload"; -"reset" = "Reset"; -"seasonAndEpisode" = "S%1$@:E%2$@"; -"search" = "Search"; -"searchDots" = "Search…"; -"seasons" = "Seasons"; -"season" = "Season"; -"seeAll" = "See All"; -"selectCastDestination" = "Select Cast Destination"; -"serverInformation" = "Server Information"; -"serverURL" = "Server URL"; -"signedInAsWithString" = "Signed in as %@"; -"sortBy" = "Sort by"; -"studio" = "STUDIO"; -"studios" = "Studios"; -"suggestions" = "Suggestions"; -"switchUser" = "Switch User"; -"tags" = "Tags"; -"tryAgain" = "Try again"; -"notImplementedYetWithType" = "Type: %@ not implemented yet :("; -"username" = "Username"; -"WhosWatching" = "Who's watching?"; -"wip" = "WIP"; -"yourFavorites" = "Your Favorites"; -/* Represents the Appearance setting label */ +/// Allow collection management +"allowCollectionManagement" = "Allow collection management"; + +/// Allowed +"allowed" = "Allowed"; + +/// Allow media item deletion +"allowItemDeletion" = "Allow media item deletion"; + +/// Allow media item editing +"allowItemEditing" = "Allow media item editing"; + +/// All Servers +"allServers" = "All Servers"; + +/// View and manage all registered users on the server, including their permissions and activity status. +"allUsersDescription" = "View and manage all registered users on the server, including their permissions and activity status."; + +/// Alternate +"alternate" = "Alternate"; + +/// Alternate DVD +"alternateDVD" = "Alternate DVD"; + +/// Anamorphic video is not supported +"anamorphicVideoNotSupported" = "Anamorphic video is not supported"; + +/// API Key Copied +"apiKeyCopied" = "API Key Copied"; + +/// Your API Key was copied to your clipboard! +"apiKeyCopiedMessage" = "Your API Key was copied to your clipboard!"; + +/// API Keys +"apiKeys" = "API Keys"; + +/// External applications require an API key to communicate with your server. +"apiKeysDescription" = "External applications require an API key to communicate with your server."; + +/// API Keys +"apiKeysTitle" = "API Keys"; + +/// Appearance "appearance" = "Appearance"; -/* Represents the system theme setting */ -"system" = "System"; +/// App Icon +"appIcon" = "App Icon"; -/* Represents the dark theme setting */ -"dark" = "Dark"; +/// Application Name +"applicationName" = "Application Name"; -/* Represents the light theme setting */ -"light" = "Light"; +/// Arranger +"arranger" = "Arranger"; -"advanced" = "Advanced"; +/// Art +"art" = "Art"; + +/// Artist +"artist" = "Artist"; + +/// Aspect Fill "aspectFill" = "Aspect Fill"; -"enabled" = "Enabled"; -"disabled" = "Disabled"; -"existingServer" = "Existing Server"; -"existingUser" = "Existing User"; -"serverAlreadyConnected" = "Server %s is already connected"; -"userAlreadySignedIn" = "User %s is already signed in"; -"unknownError" = "Unknown Error"; -"unknown" = "Unknown"; -"serverAlreadyExistsPrompt" = "Server %s already exists. Add new URL?"; -"addURL" = "Add URL"; -"noEpisodesAvailable" = "No episodes available"; -"seeMore" = "See More"; -"defaultScheme" = "Default Scheme"; -"networking" = "Networking"; -"resetUserSettings" = "Reset User Settings"; -"resetAllSettings" = "Reset all settings back to defaults."; -"resetAppSettings" = "Reset App Settings"; -"removeAllUsers" = "Remove All Users"; -"removeAllServers" = "Remove All Servers"; -"settings" = "Settings"; + +/// Audio +"audio" = "Audio"; + +/// The audio bit depth is not supported +"audioBitDepthNotSupported" = "The audio bit depth is not supported"; + +/// The audio bitrate is not supported +"audioBitrateNotSupported" = "The audio bitrate is not supported"; + +/// The number of audio channels is not supported +"audioChannelsNotSupported" = "The number of audio channels is not supported"; + +/// The audio codec is not supported +"audioCodecNotSupported" = "The audio codec is not supported"; + +/// The audio track is external and requires transcoding +"audioIsExternal" = "The audio track is external and requires transcoding"; + +/// Audio Offset +"audioOffset" = "Audio Offset"; + +/// The audio profile is not supported +"audioProfileNotSupported" = "The audio profile is not supported"; + +/// The audio sample rate is not supported +"audioSampleRateNotSupported" = "The audio sample rate is not supported"; + +/// Audio transcoding +"audioTranscoding" = "Audio transcoding"; + +/// Author +"author" = "Author"; + +/// Authorize +"authorize" = "Authorize"; + +/// Auto +"auto" = "Auto"; + +/// Optimizes playback using default settings for most devices. Some formats may require server transcoding for non-compatible media types. +"autoDescription" = "Optimizes playback using default settings for most devices. Some formats may require server transcoding for non-compatible media types."; + +/// Auto Play +"autoPlay" = "Auto Play"; + +/// Back +"back" = "Back"; + +/// Backdrop +"backdrop" = "Backdrop"; + +/// Banner +"banner" = "Banner"; + +/// Bar Buttons +"barButtons" = "Bar Buttons"; + +/// Behavior +"behavior" = "Behavior"; + +/// Tests your server connection to assess internet speed and adjust bandwidth automatically. +"birateAutoDescription" = "Tests your server connection to assess internet speed and adjust bandwidth automatically."; + +/// Birthday +"birthday" = "Birthday"; + +/// Birth year +"birthYear" = "Birth year"; + +/// Auto +"bitrateAuto" = "Auto"; + +/// Default Bitrate +"bitrateDefault" = "Default Bitrate"; + +/// Limits the internet bandwidth used during playback. +"bitrateDefaultDescription" = "Limits the internet bandwidth used during playback."; + +/// 480p - 1.5 Mbps +"bitrateKbps1500" = "480p - 1.5 Mbps"; + +/// 360p - 420 Kbps +"bitrateKbps420" = "360p - 420 Kbps"; + +/// 480p - 720 Kbps +"bitrateKbps720" = "480p - 720 Kbps"; + +/// Maximum +"bitrateMax" = "Maximum"; + +/// Maximizes bandwidth usage, up to %@, for each playback stream to ensure the highest quality. +"bitrateMaxDescription" = "Maximizes bandwidth usage, up to %@, for each playback stream to ensure the highest quality."; + +/// 1080p - 10 Mbps +"bitrateMbps10" = "1080p - 10 Mbps"; + +/// 4K - 120 Mbps +"bitrateMbps120" = "4K - 120 Mbps"; + +/// 1080p - 15 Mbps +"bitrateMbps15" = "1080p - 15 Mbps"; + +/// 1080p - 20 Mbps +"bitrateMbps20" = "1080p - 20 Mbps"; + +/// 480p - 3 Mbps +"bitrateMbps3" = "480p - 3 Mbps"; + +/// 720p - 4 Mbps +"bitrateMbps4" = "720p - 4 Mbps"; + +/// 1080p - 40 Mbps +"bitrateMbps40" = "1080p - 40 Mbps"; + +/// 720p - 6 Mbps +"bitrateMbps6" = "720p - 6 Mbps"; + +/// 1080p - 60 Mbps +"bitrateMbps60" = "1080p - 60 Mbps"; + +/// 720p - 8 Mbps +"bitrateMbps8" = "720p - 8 Mbps"; + +/// 4K - 80 Mbps +"bitrateMbps80" = "4K - 80 Mbps"; + +/// Bitrate Test +"bitrateTest" = "Bitrate Test"; + +/// 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."; + +/// bps +"bitsPerSecond" = "bps"; + +/// Blocked +"blocked" = "Blocked"; + +/// Block unrated items +"blockUnratedItems" = "Block unrated items"; + +/// Block items from this user with no or unrecognized rating information. +"blockUnratedItemsDescription" = "Block items from this user with no or unrecognized rating information."; + +/// Blue +"blue" = "Blue"; + +/// Books +"books" = "Books"; + +/// Box +"box" = "Box"; + +/// BoxRear +"boxRear" = "BoxRear"; + +/// Bugs and Features +"bugsAndFeatures" = "Bugs and Features"; + +/// Buttons +"buttons" = "Buttons"; + +/// Cancel "cancel" = "Cancel"; -"connectToJellyfinServer" = "Connect to a Jellyfin server"; -"connectToJellyfinServerStart" = "Connect to a Jellyfin server to get started"; -"searchingDots" = "Searching…"; -"noLocalServersFound" = "No local servers found"; -"continue" = "Continue"; -"noTitle" = "No title"; -"retry" = "Retry"; -"recentlyAdded" = "Recently Added"; -"overview" = "Overview"; -"noOverviewAvailable" = "No overview available"; + +/// Cancelling... +"cancelling" = "Cancelling..."; + +/// Cannot connect to host +"cannotConnectToHost" = "Cannot connect to host"; + +/// Capabilities +"capabilities" = "Capabilities"; + +/// Cast & Crew "castAndCrew" = "Cast & Crew"; -"items" = "Items"; -"series" = "Series"; -"information" = "Information"; -"media" = "Media"; -"favorites" = "Favorites"; -"liveTV" = "Live TV"; -"downloads" = "Downloads"; -"serverDetails" = "Server Details"; -"name" = "Name"; -"url" = "URL"; -"version" = "Version"; -"operatingSystem" = "Operating System"; -"remove" = "Remove"; -"servers" = "Servers"; -"experimental" = "Experimental"; -"playPreviousItem" = "Play Previous Item"; -"playNextItem" = "Play Next Item"; -"autoPlay" = "Auto Play"; -"editJumpLengths" = "Edit Jump Lengths"; -"overlay" = "Overlay"; -"overlayType" = "Overlay Type"; -"user" = "User"; -"server" = "Server"; -"nativePlayer" = "Native Player"; -"videoPlayerType" = "Video Player Type"; -"gestures" = "Gestures"; -"jumpForwardLength" = "Jump Forward Length"; -"jumpBackwardLength" = "Jump Backward Length"; -"jumpGesturesEnabled" = "Jump Gestures Enabled"; -"systemControlGesturesEnabled" = "System Control Gestures Enabled"; -"playerGesturesLockGestureEnabled" = "Player Gestures Lock Gesture Enabled"; -"seekSlideGestureEnabled" = "Seek Slide Gesture Enabled"; -"audioOffset" = "Audio Offset"; -"subtitleOffset" = "Subtitle Offset"; -"resumeOffset" = "Resume Offset"; -"resumeOffsetDescription" = "Resume content seconds before the recorded resume time"; -"resume5SecondOffset" = "Resume 5 Second Offset"; -"showPosterLabels" = "Show Poster Labels"; -"showCastAndCrew" = "Show Cast & Crew"; -"subtitleColor" = "Subtitle Color"; -"subtitleSize" = "Subtitle Size"; -"subtitleFont" = "Subtitle Font"; -"signInGetStarted" = "Sign in to get started"; -"signIn" = "Sign In"; -"signInToServer" = "Sign In to %s"; -"audio" = "Audio"; -"subtitles" = "Subtitles"; -"playbackSpeed" = "Playback Speed"; -"programs" = "Programs"; + +/// Category +"category" = "Category"; + +/// Change Pin +"changePin" = "Change Pin"; + +/// Channels "channels" = "Channels"; -"tvShows" = "TV Shows"; -"movies" = "Movies"; -"other" = "Other"; -"networkTimedOut" = "Network timed out"; -"cannotConnectToHost" = "Cannot connect to host"; -"unauthorizedUser" = "Unauthorized user"; -"unauthorized" = "Unauthorized"; -"released" = "Released"; -"rated" = "Rated"; -"runtime" = "Runtime"; -"containers" = "Containers"; -"noCodec" = "No Codec"; -"normal" = "Normal"; -"compact" = "Compact"; -"smallest" = "Smallest"; -"smaller" = "Smaller"; -"regular" = "Regular"; -"larger" = "Larger"; -"largest" = "Largest"; -"jumpLengthSeconds" = "%s seconds"; -"unableToConnectServer" = "Unable to connect to server"; -"present" = "Present"; -"oneUser" = "1 user"; -"multipleUsers" = "%d users"; -"notAvailableSlash" = "N/A"; -"next" = "Next"; -"refresh" = "Refresh"; -"recommended" = "Recommended"; -"about" = "About"; -"pressDownForMenu" = "Press Down for Menu"; -"confirmClose" = "Confirm Close"; -"cinematicViews" = "Cinematic Views"; -"none" = "None"; -"missing" = "Missing"; -"unaired" = "Unaired"; -"airWithDate" = "Airs %s"; -"missingItems" = "Missing Items"; -"showMissingSeasons" = "Show Missing Seasons"; -"showMissingEpisodes" = "Show Missing Episodes"; -"removeFromResume" = "Remove From Resume"; -"playFromBeginning" = "Play From Beginning"; -"file" = "File"; -"customize" = "Customize"; -"tooManyRedirects" = "Too Many Redirects"; + +/// Chapter +"chapter" = "Chapter"; + +/// Chapters "chapters" = "Chapters"; -"current" = "Current"; -"currentPosition" = "Current Position"; -"remainingTime" = "Remaining Time"; -"sourceCode" = "Source Code"; -"requestFeature" = "Request a Feature"; -"reportIssue" = "Report an Issue"; -"showFlattenView" = "Flatten Library Items"; -"playAndPause" = "Play / Pause"; -"jumpForward" = "Jump Forward"; -"jumpBackward" = "Jump Backward"; -"close" = "Close"; -"nextItem" = "Next Item"; -"previousItem" = "Previous Item"; -"unableToFindHost" = "Unable to find host"; -"publicUsers" = "Public Users"; -"noPublicUsers" = "No public Users"; -"quickConnect" = "Quick Connect"; -"quickConnectCode" = "Quick Connect code"; -"authorize" = "Authorize"; -"quickConnectInvalidError" = "Invalid Quick Connect code"; -"quickConnectSuccessMessage" = "Authorizing Quick Connect successful. Please continue on your other device."; -"showChaptersInfoInBottomOverlay" = "Show Chapters Info In Bottom Overlay"; -"quickConnectStep1" = "Open the Jellyfin app on your phone or web browser and sign in with your account"; -"quickConnectStep2" = "Open the user menu and go to the Quick Connect page"; -"quickConnectStep3" = "Enter the following code:"; -"quickConnectNotEnabled" = "Note: Quick Connect not enabled"; -"episodeNumber" = "Episode %1$@"; -"compactPoster" = "Compact Poster"; -"compactLogo" = "Compact Logo"; + +/// Chapter Slider +"chapterSlider" = "Chapter Slider"; + +/// Cinematic "cinematic" = "Cinematic"; -"blue" = "Blue"; -"green" = "Green"; -"orange" = "Orange"; -"red" = "Red"; -"yellow" = "Yellow"; -"primary" = "Primary"; -"bugsAndFeatures" = "Bugs and Features"; -"invertedDark" = "Inverted Dark"; -"invertedLight" = "Inverted Light"; -"appIcon" = "App Icon"; -"accentColor" = "Accent Color"; -"accentColorDescription" = "Some views may need an app restart to update."; -"dismiss" = "Dismiss"; -"played" = "Played"; -"unplayed" = "Unplayed"; -"video" = "Video"; -"audio" = "Audio"; -"subtitle" = "Subtitle"; -"specialFeatures" = "Special Features"; -"color" = "Color"; -"delivery" = "Delivery"; + +/// Cinematic Background +"cinematicBackground" = "Cinematic Background"; + +/// Client +"client" = "Client"; + +/// Close +"close" = "Close"; + +/// Collections "collections" = "Collections"; -"people" = "People"; -"usePrimaryImage" = "Use Primary Image"; -"usePrimaryImageDescription" = "Uses the primary image and hides the logo."; -"hapticFeedback" = "Haptic Feedback"; -"posters" = "Posters"; -"seriesBackdrop" = "Series Backdrop"; -"episodeLandscapePoster" = "Episode Landscape Poster"; -"randomImage" = "Random Image"; -"favorited" = "Favorited"; -"progress" = "Progress"; -"order" = "Order"; -"sort" = "Sort"; -"ratings" = "Ratings"; -"random" = "Random"; -"grid" = "Grid"; -"list" = "List"; -"logs" = "Logs"; -"likedItems" = "Liked Items"; -"onNow" = "On Now"; -"sports" = "Sports"; -"kids" = "Kids"; -"news" = "News"; +/// Color +"color" = "Color"; + +/// Colorist +"colorist" = "Colorist"; + +/// Columns +"columns" = "Columns"; + +/// Community +"community" = "Community"; + +/// Compact +"compact" = "Compact"; + +/// Compact Logo +"compactLogo" = "Compact Logo"; + +/// Compact Poster +"compactPoster" = "Compact Poster"; + +/// Compatibility +"compatibility" = "Compatibility"; + +/// Most Compatible +"compatible" = "Most Compatible"; + +/// Converts all media to H.264 video and AAC audio for maximum compatibility. May require server transcoding for non-compatible media types. +"compatibleDescription" = "Converts all media to H.264 video and AAC audio for maximum compatibility. May require server transcoding for non-compatible media types."; + +/// Composer +"composer" = "Composer"; + +/// Conductor +"conductor" = "Conductor"; + +/// Confirm +"confirm" = "Confirm"; + +/// Confirm New Password +"confirmNewPassword" = "Confirm New Password"; + +/// Confirm Password +"confirmPassword" = "Confirm Password"; + +/// Connect +"connect" = "Connect"; + +/// Connect to a Jellyfin server to get started +"connectToJellyfinServerStart" = "Connect to a Jellyfin server to get started"; + +/// Connect to Server +"connectToServer" = "Connect to Server"; + +/// The container bitrate exceeds the allowed limit +"containerBitrateExceedsLimit" = "The container bitrate exceeds the allowed limit"; + +/// The container format is not supported +"containerNotSupported" = "The container format is not supported"; + +/// Containers +"containers" = "Containers"; + +/// Continue +"continue" = "Continue"; + +/// Continuing +"continuing" = "Continuing"; + +/// Control other users +"controlOtherUsers" = "Control other users"; + +/// Control shared devices +"controlSharedDevices" = "Control shared devices"; -"buttons" = "Buttons"; -"playbackButtons" = "Playback Buttons"; -"barButtons" = "Bar Buttons"; -"menuButtons" = "Menu Buttons"; -"jump" = "Jump"; -"slider" = "Slider"; -"chapterSlider" = "Chapter Slider"; -"sliderColor" = "Slider Color"; -"sliderType" = "Slider Type"; -"timestamp" = "Timestamp"; -"scrubCurrentTime" = "Scrub Current Time"; -"timestampType" = "Timestamp Type"; -"trailingValue" = "Trailing Value"; -"transition" = "Transition"; -"pauseOnBackground" = "Pause on background"; -"playOnActive" = "Play on active"; +/// Country +"country" = "Country"; -// Retrieving Media Information - Loading Message -// Text displayed to indicate that media information is being loaded. -"retrievingMediaInformation" = "Retrieving media information"; +/// Cover Artist +"coverArtist" = "Cover Artist"; -// Right - Direction -// Label for the right side or direction. -"right" = "Right"; +/// Create & Join Groups +"createAndJoinGroups" = "Create & Join Groups"; -// Left - Direction -// Label for the left side or direction. -"left" = "Left"; +/// Create API Key +"createAPIKey" = "Create API Key"; -// Letter Picker - Option -// Picker used to select a letter for jumping to a specific section in a list. -"letterPicker" = "Letter Picker"; +/// Enter the application name for the new API key. +"createAPIKeyMessage" = "Enter the application name for the new API key."; -// Orientation - Setting -// Setting to define how an item is oriented (e.g., portrait or landscape). -"orientation" = "Orientation"; +/// Create a pin to sign in to %@ on this device +"createPinForUser" = "Create a pin to sign in to %@ on this device"; -// Test Size - Option -// Option to set the test size for bitrate testing. -"testSize" = "Test Size"; +/// Creator +"creator" = "Creator"; -// Maximum Bitrate - Option -// Option to set the maximum bitrate for playback. -"maximumBitrate" = "Maximum Bitrate"; +/// Critics +"critics" = "Critics"; -// Auto - Option -// Option for automatic bitrate selection. -"bitrateAuto" = "Auto"; +/// Current +"current" = "Current"; -// Maximum - Option -// Option for the maximum bitrate. -"bitrateMax" = "Maximum"; +/// Current Password +"currentPassword" = "Current Password"; -// 4K - 120 Mbps - Option -// Option to set the bitrate to 4K quality at 120 Mbps. -"bitrateMbps120" = "4K - 120 Mbps"; +/// Custom +"custom" = "Custom"; -// 4K - 80 Mbps - Option -// Option to set the bitrate to 4K quality at 80 Mbps. -"bitrateMbps80" = "4K - 80 Mbps"; +/// Custom bitrate +"customBitrate" = "Custom bitrate"; -// 1080p - 60 Mbps - Option -// Option to set the bitrate to 1080p quality at 60 Mbps. -"bitrateMbps60" = "1080p - 60 Mbps"; +/// Manually set the maximum number of connections a user can have to the server. +"customConnectionsDescription" = "Manually set the maximum number of connections a user can have to the server."; -// 1080p - 40 Mbps - Option -// Option to set the bitrate to 1080p quality at 40 Mbps. -"bitrateMbps40" = "1080p - 40 Mbps"; +/// Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback. +"customDescription" = "Allows advanced customization of device profiles for native playback. Incorrect settings may affect playback."; -// 1080p - 20 Mbps - Option -// Option to set the bitrate to 1080p quality at 20 Mbps. -"bitrateMbps20" = "1080p - 20 Mbps"; +/// Custom Device Name +"customDeviceName" = "Custom Device Name"; -// 1080p - 15 Mbps - Option -// Option to set the bitrate to 1080p quality at 15 Mbps. -"bitrateMbps15" = "1080p - 15 Mbps"; +/// Your custom device name '%1$@' has been saved. +"customDeviceNameSaved" = "Your custom device name '%1$@' has been saved."; -// 1080p - 10 Mbps - Option -// Option to set the bitrate to 1080p quality at 10 Mbps. -"bitrateMbps10" = "1080p - 10 Mbps"; +/// 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."; -// 720p - 8 Mbps - Option -// Option to set the bitrate to 720p quality at 8 Mbps. -"bitrateMbps8" = "720p - 8 Mbps"; +/// The custom device profiles will replace the default Swiftfin device profiles. +"customDeviceProfileReplace" = "The custom device profiles will replace the default Swiftfin device profiles."; -// 720p - 6 Mbps - Option -// Option to set the bitrate to 720p quality at 6 Mbps. -"bitrateMbps6" = "720p - 6 Mbps"; +/// Manually set the number of failed login attempts allowed before locking the user. +"customFailedLoginDescription" = "Manually set the number of failed login attempts allowed before locking the user."; -// 720p - 4 Mbps - Option -// Option to set the bitrate to 720p quality at 4 Mbps. -"bitrateMbps4" = "720p - 4 Mbps"; +/// Custom failed logins +"customFailedLogins" = "Custom failed logins"; -// 480p - 3 Mbps - Option -// Option to set the bitrate to 480p quality at 3 Mbps. -"bitrateMbps3" = "480p - 3 Mbps"; +/// Customize +"customize" = "Customize"; -// 480p - 1.5 Mbps - Option -// Option to set the bitrate to 480p quality at 1.5 Mbps. -"bitrateKbps1500" = "480p - 1.5 Mbps"; +/// Custom Profile +"customProfile" = "Custom Profile"; -// 480p - 720 Kbps - Option -// Option to set the bitrate to 480p quality at 720 Kbps. -"bitrateKbps720" = "480p - 720 Kbps"; +/// Custom Rating +"customRating" = "Custom Rating"; -// 360p - 420 Kbps - Option -// Option to set the bitrate to 360p quality at 420 Kbps. -"bitrateKbps420" = "360p - 420 Kbps"; +/// Custom sessions +"customSessions" = "Custom sessions"; -// 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."; +/// Daily +"daily" = "Daily"; -// Servers - Navigation Title -// Select Server View. -"servers" = "Servers"; +/// Dark +"dark" = "Dark"; -// Add Server - Button -// Select Server View - Add Server. -"addServer" = "Add Server"; +/// Dashboard +"dashboard" = "Dashboard"; -// All Servers - Button -// Select Server View - Select All Servers. -"allServers" = "All Servers"; +/// Perform administrative tasks for your Jellyfin server. +"dashboardDescription" = "Perform administrative tasks for your Jellyfin server."; -// Edit Server - Button -// Select Server View - Edit an Existing Server. -"editServer" = "Edit Server"; +/// Date Added +"dateAdded" = "Date Added"; + +/// Date Created +"dateCreated" = "Date Created"; -// Delete - Button -// Server Detail View - Delete. +/// Date Modified +"dateModified" = "Date Modified"; + +/// Date of death +"dateOfDeath" = "Date of death"; + +/// Dates +"dates" = "Dates"; + +/// Day of Week +"dayOfWeek" = "Day of Week"; + +/// Days +"days" = "Days"; + +/// Default +"default" = "Default"; + +/// Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts. +"defaultFailedLoginDescription" = "Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts."; + +/// Delete "delete" = "Delete"; -// Delete Server - Button -// Server Detail View - Delete Server. -"deleteServer" = "Delete Server"; +/// Are you sure you want to permanently delete this key? +"deleteAPIKeyMessage" = "Are you sure you want to permanently delete this key?"; -// Indicators - Button -// Customize Server View - Indicators. -"indicators" = "Indicators"; +/// Delete Device +"deleteDevice" = "Delete Device"; -// Posters - Button -// Customize Server View - Posters. -"posters" = "Posters"; +/// Failed to Delete Device +"deleteDeviceFailed" = "Failed to Delete Device"; -// Cinematic Background - Button -// Customize Server View - Cinematic Background. -"cinematicBackground" = "Cinematic Background"; +/// Cannot delete a session from the same device (%1$@). +"deleteDeviceSelfDeletion" = "Cannot delete a session from the same device (%1$@)."; -// Random Image - Button -// Customize Server View - Random Image. -"randomImage" = "Random Image"; +/// Are you sure you wish to delete this device? This session will be logged out. +"deleteDeviceWarning" = "Are you sure you wish to delete this device? This session will be logged out."; -// Show Favorites - Button -// Customize Server View - Show Favorites. -"showFavorites" = "Show Favorites"; +/// Delete image +"deleteImage" = "Delete image"; -// Show Recently Added - Button -// Customize Server View - Show Recently Added. -"showRecentlyAdded" = "Show Recently Added"; +/// Are you sure you want to delete this item? +"deleteItemConfirmation" = "Are you sure you want to delete this item?"; -// Show Favorited - Indicators View -// Option to show favorited items. -"showFavorited" = "Show Favorited"; +/// Are you sure you want to delete this item? This action cannot be undone. +"deleteItemConfirmationMessage" = "Are you sure you want to delete this item? This action cannot be undone."; -// Show Progress - Indicators View -// Option to show progress indicators. -"showProgress" = "Show Progress"; +/// Delete Schedule +"deleteSchedule" = "Delete Schedule"; -// Show Unwatched - Indicators View -// Option to show unwatched items. -"showUnwatched" = "Show Unwatched"; +/// Are you sure you wish to delete this schedule? +"deleteScheduleWarning" = "Are you sure you wish to delete this schedule?"; -// Show Watched - Indicators View -// Option to show watched items. -"showWatched" = "Show Watched"; +/// Are you sure you want to delete the selected items? +"deleteSelectedConfirmation" = "Are you sure you want to delete the selected items?"; -// Playback Quality - Maximum Bitrate View -// Label for the playback quality setting. -"playbackQuality" = "Playback Quality"; +/// Delete Selected Devices +"deleteSelectedDevices" = "Delete Selected Devices"; -// Video Player - Settings View -// Label for the video player settings. -"videoPlayer" = "Video Player"; +/// Delete Selected Schedules +"deleteSelectedSchedules" = "Delete Selected Schedules"; -// Customize - Settings View -// Label for customization settings. -"customize" = "Customize"; +/// Delete Selected Users +"deleteSelectedUsers" = "Delete Selected Users"; -// Logs - Settings View -// Label for viewing logs. -"logs" = "Logs"; +/// Are you sure you wish to delete all selected devices? All selected sessions will be logged out. +"deleteSelectionDevicesWarning" = "Are you sure you wish to delete all selected devices? All selected sessions will be logged out."; -// Jellyfin - General -// Label for Jellyfin branding. -"jellyfin" = "Jellyfin"; +/// Are you sure you wish to delete all selected schedules? +"deleteSelectionSchedulesWarning" = "Are you sure you wish to delete all selected schedules?"; -// Offset - Video Player Settings View -// Label for setting playback offset. -"offset" = "Offset"; +/// Are you sure you wish to delete all selected users? +"deleteSelectionUsersWarning" = "Are you sure you wish to delete all selected users?"; -// Resume - Video Player Settings View -// Label for resuming playback. -"resume" = "Resume"; +/// Delete Server +"deleteServer" = "Delete Server"; -// Subtitles Disclaimer - Video Player Settings View -// Disclaimer about subtitle compatibility. -"subtitlesDisclaimer" = "Settings only affect some subtitle types"; +/// Delete Trigger +"deleteTrigger" = "Delete Trigger"; -// Pause on Background - Video Player Settings View -// Option to pause playback when app goes to the background. -"pauseOnBackground" = "Pause on background"; +/// Are you sure you want to delete this trigger? This action cannot be undone. +"deleteTriggerConfirmationMessage" = "Are you sure you want to delete this trigger? This action cannot be undone."; -// Play on Active - Video Player Settings View -// Option to resume playback when app becomes active. -"playOnActive" = "Play on active"; +/// Delete User +"deleteUser" = "Delete User"; -// Playback - Video Player Settings View -// Header label for playback settings. -"playback" = "Playback"; +/// Failed to Delete User +"deleteUserFailed" = "Failed to Delete User"; -// Resume Offset Title - Video Player Settings View -// Title for the resume offset setting. -"resumeOffsetTitle" = "Resume Offset"; +/// Are you sure you want to delete %d users? +"deleteUserMultipleConfirmation" = "Are you sure you want to delete %d users?"; -// Resume Offset Description - Video Player Settings View -// Description for the resume offset setting. -"resumeOffsetDescription" = "Resume content seconds before the recorded resume time"; +/// Cannot delete a user from the same user (%1$@). +"deleteUserSelfDeletion" = "Cannot delete a user from the same user (%1$@)."; -// Custom Profile - Section Header -// Header label for custom device profiles. -"customProfile" = "Custom Profile"; +/// Are you sure you want to delete %@? +"deleteUserSingleConfirmation" = "Are you sure you want to delete %@?"; -// Device Profile - Section Header -// Header label for device profiles. -"deviceProfile" = "Device Profile"; +/// Are you sure you wish to delete this user? +"deleteUserWarning" = "Are you sure you wish to delete this user?"; -// 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."; +/// Deletion +"deletion" = "Deletion"; -// 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."; +/// Delivery +"delivery" = "Delivery"; -// Playback Quality - Section -// Label for the playback quality section. -"playbackQuality" = "Playback Quality"; +/// Details +"details" = "Details"; -// Use as Transcoding Profile - Option -// Option to use a profile for transcoding. -"useAsTranscodingProfile" = "Use as Transcoding Profile"; +/// Device +"device" = "Device"; -// Auto - PlaybackCompatibility Default Category -// Label for the default playback compatibility setting. -"auto" = "Auto"; +/// Device Access +"deviceAccess" = "Device Access"; -// Most Compatible - PlaybackCompatibility Compatible Category -// Label for the most compatible playback compatibility setting. -"compatible" = "Most Compatible"; +/// Device authentication failed +"deviceAuthFailed" = "Device authentication failed"; + +/// Device Profile +"deviceProfile" = "Device Profile"; + +/// Decide which media plays natively or requires server transcoding for compatibility. +"deviceProfileDescription" = "Decide which media plays natively or requires server transcoding for compatibility."; + +/// Devices +"devices" = "Devices"; + +/// Digital +"digital" = "Digital"; + +/// Dimensions +"dimensions" = "Dimensions"; -// Direct Play - PlaybackCompatibility DirectPlay Category -// Label for the direct play compatibility setting. +/// Direct Play "direct" = "Direct Play"; -// Custom - PlaybackCompatibility Custom Category -// Label for the custom playback compatibility setting. -"custom" = "Custom"; +/// Plays content in its original format. May cause playback issues on unsupported media types. +"directDescription" = "Plays content in its original format. May cause playback issues on unsupported media types."; -// Compatibility - PlaybackCompatibility Section Title -// Label for the compatibility section. -"compatibility" = "Compatibility"; +/// Director +"director" = "Director"; -// Profiles - PlaybackCompatibility Profile Sections -// Label for playback compatibility profiles. -"profiles" = "Profiles"; +/// Direct Play +"directPlay" = "Direct Play"; -// Behavior - General -// Label for behavior settings. -"behavior" = "Behavior"; +/// An error occurred during direct play +"directPlayError" = "An error occurred during direct play"; -// Bitrate Test - Bitrate Automatic Section Header -// Header label for bitrate testing settings. -"bitrateTest" = "Bitrate Test"; +/// Direct Stream +"directStream" = "Direct Stream"; -// Default Bitrate - General -// Label for the default bitrate setting. -"bitrateDefault" = "Default Bitrate"; +/// Disabled +"disabled" = "Disabled"; -// Default Bitrate Description - General -// Description for the default bitrate setting. -"bitrateDefaultDescription" = "Limits the internet bandwidth used during playback."; +/// Disc +"disc" = "Disc"; + +/// Disclaimer +"disclaimer" = "Disclaimer"; -// Playback May Fail - Warning -// Warning message for potential playback failure. -"mayResultInPlaybackFailure" = "This setting may result in media failing to start playback."; +/// Dismiss +"dismiss" = "Dismiss"; -// 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."; +/// Display Order +"displayOrder" = "Display Order"; -// Device - Session Device Section Label -// Label for the device in session details. -"device" = "Device"; +/// Done +"done" = "Done"; -// Last Seen - Session Client Last Seen Section Label -// Label for the last seen time in session details. -"lastSeen" = "Last Seen"; +/// Downloads +"downloads" = "Downloads"; -// Transcode Reasons - Section Label -// Label for reasons why transcoding is required. -"transcodeReasons" = "Transcode Reason(s)"; +/// Duplicate User +"duplicateUser" = "Duplicate User"; + +/// %@ is already saved +"duplicateUserSaved" = "%@ is already saved"; + +/// DVD +"dvd" = "DVD"; + +/// Edit +"edit" = "Edit"; + +/// Editor +"editor" = "Editor"; + +/// Edit Server +"editServer" = "Edit Server"; + +/// Edit Users +"editUsers" = "Edit Users"; + +/// Enable all devices +"enableAllDevices" = "Enable all devices"; + +/// Enable all libraries +"enableAllLibraries" = "Enable all libraries"; + +/// Enabled +"enabled" = "Enabled"; + +/// End Date +"endDate" = "End Date"; + +/// Ended +"ended" = "Ended"; + +/// End Time +"endTime" = "End Time"; + +/// Engineer +"engineer" = "Engineer"; + +/// Enter custom bitrate in Mbps +"enterCustomBitrate" = "Enter custom bitrate in Mbps"; + +/// Enter custom failed logins limit +"enterCustomFailedLogins" = "Enter custom failed logins limit"; + +/// Enter custom max sessions +"enterCustomMaxSessions" = "Enter custom max sessions"; + +/// Enter the episode number. +"enterEpisodeNumber" = "Enter the episode number."; + +/// Enter Pin +"enterPin" = "Enter Pin"; + +/// Enter PIN for %@ +"enterPinForUser" = "Enter PIN for %@"; + +/// Enter the season number. +"enterSeasonNumber" = "Enter the season number."; + +/// Episode +"episode" = "Episode"; + +/// Episode Landscape Poster +"episodeLandscapePoster" = "Episode Landscape Poster"; + +/// Episode %1$@ +"episodeNumber" = "Episode %1$@"; + +/// Episode runtime in minutes. +"episodeRuntimeDescription" = "Episode runtime in minutes."; + +/// Episodes +"episodes" = "Episodes"; + +/// Error +"error" = "Error"; + +/// Error Details +"errorDetails" = "Error Details"; + +/// Every +"every" = "Every"; + +/// Everyday +"everyday" = "Everyday"; + +/// Every %1$@ +"everyInterval" = "Every %1$@"; + +/// Executed +"executed" = "Executed"; + +/// Existing items +"existingItems" = "Existing items"; + +/// This item exists on your Jellyfin Server. +"existsOnServer" = "This item exists on your Jellyfin Server."; + +/// Experimental +"experimental" = "Experimental"; + +/// Failed logins +"failedLogins" = "Failed logins"; + +/// Favorited +"favorited" = "Favorited"; + +/// Favorites +"favorites" = "Favorites"; -// Administration - Dashboard Section -// Label for the administration dashboard. -"administration" = "Administration"; +/// Filters +"filters" = "Filters"; -// Scan All Libraries - Button -// Button to trigger a scan for all libraries. -"scanAllLibraries" = "Scan All Libraries"; +/// Find Missing +"findMissing" = "Find Missing"; -// Scheduled Tasks - Administration Dashboard -// Label for scheduled tasks in the administration dashboard. -"scheduledTasks" = "Scheduled Tasks"; +/// Find missing metadata and images. +"findMissingDescription" = "Find missing metadata and images."; -// FPS with String - Transcode FPS -// Label for frames per second, formatted with a string. -"fpsWithString" = "%@fps"; +/// Force remote media transcoding +"forceRemoteTranscoding" = "Force remote media transcoding"; -// Streams - Session Streaming Clients -// Label for active streaming clients. -"streams" = "Streams"; +/// Format +"format" = "Format"; -// Online - General -// Label to indicate that something is online. -"online" = "Online"; +/// 3D Format +"format3D" = "3D Format"; -// Shutdown Server - Button -// Button to shutdown the server. -"shutdownServer" = "Shutdown Server"; +/// Full Side-by-Side +"fullSideBySide" = "Full Side-by-Side"; -// Shutdown Warning - Confirmation -// Warning message displayed when shutting down the server. -"shutdownWarning" = "Are you sure you want to shutdown the server?"; +/// Full Top and Bottom +"fullTopAndBottom" = "Full Top and Bottom"; -// Restart Server - Button -// Button to restart the server. -"restartServer" = "Restart Server"; +/// Genres +"genres" = "Genres"; -// Restart Warning - Confirmation -// Warning message displayed when restarting the server. -"restartWarning" = "Are you sure you want to restart the server?"; +/// Categories that describe the themes or styles of media. +"genresDescription" = "Categories that describe the themes or styles of media."; -// Active Devices - ActiveSessionsView Header -// Header label for the active devices view. -"activeDevices" = "Active Devices"; +/// Gestures +"gestures" = "Gestures"; -// Dashboard - UserDashboardView Header -// Header label for the user dashboard view. -"dashboard" = "Dashboard"; +/// Gbps +"gigabitsPerSecond" = "Gbps"; -// Remaining Time - SessionPlaybackMethod -// Label for displaying remaining playback time. -"itemOverItem" = "%1$@ / %2$@"; +/// Green +"green" = "Green"; -// Canceled - Task Status -// Label for a canceled task. -"canceled" = "Cancelled"; +/// Grid +"grid" = "Grid"; -// Confirm - Button -// Button to confirm an action. -"confirm" = "Confirm"; +/// Guest Star +"guestStar" = "Guest Star"; -// Transcode - Play Method -// Label for the transcode play method. -"transcode" = "Transcode"; +/// Half Side-by-Side +"halfSideBySide" = "Half Side-by-Side"; -// Remux - Play Method -// Label for the remux play method. -"remux" = "Remux"; +/// Half Top and Bottom +"halfTopAndBottom" = "Half Top and Bottom"; -// Direct Play - Play Method -// Label for the direct play method. -"directPlay" = "Direct Play"; +/// Hidden +"hidden" = "Hidden"; -// Direct Stream - Play Method -// Label for the direct stream method. -"directStream" = "Direct Stream"; +/// Hide user from login screen +"hideUserFromLoginScreen" = "Hide user from login screen"; -// Container Not Supported - TranscodeReason -// Error message for unsupported container format. -"containerNotSupported" = "The container format is not supported"; +/// Hint +"hint" = "Hint"; -// Video Codec Not Supported - TranscodeReason -// Error message for unsupported video codec. -"videoCodecNotSupported" = "The video codec is not supported"; +/// Home +"home" = "Home"; -// Audio Codec Not Supported - TranscodeReason -// Error message for unsupported audio codec. -"audioCodecNotSupported" = "The audio codec is not supported"; +/// Hours +"hours" = "Hours"; -// Subtitle Codec Not Supported - TranscodeReason -// Error message for unsupported subtitle codec. -"subtitleCodecNotSupported" = "The subtitle codec is not supported"; +/// ID +"id" = "ID"; -// Audio Is External - TranscodeReason -// Error message when audio track is external and requires transcoding. -"audioIsExternal" = "The audio track is external and requires transcoding"; +/// Identify +"identify" = "Identify"; -// Secondary Audio Not Supported - TranscodeReason -// Error message for unsupported secondary audio. -"secondaryAudioNotSupported" = "Secondary audio is not supported"; +/// Idle +"idle" = "Idle"; -// Video Profile Not Supported - TranscodeReason -// Error message for unsupported video profile. -"videoProfileNotSupported" = "The video profile is not supported"; +/// Illustrator +"illustrator" = "Illustrator"; -// Video Level Not Supported - TranscodeReason -// Error message for unsupported video level. -"videoLevelNotSupported" = "The video level is not supported"; +/// Images +"image" = "Images"; -// Video Resolution Not Supported - TranscodeReason -// Error message for unsupported video resolution. -"videoResolutionNotSupported" = "The video resolution is not supported"; +/// Images +"images" = "Images"; -// Video Bit Depth Not Supported - TranscodeReason -// Error message for unsupported video bit depth. -"videoBitDepthNotSupported" = "The video bit depth is not supported"; +/// Image source +"imageSource" = "Image source"; -// Video Framerate Not Supported - TranscodeReason -// Error message for unsupported video framerate. -"videoFramerateNotSupported" = "The video framerate is not supported"; +/// Index +"index" = "Index"; -// Reference Frames Not Supported - TranscodeReason -// Error message for unsupported number of reference frames. -"refFramesNotSupported" = "The number of reference frames is not supported"; +/// Indicators +"indicators" = "Indicators"; -// Anamorphic Video Not Supported - TranscodeReason -// Error message for unsupported anamorphic video. -"anamorphicVideoNotSupported" = "Anamorphic video is not supported"; +/// Inker +"inker" = "Inker"; -// Interlaced Video Not Supported - TranscodeReason -// Error message for unsupported interlaced video. +/// Interlaced video is not supported "interlacedVideoNotSupported" = "Interlaced video is not supported"; -// Audio Channels Not Supported - TranscodeReason -// Error message for unsupported number of audio channels. -"audioChannelsNotSupported" = "The number of audio channels is not supported"; +/// Interval +"interval" = "Interval"; -// Audio Profile Not Supported - TranscodeReason -// Error message for unsupported audio profile. -"audioProfileNotSupported" = "The audio profile is not supported"; +/// Inverted Dark +"invertedDark" = "Inverted Dark"; -// Audio Sample Rate Not Supported - TranscodeReason -// Error message for unsupported audio sample rate. -"audioSampleRateNotSupported" = "The audio sample rate is not supported"; +/// Inverted Light +"invertedLight" = "Inverted Light"; -// Audio Bit Depth Not Supported - TranscodeReason -// Error message for unsupported audio bit depth. -"audioBitDepthNotSupported" = "The audio bit depth is not supported"; +/// %1$@ at %2$@ +"itemAtItem" = "%1$@ at %2$@"; -// Container Bitrate Exceeds Limit - TranscodeReason -// Error message when container bitrate exceeds the allowed limit. -"containerBitrateExceedsLimit" = "The container bitrate exceeds the allowed limit"; +/// Items +"items" = "Items"; -// Video Bitrate Not Supported - TranscodeReason -// Error message for unsupported video bitrate. -"videoBitrateNotSupported" = "The video bitrate is not supported"; +/// Jellyfin +"jellyfin" = "Jellyfin"; -// Audio Bitrate Not Supported - TranscodeReason -// Error message for unsupported audio bitrate. -"audioBitrateNotSupported" = "The audio bitrate is not supported"; +/// Join Groups +"joinGroups" = "Join Groups"; -// Unknown Video Stream Info - TranscodeReason -// Error message for unknown video stream information. -"unknownVideoStreamInfo" = "The video stream information is unknown"; +/// Jump +"jump" = "Jump"; -// Unknown Audio Stream Info - TranscodeReason -// Error message for unknown audio stream information. -"unknownAudioStreamInfo" = "The audio stream information is unknown"; +/// Jump Backward +"jumpBackward" = "Jump Backward"; -// Direct Play Error - TranscodeReason -// Error message for a direct play failure. -"directPlayError" = "An error occurred during direct play"; +/// Jump Backward Length +"jumpBackwardLength" = "Jump Backward Length"; -// Video Range Type Not Supported - TranscodeReason -// Error message for unsupported video range type. -"videoRangeTypeNotSupported" = "The video range type is not supported"; +/// Jump Forward +"jumpForward" = "Jump Forward"; -// No Session - General -// Message displayed when no active session is available. -"noSession" = "No session"; +/// Jump Forward Length +"jumpForwardLength" = "Jump Forward Length"; -// Session - General -// Title for the session view. -"session" = "Session"; +/// Kids +"kids" = "Kids"; -// Client - General -// Label for the client used in a session. -"client" = "Client"; +/// kbps +"kilobitsPerSecond" = "kbps"; -// Method - General -// Label for the playback method (e.g., Direct Play, Transcoding). -"method" = "Method"; +/// Language +"language" = "Language"; -// Category - General -// Label for the category of a task. -"category" = "Category"; +/// Larger +"larger" = "Larger"; -// Task - Navigation Title -// Navigation title for the task view. -"task" = "Task"; +/// Largest +"largest" = "Largest"; -// Last Run - General -// Label for the last run time of a task. +/// Last run "lastRun" = "Last run"; -// Dashboard Description - General -// Description for the administration dashboard section. -"dashboardDescription" = "Perform administrative tasks for your Jellyfin server."; +/// Last ran %@ +"lastRunTime" = "Last ran %@"; -// Tasks - Section Title -// Title for the tasks section. -"tasks" = "Tasks"; +/// Last Seen +"lastSeen" = "Last Seen"; -// Server Logs - Section Title -// Title for the server logs section. -"serverLogs" = "Server Logs"; +/// Latest %@ +"latestWithString" = "Latest %@"; -// Tasks Description - General -// Description for the tasks section. -"tasksDescription" = "Tasks are operations that are scheduled to run periodically or can be triggered manually."; +/// Layout +"layout" = "Layout"; -// Running - Status -// Label for a task that is currently running. -"running" = "Running..."; +/// Learn more... +"learnMoreEllipsis" = "Learn more..."; -// Run - Button -// Label for the button to run a task. -"run" = "Run"; +/// Left +"left" = "Left"; -// Stop - Button -// Label for the button to stop a task. -"stop" = "Stop"; +/// Letter +"letter" = "Letter"; -// Cancelling - Status -// Label for a task that is in the process of being canceled. -"cancelling" = "Cancelling..."; +/// Letterer +"letterer" = "Letterer"; -// Last Run Time - General -// Message showing the last run time of a task. -"lastRunTime" = "Last ran %@"; +/// Letter Picker +"letterPicker" = "Letter Picker"; -// Never Run - General -// Message displayed when a task has never run. -"neverRun" = "Never run"; +/// Library +"library" = "Library"; -// Edit - Button -// Label for the button to edit a task. -"edit" = "Edit"; +/// Light +"light" = "Light"; -// Task Completed - Status -// Message displayed when a task has completed successfully. -"taskCompleted" = "Completed"; +/// Liked Items +"likedItems" = "Liked Items"; -// Task Failed - Status -// Message displayed when a task has failed. -"taskFailed" = "Failed"; +/// Likes +"likes" = "Likes"; -// Task Cancelled - Status -// Message displayed when a task has been canceled. -"taskCancelled" = "Cancelled"; +/// List +"list" = "List"; -// Task Aborted - Status -// Message displayed when a task has been aborted. -"taskAborted" = "Aborted"; +/// Live TV +"liveTV" = "Live TV"; -// Rewatching in Next Up - Settings Description -// Description for enabling rewatching in the Next Up section. -"nextUpRewatch" = "Rewatching in Next Up"; +/// Live TV access +"liveTvAccess" = "Live TV access"; -// Days in Next Up - Settings Description -// Description for the day limit in the Next Up section. -"nextUpDays" = "Days in Next Up"; +/// Live TV Channels +"liveTVChannels" = "Live TV Channels"; -// 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."; +/// Live TV Programs +"liveTVPrograms" = "Live TV Programs"; -// Done - General -// Label for completing an action. -"done" = "Done"; +/// Live TV recording management +"liveTvRecordingManagement" = "Live TV recording management"; -// Save - General -// Label for saving changes. -"save" = "Save"; +/// Loading user failed +"loadingUserFailed" = "Loading user failed"; -// Days - General -// Label for days as a time interval. -"days" = "Days"; +/// Local Servers +"localServers" = "Local Servers"; -// Columns - Section Title -// Title for the column configuration section. -"columns" = "Columns"; +/// Lock All Fields +"lockAllFields" = "Lock All Fields"; // Activity Indicator - Setting // Setting to enable/disable the HomeView Activity Indicator // Appears in CustomizeSettingView "activityIndicator" = "Activity Indicator"; -// Date Created - Label -// Label for displaying the date an API key was created -"dateCreated" = "Date Created"; +"lockedFields" = "Locked Fields"; -// API Keys - Title -// Section Title for displaying API keys in the list -"apiKeysTitle" = "API Keys"; +/// Locked users +"lockedUsers" = "Locked users"; -// API Keys - Description -// Explains the usage of API keys in external applications -"apiKeysDescription" = "External applications require an API key to communicate with your server."; +/// Logo +"logo" = "Logo"; -// Add - Button -// Adds a new record -"add" = "Add"; +/// Logs +"logs" = "Logs"; -// API Key Copied - Alert -// Informs the user that the API key was copied to the clipboard -"apiKeyCopied" = "API Key Copied"; +/// Access the Jellyfin server logs for troubleshooting and monitoring purposes. +"logsDescription" = "Access the Jellyfin server logs for troubleshooting and monitoring purposes."; -// API Key Copied - Alert Message -// Informs the user that the key was copied successfully -"apiKeyCopiedMessage" = "Your API Key was copied to your clipboard!"; +/// Lyricist +"lyricist" = "Lyricist"; -// OK - Button -// Acknowledges an action -"ok" = "OK"; +/// Lyrics +"lyrics" = "Lyrics"; -// Delete API Key - Confirmation Message -// Warns the user that API Key deletion is permanent -"deleteAPIKeyMessage" = "Are you sure you want to permanently delete this key?"; +/// Management +"management" = "Management"; -// Cancel - Button -// Cancels the current action -"cancel" = "Cancel"; +/// Maximum Bitrate +"maximumBitrate" = "Maximum Bitrate"; -// Delete - Button -// Confirms the deletion of an item -"delete" = "Delete"; +/// Limits the total number of connections a user can have to the server. +"maximumConnectionsDescription" = "Limits the total number of connections a user can have to the server."; -// Create API Key - Alert -// Prompts the user to enter an app name to create an API key -"createAPIKey" = "Create API Key"; +/// Maximum failed login policy +"maximumFailedLoginPolicy" = "Maximum failed login policy"; -// Create API Key - Message -// Asks the user to enter the name of the application for the new API key -"createAPIKeyMessage" = "Enter the application name for the new API key."; +/// Sets the maximum failed login attempts before a user is locked out. +"maximumFailedLoginPolicyDescription" = "Sets the maximum failed login attempts before a user is locked out."; -// Application Name - Text Field -// Placeholder text for entering the name of the application -"applicationName" = "Application Name"; +/// Locked users must be re-enabled by an Administrator. +"maximumFailedLoginPolicyReenable" = "Locked users must be re-enabled by an Administrator."; -// Save - Button -// Confirms the creation of the new API key -"save" = "Save"; +/// Maximum remote bitrate +"maximumRemoteBitrate" = "Maximum remote bitrate"; -// API Keys - Screen Title -// Title for the API keys management screen -"apiKeys" = "API Keys"; +/// Maximum sessions +"maximumSessions" = "Maximum sessions"; -// Devices - Section Header -// Title for the devices section in the Admin Dashboard -"devices" = "Devices"; +/// Maximum sessions policy +"maximumSessionsPolicy" = "Maximum sessions policy"; -// All Devices Description - Section Description -// Description for the all devices section in the Admin Dashboard -"allDevicesDescription" = "View all past and present devices that have connected."; +/// Maximum parental rating +"maxParentalRating" = "Maximum parental rating"; -// Delete Selected Devices - Button -// Button label for deleting all selected devices -"deleteSelectedDevices" = "Delete Selected Devices"; +/// Content with a higher rating will be hidden from this user. +"maxParentalRatingDescription" = "Content with a higher rating will be hidden from this user."; -// Never - Filler Text -// Text displayed when something has never or will never occur -"never" = "Never"; +/// Media +"media" = "Media"; -// Delete Selected Devices Warning - Warning Message -// Warning message displayed when deleting all devices -"deleteSelectionDevicesWarning" = "Are you sure you wish to delete all selected devices? All selected sessions will be logged out."; +/// Media Access +"mediaAccess" = "Media Access"; -// Delete Device Warning - Warning Message -// Warning message displayed when deleting a single device -"deleteDeviceWarning" = "Are you sure you wish to delete this device? This session will be logged out."; +/// Media downloads +"mediaDownloads" = "Media downloads"; -// Delete Device - Action -// Message for deleting a single device in the all devices section -"deleteDevice" = "Delete Device"; +/// Media playback +"mediaPlayback" = "Media playback"; -// Delete Device Self-Deletion - Error Message -// Error message when attempting to delete the current session's device -"deleteDeviceSelfDeletion" = "Cannot delete a session from the same device (%1$@)."; +/// Mbps +"megabitsPerSecond" = "Mbps"; -// Delete Device Failed - Error Title -// Title for the alert when device deletion fails -"deleteDeviceFailed" = "Failed to Delete Device"; +/// Menu +"menu" = "Menu"; -// Custom Device Name - Title -// Title for setting a custom device name -"customDeviceName" = "Custom Device Name"; +/// Menu Buttons +"menuButtons" = "Menu Buttons"; -// Capabilities - Section Header -// Title for the section showing the device capabilities -"capabilities" = "Capabilities"; +/// Metadata +"metadata" = "Metadata"; -// Supports Content Uploading - Label -// Indicates whether the device supports uploading content -"supportsContentUploading" = "Content Uploading"; +/// Metadata preferences +"metadataPreferences" = "Metadata preferences"; -// Supports Media Control - Label -// Indicates whether the device supports media control (e.g., play, pause, stop) -"supportsMediaControl" = "Media Control"; +/// Method +"method" = "Method"; -// Supports Persistent Identifier - Label -// Indicates whether the device supports a persistent identifier -"supportsPersistentIdentifier" = "Persistent Identifier"; +/// Minutes +"minutes" = "Minutes"; -// Supports Sync - Label -// Indicates whether the device suppoTestrts syncing content (e.g., media sync across devices) -"supportsSync" = "Sync"; +/// Missing +"missing" = "Missing"; -// Yes - Label -// Indicates that a capability is supported -"yes" = "Yes"; +/// Missing Items +"missingItems" = "Missing Items"; -// No - Label -// Indicates that a capability is not supported -"no" = "No"; +/// Mixer +"mixer" = "Mixer"; -// Custom Device Name Saved - Label -// Confirms that the custom device name was saved successfully -"customDeviceNameSaved" = "Your custom device name '%1$@' has been saved."; +/// Movies +"movies" = "Movies"; -// Success - Label -// Indicates that an operation was successful -"success" = "Success"; +/// Music +"music" = "Music"; -// Remove All - Button -// Deselects all currently selected devices -"removeAll" = "Remove All"; +/// MVC +"mvc" = "MVC"; -// Select All - Button -// Selects all available devices -"selectAll" = "Select All"; +/// Name +"name" = "Name"; -// Users - Section -// Admin Dashboard Section with all Server Users -"users" = "Users"; +/// Native Player +"nativePlayer" = "Native Player"; -// Active - Label -// Indication whether an item is active or inactive -"active" = "Active"; +/// Network timed out +"networkTimedOut" = "Network timed out"; -// All Users Description - Section Description -// Description for the all users section in the Admin Dashboard -"allUsersDescription" = "View and manage all registered users on the server, including their permissions and activity status."; +/// Never +"never" = "Never"; -// Role - Label -// Represents the role of the user -"role" = "Role"; +/// Never run +"neverRun" = "Never run"; -// Administrator - Title -// Label for administrator role -"administrator" = "Administrator"; +/// New Password +"newPassword" = "New Password"; -// User - Title -// Label for non-administrator users -"user" = "User"; +/// News +"news" = "News"; -// Activity - Label -// Represents user activity status -"activity" = "Activity"; +/// New User +"newUser" = "New User"; -// Logs Description - View -// Access the Jellyfin server logs for troubleshooting and monitoring purposes -"logsDescription" = "Access the Jellyfin server logs for troubleshooting and monitoring purposes."; +/// Next +"next" = "Next"; -// Type - Label -// Indicate a type -"type" = "Type"; +/// Next Item +"nextItem" = "Next Item"; -// Day of Week - Section Label -// Specifies the day of the week for the trigger -"dayOfWeek" = "Day of Week"; +/// Next Up +"nextUp" = "Next Up"; -// Time - Section Label -// Specifies the time for the trigger -"time" = "Time"; +/// Days in Next Up +"nextUpDays" = "Days in Next Up"; -// Daily - Description -// Recurring trigger that runs daily -"daily" = "Daily"; +/// Set the maximum amount of days a show should stay in the 'Next Up' list without watching it. +"nextUpDaysDescription" = "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it."; -// Interval - Description -// Recurring trigger based on time intervals -"interval" = "Interval"; +/// Rewatching in Next Up +"nextUpRewatch" = "Rewatching in Next Up"; -// Weekly - Description -// Recurring trigger that runs weekly -"weekly" = "Weekly"; +/// No +"no" = "No"; -// On Application Startup - Description -// Trigger that runs when the application starts -"onApplicationStartup" = "On application startup"; +/// No episodes available +"noEpisodesAvailable" = "No episodes available"; -// Task Trigger Time Limit - Section Description -// Sets the maximum runtime (in hours) for this task trigger -"taskTriggerTimeLimit" = "Sets the maximum runtime (in hours) for this task trigger."; +/// No local servers found +"noLocalServersFound" = "No local servers found"; -// Task Trigger Interval - Section Description -// Sets the duration (in minutes) in between task triggers -"taskTriggerInterval" = "Sets the duration (in minutes) in between task triggers."; +/// None +"none" = "None"; -// Every - Label -// Used to select interval frequency -"every" = "Every"; +/// No overview available +"noOverviewAvailable" = "No overview available"; -// Time Limit with Unit - Label -// Specifies time limit along with the unit -"timeLimitWithUnit" = "Time Limit (%@)"; +/// No public Users +"noPublicUsers" = "No public Users"; -// Time Limit - Section Label -// Specifies the time limit for the task -"timeLimit" = "Time Limit"; +/// No results. +"noResults" = "No results."; -// Hours - Input Field Placeholder -// Placeholder for inputting hours -"hours" = "Hours"; +/// Normal +"normal" = "Normal"; -// Minutes - Input Field Placeholder -// Placeholder for inputting minutes -"minutes" = "Minutes"; +/// No runtime limit +"noRuntimeLimit" = "No runtime limit"; -// Add Trigger - Title -// Title for adding a new task trigger -"addTrigger" = "Add trigger"; +/// No session +"noSession" = "No session"; -// Save - Button Label -// Button to save the current task trigger -"save" = "Save"; +/// Type: %@ not implemented yet :( +"notImplementedYetWithType" = "Type: %@ not implemented yet :("; -// Changes Not Saved - Alert Title -// Title for unsaved changes alert -"changesNotSaved" = "Changes not saved"; +/// No title +"noTitle" = "No title"; -// Discard Changes - Button Label -// Button to discard unsaved changes -"discardChanges" = "Discard Changes"; +/// Official Rating +"officialRating" = "Official Rating"; -// Unsaved Changes Message - Alert -// Message for unsaved changes alert -"unsavedChangesMessage" = "You have unsaved changes. Are you sure you want to discard them?"; +/// Offset +"offset" = "Offset"; -// Delete Trigger - Confirmation Dialog Title -// Title for the delete trigger confirmation dialog -"deleteTrigger" = "Delete Trigger"; +/// OK +"ok" = "OK"; + +/// On application startup +"onApplicationStartup" = "On application startup"; + +/// On Now +"onNow" = "On Now"; + +/// Options +"options" = "Options"; -// Delete Trigger Confirmation - Message -// Message for deleting a trigger confirmation dialog -"deleteTriggerConfirmationMessage" = "Are you sure you want to delete this trigger? This action cannot be undone."; +/// Orange +"orange" = "Orange"; -// Item At Item - Label -// Used to describe an item at another item -"itemAtItem" = "%1$@ at %2$@"; +/// Order +"order" = "Order"; -// Every Interval - Label -// Describes an interval trigger with recurring time -"everyInterval" = "Every %1$@"; +/// Orientation +"orientation" = "Orientation"; -// Time Limit Label with Value - Label -// Describes time limit with a value -"timeLimitLabelWithValue" = "Time limit: %1$@"; +/// Original Air Date +"originalAirDate" = "Original Air Date"; -// Add - Button Label -// Button to add a new item -"add" = "Add"; +/// Original aspect ratio +"originalAspectRatio" = "Original aspect ratio"; -// Idle - Task State -// Describes the task state as idle -"idle" = "Idle"; +/// Original Title +"originalTitle" = "Original Title"; -// Status - Section Title -// Title for the status section -"status" = "Status"; +/// Other +"other" = "Other"; -// Error Details - Section Title -// Title for the error details section -"errorDetails" = "Error Details"; +/// Overview +"overview" = "Overview"; -// Details - Section Title -// Title for the details section -"details" = "Details"; +/// Parental controls +"parentalControls" = "Parental controls"; -// Triggers - Section Header -// Header for the scheduled task triggers section -"triggers" = "Triggers"; +/// Parental Rating +"parentalRating" = "Parental Rating"; -// Executed - Section Title -// Title for the task execution date section -"executed" = "Executed"; +/// Password +"password" = "Password"; -// No Runtime Limit - Label -// Describes a task with no runtime limit -"noRuntimeLimit" = "No runtime limit"; +/// User password has been changed. +"passwordChangedMessage" = "User password has been changed."; -// API Key Created - Success Message -// A new Access Token was successfully created for the specified application -"serverTriggerCreated" = "A new trigger was created for '%1$@'."; +/// Changes the Jellyfin server user password. This does not change any Swiftfin settings. +"passwordChangeWarning" = "Changes the Jellyfin server user password. This does not change any Swiftfin settings."; -// API Key Deleted - Success Message -// The Access Token was successfully deleted for the specified application -"serverTriggerDeleted" = "The selected trigger was deleted from '%1$@'."; +/// New passwords do not match. +"passwordsDoNotMatch" = "New passwords do not match."; -// Save - Button -// Confirms that something completed successfully or without error -"success" = "Success"; +/// Pause on background +"pauseOnBackground" = "Pause on background"; -// Trigger Already Exists - -// Message to indicate that a Task Trigger already exists -"triggerAlreadyExists" = "Trigger already exists"; +/// Penciller +"penciller" = "Penciller"; -// Add API Key - Button -// Creates an API Key if there are no keys available -"addAPIKey" = "Add API key"; +/// People +"people" = "People"; -// Hidden - Filter -// Users with a policy of isHidden == True -"hidden" = "Hidden"; +/// People who helped create or perform specific media. +"peopleDescription" = "People who helped create or perform specific media."; -// Delete Selected Users Warning - Warning Message -// Warning message displayed when deleting all users -"deleteSelectionUsersWarning" = "Are you sure you wish to delete all selected users?"; +/// Permissions +"permissions" = "Permissions"; -// Delete User Warning - Warning Message -// Warning message displayed when deleting a single user -"deleteUserWarning" = "Are you sure you wish to delete this user?"; +/// Pin +"pin" = "Pin"; -// Delete User - Action -// Message for deleting a single device in the all users section -"deleteUser" = "Delete User"; +/// Play +"play" = "Play"; -// Delete User Self-Deletion - Error Message -// Error message when attempting to delete the current session's user -"deleteUserSelfDeletion" = "Cannot delete a user from the same user (%1$@)."; +/// Play / Pause +"playAndPause" = "Play / Pause"; -// Delete Selected Users - Button -// Button label for deleting all selected users -"deleteSelectedUsers" = "Delete Selected Users"; +/// Playback +"playback" = "Playback"; -// Delete User Failed - Error Title -// Title for the alert when users deletion fails -"deleteUserFailed" = "Failed to Delete User"; +/// Playback Buttons +"playbackButtons" = "Playback Buttons"; -// Confirm Password - TextField -// Placeholder and label for confirming the password -"confirmPassword" = "Confirm Password"; +/// Playback Quality +"playbackQuality" = "Playback Quality"; -// A username is required - Footer -// Validation message shown when the username field is empty -"usernameRequired" = "A username is required"; +/// Playback Speed +"playbackSpeed" = "Playback Speed"; -// New passwords do not match - Footer -// Validation message shown when the new password and confirm password fields do not match -"passwordsDoNotMatch" = "New passwords do not match"; +/// Played +"played" = "Played"; -// New User - Title -// Title for the new user creation view -"newUser" = "New User"; +/// Play From Beginning +"playFromBeginning" = "Play From Beginning"; -// Options - Menu -// Menu title for additional actions -"options" = "Options"; +/// Play Next Item +"playNextItem" = "Play Next Item"; -// Add User - Button -// Button title to add a new user -"addUser" = "Add User"; +/// Play on active +"playOnActive" = "Play on active"; -// Edit Users - Button -// Button title to edit existing users -"editUsers" = "Edit Users"; +/// Play Previous Item +"playPreviousItem" = "Play Previous Item"; -// Bits Per Second - Unit -// Represents a speed in bits per second -"bitsPerSecond" = "bps"; +/// Posters +"posters" = "Posters"; -// Kilobits Per Second - Unit -// Represents a speed in kilobits per second -"kilobitsPerSecond" = "kbps"; +/// Premiere Date +"premiereDate" = "Premiere Date"; -// Megabits Per Second - Unit -// Represents a speed in megabits per second -"megabitsPerSecond" = "Mbps"; +/// Press Down for Menu +"pressDownForMenu" = "Press Down for Menu"; -// Gigabits Per Second - Unit -// Represents a speed in gigabits per second -"gigabitsPerSecond" = "Gbps"; +/// Previous Item +"previousItem" = "Previous Item"; -// Terabits Per Second - Unit -// Represents a speed in terabits per second -"terabitsPerSecond" = "Tbps"; +/// Primary +"primary" = "Primary"; -// Default - Setting -// Represents the default policy or limit -"default" = "Default"; +/// Producer +"producer" = "Producer"; -// Unlimited - Setting -// Represents no restriction or unlimited policy -"unlimited" = "Unlimited"; +/// Production +"production" = "Production"; -// Create & Join Groups - Action -// Allows the user to create and join groups -"createAndJoinGroups" = "Create & Join Groups"; +/// Production Locations +"productionLocations" = "Production Locations"; -// Join Groups - Action -// Allows the user to join existing groups -"joinGroups" = "Join Groups"; +/// Production Year +"productionYear" = "Production Year"; -// Permissions - Section -// Represents access control settings for users -"permissions" = "Permissions"; +/// Profile +"profile" = "Profile"; -// SyncPlay - Feature -// Represents the synchronized playback feature across multiple devices -"syncPlay" = "SyncPlay"; +/// Profile Image +"profileImage" = "Profile Image"; -// Remote connections - Section & Toggle -// Represents settings related to remote access -"remoteConnections" = "Remote connections"; +/// Profiles +"profiles" = "Profiles"; -// Maximum remote bitrate - Picker -// Represents the maximum bitrate allowed for remote connections -"maximumRemoteBitrate" = "Maximum remote bitrate"; +/// Programs +"programs" = "Programs"; -// Custom bitrate - Button -// Opens an alert to enter a custom bitrate value -"customBitrate" = "Custom bitrate"; +/// Progress +"progress" = "Progress"; -// Enter custom bitrate in Mbps - Description -// Describes the purpose of the custom bitrate entry -"enterCustomBitrate" = "Enter custom bitrate in Mbps"; +/// Provider +"provider" = "Provider"; -// Feature access - Section -// Represents settings related to feature access for users -"featureAccess" = "Feature access"; +/// Public Users +"publicUsers" = "Public Users"; -// Live TV access - Toggle -// Toggles access to live TV content -"liveTvAccess" = "Live TV access"; +/// Quick Connect +"quickConnect" = "Quick Connect"; -// Live TV recording management - Toggle -// Toggles management of live TV recordings -"liveTvRecordingManagement" = "Live TV recording management"; +/// Quick Connect code +"quickConnectCode" = "Quick Connect code"; -// Management - Section -// Represents settings related to management permissions -"management" = "Management"; +/// Enter the 6 digit code from your other device. +"quickConnectCodeInstruction" = "Enter the 6 digit code from your other device."; -// Lyrics - Toggle -// Toggles permission to manage lyrics -"lyrics" = "Lyrics"; +/// Open the Jellyfin app on your phone or web browser and sign in with your account +"quickConnectStep1" = "Open the Jellyfin app on your phone or web browser and sign in with your account"; -// Media playback - Section & Toggle -// Represents settings related to media playback permissions -"mediaPlayback" = "Media playback"; +/// Open the user menu and go to the Quick Connect page +"quickConnectStep2" = "Open the user menu and go to the Quick Connect page"; -// Audio transcoding - Toggle -// Toggles permission for audio transcoding -"audioTranscoding" = "Audio transcoding"; +/// Enter the following code: +"quickConnectStep3" = "Enter the following code:"; -// Video transcoding - Toggle -// Toggles permission for video transcoding -"videoTranscoding" = "Video transcoding"; +/// Authorizing Quick Connect successful. Please continue on your other device. +"quickConnectSuccessMessage" = "Authorizing Quick Connect successful. Please continue on your other device."; -// Video remuxing - Toggle -// Toggles permission for video remuxing -"videoRemuxing" = "Video remuxing"; +/// Random +"random" = "Random"; -// Force remote media transcoding - Toggle -// Toggles whether remote media transcoding is forced -"forceRemoteTranscoding" = "Force remote media transcoding"; +/// Random Image +"randomImage" = "Random Image"; -// Media downloads - Toggle -// Toggles permission to download media content -"mediaDownloads" = "Media downloads"; +/// Rating +"rating" = "Rating"; -// Hide user from login screen - Toggle -// Toggles whether the user is hidden from the login screen -"hideUserFromLoginScreen" = "Hide user from login screen"; +/// %@ rating on a scale from 1 to 10. +"ratingDescription" = "%@ rating on a scale from 1 to 10."; -// Remote control - Section -// Represents settings related to remote control permissions -"remoteControl" = "Remote control"; +/// Ratings +"ratings" = "Ratings"; -// Control other users - Toggle -// Toggles permission to control other users' sessions -"controlOtherUsers" = "Control other users"; +/// Recently Added +"recentlyAdded" = "Recently Added"; -// Control shared devices - Toggle -// Toggles permission to control shared devices -"controlSharedDevices" = "Control shared devices"; +/// Recommended +"recommended" = "Recommended"; -// Sessions - Section -// Represents settings related to session control -"sessions" = "Sessions"; +/// Red +"red" = "Red"; -// Maximum failed login policy - Picker -// Represents the policy for maximum failed login attempts -"maximumFailedLoginPolicy" = "Maximum failed login policy"; +/// The number of reference frames is not supported +"refFramesNotSupported" = "The number of reference frames is not supported"; -// Maximum sessions policy - Picker -// Represents the policy for maximum active sessions -"maximumSessionsPolicy" = "Maximum sessions policy"; +/// Refresh Metadata +"refreshMetadata" = "Refresh Metadata"; -// Custom failed logins - Button -// Opens an alert to enter a custom failed login limit -"customFailedLogins" = "Custom failed logins"; +/// Regional +"regional" = "Regional"; -// Enter custom failed logins limit - Description -// Describes the purpose of the custom failed logins entry -"enterCustomFailedLogins" = "Enter custom failed logins limit"; +/// Regular +"regular" = "Regular"; -// Failed logins - Text Field -// Represents the input field for custom failed logins -"failedLogins" = "Failed logins"; +/// Release Date +"releaseDate" = "Release Date"; -// Custom sessions - Button -// Opens an alert to enter a custom maximum session limit -"customSessions" = "Custom sessions"; +/// Remember layout +"rememberLayout" = "Remember layout"; -// Enter custom max sessions - Description -// Describes the purpose of the custom max sessions entry -"enterCustomMaxSessions" = "Enter custom max sessions"; +/// Remember layout for individual libraries +"rememberLayoutFooter" = "Remember layout for individual libraries"; -// Maximum sessions - Text Field -// Represents the input field for custom maximum sessions -// Used in the custom sessions section -"maximumSessions" = "Maximum sessions"; +/// Remember sorting +"rememberSorting" = "Remember sorting"; -// Refresh - Button -// Button title for the menu to refresh metadata -"refreshMetadata" = "Refresh Metadata"; +/// Remember sorting for individual libraries +"rememberSortingFooter" = "Remember sorting for individual libraries"; -// Find Missing - Menu Option -// Menu option for finding missing metadata -"findMissing" = "Find Missing"; +/// Remixer +"remixer" = "Remixer"; -// Replace Metadata - Menu Option -// Menu option for replacing existing metadata -"replaceMetadata" = "Replace Metadata"; +/// Remote connections +"remoteConnections" = "Remote connections"; -// Replace Images - Menu Option -// Menu option for replacing existing images -"replaceImages" = "Replace Images"; +/// Remote control +"remoteControl" = "Remote control"; -// Replace All - Menu Option -// Menu option for replacing both metadata and images -"replaceAll" = "Replace All"; +/// Remove All +"removeAll" = "Remove All"; -// Delete Item Confirmation Message - Warning message -// Warning message to confirm deleting a media item -"deleteItemConfirmationMessage" = "Are you sure you want to delete this item? This action cannot be undone."; +/// Remove All Servers +"removeAllServers" = "Remove All Servers"; -// Allow Media Item Editing - Toggle -// Toggle option for enabling media item editing -"allowItemEditing" = "Allow media item editing"; +/// Remux +"remux" = "Remux"; -// Allow Collection Management - Toggle -// Toggle option for enabling collection editing / deletion -"allowCollectionManagement" = "Allow collection management"; +/// Reorder +"reorder" = "Reorder"; -// Allow Media Item Deletion - Toggle -// Toggle option for enabling media item deletion -"allowItemDeletion" = "Allow media item deletion"; +/// Replace All +"replaceAll" = "Replace All"; -// Item Deletion Permission Failure - Error Message -// Alert the user they should not be able to delete something -"itemDeletionPermissionFailure" = "You do not have permission to delete this item."; +/// Replace all unlocked metadata and images with new information. +"replaceAllDescription" = "Replace all unlocked metadata and images with new information."; -// Metadata - Section Title -// Title for the ItemEditorView and Metadata related views -"metadata" = "Metadata"; +/// Replace Images +"replaceImages" = "Replace Images"; -// Learn More - Button -// Opens a modal with more information -"learnMoreEllipsis" = "Learn more..."; +/// Replace all images with new images. +"replaceImagesDescription" = "Replace all images with new images."; -// Current Password - Placeholder -// Placeholder text for the current password input field -"currentPassword" = "Current Password"; +/// Replace Metadata +"replaceMetadata" = "Replace Metadata"; -// New Password - Placeholder -// Placeholder text for the new password input field -"newPassword" = "New Password"; +/// Replace unlocked metadata with new information. +"replaceMetadataDescription" = "Replace unlocked metadata with new information."; -// Confirm New Password - Placeholder -// Placeholder text for confirming the new password input field -"confirmNewPassword" = "Confirm New Password"; +/// Required +"required" = "Required"; -// Password - Navigation Title -// Title for the password reset view. -"password" = "Password"; +/// Require device authentication when signing in to the user. +"requireDeviceAuthDescription" = "Require device authentication when signing in to the user."; -// Password Changed - Alert Message -// Message displayed in the success alert after changing the password. -"passwordChangedMessage" = "User password has been changed."; +/// Require device authentication to sign in to the Quick Connect user on this device. +"requireDeviceAuthForQuickConnectUser" = "Require device authentication to sign in to the Quick Connect user on this device."; -// Passwords Do Not Match - Footer -// Error message displayed when new passwords do not match. -"passwordsDoNotMatch" = "New passwords do not match."; +/// Require device authentication to sign in to %@ on this device. +"requireDeviceAuthForUser" = "Require device authentication to sign in to %@ on this device."; -// 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."; +/// Require a local pin when signing in to the user. This pin is unrecoverable. +"requirePinDescription" = "Require a local pin when signing in to the user. This pin is unrecoverable."; -// Find Missing - Button -// Search for missing metadata and images. -"findMissingDescription" = "Find missing metadata and images."; +/// Reset +"reset" = "Reset"; -// Replace Metadata - Button -// Overwrite metadata without affecting images. -"replaceMetadataDescription" = "Replace unlocked metadata with new information."; +/// Reset all settings back to defaults. +"resetAllSettings" = "Reset all settings back to defaults."; -// Replace Images - Button -// Overwrite existing images with new ones. -"replaceImagesDescription" = "Replace all images with new images."; +/// Reset Settings +"resetSettings" = "Reset Settings"; -// Replace All - Button -// Replace all metadata and images. -"replaceAllDescription" = "Replace all unlocked metadata and images with new information."; +/// Reset Swiftfin user settings +"resetSettingsDescription" = "Reset Swiftfin user settings"; -// Device Profile - Description -// Explains how device profiles control playback and transcoding behavior. -"deviceProfileDescription" = "Decide which media plays natively or requires server transcoding for compatibility."; +/// Are you sure you want to reset all user settings? +"resetSettingsMessage" = "Are you sure you want to reset all user settings?"; -// 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."; +/// Reset User Settings +"resetUserSettings" = "Reset User Settings"; -// 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."; +/// Restart Server +"restartServer" = "Restart Server"; -// 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."; +/// Are you sure you want to restart the server? +"restartWarning" = "Are you sure you want to restart the server?"; -// 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."; +/// Resume +"resume" = "Resume"; -// 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."; +/// Resume Offset +"resumeOffset" = "Resume Offset"; -// 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."; +/// Resume content seconds before the recorded resume time +"resumeOffsetDescription" = "Resume content seconds before the recorded resume time"; -// Bits Per Second - Unit -// Represents a speed in bits per second -"bitsPerSecond" = "bps"; +/// Resume Offset +"resumeOffsetTitle" = "Resume Offset"; -// Kilobits Per Second - Unit -// Represents a speed in kilobits per second -"kilobitsPerSecond" = "kbps"; +/// Retrieving media information +"retrievingMediaInformation" = "Retrieving media information"; -// Megabits Per Second - Unit -// Represents a speed in megabits per second -"megabitsPerSecond" = "Mbps"; +/// Retry +"retry" = "Retry"; -// Gigabits Per Second - Unit -// Represents a speed in gigabits per second -"gigabitsPerSecond" = "Gbps"; +/// Reviews +"reviews" = "Reviews"; -// Terabits Per Second - Unit -// Represents a speed in terabits per second -"terabitsPerSecond" = "Tbps"; +/// Right +"right" = "Right"; -// Display Order - Section -// Label for the display order section in metadata settings. -"displayOrder" = "Display Order"; +/// Role +"role" = "Role"; -// Dates - Section -// Label for the dates section in metadata settings. -"dates" = "Dates"; +/// Rotate +"rotate" = "Rotate"; -// Date Added - Label -// Label for the date added field. -"dateAdded" = "Date Added"; +/// Run +"run" = "Run"; -// Release Date - Label -// Label for the release date field. -"releaseDate" = "Release Date"; +/// Running... +"running" = "Running..."; -// End Date - Label -// Label for the end date field. -"endDate" = "End Date"; +/// Runtime +"runtime" = "Runtime"; -// Year - Section -// Label for the year section in metadata settings. -"year" = "Year"; +/// Save +"save" = "Save"; -// Refresh - Button -// Standard button title to refresh content. -"refresh" = "Refresh"; +/// Save the user to this device without any local authentication. +"saveUserWithoutAuthDescription" = "Save the user to this device without any local authentication."; -// Find Missing Metadata - Button -// Button title to trigger a search for missing metadata. -"findMissingMetadata" = "Find Missing Metadata"; +/// Schedule already exists +"scheduleAlreadyExists" = "Schedule already exists"; -// Replace Metadata - Button -// Button title to replace existing metadata with new data. -"replaceMetadata" = "Replace Metadata"; +/// Score +"score" = "Score"; -// Replace Images - Button -// Button title to replace existing images with new images. -"replaceImages" = "Replace Images"; +/// Screenshot +"screenshot" = "Screenshot"; -// Replace All - Button -// Button title to replace all metadata and images. -"replaceAll" = "Replace All"; +/// Scrub Current Time +"scrubCurrentTime" = "Scrub Current Time"; -// Refresh Metadata - Menu Title -// Title for the metadata refresh menu. -"refreshMetadata" = "Refresh Metadata"; +/// Search +"search" = "Search"; -// Official Rating - Label -// Label for the official rating field in media metadata -"officialRating" = "Official Rating"; +/// Season +"season" = "Season"; -// Production Locations - Label -// Label for production locations in media metadata -"productionLocations" = "Production Locations"; +/// S%1$@:E%2$@ +"seasonAndEpisode" = "S%1$@:E%2$@"; -// Locked Fields - Section Title -// Section title for locked metadata fields -"lockedFields" = "Locked Fields"; +/// Secondary audio is not supported +"secondaryAudioNotSupported" = "Secondary audio is not supported"; -// Lock All Fields - Toggle -// Toggle label to lock all metadata fields at once -"lockAllFields" = "Lock All Fields"; +/// Security +"security" = "Security"; -// Metadata preferences - Section title -// Title for the section displaying metadata localization options -"metadataPreferences" = "Metadata preferences"; +/// See All +"seeAll" = "See All"; -// Language - Picker title -// Label for the language picker for metadata -"language" = "Language"; +/// See More +"seeMore" = "See More"; -// Country - Picker title -// Label for the country picker for metadata -"country" = "Country"; +/// Select All +"selectAll" = "Select All"; -// Format - Section title -// Title for the video format section in metadata editing -"format" = "Format"; +/// Select Image +"selectImage" = "Select Image"; -// Original aspect ratio - Text field label -// Label for entering the original aspect ratio of the video -"originalAspectRatio" = "Original aspect ratio"; +/// Select server +"selectServer" = "Select server"; -// 3D Format - Picker title -// Title for the 3D format picker in video metadata -"format3D" = "3D Format"; +/// Series +"series" = "Series"; -// Parental Rating - Section title -// Title for the parental rating section in metadata editing -"parentalRating" = "Parental Rating"; +/// Series Backdrop +"seriesBackdrop" = "Series Backdrop"; -// Official Rating - Picker title -// Title for the picker to select an official rating -"officialRating" = "Official Rating"; +/// Server +"server" = "Server"; -// Custom Rating - Picker title -// Title for the picker to select a custom rating -"customRating" = "Custom Rating"; +/// %@ is already connected. +"serverAlreadyConnected" = "%@ is already connected."; -// None - Option label -// Label to represent a lack of selection or no value chosen -"none" = "None"; +/// Server %s already exists. Add new URL? +"serverAlreadyExistsPrompt" = "Server %s already exists. Add new URL?"; -// Letter - Label -// Label for the letter category in metadata options -"letter" = "Letter"; +/// Server Logs +"serverLogs" = "Server Logs"; -// Years - Label -// Label for the years category in metadata options -"years" = "Years"; +/// Servers +"servers" = "Servers"; -// Taglines - Section -// Title for the section displaying or editing taglines -"taglines" = "Taglines"; +/// Server URL +"serverURL" = "Server URL"; -// Tagline - Label -// Label for individual tagline entries -"tagline" = "Tagline"; +/// Session +"session" = "Session"; -// Reviews - Section -// Title for the reviews section in metadata editing. -"reviews" = "Reviews"; +/// Sessions +"sessions" = "Sessions"; -// Critics - Label -// Label for critic rating field in metadata editing. -"critics" = "Critics"; +/// Set +"set" = "Set"; -// Community - Label -// Label for community rating field in metadata editing. -"community" = "Community"; +/// Set Pin +"setPin" = "Set Pin"; -// Rating - Placeholder -// Placeholder text for the rating input field. -"rating" = "Rating"; +/// Set pin for new user. +"setPinForNewUser" = "Set pin for new user."; -// Rating - Description -// Description for any rating field (1 to 10 scale). -"ratingDescription" = "%@ rating on a scale from 1 to 10."; +/// Set a hint when prompting for the pin. +"setPinHintDescription" = "Set a hint when prompting for the pin."; -// Half Side-by-Side - Format -// Display title for half side-by-side 3D format. -"halfSideBySide" = "Half Side-by-Side"; +/// Settings +"settings" = "Settings"; -// Full Side-by-Side - Format -// Display title for full side-by-side 3D format. -"fullSideBySide" = "Full Side-by-Side"; +/// Show Favorited +"showFavorited" = "Show Favorited"; -// Full Top and Bottom - Format -// Display title for full top and bottom 3D format. -"fullTopAndBottom" = "Full Top and Bottom"; +/// Show Favorites +"showFavorites" = "Show Favorites"; -// Half Top and Bottom - Format -// Display title for half top and bottom 3D format. -"halfTopAndBottom" = "Half Top and Bottom"; +/// Show Missing Episodes +"showMissingEpisodes" = "Show Missing Episodes"; -// MVC - Format -// Display title for MVC 3D format. -"mvc" = "MVC"; +/// Show Missing Seasons +"showMissingSeasons" = "Show Missing Seasons"; -// Continuing - Series Status -// Display title for series status when a series is ongoing. -"continuing" = "Continuing"; +/// Show Poster Labels +"showPosterLabels" = "Show Poster Labels"; -// Ended - Series Status -// Display title for series status when a series has ended. -"ended" = "Ended"; +/// Show Progress +"showProgress" = "Show Progress"; -// Unreleased - Series Status -// Display title for series status when a series has not yet been released. -"unreleased" = "Unreleased"; +/// Show Recently Added +"showRecentlyAdded" = "Show Recently Added"; -// Aired - Series Display Order -// Display title for series display order "Aired". -"aired" = "Aired"; +/// Show Unwatched +"showUnwatched" = "Show Unwatched"; -// Original Air Date - Series Display Order -// Display title for series display order "Original Air Date". -"originalAirDate" = "Original Air Date"; +/// Show Watched +"showWatched" = "Show Watched"; -// Absolute - Series Display Order -// Display title for series display order "Absolute". -"absolute" = "Absolute"; +/// Shutdown Server +"shutdownServer" = "Shutdown Server"; -// DVD - Series Display Order -// Display title for series display order "DVD". -"dvd" = "DVD"; +/// Are you sure you want to shutdown the server? +"shutdownWarning" = "Are you sure you want to shutdown the server?"; -// Digital - Series Display Order -// Display title for series display order "Digital". -"digital" = "Digital"; +/// Sign In +"signIn" = "Sign In"; -// Story Arc - Series Display Order -// Display title for series display order "Story Arc". -"storyArc" = "Story Arc"; +/// Sign In to %s +"signInToServer" = "Sign In to %s"; -// Production - Series Display Order -// Display title for series display order "Production". -"production" = "Production"; +/// Slider +"slider" = "Slider"; -// TV - Series Display Order -// Display title for series display order "TV". -"tv" = "TV"; +/// Slider Color +"sliderColor" = "Slider Color"; -// Alternate - Series Display Order -// Display title for series display order "Alternate". -"alternate" = "Alternate"; +/// Slider Type +"sliderType" = "Slider Type"; -// Regional - Series Display Order -// Display title for series display order "Regional". -"regional" = "Regional"; +/// Smaller +"smaller" = "Smaller"; -// Alternate DVD - Series Display Order -// Display title for series display order "Alternate DVD". -"alternateDVD" = "Alternate DVD"; +/// Smallest +"smallest" = "Smallest"; -// Date Modified - Box Set Display Order -// Display title for box set display order "Date Modified". -"dateModified" = "Date Modified"; +/// Sort +"sort" = "Sort"; -// Sort Name - Box Set Display Order -// Display title for box set display order "Sort Name". +/// Sort Name "sortName" = "Sort Name"; -// Premiere Date - Box Set Display Order -// Display title for box set display order "Premiere Date". -"premiereDate" = "Premiere Date"; +/// Sort Title +"sortTitle" = "Sort Title"; -// File Path - Section Title -// Section title for displaying the file path of the item. -"filePath" = "File Path"; +/// Source Code +"sourceCode" = "Source Code"; -// Title - Section Title -// Section title for displaying or editing the main title of the item. -"title" = "Title"; +/// Special Features +"specialFeatures" = "Special Features"; -// Original Title - Section Title -// Section title for displaying or editing the original title of the item. -"originalTitle" = "Original Title"; +/// Sports +"sports" = "Sports"; -// Sort Title - Section Title -// Section title for displaying or editing the title used for sorting the item. -"sortTitle" = "Sort Title"; +/// Start Time +"startTime" = "Start Time"; -// Unknown - Placeholder -// Placeholder text for unknown file paths or values. -"unknown" = "Unknown"; +/// Status +"status" = "Status"; -// Air Time - Label for air time date picker -// Label for selecting the air time of an episode. -"airTime" = "Air Time"; +/// Stop +"stop" = "Stop"; -// Episode Runtime Description - Description for runtime input -// Description displayed below runtime input for episodes. -"episodeRuntimeDescription" = "Episode runtime in minutes."; +/// Story Arc +"storyArc" = "Story Arc"; -// Episode - Label for episode input -// Title for the episode input field. -"episode" = "Episode"; +/// Streams +"streams" = "Streams"; -// Enter Season Number - Description for season input field -// Description explaining the purpose of the season input. -"enterSeasonNumber" = "Enter the season number."; +/// Studios +"studios" = "Studios"; -// Enter Episode Number - Description for episode input field -// Description explaining the purpose of the episode input. -"enterEpisodeNumber" = "Enter the episode number."; +/// Studio(s) involved in the creation of media. +"studiosDescription" = "Studio(s) involved in the creation of media."; -// Birthday - Label for birthday input -// Title for the birthday input field. -"birthday" = "Birthday"; +/// Subtitle +"subtitle" = "Subtitle"; -// Birth Year - Label for birth year input -// Title for the birth year input field. -"birthYear" = "Birth year"; +/// The subtitle codec is not supported +"subtitleCodecNotSupported" = "The subtitle codec is not supported"; -// Date of Death - Label for date of death input -// Title for the date of death input field. -"dateOfDeath" = "Date of death"; +/// Subtitle Color +"subtitleColor" = "Subtitle Color"; -// Maximum Failed Login Policy - Description -// Explanation of the maximum failed login attempts policy -"maximumFailedLoginPolicyDescription" = "Sets the maximum failed login attempts before a user is locked out."; +/// Subtitle Font +"subtitleFont" = "Subtitle Font"; -// Maximum Failed Login Policy Re-enable - Description -// Explanation of the resetting locked users -"maximumFailedLoginPolicyReenable" = "Locked users must be re-enabled by an Administrator."; +/// Subtitle Offset +"subtitleOffset" = "Subtitle Offset"; -// Locked Users - Title -// Section title for description on Locked Users -"lockedUsers" = "Locked users"; +/// Subtitles +"subtitles" = "Subtitles"; -// Unlimited - Description -// Explanation of the unlimited login attempts policy -"unlimitedFailedLoginDescription" = "Allows unlimited failed login attempts without locking the user."; +/// Settings only affect some subtitle types +"subtitlesDisclaimer" = "Settings only affect some subtitle types"; -// Default - Description -// Explanation of the default login attempts policy -"defaultFailedLoginDescription" = "Admins are locked out after 5 failed attempts. Non-admins are locked out after 3 attempts."; +/// Subtitle Size +"subtitleSize" = "Subtitle Size"; -// Custom - Description -// Explanation of the custom login attempts policy -"customFailedLoginDescription" = "Manually set the number of failed login attempts allowed before locking the user."; +/// Success +"success" = "Success"; -// Maximum Connections Policy - Description -// Explanation of the maximum connections policy -"maximumConnectionsDescription" = "Limits the total number of connections a user can have to the server."; +/// Content Uploading +"supportsContentUploading" = "Content Uploading"; -// Unlimited Connections - Description -// Explanation of unlimited connections policy -"unlimitedConnectionsDescription" = "The user can connect to the server without any limits."; +/// Media Control +"supportsMediaControl" = "Media Control"; -// Custom Connections - Description -// Explanation of custom connections policy -"customConnectionsDescription" = "Manually set the maximum number of connections a user can have to the server."; +/// Persistent Identifier +"supportsPersistentIdentifier" = "Persistent Identifier"; -// Required - Validation -// Indicates a field is required -"required" = "Required"; +/// Sync +"supportsSync" = "Sync"; -// Reorder - Menu Option -// Menu option to allow for reorder items in a set or array -"reorder" = "Reorder"; +/// Switch User +"switchUser" = "Switch User"; -// Exists on Server - Validation -// Indicates a specific item exists on your Jellyfin Server -"existsOnServer" = "This item exists on your Jellyfin Server."; +/// SyncPlay +"syncPlay" = "SyncPlay"; -// Will Be Created on Server - Notification -// Indicates a specific item will be created as new on your Jellyfin Server -"willBeCreatedOnServer" = "This will be created as a new item on your Jellyfin Server."; +/// System +"system" = "System"; -// Genres - Description -// A brief explanation of genres in the context of media items -"genresDescription" = "Categories that describe the themes or styles of media."; +/// Tag +"tag" = "Tag"; -// Tags - Description -// A brief explanation of tags in the context of media items -"tagsDescription" = "Labels used to organize or highlight specific attributes of media."; +/// Tagline +"tagline" = "Tagline"; -// Studios - Description -// A brief explanation of studios in the context of media items -"studiosDescription" = "Studio(s) involved in the creation of media."; +/// Taglines +"taglines" = "Taglines"; -// People - Description -// A brief explanation of tags in the people of media items -"peopleDescription" = "People who helped create or perform specific media."; +/// Tags +"tags" = "Tags"; -// Delete Item - Confirmation -// Asks the user to confirm the deletion of a single item -"deleteItemConfirmation" = "Are you sure you want to delete this item?"; +/// Labels used to organize or highlight specific attributes of media. +"tagsDescription" = "Labels used to organize or highlight specific attributes of media."; -// Delete Selected Items - Confirmation -// Asks the user to confirm the deletion of selected item -"deleteSelectedConfirmation" = "Are you sure you want to delete the selected items?"; +/// Task +"task" = "Task"; -// Existing items - Section Title -// Section for Items that already exist on the Jellyfin Server -"existingItems" = "Existing items"; +/// Aborted +"taskAborted" = "Aborted"; -// Enable All Libraries - Toggle -// Toggle to enable a setting for all Libraries -"enableAllLibraries" = "Enable all libraries"; +/// Cancelled +"taskCancelled" = "Cancelled"; -// Enable All Devices - Toggle -// Toggle to enable a setting for all devices -"enableAllDevices" = "Enable all devices"; +/// Completed +"taskCompleted" = "Completed"; -// Media Access - Section Title -// Section Title for Server User Media Access Editing -"mediaAccess" = "Media Access"; +/// Failed +"taskFailed" = "Failed"; -// Device Access - Section Title -// Section Title for Server User Device Access Editing -"deviceAccess" = "Device Access"; +/// Tasks +"tasks" = "Tasks"; -// (Live) TV Access - Section Title -// Section Title for Server User Live TV Access Editing -"tvAccess" = "TV Access"; +/// Tasks are operations that are scheduled to run periodically or can be triggered manually. +"tasksDescription" = "Tasks are operations that are scheduled to run periodically or can be triggered manually."; -// Deletion - Section Description -// Section Title for Media Deletion -"deletion" = "Deletion"; +/// Sets the duration (in minutes) in between task triggers. +"taskTriggerInterval" = "Sets the duration (in minutes) in between task triggers."; -// Access - Section Description -// Section Title for Media Access -"access" = "Access"; +/// Sets the maximum runtime (in hours) for this task trigger. +"taskTriggerTimeLimit" = "Sets the maximum runtime (in hours) for this task trigger."; -// Actor - Enum -// Represents an actor -"actor" = "Actor"; +/// Tbps +"terabitsPerSecond" = "Tbps"; -// Composer - Enum -// Represents a composer -"composer" = "Composer"; +/// Test Size +"testSize" = "Test Size"; -// Director - Enum -// Represents a director -"director" = "Director"; +/// Thumb +"thumb" = "Thumb"; -// Writer - Enum -// Represents a writer -"writer" = "Writer"; +/// Time +"time" = "Time"; -// Guest Star - Enum -// Represents a guest star -"guestStar" = "Guest Star"; +/// Time Limit +"timeLimit" = "Time Limit"; -// Producer - Enum -// Represents a producer -"producer" = "Producer"; +/// Time limit: %1$@ +"timeLimitLabelWithValue" = "Time limit: %1$@"; -// Conductor - Enum -// Represents a conductor -"conductor" = "Conductor"; +/// Timestamp +"timestamp" = "Timestamp"; -// Lyricist - Enum -// Represents a lyricist -"lyricist" = "Lyricist"; +/// Timestamp Type +"timestampType" = "Timestamp Type"; -// Arranger - Enum -// Represents an arranger -"arranger" = "Arranger"; +/// Title +"title" = "Title"; -// Engineer - Enum -// Represents an engineer -"engineer" = "Engineer"; +/// Trailers +"trailers" = "Trailers"; -// Mixer - Enum -// Represents a mixer -"mixer" = "Mixer"; +/// Trailing Value +"trailingValue" = "Trailing Value"; -// Remixer - Enum -// Represents a remixer -"remixer" = "Remixer"; +/// Transcode +"transcode" = "Transcode"; -// Creator - Enum -// Represents a creator -"creator" = "Creator"; +/// Transcode Reason(s) +"transcodeReasons" = "Transcode Reason(s)"; -// Artist - Enum -// Represents an artist -"artist" = "Artist"; +/// Transition +"transition" = "Transition"; -// Album Artist - Enum -// Represents an album artist -"albumArtist" = "Album Artist"; +/// Translator +"translator" = "Translator"; -// Author - Enum -// Represents an author -"author" = "Author"; +/// Trigger already exists +"triggerAlreadyExists" = "Trigger already exists"; -// Illustrator - Enum -// Represents an illustrator -"illustrator" = "Illustrator"; +/// Triggers +"triggers" = "Triggers"; -// Penciller - Enum -// Represents a penciller -"penciller" = "Penciller"; +/// TV +"tv" = "TV"; -// Inker - Enum -// Represents an inker -"inker" = "Inker"; +/// TV Access +"tvAccess" = "TV Access"; -// Colorist - Enum -// Represents a colorist -"colorist" = "Colorist"; +/// TV Shows +"tvShows" = "TV Shows"; -// Letterer - Enum -// Represents a letterer -"letterer" = "Letterer"; +/// Type +"type" = "Type"; -// Cover Artist - Enum -// Represents a cover artist -"coverArtist" = "Cover Artist"; +/// Unable to find host +"unableToFindHost" = "Unable to find host"; -// Editor - Enum -// Represents an editor -"editor" = "Editor"; +/// Unable to perform device authentication +"unableToPerformDeviceAuth" = "Unable to perform device authentication"; -// Translator - Enum -// Represents a translator -"translator" = "Translator"; +/// Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin. +"unableToPerformDeviceAuthFaceID" = "Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin."; -// Parental controls - Section Title -// Parental controls section & view titles -"parentalControls" = "Parental controls"; +/// Unaired +"unaired" = "Unaired"; -// Block unrated items - Section Title -// Parental ratings block unrated items section -"blockUnratedItems" = "Block unrated items"; +/// Unauthorized +"unauthorized" = "Unauthorized"; -// Block unrated items - Footer -// Parental ratings block unrated items description -"blockUnratedItemsDescription" = "Block items from this user with no or unrecognized rating information."; +/// Unauthorized user +"unauthorizedUser" = "Unauthorized user"; -// Maximum parental rating - Section Title -// Parental ratings maximum parental rating section -"maxParentalRating" = "Maximum parental rating"; +/// Unknown +"unknown" = "Unknown"; -// Maximum parental rating - Footer -// Parental ratings maximum parental rating description -"maxParentalRatingDescription" = "Content with a higher rating will be hidden from this user."; +/// The audio stream information is unknown +"unknownAudioStreamInfo" = "The audio stream information is unknown"; -// Allowed tags - View Title -// Parental ratings section for allowed tags -"allowedTags" = "Allowed tags"; +/// Unknown Error +"unknownError" = "Unknown Error"; -// Allowed tags - Footer -// Parental ratings description for allowed tags -"allowedTagsDescription" = "Only show media to this user with at least one of the specified tags."; +/// The video stream information is unknown +"unknownVideoStreamInfo" = "The video stream information is unknown"; -// Blocked tags - View Title -// Parental ratings section for blocked tags -"blockedTags" = "Blocked tags"; +/// Unlimited +"unlimited" = "Unlimited"; -// Blocked tags - Footer -// Parental ratings description for blocked tags -"blockedTagsDescription" = "Hide media with at least one of the specified tags."; +/// The user can connect to the server without any limits. +"unlimitedConnectionsDescription" = "The user can connect to the server without any limits."; -// Access Schedule - View Title -// Parental ratings section for blocked titles -"accessSchedule" = "Access schedule"; +/// Allows unlimited failed login attempts without locking the user. +"unlimitedFailedLoginDescription" = "Allows unlimited failed login attempts without locking the user."; -// Access Schedule - Footer -// Parental ratings section for allowed titles -"accessScheduleDescription" = "Create an access schedule to limit access to certain hours."; +/// Unplayed +"unplayed" = "Unplayed"; -// Trailers - Section Title -// Title for content classified as trailers -"trailers" = "Trailers"; +/// Unreleased +"unreleased" = "Unreleased"; -// Music - Section Title -// Title for content classified as music -"music" = "Music"; +/// You have unsaved changes. Are you sure you want to discard them? +"unsavedChangesMessage" = "You have unsaved changes. Are you sure you want to discard them?"; -// Books - Section Title -// Title for content classified as books -"books" = "Books"; +/// Upload file +"uploadFile" = "Upload file"; -// Live TV Channels - Section Title -// Title for content classified as live TV channels -"liveTVChannels" = "Live TV Channels"; +/// Upload photo +"uploadPhoto" = "Upload photo"; -// Live TV Programs - Section Title -// Title for content classified as live TV programs -"liveTVPrograms" = "Live TV Programs"; +/// URL +"url" = "URL"; -// Loading User Failed - Error Message -// Displayed when loading user data fails -"loadingUserFailed" = "Loading user failed"; +/// Use as Transcoding Profile +"useAsTranscodingProfile" = "Use as Transcoding Profile"; -// Pin - Personal Identification Number -// Abbreviation to describe the login code for users -"pin" = "Pin"; +/// Use Primary Image +"usePrimaryImage" = "Use Primary Image"; -// Are You Sure Delete Single User - Alert Message -// Message asking for confirmation when deleting a single user -"deleteUserSingleConfirmation" = "Are you sure you want to delete %@?"; +/// Uses the primary image and hides the logo. +"usePrimaryImageDescription" = "Uses the primary image and hides the logo."; -// Are You Sure Delete Multiple Users - Alert Message -// Message asking for confirmation when deleting multiple users -"deleteUserMultipleConfirmation" = "Are you sure you want to delete %d users?"; +/// User +"user" = "User"; -// Enter Pin - Alert Message -// Message asking for a PIN to sign in for a specific user -"enterPinForUser" = "Enter PIN for %@"; +/// This user will require device authentication. +"userDeviceAuthRequiredDescription" = "This user will require device authentication."; -// Layout - Label -// Label for selecting a display layout in the advanced menu -"layout" = "Layout"; +/// Username +"username" = "Username"; -// User Requires Device Authentication - Error Message -// Message indicating that a specific user requires device authentication -"userRequiresDeviceAuthentication" = "User %@ requires device authentication"; +/// A username is required +"usernameRequired" = "A username is required"; -// Unable to Perform Device Authentication - Error Message -// Informs the user that device authentication is not possible and suggests enabling Face ID in the Settings app for Swiftfin -"unableToPerformDeviceAuthFaceID" = "Unable to perform device authentication. You may need to enable Face ID in the Settings app for Swiftfin."; +/// This user will require a pin. +"userPinRequiredDescription" = "This user will require a pin."; -// Device Authentication Failed - Error Message -// Indicates that device authentication has failed -"deviceAuthFailed" = "Device authentication failed"; +/// User %@ requires device authentication +"userRequiresDeviceAuthentication" = "User %@ requires device authentication"; -// Unable to Perform Device Authentication - Error Message -// Indicates that device authentication cannot be performed -"unableToPerformDeviceAuth" = "Unable to perform device authentication"; +/// Users +"users" = "Users"; -// Rotate - Button -// Label for an action that rotates an element -"rotate" = "Rotate"; +/// Version +"version" = "Version"; -// Quick Connect Code - Instruction -// Prompts the user to enter a 6-digit Quick Connect code from another device -"quickConnectCodeInstruction" = "Enter the 6 digit code from your other device."; +/// Video +"video" = "Video"; -// Security - Section Title -// Title for sections or settings related to security features -"security" = "Security"; +/// The video bit depth is not supported +"videoBitDepthNotSupported" = "The video bit depth is not supported"; -// Additional Security Access - Description -// Explains additional security options for users signed in to the current device, without affecting Jellyfin server settings -"additionalSecurityAccessDescription" = "Additional security access for users signed in to this device. This does not change any Jellyfin server user settings."; +/// The video bitrate is not supported +"videoBitrateNotSupported" = "The video bitrate is not supported"; -// Hint - Label -// Label for a field or section providing additional guidance or information -"hint" = "Hint"; +/// The video codec is not supported +"videoCodecNotSupported" = "The video codec is not supported"; -// Set - Button -// Button label for confirming or applying a setting -"set" = "Set"; +/// The video framerate is not supported +"videoFramerateNotSupported" = "The video framerate is not supported"; -// Create PIN - Instruction -// Prompts the user to create a PIN to sign in to a specific user account on the device -"createPinForUser" = "Create a pin to sign in to %@ on this device"; +/// The video level is not supported +"videoLevelNotSupported" = "The video level is not supported"; -// Set PIN - Button -// Button label for setting a PIN -"setPin" = "Set Pin"; +/// Video Player +"videoPlayer" = "Video Player"; -// Enter PIN - Instruction -// Prompts the user to enter their PIN -"enterPin" = "Enter Pin"; +/// Video Player Type +"videoPlayerType" = "Video Player Type"; -// Change PIN - Button -// Button label for changing an existing PIN -"changePin" = "Change Pin"; +/// The video profile is not supported +"videoProfileNotSupported" = "The video profile is not supported"; -// PIN Hint - Description -// Explains the option to set a hint when prompting for the PIN -"setPinHintDescription" = "Set a hint when prompting for the pin."; +/// The video range type is not supported +"videoRangeTypeNotSupported" = "The video range type is not supported"; -// Save User Without Local Authentication - Description -// Explains the option to save a user without requiring local authentication -"saveUserWithoutAuthDescription" = "Save the user to this device without any local authentication."; +/// Video remuxing +"videoRemuxing" = "Video remuxing"; -// Require PIN - Description -// Explains the option to require a local PIN when signing in -"requirePinDescription" = "Require a local pin when signing in to the user. This pin is unrecoverable."; +/// The video resolution is not supported +"videoResolutionNotSupported" = "The video resolution is not supported"; -// Require Device Authentication - Description -// Explains the option to require device authentication when signing in -"requireDeviceAuthDescription" = "Require device authentication when signing in to the user."; +/// Video transcoding +"videoTranscoding" = "Video transcoding"; -// Set PIN for New User - Instruction -// Prompts the user to set a PIN for a new user account -"setPinForNewUser" = "Set pin for new user."; +/// Some views may need an app restart to update. +"viewsMayRequireRestart" = "Some views may need an app restart to update."; -// Duplicate User Saved - Error Message -// Indicates that the specified user is already saved on the device -"duplicateUserSaved" = "%@ is already saved"; +/// Votes +"votes" = "Votes"; -// Duplicate User - Error Title -// Title for an error indicating a duplicate user -"duplicateUser" = "Duplicate User"; +/// Weekday +"weekday" = "Weekday"; -// Disclaimer - Section Title -// Title for a section providing important information or warnings -"disclaimer" = "Disclaimer"; +/// Weekend +"weekend" = "Weekend"; -// PIN Required - Description -// Indicates that the user will require a PIN for authentication -"userPinRequiredDescription" = "This user will require a pin."; +/// Weekly +"weekly" = "Weekly"; -// Device Authentication Required - Description -// Indicates that the user will require device authentication -"userDeviceAuthRequiredDescription" = "This user will require device authentication."; +/// This will be created as a new item on your Jellyfin Server. +"willBeCreatedOnServer" = "This will be created as a new item on your Jellyfin Server."; -// Require Device Authentication for User - Description -// Explains that device authentication is required to sign in to a specific user on this device -"requireDeviceAuthForUser" = "Require device authentication to sign in to %@ on this device."; +/// Writer +"writer" = "Writer"; -// Require Device Authentication for Quick Connect User - Description -// Explains that device authentication is required to sign in to the Quick Connect user on this device -"requireDeviceAuthForQuickConnectUser" = "Require device authentication to sign in to the Quick Connect user on this device."; +/// Year +"year" = "Year"; -// Server Already Connected - Error Message -// Indicates that the specified server is already connected -"serverAlreadyConnected" = "%@ is already connected."; +/// Years +"years" = "Years"; -// All Audiences - Group Name -// Label for content suitable for all audiences -"allAudiences" = "All Audiences"; +/// Yellow +"yellow" = "Yellow"; -// Ages Group - Group Name -// Label for content suitable for a specific age group -"agesGroup" = "Age %@"; +/// Yes +"yes" = "Yes"; \ No newline at end of file diff --git a/fastlane/Fastfile.swift b/fastlane/Fastfile.swift index 13e04d022..ab9159bd9 100644 --- a/fastlane/Fastfile.swift +++ b/fastlane/Fastfile.swift @@ -10,71 +10,18 @@ import Foundation class Fastfile: LaneFile { - // MARK: tag - - func tagLane(withOptions options: [String: String]?) { - - guard let options, - let tag = options["tag"] else { - puts(message: "ERROR: missing options") - exit(1) - } - - guard !gitTagExists(tag: tag) else { - puts(message: "ERROR: tag \(tag) already exists") - exit(1) - } - - addGitTag( - tag: .userDefined(tag), - commit: .userDefined(options["commit"]) - ) - - pushGitTags( - force: true - ) - } - - // MARK: draft release - - func draftReleaseLane(withOptions options: [String: String]?) { - - guard let options, - let repository = options["repository"], - let apiToken = options["apiToken"], - let tag = options["tag"], - let name64 = options["name64"] else { - puts(message: "ERROR: missing options") - exit(1) - } - - guard let name = decodeBase64(encoded: name64) else { - puts(message: "ERROR: name not valid base 64") - exit(1) - } - - setGithubRelease( - repositoryName: repository, - apiToken: .userDefined(apiToken), - tagName: tag, - name: .userDefined(name), - isDraft: true, - isGenerateReleaseNotes: true - ) - } - // MARK: TestFlight // TODO: verify tvOS func testFlightLane(withOptions options: [String: String]?) { guard let options, - let keyID = options["keyID"], - let issuerID = options["issuerID"], - let keyContents = options["keyContents"], - let scheme = options["scheme"], - let codeSign64 = options["codeSign64"], - let profileName64 = options["profileName64"] + let keyID = options["keyID"]?.trimOption(), + let issuerID = options["issuerID"]?.trimOption(), + let keyContents = options["keyContents"]?.trimOption(), + let scheme = options["scheme"]?.trimOption(), + let codeSign64 = options["codeSign64"]?.trimOption(), + let profileName64 = options["profileName64"]?.trimOption() else { puts(message: "ERROR: missing or incorrect options") exit(1) @@ -150,3 +97,12 @@ class Fastfile: LaneFile { return decoded } } + +extension String { + + /// Trim the option value from whitespaces and newlines, which may + /// accidentally be present in GitHub secrets. + func trimOption() -> String { + trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/fastlane/FastlaneRunner b/fastlane/FastlaneRunner index 917c12370..935461e24 100755 Binary files a/fastlane/FastlaneRunner and b/fastlane/FastlaneRunner differ diff --git a/fastlane/swift/DeliverfileProtocol.swift b/fastlane/swift/DeliverfileProtocol.swift index 653cabe07..a9f12c0d9 100644 --- a/fastlane/swift/DeliverfileProtocol.swift +++ b/fastlane/swift/DeliverfileProtocol.swift @@ -272,4 +272,4 @@ public extension DeliverfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.131] +// FastlaneRunnerAPIVersion [0.9.132] diff --git a/fastlane/swift/Fastlane.swift b/fastlane/swift/Fastlane.swift index be282cbef..9daed81ad 100644 --- a/fastlane/swift/Fastlane.swift +++ b/fastlane/swift/Fastlane.swift @@ -11602,9 +11602,13 @@ public func teamName() { - autoUpdate: Allows an easy upgrade of all users to the current version. To enable set to 'on' - notify: Send email to testers - options: Array of options (shake,video_only_wifi,anonymous) - - custom: Array of custom options. Contact support@testfairy.com for more information + - custom: Array of custom options. Contact support for more information - timeout: Request timeout in seconds - tags: Custom tags that can be used to organize your builds + - folderName: Name of the dashboard folder that contains this app + - landingPageMode: Visibility of build landing after upload. Can be 'open' or 'closed' + - uploadToSaucelabs: Upload file directly to Sauce Labs. It can be 'on' or 'off' + - platform: Use if upload build is not iOS or Android. Contact support for more information You can retrieve your API key on [your settings page](https://free.testfairy.com/settings/) */ @@ -11621,7 +11625,11 @@ public func testfairy(apiKey: String, options: [String] = [], custom: String = "", timeout: OptionalConfigValue = .fastlaneDefault(nil), - tags: [String] = []) + tags: [String] = [], + folderName: String = "", + landingPageMode: String = "open", + uploadToSaucelabs: String = "off", + platform: String = "") { let apiKeyArg = RubyCommand.Argument(name: "api_key", value: apiKey, type: nil) let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) @@ -11637,6 +11645,10 @@ public func testfairy(apiKey: String, let customArg = RubyCommand.Argument(name: "custom", value: custom, type: nil) let timeoutArg = timeout.asRubyArgument(name: "timeout", type: nil) let tagsArg = RubyCommand.Argument(name: "tags", value: tags, type: nil) + let folderNameArg = RubyCommand.Argument(name: "folder_name", value: folderName, type: nil) + let landingPageModeArg = RubyCommand.Argument(name: "landing_page_mode", value: landingPageMode, type: nil) + let uploadToSaucelabsArg = RubyCommand.Argument(name: "upload_to_saucelabs", value: uploadToSaucelabs, type: nil) + let platformArg = RubyCommand.Argument(name: "platform", value: platform, type: nil) let array: [RubyCommand.Argument?] = [apiKeyArg, ipaArg, apkArg, @@ -11650,7 +11662,11 @@ public func testfairy(apiKey: String, optionsArg, customArg, timeoutArg, - tagsArg] + tagsArg, + folderNameArg, + landingPageModeArg, + uploadToSaucelabsArg, + platformArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -13861,4 +13877,4 @@ public let snapshotfile: Snapshotfile = .init() // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.184] +// FastlaneRunnerAPIVersion [0.9.185] diff --git a/fastlane/swift/GymfileProtocol.swift b/fastlane/swift/GymfileProtocol.swift index f6c7eca5e..bac95f664 100644 --- a/fastlane/swift/GymfileProtocol.swift +++ b/fastlane/swift/GymfileProtocol.swift @@ -212,4 +212,4 @@ public extension GymfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.134] +// FastlaneRunnerAPIVersion [0.9.135] diff --git a/fastlane/swift/MatchfileProtocol.swift b/fastlane/swift/MatchfileProtocol.swift index 292f8771b..92156bd85 100644 --- a/fastlane/swift/MatchfileProtocol.swift +++ b/fastlane/swift/MatchfileProtocol.swift @@ -232,4 +232,4 @@ public extension MatchfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.128] +// FastlaneRunnerAPIVersion [0.9.129] diff --git a/fastlane/swift/PrecheckfileProtocol.swift b/fastlane/swift/PrecheckfileProtocol.swift index fcac61a56..b0a0f2ac1 100644 --- a/fastlane/swift/PrecheckfileProtocol.swift +++ b/fastlane/swift/PrecheckfileProtocol.swift @@ -52,4 +52,4 @@ public extension PrecheckfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.127] +// FastlaneRunnerAPIVersion [0.9.128] diff --git a/fastlane/swift/ScanfileProtocol.swift b/fastlane/swift/ScanfileProtocol.swift index 16592f711..5b27356ed 100644 --- a/fastlane/swift/ScanfileProtocol.swift +++ b/fastlane/swift/ScanfileProtocol.swift @@ -324,4 +324,4 @@ public extension ScanfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.139] +// FastlaneRunnerAPIVersion [0.9.140] diff --git a/fastlane/swift/ScreengrabfileProtocol.swift b/fastlane/swift/ScreengrabfileProtocol.swift index e3c386a60..c57050eed 100644 --- a/fastlane/swift/ScreengrabfileProtocol.swift +++ b/fastlane/swift/ScreengrabfileProtocol.swift @@ -96,4 +96,4 @@ public extension ScreengrabfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.129] +// FastlaneRunnerAPIVersion [0.9.130] diff --git a/fastlane/swift/SnapshotfileProtocol.swift b/fastlane/swift/SnapshotfileProtocol.swift index 6963e987a..22829eb73 100644 --- a/fastlane/swift/SnapshotfileProtocol.swift +++ b/fastlane/swift/SnapshotfileProtocol.swift @@ -208,4 +208,4 @@ public extension SnapshotfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.123] +// FastlaneRunnerAPIVersion [0.9.124]