Merge branch 'jellyfin:main' into jhays/ios-livetv

This commit is contained in:
Julian Hays 2022-05-03 21:52:36 -05:00 committed by GitHub
commit 7833280175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 475 additions and 390 deletions

View File

@ -15,12 +15,12 @@ jobs:
- "Swiftfin" - "Swiftfin"
- "Swiftfin tvOS" - "Swiftfin tvOS"
runs-on: macos-latest runs-on: macos-12
steps: steps:
- uses: maxim-lobanov/setup-xcode@v1 - uses: maxim-lobanov/setup-xcode@v1
with: with:
xcode-version: latest xcode-version: '13.3'
- name: Checkout - name: Checkout
uses: actions/checkout@v1 uses: actions/checkout@v1
@ -53,7 +53,6 @@ jobs:
- name: xcodebuild! - name: xcodebuild!
run: | run: |
xcodebuild build -project "Swiftfin.xcodeproj" \ xcodebuild build -project "Swiftfin.xcodeproj" \
-resolvePackageDependencies \
-scheme "${{ matrix.scheme }}" \ -scheme "${{ matrix.scheme }}" \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO

View File

@ -59,13 +59,13 @@ final class MainCoordinator: NavigationCoordinatable {
@objc @objc
func didSignIn() { func didSignIn() {
LogManager.shared.log.info("Received `didSignIn` from SwiftfinNotificationCenter.") LogManager.log.info("Received `didSignIn` from SwiftfinNotificationCenter.")
root(\.mainTab) root(\.mainTab)
} }
@objc @objc
func didSignOut() { func didSignOut() {
LogManager.shared.log.info("Received `didSignOut` from SwiftfinNotificationCenter.") LogManager.log.info("Received `didSignOut` from SwiftfinNotificationCenter.")
root(\.serverList) root(\.serverList)
} }

View File

@ -46,13 +46,13 @@ final class MainCoordinator: NavigationCoordinatable {
@objc @objc
func didSignIn() { func didSignIn() {
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.") LogManager.log.info("Received `didSignIn` from NSNotificationCenter.")
root(\.mainTab) root(\.mainTab)
} }
@objc @objc
func didSignOut() { func didSignOut() {
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.") LogManager.log.info("Received `didSignOut` from NSNotificationCenter.")
root(\.serverList) root(\.serverList)
} }

View File

@ -13,22 +13,19 @@ struct ErrorMessage: Identifiable {
let code: Int let code: Int
let title: String let title: String
let displayMessage: String let message: String
let logConstructor: LogConstructor
// Chosen value such that if an error has this code, don't show the code to the UI // Chosen value such that if an error has this code, don't show the code to the UI
// This was chosen because of its unlikelyhood to ever be used // This was chosen because of its unlikelyhood to ever be used
static let noShowErrorCode = -69420 static let noShowErrorCode = -69420
var id: String { var id: String {
"\(code)\(title)\(logConstructor.message)" "\(code)\(title)\(message)"
} }
/// If the custom displayMessage is `nil`, it will be set to the given logConstructor's message init(code: Int, title: String, message: String) {
init(code: Int, title: String, displayMessage: String?, logConstructor: LogConstructor) {
self.code = code self.code = code
self.title = title self.title = title
self.displayMessage = displayMessage ?? logConstructor.message self.message = message
self.logConstructor = logConstructor
} }
} }

View File

@ -1,19 +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) 2022 Jellyfin & Jellyfin Contributors
//
import Foundation
import JellyfinAPI
struct LogConstructor {
var message: String
let tag: String
let level: LogLevel
let function: String
let file: String
let line: UInt
}

View File

@ -9,127 +9,75 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
/**
The implementation of the network errors here are a temporary measure.
It is very repetitive, messy, and doesn't fulfill the entire specification of "error reporting".
Needs to be replaced
*/
enum NetworkError: Error { enum NetworkError: Error {
/// For the case that the ErrorResponse object has a code of -1 /// For the case that the ErrorResponse object has a code of -1
case URLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) case URLError(response: ErrorResponse, displayMessage: String?)
/// For the case that the ErrorRespones object has a code of -2 /// For the case that the ErrorRespones object has a code of -2
case HTTPURLError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) case HTTPURLError(response: ErrorResponse, displayMessage: String?)
/// For the case that the ErrorResponse object has a positive code /// For the case that the ErrorResponse object has a positive code
case JellyfinError(response: ErrorResponse, displayMessage: String?, logConstructor: LogConstructor) case JellyfinError(response: ErrorResponse, displayMessage: String?)
var errorMessage: ErrorMessage { var errorMessage: ErrorMessage {
switch self { switch self {
case let .URLError(response, displayMessage, logConstructor): case let .URLError(response, displayMessage):
return NetworkError.parseURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor) return NetworkError.parseURLError(from: response, displayMessage: displayMessage)
case let .HTTPURLError(response, displayMessage, logConstructor): case let .HTTPURLError(response, displayMessage):
return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage, logConstructor: logConstructor) return NetworkError.parseHTTPURLError(from: response, displayMessage: displayMessage)
case let .JellyfinError(response, displayMessage, logConstructor): case let .JellyfinError(response, displayMessage):
return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage, logConstructor: logConstructor) return NetworkError.parseJellyfinError(from: response, displayMessage: displayMessage)
} }
} }
func logMessage() { private static func parseURLError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
let logConstructor = errorMessage.logConstructor
let logFunction: (@autoclosure () -> String, String, String, String, UInt) -> Void
switch logConstructor.level {
case .trace:
logFunction = LogManager.shared.log.trace
case .debug:
logFunction = LogManager.shared.log.debug
case .information:
logFunction = LogManager.shared.log.info
case .warning:
logFunction = LogManager.shared.log.warning
case .error:
logFunction = LogManager.shared.log.error
case .critical:
logFunction = LogManager.shared.log.critical
case ._none:
logFunction = LogManager.shared.log.debug
}
logFunction(logConstructor.message, logConstructor.tag, logConstructor.function, logConstructor.file, logConstructor.line)
}
private static func parseURLError(from response: ErrorResponse, displayMessage: String?,
logConstructor: LogConstructor) -> ErrorMessage
{
let errorMessage: ErrorMessage let errorMessage: ErrorMessage
var logMessage = L10n.unknownError
var logConstructor = logConstructor
switch response { switch response {
case let .error(_, _, _, err): case let .error(_, _, _, err):
// These codes are currently referenced from: // Code references:
// https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes // https://developer.apple.com/documentation/foundation/1508628-url_loading_system_error_codes
switch err._code { switch err._code {
case -1001: case -1001:
logMessage = L10n.networkTimedOut
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code, errorMessage = ErrorMessage(code: err._code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, message: L10n.networkTimedOut)
logConstructor: logConstructor) case -1003:
errorMessage = ErrorMessage(code: err._code,
title: L10n.error,
message: L10n.unableToFindHost)
case -1004: case -1004:
logMessage = L10n.cannotConnectToHost
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code, errorMessage = ErrorMessage(code: err._code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, message: L10n.cannotConnectToHost)
logConstructor: logConstructor)
default: default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: err._code, errorMessage = ErrorMessage(code: err._code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, message: L10n.unknownError)
logConstructor: logConstructor)
} }
} }
return errorMessage return errorMessage
} }
private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?, private static func parseHTTPURLError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
logConstructor: LogConstructor) -> ErrorMessage
{
let errorMessage: ErrorMessage let errorMessage: ErrorMessage
let logMessage = "An HTTP URL error has occurred"
var logConstructor = logConstructor
// Not implemented as has not run into one of these errors as time of writing // Not implemented as has not run into one of these errors as time of writing
switch response { switch response {
case .error: case .error:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: 0, errorMessage = ErrorMessage(code: 0,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, message: "An HTTP URL error has occurred")
logConstructor: logConstructor)
} }
return errorMessage return errorMessage
} }
private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?, private static func parseJellyfinError(from response: ErrorResponse, displayMessage: String?) -> ErrorMessage {
logConstructor: LogConstructor) -> ErrorMessage
{
let errorMessage: ErrorMessage let errorMessage: ErrorMessage
var logMessage = L10n.unknownError
var logConstructor = logConstructor
switch response { switch response {
case let .error(code, _, _, _): case let .error(code, _, _, _):
@ -137,18 +85,13 @@ enum NetworkError: Error {
// Generic HTTP status codes // Generic HTTP status codes
switch code { switch code {
case 401: case 401:
logMessage = L10n.unauthorizedUser
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code, errorMessage = ErrorMessage(code: code,
title: L10n.unauthorized, title: L10n.unauthorized,
displayMessage: displayMessage, message: L10n.unauthorizedUser)
logConstructor: logConstructor)
default: default:
logConstructor.message = logMessage
errorMessage = ErrorMessage(code: code, errorMessage = ErrorMessage(code: code,
title: L10n.error, title: L10n.error,
displayMessage: displayMessage, message: L10n.unknownError)
logConstructor: logConstructor)
} }
} }

View File

@ -14,7 +14,7 @@ import UIKit
extension BaseItemDto { extension BaseItemDto {
func createVideoPlayerViewModel() -> AnyPublisher<[VideoPlayerViewModel], Error> { func createVideoPlayerViewModel() -> AnyPublisher<[VideoPlayerViewModel], Error> {
LogManager.shared.log.debug("Creating video player view model for item: \(id ?? "")") LogManager.log.debug("Creating video player view model for item: \(id ?? "")")
let builder = DeviceProfileBuilder() let builder = DeviceProfileBuilder()
// TODO: fix bitrate settings // TODO: fix bitrate settings
@ -169,7 +169,7 @@ extension BaseItemDto {
func createLiveTVVideoPlayerViewModel() -> AnyPublisher<[VideoPlayerViewModel], Error> { func createLiveTVVideoPlayerViewModel() -> AnyPublisher<[VideoPlayerViewModel], Error> {
LogManager.shared.log.debug("Creating liveTV video player view model for item: \(id ?? "")") LogManager.log.debug("Creating liveTV video player view model for item: \(id ?? "")")
let builder = DeviceProfileBuilder() let builder = DeviceProfileBuilder()
// TODO: fix bitrate settings // TODO: fix bitrate settings

View File

@ -240,6 +240,8 @@ internal enum L10n {
internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") } internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") }
/// Playback Speed /// Playback Speed
internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") } internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") }
/// Player Gestures Lock Gesture Enabled
internal static var playerGesturesLockGestureEnabled: String { return L10n.tr("Localizable", "playerGesturesLockGestureEnabled") }
/// Play From Beginning /// Play From Beginning
internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") } internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") }
/// Play Next /// Play Next
@ -390,6 +392,8 @@ internal enum L10n {
internal static var tvShows: String { return L10n.tr("Localizable", "tvShows") } internal static var tvShows: String { return L10n.tr("Localizable", "tvShows") }
/// Unable to connect to server /// Unable to connect to server
internal static var unableToConnectServer: String { return L10n.tr("Localizable", "unableToConnectServer") } internal static var unableToConnectServer: String { return L10n.tr("Localizable", "unableToConnectServer") }
/// Unable to find host
internal static var unableToFindHost: String { return L10n.tr("Localizable", "unableToFindHost") }
/// Unaired /// Unaired
internal static var unaired: String { return L10n.tr("Localizable", "unaired") } internal static var unaired: String { return L10n.tr("Localizable", "unaired") }
/// Unauthorized /// Unauthorized

View File

@ -59,7 +59,7 @@ public class ServerDiscovery {
func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) { func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) {
do { do {
let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data) let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data)
LogManager.shared.log.debug("Received JellyfinServer from \"\(response.name)\"", tag: "ServerDiscovery") LogManager.log.debug("Received JellyfinServer from \"\(response.name)\"", tag: "ServerDiscovery")
completion(response) completion(response)
} catch { } catch {
completion(nil) completion(nil)
@ -68,7 +68,7 @@ public class ServerDiscovery {
self.broadcastConn.handler = receiveHandler self.broadcastConn.handler = receiveHandler
do { do {
try broadcastConn.sendBroadcast("Who is JellyfinServer?") try broadcastConn.sendBroadcast("Who is JellyfinServer?")
LogManager.shared.log.debug("Discovery broadcast sent", tag: "ServerDiscovery") LogManager.log.debug("Discovery broadcast sent", tag: "ServerDiscovery")
} catch { } catch {
print(error) print(error)
} }

View File

@ -16,7 +16,6 @@ let INADDR_BROADCAST = in_addr(s_addr: 0xFFFF_FFFF)
/// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket. /// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket.
open class UDPBroadcastConnection { open class UDPBroadcastConnection {
// MARK: Properties // MARK: Properties
/// The address of the UDP socket. /// The address of the UDP socket.
@ -80,7 +79,6 @@ open class UDPBroadcastConnection {
/// ///
/// - Throws: Throws a `ConnectionError` if an error occurs. /// - Throws: Throws a `ConnectionError` if an error occurs.
fileprivate func createSocket() throws { fileprivate func createSocket() throws {
// Create new socket // Create new socket
let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
guard newSocket > 0 else { throw ConnectionError.createSocketFailed } guard newSocket > 0 else { throw ConnectionError.createSocketFailed }
@ -98,9 +96,9 @@ open class UDPBroadcastConnection {
if shouldBeBound { if shouldBeBound {
var saddr = sockaddr(sa_len: 0, sa_family: 0, var saddr = sockaddr(sa_len: 0, sa_family: 0,
sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) sa_data: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0))
self.address.sin_addr = INADDR_ANY address.sin_addr = INADDR_ANY
memcpy(&saddr, &self.address, MemoryLayout<sockaddr_in>.size) memcpy(&saddr, &address, MemoryLayout<sockaddr_in>.size)
self.address.sin_addr = INADDR_BROADCAST address.sin_addr = INADDR_BROADCAST
let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.size)) let isBound = bind(newSocket, &saddr, socklen_t(MemoryLayout<sockaddr_in>.size))
if isBound == -1 { if isBound == -1 {
debugPrint("Couldn't bind socket") debugPrint("Couldn't bind socket")
@ -131,9 +129,11 @@ open class UDPBroadcastConnection {
var socketAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size) var socketAddressLength = socklen_t(MemoryLayout<sockaddr_storage>.size)
let response = [UInt8](repeating: 0, count: 4096) let response = [UInt8](repeating: 0, count: 4096)
let UDPSocket = Int32(source.handle) let UDPSocket = Int32(source.handle)
let pointer = UnsafeMutablePointer<[UInt8]>.allocate(capacity: response.capacity)
pointer.initialize(to: response)
let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { let bytesRead = withUnsafeMutablePointer(to: &socketAddress) {
recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, recvfrom(UDPSocket, pointer, response.count, 0,
UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength)
} }
@ -151,11 +151,10 @@ open class UDPBroadcastConnection {
} }
} }
guard let endpoint = withUnsafePointer(to: &socketAddress, guard let endpoint = withUnsafePointer(to: &socketAddress, {
{ self
self .getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0)
.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0) .bindMemory(to: sockaddr.self, capacity: 1)) })
.bindMemory(to: sockaddr.self, capacity: 1)) })
else { else {
// debugPrint("Failed to get the address and port from the socket address received from recvfrom") // debugPrint("Failed to get the address and port from the socket address received from recvfrom")
self.closeConnection() self.closeConnection()
@ -224,7 +223,7 @@ open class UDPBroadcastConnection {
source.cancel() source.cancel()
responseSource = nil responseSource = nil
} }
if shouldBeBound && reopen { if shouldBeBound, reopen {
dispatchQueue.async { dispatchQueue.async {
do { do {
try self.createSocket() try self.createSocket()
@ -290,7 +289,6 @@ open class UDPBroadcastConnection {
// Copyright © 2019 Gunter Hager. All rights reserved. // Copyright © 2019 Gunter Hager. All rights reserved.
// //
public extension UDPBroadcastConnection { public extension UDPBroadcastConnection {
enum ConnectionError: Error { enum ConnectionError: Error {
// Creating socket // Creating socket
case createSocketFailed case createSocketFailed

View File

@ -10,33 +10,35 @@ import Foundation
import Puppy import Puppy
class LogManager { class LogManager {
static let shared = LogManager()
let log = Puppy()
init() { static let log = Puppy()
let console = ConsoleLogger("com.swiftfin.ConsoleLogger")
let fileURL = self.getDocumentsDirectory().appendingPathComponent("logs.txt") static func setup() {
let FM = FileManager()
_ = try? FM.removeItem(at: fileURL) let logsDirectory = getDocumentsDirectory().appendingPathComponent("logs", isDirectory: true)
do { do {
let file = try FileLogger("com.swiftfin", fileURL: fileURL) try FileManager.default.createDirectory(atPath: logsDirectory.path,
file.format = LogFormatter() withIntermediateDirectories: true,
log.add(file, withLevel: .debug) attributes: nil)
} catch let err { } catch {
log.error("Couldn't initialize file logger.") // logs directory already created
print(err)
} }
console.format = LogFormatter()
log.add(console, withLevel: .debug) let logFileURL = logsDirectory.appendingPathComponent("swiftfin_log.log")
log.info("Logger initialized.")
let fileRotationLogger = try! FileRotationLogger("org.jellyfin.swiftfin.logger.file-rotation",
fileURL: logFileURL)
fileRotationLogger.format = LogFormatter()
let consoleLogger = ConsoleLogger("org.jellyfin.swiftfin.logger.console")
consoleLogger.format = LogFormatter()
log.add(fileRotationLogger, withLevel: .debug)
log.add(consoleLogger, withLevel: .debug)
} }
func logFileURL() -> URL { private static func getDocumentsDirectory() -> URL {
self.getDocumentsDirectory().appendingPathComponent("logs.txt")
}
func getDocumentsDirectory() -> URL {
// find all possible documents directories for this user // find all possible documents directories for this user
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)

View File

@ -104,7 +104,7 @@ final class SessionManager {
[Where<SwiftfinStore.Models.StoredServer>("id == %@", [Where<SwiftfinStore.Models.StoredServer>("id == %@",
newServer.id)]) newServer.id)])
{ {
throw SwiftfinStore.Errors.existingServer(existingServer.state) throw SwiftfinStore.Error.existingServer(existingServer.state)
} }
return (newServer, transaction) return (newServer, transaction)
@ -210,7 +210,7 @@ final class SessionManager {
[Where<SwiftfinStore.Models.StoredUser>("id == %@", [Where<SwiftfinStore.Models.StoredUser>("id == %@",
newUser.id)]) newUser.id)])
{ {
throw SwiftfinStore.Errors.existingUser(existingUser.state) throw SwiftfinStore.Error.existingUser(existingUser.state)
} }
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>()) let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())

View File

@ -147,9 +147,9 @@ enum SwiftfinStore {
} }
} }
// MARK: Errors // MARK: Error
enum Errors { enum Error {
case existingServer(State.Server) case existingServer(State.Server)
case existingUser(State.User) case existingUser(State.User)
} }
@ -193,7 +193,7 @@ enum SwiftfinStore {
// MARK: LocalizedError // MARK: LocalizedError
extension SwiftfinStore.Errors: LocalizedError { extension SwiftfinStore.Error: LocalizedError {
var title: String { var title: String {
switch self { switch self {

View File

@ -48,6 +48,8 @@ extension Defaults.Keys {
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true, static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,

View File

@ -48,14 +48,14 @@ final class ConnectToServerViewModel: ViewModel {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
var uri = uri var uri = uri
if uri == "localhost" { if uri == "http://localhost" || uri == "localhost" {
uri = "http://localhost:8096" uri = "http://localhost:8096"
} }
#endif #endif
let trimmedURI = uri.trimmingCharacters(in: .whitespaces) let trimmedURI = uri.trimmingCharacters(in: .whitespaces)
LogManager.shared.log.debug("Attempting to connect to server at \"\(trimmedURI)\"", tag: "connectToServer") LogManager.log.debug("Attempting to connect to server at \"\(trimmedURI)\"", tag: "connectToServer")
SessionManager.main.connectToServer(with: trimmedURI) SessionManager.main.connectToServer(with: trimmedURI)
.trackActivity(loading) .trackActivity(loading)
.sink(receiveCompletion: { completion in .sink(receiveCompletion: { completion in
@ -71,10 +71,7 @@ final class ConnectToServerViewModel: ViewModel {
// a url in the response is the result if a redirect // a url in the response is the result if a redirect
if let newURL = response?.url { if let newURL = response?.url {
if redirectCount > 2 { if redirectCount > 2 {
self.handleAPIRequestError(displayMessage: L10n.tooManyRedirects, self.handleAPIRequestError(displayMessage: L10n.tooManyRedirects, completion: completion)
logLevel: .critical,
tag: "connectToServer",
completion: completion)
} else { } else {
self self
.connectToServer(uri: newURL.absoluteString .connectToServer(uri: newURL.absoluteString
@ -85,25 +82,21 @@ final class ConnectToServerViewModel: ViewModel {
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} }
} }
case is SwiftfinStore.Errors: case is SwiftfinStore.Error:
let swiftfinError = error as! SwiftfinStore.Errors let swiftfinError = error as! SwiftfinStore.Error
switch swiftfinError { switch swiftfinError {
case let .existingServer(server): case let .existingServer(server):
self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri) self.addServerURIPayload = AddServerURIPayload(server: server, uri: uri)
self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri) self.backAddServerURIPayload = AddServerURIPayload(server: server, uri: uri)
default: default:
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, logLevel: .critical, self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
tag: "connectToServer",
completion: completion)
} }
default: default:
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, logLevel: .critical, self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
tag: "connectToServer",
completion: completion)
} }
} }
}, receiveValue: { server in }, receiveValue: { server in
LogManager.shared.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer") LogManager.log.debug("Connected to server at \"\(uri)\"", tag: "connectToServer")
self.router?.route(to: \.userSignIn, server) self.router?.route(to: \.userSignIn, server)
}) })
.store(in: &cancellables) .store(in: &cancellables)
@ -128,14 +121,11 @@ final class ConnectToServerViewModel: ViewModel {
func addURIToServer(addServerURIPayload: AddServerURIPayload) { func addURIToServer(addServerURIPayload: AddServerURIPayload) {
SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri) SessionManager.main.addURIToServer(server: addServerURIPayload.server, uri: addServerURIPayload.uri)
.sink { completion in .sink { completion in
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, logLevel: .critical, tag: "connectToServer", self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
completion: completion)
} receiveValue: { server in } receiveValue: { server in
SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri) SessionManager.main.setServerCurrentURI(server: server, uri: addServerURIPayload.uri)
.sink { completion in .sink { completion in
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, logLevel: .critical, self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
tag: "connectToServer",
completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
self.router?.dismissCoordinator() self.router?.dismissCoordinator()
} }

View File

@ -63,7 +63,7 @@ final class HomeViewModel: ViewModel {
@objc @objc
func refresh() { func refresh() {
LogManager.shared.log.debug("Refresh called.") LogManager.log.debug("Refresh called.")
refreshLibrariesLatest() refreshLibrariesLatest()
refreshLatestAddedItems() refreshLatestAddedItems()
@ -89,7 +89,7 @@ final class HomeViewModel: ViewModel {
var newLibraries: [BaseItemDto] = [] var newLibraries: [BaseItemDto] = []
response.items!.forEach { item in response.items!.forEach { item in
LogManager.shared.log LogManager.log
.debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")") .debug("Retrieved user view: \(item.id!) (\(item.name ?? "nil")) with type \(item.collectionType ?? "nil")")
if item.collectionType == "movies" || item.collectionType == "tvshows" { if item.collectionType == "movies" || item.collectionType == "tvshows" {
newLibraries.append(item) newLibraries.append(item)
@ -147,7 +147,7 @@ final class HomeViewModel: ViewModel {
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} }
} receiveValue: { items in } receiveValue: { items in
LogManager.shared.log.debug("Retrieved \(String(items.count)) resume items") LogManager.log.debug("Retrieved \(String(items.count)) resume items")
self.latestAddedItems = items self.latestAddedItems = items
} }
@ -178,7 +178,7 @@ final class HomeViewModel: ViewModel {
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} }
}, receiveValue: { response in }, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) resume items") LogManager.log.debug("Retrieved \(String(response.items!.count)) resume items")
self.resumeItems = response.items ?? [] self.resumeItems = response.items ?? []
}) })
@ -223,7 +223,7 @@ final class HomeViewModel: ViewModel {
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} }
}, receiveValue: { response in }, receiveValue: { response in
LogManager.shared.log.debug("Retrieved \(String(response.items!.count)) nextup items") LogManager.log.debug("Retrieved \(String(response.items!.count)) nextup items")
self.nextUpItems = response.items ?? [] self.nextUpItems = response.items ?? []
}) })

View File

@ -44,7 +44,7 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
} }
private func requestEpisodes() { private func requestEpisodes() {
LogManager.shared.log LogManager.log
.debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))") .debug("Getting episodes in season \(item.id!) (\(item.name!)) of show \(item.seriesId!) (\(item.seriesName!))")
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id, TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
@ -55,7 +55,7 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
guard let self = self else { return } guard let self = self else { return }
self.episodes = response.items ?? [] self.episodes = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self.episodes.count)) episodes") LogManager.log.debug("Retrieved \(String(self.episodes.count)) episodes")
self.setNextUpInSeason() self.setNextUpInSeason()
}) })
@ -78,7 +78,7 @@ final class SeasonItemViewModel: ItemViewModel, EpisodesRowManager {
!episode.unaired && !episode.missing && episode.seasonId ?? "" == self.item.id! !episode.unaired && !episode.missing && episode.seasonId ?? "" == self.item.id!
}) { }) {
self.playButtonItem = nextUpItem self.playButtonItem = nextUpItem
LogManager.shared.log.debug("Nextup in season \(self.item.id!) (\(self.item.name!)): \(nextUpItem.id!)") LogManager.log.debug("Nextup in season \(self.item.id!) (\(self.item.name!)): \(nextUpItem.id!)")
} }
if self.playButtonItem == nil && !self.episodes.isEmpty { if self.playButtonItem == nil && !self.episodes.isEmpty {

View File

@ -43,7 +43,7 @@ final class SeriesItemViewModel: ItemViewModel {
private func getNextUp() { private func getNextUp() {
LogManager.shared.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))") LogManager.log.debug("Getting next up for show \(self.item.id!) (\(self.item.name!))")
TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id, TvShowsAPI.getNextUp(userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
seriesId: self.item.id!, seriesId: self.item.id!,
@ -78,7 +78,7 @@ final class SeriesItemViewModel: ItemViewModel {
} }
private func requestSeasons() { private func requestSeasons() {
LogManager.shared.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))") LogManager.log.debug("Getting seasons of show \(self.item.id!) (\(self.item.name!))")
TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id, TvShowsAPI.getSeasons(seriesId: item.id ?? "", userId: SessionManager.main.currentLogin.user.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false, isMissing: Defaults[.shouldShowMissingSeasons] ? nil : false,
@ -88,7 +88,7 @@ final class SeriesItemViewModel: ItemViewModel {
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.seasons = response.items ?? [] self?.seasons = response.items ?? []
LogManager.shared.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons") LogManager.log.debug("Retrieved \(String(self?.seasons.count ?? 0)) seasons")
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -25,7 +25,7 @@ final class LatestMediaViewModel: ViewModel {
} }
func requestLatestMedia() { func requestLatestMedia() {
LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)") LogManager.log.debug("Requesting latest media for user id \(SessionManager.main.currentLogin.user.id)")
UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id, UserLibraryAPI.getLatestMedia(userId: SessionManager.main.currentLogin.user.id,
parentId: library.id ?? "", parentId: library.id ?? "",
fields: [ fields: [
@ -43,7 +43,7 @@ final class LatestMediaViewModel: ViewModel {
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
self?.items = response self?.items = response
LogManager.shared.log.debug("Retrieved \(String(self?.items.count ?? 0)) items") LogManager.log.debug("Retrieved \(String(self?.items.count ?? 0)) items")
}) })
.store(in: &cancellables) .store(in: &cancellables)
} }

View File

@ -69,7 +69,7 @@ final class LiveTVChannelsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] _ in }, receiveValue: { [weak self] _ in
LogManager.shared.log.debug("Received Guide Info") LogManager.log.debug("Received Guide Info")
guard let self = self else { return } guard let self = self else { return }
self.getChannels() self.getChannels()
}) })
@ -87,7 +87,7 @@ final class LiveTVChannelsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels") LogManager.log.debug("Received \(response.items?.count ?? 0) Channels")
guard let self = self else { return } guard let self = self else { return }
self.channels = response.items ?? [] self.channels = response.items ?? []
self.getPrograms() self.getPrograms()
@ -98,7 +98,7 @@ final class LiveTVChannelsViewModel: ViewModel {
private func getPrograms() { private func getPrograms() {
// http://192.168.1.50:8096/LiveTv/Programs // http://192.168.1.50:8096/LiveTv/Programs
guard !channels.isEmpty else { guard !channels.isEmpty else {
LogManager.shared.log.debug("Cannot get programs, channels list empty. ") LogManager.log.debug("Cannot get programs, channels list empty. ")
return return
} }
let channelIds = channels.compactMap(\.id) let channelIds = channels.compactMap(\.id)
@ -122,7 +122,7 @@ final class LiveTVChannelsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Programs") LogManager.log.debug("Received \(response.items?.count ?? 0) Programs")
guard let self = self else { return } guard let self = self else { return }
self.programs = response.items ?? [] self.programs = response.items ?? []
self.channelPrograms = self.processChannelPrograms() self.channelPrograms = self.processChannelPrograms()
@ -174,7 +174,7 @@ final class LiveTVChannelsViewModel: ViewModel {
} }
timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] _ in timer = Timer(fire: nextMinute, interval: 60 * 10, repeats: true) { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
LogManager.shared.log.debug("LiveTVChannels schedule check...") LogManager.log.debug("LiveTVChannels schedule check...")
DispatchQueue.global(qos: .background).async { DispatchQueue.global(qos: .background).async {
let newChanPrgs = self.processChannelPrograms() let newChanPrgs = self.processChannelPrograms()
DispatchQueue.main.async { DispatchQueue.main.async {

View File

@ -47,7 +47,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(response.items?.count ?? 0) Channels") LogManager.log.debug("Received \(response.items?.count ?? 0) Channels")
guard let self = self else { return } guard let self = self else { return }
if let chans = response.items { if let chans = response.items {
for chan in chans { for chan in chans {
@ -78,7 +78,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Recommended Programs") LogManager.log.debug("Received \(String(response.items?.count ?? 0)) Recommended Programs")
guard let self = self else { return } guard let self = self else { return }
self.recommendedItems = response.items ?? [] self.recommendedItems = response.items ?? []
}) })
@ -103,7 +103,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Series Items") LogManager.log.debug("Received \(String(response.items?.count ?? 0)) Series Items")
guard let self = self else { return } guard let self = self else { return }
self.seriesItems = response.items ?? [] self.seriesItems = response.items ?? []
}) })
@ -128,7 +128,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Movie Items") LogManager.log.debug("Received \(String(response.items?.count ?? 0)) Movie Items")
guard let self = self else { return } guard let self = self else { return }
self.movieItems = response.items ?? [] self.movieItems = response.items ?? []
}) })
@ -149,7 +149,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Sports Items") LogManager.log.debug("Received \(String(response.items?.count ?? 0)) Sports Items")
guard let self = self else { return } guard let self = self else { return }
self.sportsItems = response.items ?? [] self.sportsItems = response.items ?? []
}) })
@ -170,7 +170,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) Kids Items") LogManager.log.debug("Received \(String(response.items?.count ?? 0)) Kids Items")
guard let self = self else { return } guard let self = self else { return }
self.kidsItems = response.items ?? [] self.kidsItems = response.items ?? []
}) })
@ -191,7 +191,7 @@ final class LiveTVProgramsViewModel: ViewModel {
.sink(receiveCompletion: { [weak self] completion in .sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion) self?.handleAPIRequestError(completion: completion)
}, receiveValue: { [weak self] response in }, receiveValue: { [weak self] response in
LogManager.shared.log.debug("Received \(String(response.items?.count ?? 0)) News Items") LogManager.log.debug("Received \(String(response.items?.count ?? 0)) News Items")
guard let self = self else { return } guard let self = self else { return }
self.newsItems = response.items ?? [] self.newsItems = response.items ?? []
}) })

View File

@ -31,10 +31,10 @@ final class SettingsViewModel: ObservableObject {
do { do {
self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData) self.bitrates = try JSONDecoder().decode([Bitrates].self, from: jsonData)
} catch { } catch {
LogManager.shared.log.error("Error converting processed JSON into Swift compatible schema.") LogManager.log.error("Error converting processed JSON into Swift compatible schema.")
} }
} catch { } catch {
LogManager.shared.log.error("Error processing JSON file `bitrates.json`") LogManager.log.error("Error processing JSON file `bitrates.json`")
} }
// Track languages // Track languages

View File

@ -30,14 +30,12 @@ final class UserSignInViewModel: ViewModel {
} }
func login(username: String, password: String) { func login(username: String, password: String) {
LogManager.shared.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login") LogManager.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
LogManager.shared.log.debug("username: \(username), password: \(password)", tag: "login")
SessionManager.main.loginUser(server: server, username: username, password: password) SessionManager.main.loginUser(server: server, username: username, password: password)
.trackActivity(loading) .trackActivity(loading)
.sink { completion in .sink { completion in
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, logLevel: .critical, tag: "login", self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
} }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -95,6 +95,9 @@ final class VideoPlayerViewModel: ViewModel {
@Published @Published
var mediaItems: [BaseItemDto.ItemDetail] var mediaItems: [BaseItemDto.ItemDetail]
@Published
var isHiddenOverlay = false
// MARK: ShouldShowItems // MARK: ShouldShowItems
let shouldShowPlayPreviousItem: Bool let shouldShowPlayPreviousItem: Bool
@ -116,6 +119,7 @@ final class VideoPlayerViewModel: ViewModel {
let overlayType: OverlayType let overlayType: OverlayType
let jumpGesturesEnabled: Bool let jumpGesturesEnabled: Bool
let systemControlGesturesEnabled: Bool let systemControlGesturesEnabled: Bool
let playerGesturesLockGestureEnabled: Bool
let resumeOffset: Bool let resumeOffset: Bool
let streamType: ServerStreamType let streamType: ServerStreamType
let container: String let container: String
@ -244,6 +248,7 @@ final class VideoPlayerViewModel: ViewModel {
self.jumpForwardLength = Defaults[.videoPlayerJumpForward] self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled] self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled]
self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled] self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled]
self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled]
self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu] self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu]
self.resumeOffset = Defaults[.resumeOffset] self.resumeOffset = Defaults[.resumeOffset]
@ -477,7 +482,7 @@ extension VideoPlayerViewModel {
.sink { completion in .sink { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
LogManager.shared.log.debug("Start report sent for item: \(self.item.id ?? "No ID")") LogManager.log.debug("Start report sent for item: \(self.item.id ?? "No ID")")
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -513,7 +518,7 @@ extension VideoPlayerViewModel {
.sink { completion in .sink { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
LogManager.shared.log.debug("Pause report sent for item: \(self.item.id ?? "No ID")") LogManager.log.debug("Pause report sent for item: \(self.item.id ?? "No ID")")
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
@ -558,7 +563,7 @@ extension VideoPlayerViewModel {
.sink { completion in .sink { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
LogManager.shared.log.debug("Playback progress sent for item: \(self.item.id ?? "No ID")") LogManager.log.debug("Playback progress sent for item: \(self.item.id ?? "No ID")")
} }
.store(in: &cancellables) .store(in: &cancellables)
@ -585,7 +590,7 @@ extension VideoPlayerViewModel {
.sink { completion in .sink { completion in
self.handleAPIRequestError(completion: completion) self.handleAPIRequestError(completion: completion)
} receiveValue: { _ in } receiveValue: { _ in
LogManager.shared.log.debug("Stop report sent for item: \(self.item.id ?? "No ID")") LogManager.log.debug("Stop report sent for item: \(self.item.id ?? "No ID")")
Notifications[.didSendStopReport].post(object: self.item.id) Notifications[.didSendStopReport].post(object: self.item.id)
} }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -25,57 +25,49 @@ class ViewModel: ObservableObject {
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables) loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
} }
func handleAPIRequestError(displayMessage: String? = nil, logLevel: LogLevel = .error, tag: String = "", function: String = #function, func handleAPIRequestError(displayMessage: String? = nil, completion: Subscribers.Completion<Error>) {
file: String = #file, line: UInt = #line, completion: Subscribers.Completion<Error>)
{
switch completion { switch completion {
case .finished: case .finished:
self.errorMessage = nil self.errorMessage = nil
case let .failure(error): case let .failure(error):
let logConstructor = LogConstructor(message: "__NOTHING__", tag: tag, level: logLevel, function: function, file: file,
line: line)
switch error { switch error {
case is ErrorResponse: case is ErrorResponse:
let networkError: NetworkError let networkError: NetworkError
let errorResponse = error as! ErrorResponse let errorResponse = error as! ErrorResponse
switch errorResponse { switch errorResponse {
case .error(-1, _, _, _): case .error(-1, _, _, _):
networkError = .URLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) networkError = .URLError(response: errorResponse, displayMessage: displayMessage)
// Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented // Use the errorResponse description for debugging, rather than the user-facing friendly description which may not be implemented
LogManager.shared.log LogManager.log
.error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)") .error("Request failed: URL request failed with error \(networkError.errorMessage.code): \(errorResponse.localizedDescription)")
case .error(-2, _, _, _): case .error(-2, _, _, _):
networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) networkError = .HTTPURLError(response: errorResponse, displayMessage: displayMessage)
LogManager.shared.log LogManager.log
.error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)") .error("Request failed: HTTP URL request failed with description: \(errorResponse.localizedDescription)")
default: default:
networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage, logConstructor: logConstructor) networkError = .JellyfinError(response: errorResponse, displayMessage: displayMessage)
// Able to use user-facing friendly description here since just HTTP status codes // Able to use user-facing friendly description here since just HTTP status codes
LogManager.shared.log LogManager.log
.error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.logConstructor.message)\n\(error.localizedDescription)") .error("Request failed: \(networkError.errorMessage.code) - \(networkError.errorMessage.title): \(networkError.errorMessage.message)\n\(error.localizedDescription)")
} }
self.errorMessage = networkError.errorMessage self.errorMessage = networkError.errorMessage
networkError.logMessage() case is SwiftfinStore.Error:
let swiftfinError = error as! SwiftfinStore.Error
case is SwiftfinStore.Errors:
let swiftfinError = error as! SwiftfinStore.Errors
let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, let errorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: swiftfinError.title, title: swiftfinError.title,
displayMessage: swiftfinError.errorDescription ?? "", message: swiftfinError.errorDescription ?? "")
logConstructor: logConstructor)
self.errorMessage = errorMessage self.errorMessage = errorMessage
LogManager.shared.log.error("Request failed: \(swiftfinError.errorDescription ?? "")") LogManager.log.error("Request failed: \(swiftfinError.errorDescription ?? "")")
default: default:
let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode, let genericErrorMessage = ErrorMessage(code: ErrorMessage.noShowErrorCode,
title: "Generic Error", title: "Generic Error",
displayMessage: error.localizedDescription, message: error.localizedDescription)
logConstructor: logConstructor)
self.errorMessage = genericErrorMessage self.errorMessage = genericErrorMessage
LogManager.shared.log.error("Request failed: Generic error - \(error.localizedDescription)") LogManager.log.error("Request failed: Generic error - \(error.localizedDescription)")
} }
} }
} }

View File

@ -28,13 +28,13 @@ struct CinematicNextUpCardView: View {
item.getSeriesThumbImage(maxWidth: 350), item.getSeriesThumbImage(maxWidth: 350),
item.getSeriesBackdropImage(maxWidth: 350), item.getSeriesBackdropImage(maxWidth: 350),
]) ])
.frame(width: 350, height: 210) .frame(width: 350, height: 210)
} else { } else {
ImageView([ ImageView([
.init(url: item.getThumbImage(maxWidth: 350)), .init(url: item.getThumbImage(maxWidth: 350)),
.init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()), .init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()),
]) ])
.frame(width: 350, height: 210) .frame(width: 350, height: 210)
} }
LinearGradient(colors: [.clear, .black], LinearGradient(colors: [.clear, .black],

View File

@ -29,13 +29,13 @@ struct CinematicResumeCardView: View {
item.getSeriesThumbImage(maxWidth: 350), item.getSeriesThumbImage(maxWidth: 350),
item.getSeriesBackdropImage(maxWidth: 350), item.getSeriesBackdropImage(maxWidth: 350),
]) ])
.frame(width: 350, height: 210) .frame(width: 350, height: 210)
} else { } else {
ImageView([ ImageView([
.init(url: item.getThumbImage(maxWidth: 350)), .init(url: item.getThumbImage(maxWidth: 350)),
.init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()), .init(url: item.getBackdropImage(maxWidth: 350), blurHash: item.getBackdropImageBlurHash()),
]) ])
.frame(width: 350, height: 210) .frame(width: 350, height: 210)
} }
LinearGradient(colors: [.clear, .black], LinearGradient(colors: [.clear, .black],

View File

@ -25,7 +25,7 @@ struct MediaPlayButtonRowView: View {
MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView) MediaViewActionButton(icon: "play.fill", scrollView: $wrappedScrollView)
} }
Text(viewModel.item.getItemProgressString() != "" ? "\(viewModel.item.getItemProgressString() ?? "") left" : L10n.play) Text((viewModel.item.getItemProgressString() != nil) ? "\(viewModel.item.getItemProgressString() ?? "") left" : L10n.play)
.font(.caption) .font(.caption)
} }
VStack { VStack {

View File

@ -77,7 +77,7 @@ struct ConnectToServerView: View {
} }
.alert(item: $viewModel.errorMessage) { _ in .alert(item: $viewModel.errorMessage) { _ in
Alert(title: Text(viewModel.alertTitle), Alert(title: Text(viewModel.alertTitle),
message: Text(viewModel.errorMessage?.displayMessage ?? L10n.unknownError), message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
dismissButton: .cancel()) dismissButton: .cancel())
} }
.navigationTitle(L10n.connect) .navigationTitle(L10n.connect)

View File

@ -62,7 +62,7 @@ struct CinematicItemViewTopRow: View {
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
} else { } else {
LogManager.shared.log.error("Attempted to play item but no playback information available") LogManager.log.error("Attempted to play item but no playback information available")
} }
} label: { } label: {
HStack(spacing: 15) { HStack(spacing: 15) {
@ -85,7 +85,7 @@ struct CinematicItemViewTopRow: View {
selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
} else { } else {
LogManager.shared.log.error("Attempted to play item but no playback information available") LogManager.log.error("Attempted to play item but no playback information available")
} }
} label: { } label: {
Label(L10n.playFromBeginning, systemImage: "gobackward") Label(L10n.playFromBeginning, systemImage: "gobackward")

View File

@ -49,7 +49,7 @@ struct UserSignInView: View {
} }
.alert(item: $viewModel.errorMessage) { _ in .alert(item: $viewModel.errorMessage) { _ in
Alert(title: Text(viewModel.alertTitle), Alert(title: Text(viewModel.alertTitle),
message: Text(viewModel.errorMessage?.displayMessage ?? L10n.unknownError), message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
dismissButton: .cancel()) dismissButton: .cancel())
} }
.navigationTitle(L10n.signIn) .navigationTitle(L10n.signIn)

View File

@ -464,11 +464,11 @@ extension LiveTVPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { if viewModel.streamType == .direct {
LogManager.shared.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")")
} else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] {
LogManager.shared.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")")
} else { } else {
LogManager.shared.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")")
} }
} }

View File

@ -324,7 +324,7 @@ struct SmallMediaStreamSelectionView: View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
ScrollViewReader { reader in ScrollViewReader { reader in
HStack { HStack {
ForEach(0 ..< viewModel.chapters.count) { chapterIndex in ForEach(0 ..< viewModel.chapters.count, id: \.self) { chapterIndex in
VStack(alignment: .leading) { VStack(alignment: .leading) {
Button { Button {
viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex])

View File

@ -62,18 +62,18 @@ struct tvOSLiveTVOverlay: View {
SFSymbolButton(systemName: "chevron.left.circle", action: { SFSymbolButton(systemName: "chevron.left.circle", action: {
viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem() viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem()
}) })
.frame(maxWidth: 30, maxHeight: 30) .frame(maxWidth: 30, maxHeight: 30)
.disabled(viewModel.previousItemVideoPlayerViewModel == nil) .disabled(viewModel.previousItemVideoPlayerViewModel == nil)
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
} }
if viewModel.shouldShowPlayNextItem { if viewModel.shouldShowPlayNextItem {
SFSymbolButton(systemName: "chevron.right.circle", action: { SFSymbolButton(systemName: "chevron.right.circle", action: {
viewModel.playerOverlayDelegate?.didSelectPlayNextItem() viewModel.playerOverlayDelegate?.didSelectPlayNextItem()
}) })
.frame(maxWidth: 30, maxHeight: 30) .frame(maxWidth: 30, maxHeight: 30)
.disabled(viewModel.nextItemVideoPlayerViewModel == nil) .disabled(viewModel.nextItemVideoPlayerViewModel == nil)
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
} }
if viewModel.shouldShowAutoPlay { if viewModel.shouldShowAutoPlay {

View File

@ -62,18 +62,18 @@ struct tvOSVLCOverlay: View {
SFSymbolButton(systemName: "chevron.left.circle", action: { SFSymbolButton(systemName: "chevron.left.circle", action: {
viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem() viewModel.playerOverlayDelegate?.didSelectPlayPreviousItem()
}) })
.frame(maxWidth: 30, maxHeight: 30) .frame(maxWidth: 30, maxHeight: 30)
.disabled(viewModel.previousItemVideoPlayerViewModel == nil) .disabled(viewModel.previousItemVideoPlayerViewModel == nil)
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
} }
if viewModel.shouldShowPlayNextItem { if viewModel.shouldShowPlayNextItem {
SFSymbolButton(systemName: "chevron.right.circle", action: { SFSymbolButton(systemName: "chevron.right.circle", action: {
viewModel.playerOverlayDelegate?.didSelectPlayNextItem() viewModel.playerOverlayDelegate?.didSelectPlayNextItem()
}) })
.frame(maxWidth: 30, maxHeight: 30) .frame(maxWidth: 30, maxHeight: 30)
.disabled(viewModel.nextItemVideoPlayerViewModel == nil) .disabled(viewModel.nextItemVideoPlayerViewModel == nil)
.foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white) .foregroundColor(viewModel.nextItemVideoPlayerViewModel == nil ? .gray : .white)
} }
if viewModel.shouldShowAutoPlay { if viewModel.shouldShowAutoPlay {

View File

@ -464,11 +464,11 @@ extension VLCPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { if viewModel.streamType == .direct {
LogManager.shared.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")")
} else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] {
LogManager.shared.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")")
} else { } else {
LogManager.shared.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")")
} }
} }

View File

@ -53,12 +53,9 @@
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; }; 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; }; 535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; };
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAC269CFAEA00A2D8B7 /* Puppy */; };
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AAE269CFAF600A2D8B7 /* Puppy */; };
53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; };
53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; };
53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; }; 53649AB3269D3F5B00A2D8B7 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53649AB0269CFB1900A2D8B7 /* LogManager.swift */; };
53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = 53649AB4269D423A00A2D8B7 /* Puppy */; };
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; 5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; };
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; }; 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */; };
536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D73267BA8170004248C /* BackgroundManager.swift */; }; 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536D3D73267BA8170004248C /* BackgroundManager.swift */; };
@ -327,6 +324,7 @@
E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; }; E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA4E277BBCC4000269ED /* CGSizeExtensions.swift */; };
E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E10EAA53277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; }; E10EAA54277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */; };
E1101177281B1E8A006A3584 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1101176281B1E8A006A3584 /* Puppy */; };
E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */; }; E111DE222790BB46008118A3 /* DetectBottomScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E111DE212790BB46008118A3 /* DetectBottomScrollView.swift */; };
E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; }; E11B1B6D2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */; };
@ -341,9 +339,9 @@
E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; }; E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; };
E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E126F741278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; };
E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; }; E126F742278A656C00A522BF /* ServerStreamType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E126F740278A656C00A522BF /* ServerStreamType.swift */; };
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB1279E3C6200BC6161 /* Puppy */; };
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E1347DB4279E3C9E00BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB3279E3C9E00BC6161 /* Puppy */; };
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */ = {isa = PBXBuildFile; productRef = E1347DB5279E3CA500BC6161 /* Puppy */; };
E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; }; E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; };
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; };
E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; };
@ -801,7 +799,6 @@
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = "<group>"; }; E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = "<group>"; };
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = "<group>"; }; E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = "<group>"; };
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; }; E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; }; E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
@ -913,7 +910,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
62666E1727E501CC00EC0ECD /* CFNetwork.framework in Frameworks */, 62666E1727E501CC00EC0ECD /* CFNetwork.framework in Frameworks */,
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
E11D83AF278FA998006E9776 /* NukeUI in Frameworks */, E11D83AF278FA998006E9776 /* NukeUI in Frameworks */,
62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */, 62666DFA27E5013700EC0ECD /* TVVLCKit.xcframework in Frameworks */,
62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */, 62666E3227E5021E00EC0ECD /* UIKit.framework in Frameworks */,
@ -940,6 +936,7 @@
E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */, E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */,
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */, E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */,
E12186DE2718F1C50010884C /* Defaults in Frameworks */, E12186DE2718F1C50010884C /* Defaults in Frameworks */,
E1347DB6279E3CA500BC6161 /* Puppy in Frameworks */,
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -951,8 +948,8 @@
62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */, 62666E3E27E503FA00EC0ECD /* MediaAccessibility.framework in Frameworks */,
62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */, 62666DFF27E5016400EC0ECD /* CFNetwork.framework in Frameworks */,
E13DD3D327168E65009D4DAF /* Defaults in Frameworks */, E13DD3D327168E65009D4DAF /* Defaults in Frameworks */,
E1101177281B1E8A006A3584 /* Puppy in Frameworks */,
E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */, E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */,
53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */,
E1002B682793CFBA00E47059 /* Algorithms in Frameworks */, E1002B682793CFBA00E47059 /* Algorithms in Frameworks */,
62666E1127E501B900EC0ECD /* UIKit.framework in Frameworks */, 62666E1127E501B900EC0ECD /* UIKit.framework in Frameworks */,
62666DF727E5012C00EC0ECD /* MobileVLCKit.xcframework in Frameworks */, 62666DF727E5012C00EC0ECD /* MobileVLCKit.xcframework in Frameworks */,
@ -980,6 +977,7 @@
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */, 625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
62666E3927E502CE00EC0ECD /* SwizzleSwift in Frameworks */, 62666E3927E502CE00EC0ECD /* SwizzleSwift in Frameworks */,
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */, E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */,
E1347DB2279E3C6200BC6161 /* Puppy in Frameworks */,
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */, E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -988,11 +986,11 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E1347DB4279E3C9E00BC6161 /* Puppy in Frameworks */,
E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */, E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */,
628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */, 628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */,
531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */, 531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */,
E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */, E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */,
53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */,
536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */, 536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */,
E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */, E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */,
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */, E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */,
@ -1886,7 +1884,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1FCD09526C47118007C8DCF /* ErrorMessage.swift */, E1FCD09526C47118007C8DCF /* ErrorMessage.swift */,
E131691626C583BC0074BFEE /* LogConstructor.swift */,
E1FCD08726C35A0D007C8DCF /* NetworkError.swift */, E1FCD08726C35A0D007C8DCF /* NetworkError.swift */,
); );
path = Errors; path = Errors;
@ -1914,7 +1911,6 @@
535870902669D7A800D05A09 /* Introspect */, 535870902669D7A800D05A09 /* Introspect */,
53ABFDEC26799D7700886593 /* ActivityIndicator */, 53ABFDEC26799D7700886593 /* ActivityIndicator */,
536D3D83267BEA550004248C /* ParallaxView */, 536D3D83267BEA550004248C /* ParallaxView */,
53649AAE269CFAF600A2D8B7 /* Puppy */,
6220D0C826D63F3700B8E046 /* Stinsen */, 6220D0C826D63F3700B8E046 /* Stinsen */,
E13DD3CC27164CA7009D4DAF /* CoreStore */, E13DD3CC27164CA7009D4DAF /* CoreStore */,
E12186DD2718F1C50010884C /* Defaults */, E12186DD2718F1C50010884C /* Defaults */,
@ -1924,6 +1920,7 @@
E1AE8E7D2789136D00FBDDAA /* Nuke */, E1AE8E7D2789136D00FBDDAA /* Nuke */,
E11D83AE278FA998006E9776 /* NukeUI */, E11D83AE278FA998006E9776 /* NukeUI */,
E1002B6A2793E36600E47059 /* Algorithms */, E1002B6A2793E36600E47059 /* Algorithms */,
E1347DB5279E3CA500BC6161 /* Puppy */,
); );
productName = "JellyfinPlayer tvOS"; productName = "JellyfinPlayer tvOS";
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
@ -1950,7 +1947,6 @@
packageProductDependencies = ( packageProductDependencies = (
53352570265EA0A0006CCA86 /* Introspect */, 53352570265EA0A0006CCA86 /* Introspect */,
625CB5792678C4A400530A6E /* ActivityIndicator */, 625CB5792678C4A400530A6E /* ActivityIndicator */,
53649AAC269CFAEA00A2D8B7 /* Puppy */,
62C29E9B26D0FE4200C1D2E7 /* Stinsen */, 62C29E9B26D0FE4200C1D2E7 /* Stinsen */,
E13DD3C52716499E009D4DAF /* CoreStore */, E13DD3C52716499E009D4DAF /* CoreStore */,
E13DD3D227168E65009D4DAF /* Defaults */, E13DD3D227168E65009D4DAF /* Defaults */,
@ -1963,6 +1959,7 @@
E1361DA6278FA7A300BEC523 /* NukeUI */, E1361DA6278FA7A300BEC523 /* NukeUI */,
E1002B672793CFBA00E47059 /* Algorithms */, E1002B672793CFBA00E47059 /* Algorithms */,
62666E3827E502CE00EC0ECD /* SwizzleSwift */, 62666E3827E502CE00EC0ECD /* SwizzleSwift */,
E1101176281B1E8A006A3584 /* Puppy */,
); );
productName = JellyfinPlayer; productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
@ -1983,11 +1980,11 @@
name = "Swiftfin Widget"; name = "Swiftfin Widget";
packageProductDependencies = ( packageProductDependencies = (
536D3D7C267BD5F90004248C /* ActivityIndicator */, 536D3D7C267BD5F90004248C /* ActivityIndicator */,
53649AB4269D423A00A2D8B7 /* Puppy */,
E13DD3CE27164E1F009D4DAF /* CoreStore */, E13DD3CE27164E1F009D4DAF /* CoreStore */,
E13DD3DC27175CE3009D4DAF /* Defaults */, E13DD3DC27175CE3009D4DAF /* Defaults */,
E10EAA46277BB670000269ED /* JellyfinAPI */, E10EAA46277BB670000269ED /* JellyfinAPI */,
E1D7E5A727892566009D0EF7 /* Nuke */, E1D7E5A727892566009D0EF7 /* Nuke */,
E1347DB3279E3C9E00BC6161 /* Puppy */,
); );
productName = WidgetExtensionExtension; productName = WidgetExtensionExtension;
productReference = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */; productReference = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */;
@ -2044,7 +2041,6 @@
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */,
53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */,
62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */,
E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */, E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */,
E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */,
@ -2057,6 +2053,7 @@
E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */, E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */,
E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */, E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */,
62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */, 62666E3727E502CE00EC0ECD /* XCRemoteSwiftPackageReference "SwizzleSwift" */,
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */,
); );
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -2368,7 +2365,6 @@
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */, 5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */, C4BE0764271FC0BB003F4AD1 /* TVLibrariesCoordinator.swift in Sources */,
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */, E1E5D5512783E67700692DFE /* ExperimentalSettingsView.swift in Sources */,
E1A2C160279A7DCA005EC829 /* AboutView.swift in Sources */, E1A2C160279A7DCA005EC829 /* AboutView.swift in Sources */,
C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */, C4BE076A271FC164003F4AD1 /* TVLibrariesView.swift in Sources */,
@ -2451,7 +2447,6 @@
C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */, C4AE2C3227498D6A00AE13CF /* LiveTVProgramsView.swift in Sources */,
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */, 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, 62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */, 5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */, E14F7D0926DB36F7007C3AE6 /* ItemLandscapeMainView.swift in Sources */,
E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */, E107BB9327880A8F00354E07 /* CollectionItemViewModel.swift in Sources */,
@ -2577,7 +2572,6 @@
E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */, E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */,
6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */, 6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */,
E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */, E13DD3D7271693CD009D4DAF /* SwiftfinStoreDefaults.swift in Sources */,
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */,
E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */, E13DD3CA27164B80009D4DAF /* SwiftfinStore.swift in Sources */,
E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E10EAA51277BBCC4000269ED /* CGSizeExtensions.swift in Sources */,
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */, 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */,
@ -2989,7 +2983,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = 4BHXT8RHFR; DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = WidgetExtension/Info.plist; INFOPLIST_FILE = WidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -3015,7 +3009,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 70; CURRENT_PROJECT_VERSION = 70;
DEVELOPMENT_TEAM = 4BHXT8RHFR; DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = WidgetExtension/Info.plist; INFOPLIST_FILE = WidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.0; IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@ -3083,14 +3077,6 @@
minimumVersion = 0.1.3; minimumVersion = 0.1.3;
}; };
}; };
53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sushichop/Puppy";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.2.0;
};
};
536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */ = { 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/PGSSoft/ParallaxView"; repositoryURL = "https://github.com/PGSSoft/ParallaxView";
@ -3155,6 +3141,14 @@
kind = branch; kind = branch;
}; };
}; };
E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sushichop/Puppy";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.5.0;
};
};
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = { E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CombineCommunity/CombineExt"; repositoryURL = "https://github.com/CombineCommunity/CombineExt";
@ -3163,6 +3157,14 @@
minimumVersion = 1.0.0; minimumVersion = 1.0.0;
}; };
}; };
E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/LePips/Puppy";
requirement = {
branch = main;
kind = branch;
};
};
E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */ = { E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/NukeUI"; repositoryURL = "https://github.com/kean/NukeUI";
@ -3216,21 +3218,6 @@
package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; package = 5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
productName = Introspect; productName = Introspect;
}; };
53649AAC269CFAEA00A2D8B7 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
53649AAE269CFAF600A2D8B7 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
53649AB4269D423A00A2D8B7 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
536D3D7C267BD5F90004248C /* ActivityIndicator */ = { 536D3D7C267BD5F90004248C /* ActivityIndicator */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */; package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
@ -3291,6 +3278,11 @@
package = E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */; package = E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */;
productName = Sliders; productName = Sliders;
}; };
E1101176281B1E8A006A3584 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = E1101175281B1E8A006A3584 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
E11D83AE278FA998006E9776 /* NukeUI */ = { E11D83AE278FA998006E9776 /* NukeUI */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */; package = E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */;
@ -3306,6 +3298,21 @@
package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */; package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */;
productName = CombineExt; productName = CombineExt;
}; };
E1347DB1279E3C6200BC6161 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
E1347DB3279E3C9E00BC6161 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
E1347DB5279E3CA500BC6161 /* Puppy */ = {
isa = XCSwiftPackageProductDependency;
package = E1347DB0279E3C6200BC6161 /* XCRemoteSwiftPackageReference "Puppy" */;
productName = Puppy;
};
E1361DA6278FA7A300BEC523 /* NukeUI */ = { E1361DA6278FA7A300BEC523 /* NukeUI */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */; package = E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */;

View File

@ -14,8 +14,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/Flight-School/AnyCodable", "location" : "https://github.com/Flight-School/AnyCodable",
"state" : { "state" : {
"revision" : "22f302d4c048aafcda09a4ab5b8c0b03855316fb", "revision" : "11423ef0c756e8a1f6b4bb576dab9d97bc016c70",
"version" : "0.6.3" "version" : "0.6.4"
} }
}, },
{ {
@ -50,8 +50,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/kaishin/Gifu", "location" : "https://github.com/kaishin/Gifu",
"state" : { "state" : {
"revision" : "0ffe24744cc3d82ab9edece53670d0352c6d5507", "revision" : "51f2eab32903e336f590c013267cfa4d7f8b06c4",
"version" : "3.3.0" "version" : "3.3.1"
} }
}, },
{ {
@ -68,8 +68,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Nuke.git", "location" : "https://github.com/kean/Nuke.git",
"state" : { "state" : {
"revision" : "78fa963b8491fc520791d8c2a509f1b8593d8aae", "revision" : "0ea7545b5c918285aacc044dc75048625c8257cc",
"version" : "10.7.1" "version" : "10.8.0"
} }
}, },
{ {
@ -77,8 +77,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/kean/NukeUI", "location" : "https://github.com/kean/NukeUI",
"state" : { "state" : {
"revision" : "71398392943f2538fd0f2ebc6f282920f6775b0c", "revision" : "17f26c07e6b1d3b9258287f99f528111fcd7b7ad",
"version" : "0.8.0" "version" : "0.8.1"
} }
}, },
{ {
@ -140,8 +140,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect", "location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : { "state" : {
"revision" : "2e09be8af614401bc9f87d40093ec19ce56ccaf2", "revision" : "f2616860a41f9d9932da412a8978fec79c06fe24",
"version" : "0.1.3" "version" : "0.1.4"
} }
}, },
{ {
@ -150,7 +150,7 @@
"location" : "https://github.com/spacenation/swiftui-sliders", "location" : "https://github.com/spacenation/swiftui-sliders",
"state" : { "state" : {
"branch" : "master", "branch" : "master",
"revision" : "518bed3bfc7bd522f3c49404a0d1efb98fa1bf2c" "revision" : "538e16b35ad7a066a8f5624da9ecee6327886bf7"
} }
}, },
{ {

View File

@ -19,6 +19,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
// Lazily initialize datastack // Lazily initialize datastack
_ = SwiftfinStore.dataStack _ = SwiftfinStore.dataStack
LogManager.setup()
let audioSession = AVAudioSession.sharedInstance() let audioSession = AVAudioSession.sharedInstance()
do { do {

View File

@ -48,9 +48,9 @@ struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackabl
failureView: { failureView: {
InitialFailureView(item.failureInitials) InitialFailureView(item.failureInitials)
}) })
.portraitPoster(width: maxWidth) .portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2) .shadow(radius: 4, y: 2)
.accessibilityIgnoresInvertColors() .accessibilityIgnoresInvertColors()
if item.showTitle { if item.showTitle {
Text(item.title) Text(item.title)

View File

@ -40,9 +40,9 @@ struct PortraitItemButton<ItemType: PortraitImageStackable>: View {
failureView: { failureView: {
InitialFailureView(item.failureInitials) InitialFailureView(item.failureInitials)
}) })
.portraitPoster(width: maxWidth) .portraitPoster(width: maxWidth)
.shadow(radius: 4, y: 2) .shadow(radius: 4, y: 2)
.accessibilityIgnoresInvertColors() .accessibilityIgnoresInvertColors()
if item.showTitle { if item.showTitle {
Text(item.title) Text(item.title)

View File

@ -35,6 +35,8 @@
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
@ -61,6 +63,8 @@ network.</string>
</dict> </dict>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchScreen</key> <key>UILaunchScreen</key>
<dict> <dict>
<key>UIColorName</key> <key>UIColorName</key>

View File

@ -103,7 +103,7 @@ struct ConnectToServerView: View {
} }
.alert(item: $viewModel.errorMessage) { _ in .alert(item: $viewModel.errorMessage) { _ in
Alert(title: Text(viewModel.alertTitle), Alert(title: Text(viewModel.alertTitle),
message: Text(viewModel.errorMessage?.displayMessage ?? L10n.unknownError), message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
dismissButton: .cancel()) dismissButton: .cancel())
} }
.alert(item: $viewModel.addServerURIPayload) { _ in .alert(item: $viewModel.addServerURIPayload) { _ in

View File

@ -33,13 +33,13 @@ struct ContinueWatchingView: View {
item.getSeriesThumbImage(maxWidth: 320), item.getSeriesThumbImage(maxWidth: 320),
item.getSeriesBackdropImage(maxWidth: 320), item.getSeriesBackdropImage(maxWidth: 320),
]) ])
.frame(width: 320, height: 180) .frame(width: 320, height: 180)
} else { } else {
ImageView(sources: [ ImageView(sources: [
item.getThumbImage(maxWidth: 320), item.getThumbImage(maxWidth: 320),
item.getBackdropImage(maxWidth: 320), item.getBackdropImage(maxWidth: 320),
]) ])
.frame(width: 320, height: 180) .frame(width: 320, height: 180)
} }
} }
.accessibilityIgnoresInvertColors() .accessibilityIgnoresInvertColors()

View File

@ -35,7 +35,7 @@ struct HomeView: View {
} }
Text("\(errorMessage.code)") Text("\(errorMessage.code)")
Text(errorMessage.displayMessage) Text(errorMessage.message)
.frame(minWidth: 50, maxWidth: 240) .frame(minWidth: 50, maxWidth: 240)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@ -69,7 +69,7 @@ struct ItemViewBody: View {
selectedAction: { genre in selectedAction: { genre in
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title)) itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
}) })
.padding(.bottom) .padding(.bottom)
} }
// MARK: Studios // MARK: Studios

View File

@ -58,7 +58,7 @@ struct ItemLandscapeMainView: View {
selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
} else { } else {
LogManager.shared.log.error("Attempted to play item but no playback information available") LogManager.log.error("Attempted to play item but no playback information available")
} }
} label: { } label: {
Label(L10n.playFromBeginning, systemImage: "gobackward") Label(L10n.playFromBeginning, systemImage: "gobackward")

View File

@ -118,7 +118,7 @@ struct PortraitHeaderOverlayView: View {
if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel { if let selectedVideoPlayerViewModel = viewModel.selectedVideoPlayerViewModel {
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
} else { } else {
LogManager.shared.log.error("Attempted to play item but no playback information available") LogManager.log.error("Attempted to play item but no playback information available")
} }
} label: { } label: {
HStack { HStack {
@ -142,7 +142,7 @@ struct PortraitHeaderOverlayView: View {
selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true) selectedVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel) itemRouter.route(to: \.videoPlayer, selectedVideoPlayerViewModel)
} else { } else {
LogManager.shared.log.error("Attempted to play item but no playback information available") LogManager.log.error("Attempted to play item but no playback information available")
} }
} label: { } label: {
Label(L10n.playFromBeginning, systemImage: "gobackward") Label(L10n.playFromBeginning, systemImage: "gobackward")

View File

@ -40,6 +40,8 @@ struct SettingsView: View {
var jumpGesturesEnabled var jumpGesturesEnabled
@Default(.systemControlGesturesEnabled) @Default(.systemControlGesturesEnabled)
var systemControlGesturesEnabled var systemControlGesturesEnabled
@Default(.playerGesturesLockGestureEnabled)
var playerGesturesLockGestureEnabled
@Default(.resumeOffset) @Default(.resumeOffset)
var resumeOffset var resumeOffset
@Default(.subtitleSize) @Default(.subtitleSize)
@ -111,6 +113,8 @@ struct SettingsView: View {
Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled) Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled)
Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled)
Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset) Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset)
Button { Button {

View File

@ -50,7 +50,7 @@ struct UserSignInView: View {
} }
.alert(item: $viewModel.errorMessage) { _ in .alert(item: $viewModel.errorMessage) { _ in
Alert(title: Text(viewModel.alertTitle), Alert(title: Text(viewModel.alertTitle),
message: Text(viewModel.errorMessage?.displayMessage ?? L10n.unknownError), message: Text(viewModel.errorMessage?.message ?? L10n.unknownError),
dismissButton: .cancel()) dismissButton: .cancel())
} }
.navigationTitle(L10n.signIn) .navigationTitle(L10n.signIn)

View File

@ -375,16 +375,16 @@ struct VLCPlayerOverlayView: View {
ValueSlider(value: $viewModel.sliderPercentage, onEditingChanged: { editing in ValueSlider(value: $viewModel.sliderPercentage, onEditingChanged: { editing in
viewModel.sliderIsScrubbing = editing viewModel.sliderIsScrubbing = editing
}) })
.valueSliderStyle(HorizontalValueSliderStyle(track: .valueSliderStyle(HorizontalValueSliderStyle(track:
HorizontalValueTrack(view: HorizontalValueTrack(view:
Capsule().foregroundColor(.purple)) Capsule().foregroundColor(.purple))
.background(Capsule().foregroundColor(Color.gray.opacity(0.25))) .background(Capsule().foregroundColor(Color.gray.opacity(0.25)))
.frame(height: 4), .frame(height: 4),
thumb: Circle().foregroundColor(.purple), thumb: Circle().foregroundColor(.purple),
thumbSize: CGSize.Circle(radius: viewModel.sliderIsScrubbing ? 20 : 15), thumbSize: CGSize.Circle(radius: viewModel.sliderIsScrubbing ? 20 : 15),
thumbInteractiveSize: CGSize.Circle(radius: 40), thumbInteractiveSize: CGSize.Circle(radius: 40),
options: .defaultOptions)) options: .defaultOptions))
.frame(maxHeight: 50) .frame(maxHeight: 50)
Text(viewModel.rightLabelText) Text(viewModel.rightLabelText)
.font(.system(size: 18, weight: .semibold, design: .default)) .font(.system(size: 18, weight: .semibold, design: .default))
@ -400,13 +400,11 @@ struct VLCPlayerOverlayView: View {
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }
var body: some View { @ViewBuilder
var contents: some View {
if viewModel.overlayType == .normal { if viewModel.overlayType == .normal {
mainBody mainBody
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture {
viewModel.playerOverlayDelegate?.didGenerallyTap()
}
.background { .background {
Color(uiColor: .black.withAlphaComponent(0.5)) Color(uiColor: .black.withAlphaComponent(0.5))
.ignoresSafeArea() .ignoresSafeArea()
@ -414,11 +412,22 @@ struct VLCPlayerOverlayView: View {
} else { } else {
mainBody mainBody
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture {
viewModel.playerOverlayDelegate?.didGenerallyTap()
}
} }
} }
var body: some View {
contents
.onLongPressGesture {
guard viewModel.playerGesturesLockGestureEnabled else { return }
viewModel.playerOverlayDelegate?.didGenerallyTap(point: nil)
viewModel.playerOverlayDelegate?.didLongPress()
}
.gesture(DragGesture(minimumDistance: 0)
.onEnded { value in
viewModel.playerOverlayDelegate?.didGenerallyTap(point: value.location)
})
.opacity(viewModel.isHiddenOverlay ? 0 : 1)
}
} }
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
import JellyfinAPI import JellyfinAPI
import UIKit
protocol PlayerOverlayDelegate { protocol PlayerOverlayDelegate {
@ -19,7 +20,8 @@ protocol PlayerOverlayDelegate {
func didSelectForward() func didSelectForward()
func didSelectMain() func didSelectMain()
func didGenerallyTap() func didGenerallyTap(point: CGPoint?)
func didLongPress()
func didBeginScrubbing() func didBeginScrubbing()
func didEndScrubbing() func didEndScrubbing()

View File

@ -28,6 +28,7 @@ class VLCPlayerViewController: UIViewController {
private var viewModelListeners = Set<AnyCancellable>() private var viewModelListeners = Set<AnyCancellable>()
private var overlayDismissTimer: Timer? private var overlayDismissTimer: Timer?
private var isScreenFilled: Bool = false private var isScreenFilled: Bool = false
private var isGesturesLocked = false
private var pinchScale: CGFloat = 1 private var pinchScale: CGFloat = 1
private var currentPlayerTicks: Int64 { private var currentPlayerTicks: Int64 {
@ -45,10 +46,15 @@ class VLCPlayerViewController: UIViewController {
private var panBeganBrightness = CGFloat.zero private var panBeganBrightness = CGFloat.zero
private var panBeganVolumeValue = Float.zero private var panBeganVolumeValue = Float.zero
private var panBeganPoint = CGPoint.zero private var panBeganPoint = CGPoint.zero
private var tapLocationStack = [CGPoint]()
private var isJumping = false
private var jumpingCompletionWork: DispatchWorkItem?
private var isTapWhenJumping = false
private lazy var videoContentView = makeVideoContentView() private lazy var videoContentView = makeVideoContentView()
private lazy var mainGestureView = makeMainGestureView() private lazy var mainGestureView = makeMainGestureView()
private lazy var systemControlOverlayLabel = makeSystemControlOverlayLabel() private lazy var systemControlOverlayLabel = makeSystemControlOverlayLabel()
private lazy var lockedOverlayView = makeGestureLockedOverlayView()
private var currentOverlayHostingController: UIHostingController<VLCPlayerOverlayView>? private var currentOverlayHostingController: UIHostingController<VLCPlayerOverlayView>?
private var currentChapterOverlayHostingController: UIHostingController<VLCPlayerChapterOverlayView>? private var currentChapterOverlayHostingController: UIHostingController<VLCPlayerChapterOverlayView>?
private var currentJumpBackwardOverlayView: UIImageView? private var currentJumpBackwardOverlayView: UIImageView?
@ -60,23 +66,33 @@ class VLCPlayerViewController: UIViewController {
UIKeyCommand(title: L10n.playAndPause, action: #selector(didSelectMain), input: " "), UIKeyCommand(title: L10n.playAndPause, action: #selector(didSelectMain), input: " "),
UIKeyCommand(title: L10n.jumpForward, action: #selector(didSelectForward), input: UIKeyCommand.inputRightArrow), UIKeyCommand(title: L10n.jumpForward, action: #selector(didSelectForward), input: UIKeyCommand.inputRightArrow),
UIKeyCommand(title: L10n.jumpBackward, action: #selector(didSelectBackward), input: UIKeyCommand.inputLeftArrow), UIKeyCommand(title: L10n.jumpBackward, action: #selector(didSelectBackward), input: UIKeyCommand.inputLeftArrow),
UIKeyCommand(title: L10n.nextItem, action: #selector(didSelectPlayNextItem), input: UIKeyCommand.inputRightArrow, UIKeyCommand(title: L10n.nextItem,
action: #selector(didSelectPlayNextItem),
input: UIKeyCommand.inputRightArrow,
modifierFlags: .command), modifierFlags: .command),
UIKeyCommand(title: L10n.previousItem, action: #selector(didSelectPlayPreviousItem), input: UIKeyCommand.inputLeftArrow, UIKeyCommand(title: L10n.previousItem,
action: #selector(didSelectPlayPreviousItem),
input: UIKeyCommand.inputLeftArrow,
modifierFlags: .command), modifierFlags: .command),
UIKeyCommand(title: L10n.close, action: #selector(didSelectClose), input: UIKeyCommand.inputEscape), UIKeyCommand(title: L10n.close, action: #selector(didSelectClose), input: UIKeyCommand.inputEscape),
] ]
if let previous = viewModel.playbackSpeed.previous { if let previous = viewModel.playbackSpeed.previous {
commands.append(.init(title: "\(L10n.playbackSpeed) \(previous.displayTitle)", commands.append(.init(title: "\(L10n.playbackSpeed) \(previous.displayTitle)",
action: #selector(didSelectPreviousPlaybackSpeed), input: "[", modifierFlags: .command)) action: #selector(didSelectPreviousPlaybackSpeed),
input: "[",
modifierFlags: .command))
} }
if let next = viewModel.playbackSpeed.next { if let next = viewModel.playbackSpeed.next {
commands.append(.init(title: "\(L10n.playbackSpeed) \(next.displayTitle)", action: #selector(didSelectNextPlaybackSpeed), commands.append(.init(title: "\(L10n.playbackSpeed) \(next.displayTitle)",
input: "]", modifierFlags: .command)) action: #selector(didSelectNextPlaybackSpeed),
input: "]",
modifierFlags: .command))
} }
if viewModel.playbackSpeed != .one { if viewModel.playbackSpeed != .one {
commands.append(.init(title: "\(L10n.playbackSpeed) \(PlaybackSpeed.one.displayTitle)", commands.append(.init(title: "\(L10n.playbackSpeed) \(PlaybackSpeed.one.displayTitle)",
action: #selector(didSelectNormalPlaybackSpeed), input: "\\", modifierFlags: .command)) action: #selector(didSelectNormalPlaybackSpeed),
input: "\\",
modifierFlags: .command))
} }
commands.forEach { $0.wantsPriorityOverSystemBehavior = true } commands.forEach { $0.wantsPriorityOverSystemBehavior = true }
return commands return commands
@ -102,6 +118,7 @@ class VLCPlayerViewController: UIViewController {
view.addSubview(videoContentView) view.addSubview(videoContentView)
view.addSubview(mainGestureView) view.addSubview(mainGestureView)
view.addSubview(systemControlOverlayLabel) view.addSubview(systemControlOverlayLabel)
view.addSubview(lockedOverlayView)
} }
private func setupConstraints() { private func setupConstraints() {
@ -121,6 +138,12 @@ class VLCPlayerViewController: UIViewController {
systemControlOverlayLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), systemControlOverlayLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
systemControlOverlayLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), systemControlOverlayLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
]) ])
NSLayoutConstraint.activate([
lockedOverlayView.topAnchor.constraint(equalTo: view.topAnchor),
lockedOverlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
lockedOverlayView.leftAnchor.constraint(equalTo: view.leftAnchor),
lockedOverlayView.rightAnchor.constraint(equalTo: view.rightAnchor),
])
} }
// MARK: viewWillDisappear // MARK: viewWillDisappear
@ -148,12 +171,18 @@ class VLCPlayerViewController: UIViewController {
refreshJumpForwardOverlayView(with: viewModel.jumpForwardLength) refreshJumpForwardOverlayView(with: viewModel.jumpForwardLength)
let defaultNotificationCenter = NotificationCenter.default let defaultNotificationCenter = NotificationCenter.default
defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, defaultNotificationCenter.addObserver(self,
selector: #selector(appWillTerminate),
name: UIApplication.willTerminateNotification,
object: nil)
defaultNotificationCenter.addObserver(self,
selector: #selector(appWillResignActive),
name: UIApplication.willResignActiveNotification,
object: nil)
defaultNotificationCenter.addObserver(self,
selector: #selector(appWillResignActive),
name: UIApplication.didEnterBackgroundNotification,
object: nil) object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive),
name: UIApplication.willResignActiveNotification, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive),
name: UIApplication.didEnterBackgroundNotification, object: nil)
} }
@objc @objc
@ -205,22 +234,17 @@ class VLCPlayerViewController: UIViewController {
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didRightSwipe))
rightSwipeGesture.direction = .right
let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didLeftSwipe))
leftSwipeGesture.direction = .left
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:))) let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:)))
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:))) let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
view.addGestureRecognizer(singleTapGesture) view.addGestureRecognizer(singleTapGesture)
view.addGestureRecognizer(pinchGesture) view.addGestureRecognizer(pinchGesture)
if viewModel.jumpGesturesEnabled { if viewModel.playerGesturesLockGestureEnabled {
view.addGestureRecognizer(rightSwipeGesture) view.addGestureRecognizer(longPressGesture)
view.addGestureRecognizer(leftSwipeGesture)
} }
if viewModel.systemControlGesturesEnabled { if viewModel.systemControlGesturesEnabled {
@ -237,24 +261,51 @@ class VLCPlayerViewController: UIViewController {
label.alpha = 0 label.alpha = 0
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 48) label.font = .systemFont(ofSize: 48)
label.layer.zPosition = 1
return label return label
} }
// MARK: GestureLockedOverlayView
private func makeGestureLockedOverlayView() -> UIView {
let backgroundView = UIView()
backgroundView.layer.zPosition = 1
backgroundView.alpha = 0
backgroundView.translatesAutoresizingMaskIntoConstraints = false
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
self?.isGesturesLocked = false
self?.hideLockedOverlay()
self?.didGenerallyTap()
}))
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "lock.circle.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))?
.withTintColor(.white),
for: .normal)
backgroundView.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor),
button.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor),
])
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
backgroundView.addGestureRecognizer(singleTapGesture)
return backgroundView
}
@objc @objc
private func didTap() { private func didTap(_ gestureRecognizer: UITapGestureRecognizer) {
didGenerallyTap(point: gestureRecognizer.location(in: mainGestureView))
}
@objc
func didLongPress() {
guard !isGesturesLocked else { return }
isGesturesLocked = true
didGenerallyTap() didGenerallyTap()
} }
@objc
private func didRightSwipe() {
didSelectForward()
}
@objc
private func didLeftSwipe() {
didSelectBackward()
}
@objc @objc
private func didPinch(_ gestureRecognizer: UIPinchGestureRecognizer) { private func didPinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
@ -500,11 +551,11 @@ extension VLCPlayerViewController {
viewModel = newViewModel viewModel = newViewModel
if viewModel.streamType == .direct { if viewModel.streamType == .direct {
LogManager.shared.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with direct play stream for item: \(viewModel.item.id ?? "--")")
} else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] { } else if viewModel.streamType == .transcode && Defaults[.Experimental.forceDirectPlay] {
LogManager.shared.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with forced direct stream for item: \(viewModel.item.id ?? "--")")
} else { } else {
LogManager.shared.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")") LogManager.log.debug("Player set up with transcoded stream for item: \(viewModel.item.id ?? "--")")
} }
} }
@ -587,9 +638,10 @@ extension VLCPlayerViewController {
guard let overlayHostingController = currentOverlayHostingController else { return } guard let overlayHostingController = currentOverlayHostingController else { return }
guard overlayHostingController.view.alpha != 1 else { return } guard overlayHostingController.view.alpha != 1 else { return }
overlayHostingController.view.alpha = 1
UIView.animate(withDuration: 0.2) { withAnimation(.easeInOut(duration: 0.2)) { [weak self] in
overlayHostingController.view.alpha = 1 self?.viewModel.isHiddenOverlay = false
} }
} }
@ -600,8 +652,16 @@ extension VLCPlayerViewController {
guard overlayHostingController.view.alpha != 0 else { return } guard overlayHostingController.view.alpha != 0 else { return }
UIView.animate(withDuration: 0.2) { // for gestures UX
view.exchangeSubview(at: view.subviews.firstIndex(of: mainGestureView)!,
withSubviewAt: view.subviews.firstIndex(of: overlayHostingController.view)!)
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
overlayHostingController.view.alpha = 0 overlayHostingController.view.alpha = 0
} completion: { [weak self] _ in
guard let self = self else { return }
self.view.exchangeSubview(at: self.view.subviews.firstIndex(of: self.mainGestureView)!,
withSubviewAt: self.view.subviews.firstIndex(of: overlayHostingController.view)!)
self.viewModel.isHiddenOverlay = true
} }
} }
@ -616,6 +676,36 @@ extension VLCPlayerViewController {
} }
} }
// MARK: Show/Hide Locked Overlay
extension VLCPlayerViewController {
private func showLockedOverlay() {
guard lockedOverlayView.alpha != 1 else { return }
UIView.animate(withDuration: 0.2) {
self.lockedOverlayView.alpha = 1
}
}
private func hideLockedOverlay() {
guard !UIAccessibility.isVoiceOverRunning else { return }
guard lockedOverlayView.alpha != 0 else { return }
UIView.animate(withDuration: 0.2) {
self.lockedOverlayView.alpha = 0
}
}
private func toggleLockedOverlay() {
if lockedOverlayView.alpha < 1 {
showLockedOverlay()
} else {
hideLockedOverlay()
}
}
}
// MARK: Show/Hide System Control // MARK: Show/Hide System Control
extension VLCPlayerViewController { extension VLCPlayerViewController {
@ -736,13 +826,17 @@ extension VLCPlayerViewController {
extension VLCPlayerViewController { extension VLCPlayerViewController {
private func restartOverlayDismissTimer(interval: Double = 3) { private func restartOverlayDismissTimer(interval: Double = 3) {
overlayDismissTimer?.invalidate() overlayDismissTimer?.invalidate()
overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(dismissTimerFired), overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval,
userInfo: nil, repeats: false) target: self,
selector: #selector(dismissTimerFired),
userInfo: nil,
repeats: false)
} }
@objc @objc
private func dismissTimerFired() { private func dismissTimerFired() {
hideOverlay() hideOverlay()
hideLockedOverlay()
} }
private func stopOverlayDismissTimer() { private func stopOverlayDismissTimer() {
@ -904,12 +998,65 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
} }
} }
func didGenerallyTap() { func didGenerallyTap(point: CGPoint? = nil) {
toggleOverlay() if isGesturesLocked {
toggleLockedOverlay()
} else {
if viewModel.jumpGesturesEnabled,
let point = point
{
let tempStack = tapLocationStack
tapLocationStack.append(point)
if isSameLocationWithLast(point: point, in: tempStack) {
isTapWhenJumping = false
isJumping = true
tapLocationStack.removeAll()
jumpingCompletionWork?.cancel()
jumpingCompletionWork = DispatchWorkItem(block: { [weak self] in
guard let self = self else { return }
self.isJumping = false
guard self.isTapWhenJumping else { return }
self.isTapWhenJumping = false
self.toggleOverlay()
})
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: jumpingCompletionWork!)
hideOverlay()
if point.x > (mainGestureView.frame.width / 2) {
didSelectForward()
} else {
didSelectBackward()
}
return
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
guard let self = self else { return }
guard !self.tapLocationStack.isEmpty else { return }
self.tapLocationStack.removeFirst()
}
}
}
guard !isJumping else {
isTapWhenJumping = true
return
}
toggleOverlay()
}
restartOverlayDismissTimer(interval: 5) restartOverlayDismissTimer(interval: 5)
} }
private func isSameLocationWithLast(point: CGPoint, in stack: [CGPoint]) -> Bool {
guard let last = stack.last else { return false }
if last.x > (mainGestureView.frame.width / 2) {
return point.x > (mainGestureView.frame.width / 2)
} else {
return point.x <= (mainGestureView.frame.width / 2)
}
}
func didBeginScrubbing() { func didBeginScrubbing() {
stopOverlayDismissTimer() stopOverlayDismissTimer()
} }