diff --git a/Screenshots/SnapshotHelper.swift b/Screenshots/SnapshotHelper.swift new file mode 100644 index 0000000000..d518cc016f --- /dev/null +++ b/Screenshots/SnapshotHelper.swift @@ -0,0 +1,318 @@ +// +// 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 +// + +// ----------------------------------------------------- +// IMPORTANT: When modifying this file, make sure to +// increment the version number at the very +// bottom of the file to notify users about +// the new SnapshotHelper.swift +// ----------------------------------------------------- + +import Foundation +import XCTest + +@MainActor +func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations) +} + +@MainActor +func snapshot(_ name: String, waitForLoadingIndicator: Bool) { + if waitForLoadingIndicator { + Snapshot.snapshot(name) + } else { + Snapshot.snapshot(name, timeWaitingForIdle: 0) + } +} + +/// - Parameters: +/// - name: The name of the snapshot +/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait. +@MainActor +func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + Snapshot.snapshot(name, timeWaitingForIdle: timeout) +} + +enum SnapshotError: Error, CustomDebugStringConvertible { + case cannotFindSimulatorHomeDirectory + case cannotRunOnPhysicalDevice + + var debugDescription: String { + switch self { + case .cannotFindSimulatorHomeDirectory: + return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable." + case .cannotRunOnPhysicalDevice: + return "Can't use Snapshot on a physical device." + } + } +} + +@objcMembers +@MainActor +open class Snapshot: NSObject { + static var app: XCUIApplication? + static var waitForAnimations = true + static var cacheDirectory: URL? + static var screenshotsDirectory: URL? { + cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true) + } + + static var deviceLanguage = "" + static var currentLocale = "" + + open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) { + + Snapshot.app = app + Snapshot.waitForAnimations = waitForAnimations + + do { + let cacheDir = try getCacheDirectory() + Snapshot.cacheDirectory = cacheDir + setLanguage(app) + setLocale(app) + setLaunchArguments(app) + } catch { + NSLog(error.localizedDescription) + } + } + + class func setLanguage(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("language.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"] + } catch { + NSLog("Couldn't detect/set language...") + } + } + + class func setLocale(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("locale.txt") + + do { + let trimCharacterSet = CharacterSet.whitespacesAndNewlines + currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet) + } catch { + NSLog("Couldn't detect/set locale...") + } + + if currentLocale.isEmpty && !deviceLanguage.isEmpty { + currentLocale = Locale(identifier: deviceLanguage).identifier + } + + if !currentLocale.isEmpty { + app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""] + } + } + + class func setLaunchArguments(_ app: XCUIApplication) { + guard let cacheDirectory = self.cacheDirectory else { + NSLog("CacheDirectory is not set - probably running on a physical device?") + return + } + + let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt") + app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"] + + do { + let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8) + let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: []) + let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count)) + let results = matches.map { result -> String in + (launchArguments as NSString).substring(with: result.range) + } + app.launchArguments += results + } catch { + NSLog("Couldn't detect/set launch_arguments...") + } + } + + open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) { + if timeout > 0 { + waitForLoadingIndicatorToDisappear(within: timeout) + } + + NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work + + if Snapshot.waitForAnimations { + sleep(1) // Waiting for the animation to be finished (kind of) + } + + #if os(OSX) + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: []) + #else + + guard self.app != nil else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let screenshot = XCUIScreen.main.screenshot() + #if os(iOS) && !targetEnvironment(macCatalyst) + let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image + #else + let image = screenshot.image + #endif + + guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return } + + do { + // The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices + let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ") + let range = NSRange(location: 0, length: simulator.count) + simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "") + + let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png") + #if swift(<5.0) + try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic) + #else + try image.pngData()?.write(to: path, options: .atomic) + #endif + } catch { + NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png") + NSLog(error.localizedDescription) + } + #endif + } + + class func fixLandscapeOrientation(image: UIImage) -> UIImage { + #if os(watchOS) + return image + #else + if #available(iOS 10.0, *) { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { _ in + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + } + } else { + return image + } + #endif + } + + class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) { + #if os(tvOS) + return + #endif + + guard let app = self.app else { + NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + return + } + + let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element + let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation( + predicate: NSPredicate(format: "exists == false"), + object: networkLoadingIndicator + ) + _ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout) + } + + class func getCacheDirectory() throws -> URL { + let cachePath = "Library/Caches/tools.fastlane" + // on OSX config is stored in /Users//Library + // and on iOS/tvOS/WatchOS it's in simulator's home dir + #if os(OSX) + let homeDir = URL(fileURLWithPath: NSHomeDirectory()) + return homeDir.appendingPathComponent(cachePath) + #elseif arch(i386) || arch(x86_64) || arch(arm64) + guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else { + throw SnapshotError.cannotFindSimulatorHomeDirectory + } + let homeDir = URL(fileURLWithPath: simulatorHostHome) + return homeDir.appendingPathComponent(cachePath) + #else + throw SnapshotError.cannotRunOnPhysicalDevice + #endif + } +} + +private extension XCUIElementAttributes { + var isNetworkLoadingIndicator: Bool { + if hasAllowListedIdentifier { return false } + + let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20) + let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3) + + return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize + } + + var hasAllowListedIdentifier: Bool { + let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"] + + return allowListedIdentifiers.contains(identifier) + } + + func isStatusBar(_ deviceWidth: CGFloat) -> Bool { + if elementType == .statusBar { return true } + guard frame.origin == .zero else { return false } + + let oldStatusBarSize = CGSize(width: deviceWidth, height: 20) + let newStatusBarSize = CGSize(width: deviceWidth, height: 44) + + return [oldStatusBarSize, newStatusBarSize].contains(frame.size) + } +} + +private extension XCUIElementQuery { + var networkLoadingIndicators: XCUIElementQuery { + let isNetworkLoadingIndicator = NSPredicate { evaluatedObject, _ in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isNetworkLoadingIndicator + } + + return self.containing(isNetworkLoadingIndicator) + } + + @MainActor + var deviceStatusBars: XCUIElementQuery { + guard let app = Snapshot.app else { + fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().") + } + + let deviceWidth = app.windows.firstMatch.frame.width + + let isStatusBar = NSPredicate { evaluatedObject, _ in + guard let element = evaluatedObject as? XCUIElementAttributes else { return false } + + return element.isStatusBar(deviceWidth) + } + + return self.containing(isStatusBar) + } +} + +private extension CGFloat { + func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool { + numberA ... numberB ~= self + } +} + +// Please don't remove the lines below +// They are used to detect outdated configuration files +// SnapshotHelperVersion [1.30] diff --git a/Screenshots/iOS/iOS_Screenshots.swift b/Screenshots/iOS/iOS_Screenshots.swift new file mode 100644 index 0000000000..457562db1a --- /dev/null +++ b/Screenshots/iOS/iOS_Screenshots.swift @@ -0,0 +1,155 @@ +// +// 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 XCTest + +final class iOS_Screenshots: XCTestCase { + let demoServerUrl = "127.0.0.1:8096" + let demoServerName = "Jellyfin Server" + let demoUsername = "username" + let demoPassword = "password" + + let movieTitle = "Sintel" + + let showTitle = "Pioneer One" + let episodeTitle = "The Man From Mars" + + override func setUpWithError() throws { + continueAfterFailure = false + } + + override func tearDownWithError() throws {} + + // Connect to the demo server from the ConnectToServer view + func connectToDemoServer(_ app: XCUIApplication) { + app.textFields["Server URL"].tap() + app.typeText(demoServerUrl) + app.buttons["ConnectToServer"].tap() + } + + // Select/Add the demo server from the user selection view + func selectDemoServer(_ app: XCUIApplication) { + app.buttons["SelectServerMenu"].firstMatch.tap() + + if app.buttons["\(demoServerName)"].exists { + app.buttons["\(demoServerName)"].firstMatch.tap() + } else { + app.buttons["Add Server"].firstMatch.tap() + + connectToDemoServer(app) + } + } + + // Select the demo user (or log in) from the user selection view + func signInDemoUser(_ app: XCUIApplication) { + if app.staticTexts[demoUsername].exists { + app.staticTexts[demoUsername].firstMatch.tap() + } else { + app.buttons["Add User"].tap() + + app.typeText(demoUsername) + app.secureTextFields["Password"].tap() + app.typeText(demoPassword) + + app.buttons["Sign In"].tap() + } + } + + @MainActor + func testScreenshots() throws { + let app = XCUIApplication() + + setupSnapshot(app) + app.launch() + + if UIDevice.current.userInterfaceIdiom == .pad { + XCUIDevice.shared.orientation = .landscapeLeft + } + + if app.buttons["Connect"].exists { + app.buttons["ShowConnectToServer"].tap() + connectToDemoServer(app) + } + + if app.buttons["SelectServerMenu"].exists { + selectDemoServer(app) + signInDemoUser(app) + + sleep(2) + } + + app.buttons["Settings"].firstMatch.tap() + app.staticTexts["Server"].firstMatch.tap() + if !app.staticTexts["http://\(demoServerUrl)"].exists { + // Log out of this other server + + app.buttons["Settings"].tap() + app.buttons["Switch User"].tap() + + selectDemoServer(app) + signInDemoUser(app) + + sleep(2) + } else { + app.navigationBars["Server"] + .buttons["Settings"].tap() + app.buttons["NavigationBarClose"].tap() + } + + snapshot("Home") + + let mediaTab = app.buttons["Media"].firstMatch + + mediaTab.tap() + mediaTab.tap() + + snapshot("Media") + + app.buttons["Movies"].firstMatch.tap() + + snapshot("Movies") + + app.staticTexts[movieTitle].tap() + app.images["play.fill"].tap() + + sleep(8) + + // TODO: There should be a better way to reveal the overlay + app.buttons["Exit"].firstMatch.coordinate(withNormalizedOffset: .zero).tap() + + snapshot("PlaybackPortrait") + + app.buttons["Exit"].firstMatch.tap() + + app.images["play.fill"].tap() + + XCUIDevice.shared.orientation = .landscapeLeft + + sleep(8) + + app.buttons["Exit"].firstMatch.coordinate(withNormalizedOffset: .zero).tap() + + snapshot("Playback") + + XCUIDevice.shared.orientation = .portrait + + app.buttons["Exit"].firstMatch.tap() + + mediaTab.tap() + mediaTab.tap() + + app.buttons["Shows"].tap() + app.staticTexts[showTitle].tap() + + snapshot("Series") + + app.staticTexts[episodeTitle].tap() + + snapshot("Episode") + } +} diff --git a/Screenshots/process.sh b/Screenshots/process.sh new file mode 100755 index 0000000000..8229909852 --- /dev/null +++ b/Screenshots/process.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +pushd "$(dirname "$0")" > /dev/null + + +frame_image () { + screenshot="$1" + echo "processing $screenshot" + + screenshotdir=$(dirname "$screenshot") + screenshotfile=$(basename "$screenshot") + + # turn "iPad Pro 13-inch (M4)-..." into "iPad Pro 13-inch (M4)" + dashcount=$(echo "$screenshotfile"| tr -dc "-" | awk '{ print length; }') + device=$(echo "$screenshotfile" | cut -d "-" -f "1-$dashcount") + + frame="./resources/$device-frame.png" + mask="./resources/$device-mask.png" + + maskedfile="$screenshotdir/masked-$screenshotfile" + + if [ ! -f "$frame" ]; then + return + fi + + if [ ! -f "$mask" ]; then + cp "$screenshot" "$maskedfile" + else + # mask off the corners of the screenshot + magick "$screenshot" "$mask" -alpha Off -compose CopyOpacity -composite "$maskedfile" + fi + + framedfile=$(echo "$screenshot" | sed 's|./output|./framed|g') + + mkdir -p $(dirname "$framedfile") + + magick composite -gravity center -compose dst-over "$maskedfile" "$frame" "$framedfile" + + rm "$maskedfile" +} + +export -f frame_image + +find ./output -not -path '*/.*' -type f -depth 2 -exec bash -c 'frame_image "$1"' _ "{}" \; + + +popd > /dev/null diff --git a/Screenshots/resources/iPad Pro 13-inch (M4)-frame.png b/Screenshots/resources/iPad Pro 13-inch (M4)-frame.png new file mode 100644 index 0000000000..5a176719d1 Binary files /dev/null and b/Screenshots/resources/iPad Pro 13-inch (M4)-frame.png differ diff --git a/Screenshots/resources/iPhone 16 Pro-frame.png b/Screenshots/resources/iPhone 16 Pro-frame.png new file mode 100644 index 0000000000..abfa1600fc Binary files /dev/null and b/Screenshots/resources/iPhone 16 Pro-frame.png differ diff --git a/Screenshots/resources/iPhone 16 Pro-mask.png b/Screenshots/resources/iPhone 16 Pro-mask.png new file mode 100644 index 0000000000..7403447f7b Binary files /dev/null and b/Screenshots/resources/iPhone 16 Pro-mask.png differ diff --git a/Screenshots/tvOS/tvOS_Screenshots.swift b/Screenshots/tvOS/tvOS_Screenshots.swift new file mode 100644 index 0000000000..4303205344 --- /dev/null +++ b/Screenshots/tvOS/tvOS_Screenshots.swift @@ -0,0 +1,245 @@ +// +// 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 XCTest + +final class tvOS_Screenshots: XCTestCase { + let demoServerUrl = "127.0.0.1:8096" + let demoServerName = "Jellyfin Server" + let demoUsername = "username" + let demoPassword = "password" + + let movieTitle = "Sintel" + + let showTitle = "Pioneer One" + let episodeTitle = "The Man From Mars" + + let app = XCUIApplication() + let remote = XCUIRemote.shared + + override func setUpWithError() throws { + continueAfterFailure = false + } + + override func tearDownWithError() throws {} + + func connectToDemoServer() { + // Start with "Connect" Selected + press(.select) + app.typeText(demoServerUrl) + + // Navigate to "done" button + press(.down, times: 5) + press(.select) + + // Press connect + press(.down) + press(.select) + } + + func selectDemoServer() { + // Navigate to server select menu + press(.down, times: 3) + press(.right, times: 3) + press(.left) + + press(.select) + + // Make sure we're at the top of the server list + press(.up, times: 5) + + // Using the server name is necessary since the text with the URL isn't accessible for some reason + search(repeating: .down) { + app.focused.buttons[demoServerName].exists + || app.focused.buttons["Add Server"].exists + } + + let needAddServer = app.focused.buttons["Add Server"].exists + + press(.select) + + if needAddServer { + connectToDemoServer() + } + } + + func signInDemoUser() { + // Assuming we've already selected the demo server, focus on the user button + press(.up, times: 2) + + _ = app.focused.waitForExistence(timeout: 5) + + if app.focused.staticTexts["Add User"].exists { + press(.select) + + // Select username, type, and hit "next" + press(.select) + app.typeText(demoUsername) + press(.down, times: 4) + press(.select) + + // Select password, type, and hit "done" + app.typeText(demoPassword) + press(.down, times: 4) + press(.select) + } else { + press(.select) + } + } + + @MainActor + func testScreenshots() throws { + // UI tests must launch the application that they test. + setupSnapshot(app) + app.launch() + + if app.staticTexts["Connect to a Jellyfin server to get started"].exists { + // Press "Connect" + press(.select) + connectToDemoServer() + } + + if app.images["server.rack"].exists { + selectDemoServer() + signInDemoUser() + + sleep(2) + } + + // Go to settings and inspect the server + press(.right, times: 5) + press(.select) + press(.down) + press(.select) + + let onDemoServer = app.staticTexts["http://\(demoServerUrl)"].exists + + // Go back up to settings + press(.menu) + + if !onDemoServer { + // Go to "Switch User" + press(.down) + press(.select) + + selectDemoServer() + signInDemoUser() + } + + // Go home + press(.up, times: 3) + press(.left, times: 5) + // Select to focus one of the 'up next' items and get a nice backdrop + press(.select) + // Then go back and focus the tab button + press(.up) + sleep(2) + + snapshot("Home") + + press(.right) + sleep(2) + + snapshot("Shows") + + press(.right) + sleep(2) + + snapshot("Movies") + + press(.select) + + press(.left, times: 5) + search(repeating: .right) { + app.focused.staticTexts[movieTitle].exists + } + + press(.select) + + snapshot("Movie") + + // Press play + press(.select) + sleep(5) + // Show player UI + press(.select, wait: false) + snapshot("Playback") + + // Exit playback, wait, exit to Movies view + press(.menu, times: 2, wait: false) + _ = app.focused.waitForExistence(timeout: 5) + press(.menu) + + // Go to TV + press(.up) + press(.left) + press(.select) + + // Find show + search(repeating: .right) { + app.focused.staticTexts[showTitle].exists + } + + press(.select) + + snapshot("Series") + + // Go down from Play -> action buttons -> season selector -> episode thumbnail -> description + press(.down, times: 4) + // Find episode + search(repeating: .right) { + app.focused.staticTexts[episodeTitle].exists + } + + press(.select) + + snapshot("Episode") + } + + func press(_ remoteButton: XCUIRemote.Button, times: Int = 1, wait: Bool? = nil) { + for _ in 0 ..< times { + remote.press(remoteButton) + + // It often takes a bit for the cursor to reappear after moving between views + if wait ?? (remoteButton == .select || remoteButton == .menu) { + _ = app.focused.waitForExistence(timeout: 5) + } + } + } + + // Repeatedly press the button until the condition returns true, and fail if we reach the end + func search(repeating: XCUIRemote.Button, condition: () -> Bool) { + var prevDetails = "" + + while !condition() { + press(repeating) + _ = app.focused.waitForExistence(timeout: 1) + + let thisDetail = app.focused.details + if thisDetail == prevDetails { + XCTFail("Search failed") + } + prevDetails = thisDetail + } + } +} + +extension XCUIApplication { + var focused: XCUIElement { + descendants(matching: .any) + .element(matching: NSPredicate(format: "hasFocus == true")) + } +} + +extension XCUIElement { + var details: String { + // Remove instances of " 0x123...," since these addresses change moment to moment + let pattern = /\ 0x\S+/ + return debugDescription.replacing(pattern, with: "") + } +} diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 11fbe5ec49..efb193c65b 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -118,6 +118,23 @@ E1FAD1C62A0375BA007F5521 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E1FAD1C52A0375BA007F5521 /* UDPBroadcast */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 5E8A31062E72277E00964879 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5377CBE9263B596A003A4E83 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5377CBF0263B596A003A4E83; + remoteInfo = "Swiftfin iOS"; + }; + 5E91B8612E6259900061D4F0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5377CBE9263B596A003A4E83 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5358705F2669D21600D05A09; + remoteInfo = "Swiftfin tvOS"; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 62666DF927E5012C00EC0ECD /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -165,6 +182,8 @@ 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Swiftfin iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 53ABFDDB267972BF00886593 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.0.sdk/System/Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = ""; }; + 5E8B619E2E4EF4DB00BE6AB5 /* Swiftfin Screenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Swiftfin Screenshots.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5E91B8562E6259560061D4F0 /* Swiftfin tvOS Screenshots.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Swiftfin tvOS Screenshots.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TVVLCKit.xcframework; path = Carthage/Build/TVVLCKit.xcframework; sourceTree = ""; }; 62666E0027E5016900EC0ECD /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 62666E0527E5017A00EC0ECD /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; @@ -200,6 +219,45 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 5E8A30FA2E72207900964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + iOS/iOS_Screenshots.swift, + output/screenshots.html, + process.sh, + resources, + SnapshotHelper.swift, + tvOS/tvOS_Screenshots.swift, + ); + target = 5377CBF0263B596A003A4E83 /* Swiftfin iOS */; + }; + 5E8A30FB2E72207900964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + iOS/iOS_Screenshots.swift, + process.sh, + resources, + SnapshotHelper.swift, + tvOS/tvOS_Screenshots.swift, + ); + target = 5358705F2669D21600D05A09 /* Swiftfin tvOS */; + }; + 5E8A30FC2E72207900964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + iOS/iOS_Screenshots.swift, + SnapshotHelper.swift, + ); + target = 5E8B619D2E4EF4DB00BE6AB5 /* Swiftfin Screenshots */; + }; + 5E8A31022E72207D00964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + SnapshotHelper.swift, + tvOS/tvOS_Screenshots.swift, + ); + target = 5E91B8552E6259560061D4F0 /* Swiftfin tvOS Screenshots */; + }; E14561A22DFCAE51008FF700 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -253,6 +311,7 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + 5E8A30F22E72206C00964879 /* Screenshots */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (5E8A30FA2E72207900964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5E8A30FB2E72207900964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5E8A30FC2E72207900964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 5E8A31022E72207D00964879 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (resources, ); path = Screenshots; sourceTree = ""; }; E14560852DFCAE51008FF700 /* Swiftfin */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (E14561A22DFCAE51008FF700 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, E14561A32DFCAE51008FF700 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Swiftfin; sourceTree = ""; }; E14563272DFCAE6E008FF700 /* Shared */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (E14565D92DFCAE6E008FF700 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Shared; sourceTree = ""; }; E14565DD2DFCAE78008FF700 /* Scripts */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Scripts; sourceTree = ""; }; @@ -383,12 +442,27 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5E8B619B2E4EF4DB00BE6AB5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5E91B8532E6259560061D4F0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 5377CBE8263B596A003A4E83 = { isa = PBXGroup; children = ( + 5E8A30F22E72206C00964879 /* Screenshots */, E14560852DFCAE51008FF700 /* Swiftfin */, E145669F2DFCAEFD008FF700 /* Swiftfin tvOS */, E14563272DFCAE6E008FF700 /* Shared */, @@ -405,6 +479,8 @@ children = ( 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */, 535870602669D21600D05A09 /* Swiftfin tvOS.app */, + 5E8B619E2E4EF4DB00BE6AB5 /* Swiftfin Screenshots.xctest */, + 5E91B8562E6259560061D4F0 /* Swiftfin tvOS Screenshots.xctest */, ); name = Products; sourceTree = ""; @@ -487,6 +563,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( + 5E8A30F22E72206C00964879 /* Screenshots */, E14563272DFCAE6E008FF700 /* Shared */, E145669F2DFCAEFD008FF700 /* Swiftfin tvOS */, E1456FC82DFCB323008FF700 /* Translations */, @@ -543,6 +620,7 @@ dependencies = ( ); fileSystemSynchronizedGroups = ( + 5E8A30F22E72206C00964879 /* Screenshots */, E14560852DFCAE51008FF700 /* Swiftfin */, E14563272DFCAE6E008FF700 /* Shared */, E1456FC82DFCB323008FF700 /* Translations */, @@ -599,6 +677,46 @@ productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; productType = "com.apple.product-type.application"; }; + 5E8B619D2E4EF4DB00BE6AB5 /* Swiftfin Screenshots */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5E8B61A82E4EF4DB00BE6AB5 /* Build configuration list for PBXNativeTarget "Swiftfin Screenshots" */; + buildPhases = ( + 5E8B619A2E4EF4DB00BE6AB5 /* Sources */, + 5E8B619B2E4EF4DB00BE6AB5 /* Frameworks */, + 5E8B619C2E4EF4DB00BE6AB5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5E8A31072E72277E00964879 /* PBXTargetDependency */, + ); + name = "Swiftfin Screenshots"; + packageProductDependencies = ( + ); + productName = "Swiftfin Screenshots"; + productReference = 5E8B619E2E4EF4DB00BE6AB5 /* Swiftfin Screenshots.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 5E91B8552E6259560061D4F0 /* Swiftfin tvOS Screenshots */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5E91B8602E6259560061D4F0 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS Screenshots" */; + buildPhases = ( + 5E91B8522E6259560061D4F0 /* Sources */, + 5E91B8532E6259560061D4F0 /* Frameworks */, + 5E91B8542E6259560061D4F0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5E91B8622E6259900061D4F0 /* PBXTargetDependency */, + ); + name = "Swiftfin tvOS Screenshots"; + packageProductDependencies = ( + ); + productName = "Swiftfin tvOS Screenshots"; + productReference = 5E91B8562E6259560061D4F0 /* Swiftfin tvOS Screenshots.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -609,7 +727,7 @@ KnownAssetTags = ( New, ); - LastSwiftUpdateCheck = 1250; + LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1610; TargetAttributes = { 5358705F2669D21600D05A09 = { @@ -618,6 +736,14 @@ 5377CBF0263B596A003A4E83 = { CreatedOnToolsVersion = 12.5; }; + 5E8B619D2E4EF4DB00BE6AB5 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 5377CBF0263B596A003A4E83; + }; + 5E91B8552E6259560061D4F0 = { + CreatedOnToolsVersion = 16.4; + TestTargetID = 5358705F2669D21600D05A09; + }; }; }; buildConfigurationList = 5377CBEC263B596A003A4E83 /* Build configuration list for PBXProject "Swiftfin" */; @@ -708,6 +834,8 @@ targets = ( 5377CBF0263B596A003A4E83 /* Swiftfin iOS */, 5358705F2669D21600D05A09 /* Swiftfin tvOS */, + 5E8B619D2E4EF4DB00BE6AB5 /* Swiftfin Screenshots */, + 5E91B8552E6259560061D4F0 /* Swiftfin tvOS Screenshots */, ); }; /* End PBXProject section */ @@ -727,6 +855,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5E8B619C2E4EF4DB00BE6AB5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5E91B8542E6259560061D4F0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -863,8 +1005,35 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 5E8B619A2E4EF4DB00BE6AB5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5E91B8522E6259560061D4F0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 5E8A31072E72277E00964879 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5377CBF0263B596A003A4E83 /* Swiftfin iOS */; + targetProxy = 5E8A31062E72277E00964879 /* PBXContainerItemProxy */; + }; + 5E91B8622E6259900061D4F0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5358705F2669D21600D05A09 /* Swiftfin tvOS */; + targetProxy = 5E91B8612E6259900061D4F0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ 535870722669D21700D05A09 /* Debug */ = { isa = XCBuildConfiguration; @@ -1065,6 +1234,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 78; DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1080,6 +1250,7 @@ ); MARKETING_VERSION = 1.0.0; OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -1103,6 +1274,7 @@ CURRENT_PROJECT_VERSION = 78; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -1129,6 +1301,102 @@ }; name = Release; }; + 5E8B61A62E4EF4DB00BE6AB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.jellyfin.swiftfin.Swiftfin-Screenshots"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Swiftfin iOS"; + }; + name = Debug; + }; + 5E8B61A72E4EF4DB00BE6AB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.jellyfin.swiftfin.Swiftfin-Screenshots"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "Swiftfin iOS"; + }; + name = Release; + }; + 5E91B85E2E6259560061D4F0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.jellyfin.swiftfin.Swiftfin-tvOS-Screenshots"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TEST_TARGET_NAME = "Swiftfin tvOS"; + TVOS_DEPLOYMENT_TARGET = 18.0; + }; + name = Debug; + }; + 5E91B85F2E6259560061D4F0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "org.jellyfin.swiftfin.Swiftfin-tvOS-Screenshots"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TEST_TARGET_NAME = "Swiftfin tvOS"; + TVOS_DEPLOYMENT_TARGET = 18.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1159,6 +1427,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 5E8B61A82E4EF4DB00BE6AB5 /* Build configuration list for PBXNativeTarget "Swiftfin Screenshots" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5E8B61A62E4EF4DB00BE6AB5 /* Debug */, + 5E8B61A72E4EF4DB00BE6AB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5E91B8602E6259560061D4F0 /* Build configuration list for PBXNativeTarget "Swiftfin tvOS Screenshots" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5E91B85E2E6259560061D4F0 /* Debug */, + 5E91B85F2E6259560061D4F0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ diff --git a/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Screenshots.xcscheme b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Screenshots.xcscheme new file mode 100644 index 0000000000..d4a0468162 --- /dev/null +++ b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin Screenshots.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin tvOS Screenshots.xcscheme b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin tvOS Screenshots.xcscheme new file mode 100644 index 0000000000..3728400321 --- /dev/null +++ b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin tvOS Screenshots.xcscheme @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin.xcscheme b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin.xcscheme index edaa97ae09..bb171cbc09 100644 --- a/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin.xcscheme +++ b/Swiftfin.xcodeproj/xcshareddata/xcschemes/Swiftfin.xcscheme @@ -28,6 +28,28 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + + + +