[create-pull-request] automated change (#47)
Co-authored-by: acvigue <acvigue@users.noreply.github.com>
This commit is contained in:
parent
9ad53092a4
commit
b26f81247a
|
@ -35,7 +35,7 @@ struct PersistenceController {
|
|||
if inMemory {
|
||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||
}
|
||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||
container.loadPersistentStores(completionHandler: { (_, error) in
|
||||
if let error = error as NSError? {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
|
|
@ -15,83 +15,83 @@ struct ConnectToServerView: View {
|
|||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
|
||||
@State private var uri = "";
|
||||
@State private var isWorking = false;
|
||||
@State private var isErrored = false;
|
||||
@State private var isDone = false;
|
||||
@State private var isSignInErrored = false;
|
||||
@State private var isConnected = false;
|
||||
@State private var serverName = "";
|
||||
@State private var usernameDisabled: Bool = false;
|
||||
@State private var publicUsers: [UserDto] = [];
|
||||
@State private var lastPublicUsers: [UserDto] = [];
|
||||
@State private var username = "";
|
||||
@State private var password = "";
|
||||
@State private var server_id = "";
|
||||
@State private var serverSkipped: Bool = false;
|
||||
@State private var serverSkippedAlert: Bool = false;
|
||||
@State private var skip_server_bool: Bool = false;
|
||||
@State private var skip_server_obj: Server!;
|
||||
|
||||
|
||||
@State private var uri = ""
|
||||
@State private var isWorking = false
|
||||
@State private var isErrored = false
|
||||
@State private var isDone = false
|
||||
@State private var isSignInErrored = false
|
||||
@State private var isConnected = false
|
||||
@State private var serverName = ""
|
||||
@State private var usernameDisabled: Bool = false
|
||||
@State private var publicUsers: [UserDto] = []
|
||||
@State private var lastPublicUsers: [UserDto] = []
|
||||
@State private var username = ""
|
||||
@State private var password = ""
|
||||
@State private var server_id = ""
|
||||
@State private var serverSkipped: Bool = false
|
||||
@State private var serverSkippedAlert: Bool = false
|
||||
@State private var skip_server_bool: Bool = false
|
||||
@State private var skip_server_obj: Server!
|
||||
|
||||
@Binding var rootIsActive: Bool
|
||||
|
||||
private var reauthDeviceID: String = "";
|
||||
private let userUUID = UUID();
|
||||
|
||||
|
||||
private var reauthDeviceID: String = ""
|
||||
private let userUUID = UUID()
|
||||
|
||||
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
|
||||
_rootIsActive = isActive
|
||||
skip_server_bool = skip_server
|
||||
skip_server_obj = skip_server_prefill
|
||||
reauthDeviceID = reauth_deviceId
|
||||
}
|
||||
|
||||
|
||||
init(isActive: Binding<Bool>) {
|
||||
_rootIsActive = isActive
|
||||
}
|
||||
|
||||
|
||||
func start() {
|
||||
if(skip_server_bool) {
|
||||
if skip_server_bool {
|
||||
uri = skip_server_obj.baseURI!
|
||||
|
||||
|
||||
UserAPI.getPublicUsers()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure(_):
|
||||
skip_server_bool = false;
|
||||
skip_server_obj = Server();
|
||||
case .failure:
|
||||
skip_server_bool = false
|
||||
skip_server_obj = Server()
|
||||
break
|
||||
}
|
||||
}, receiveValue: { response in
|
||||
publicUsers = response
|
||||
|
||||
serverSkipped = true;
|
||||
serverSkippedAlert = true;
|
||||
|
||||
serverSkipped = true
|
||||
serverSkippedAlert = true
|
||||
server_id = skip_server_obj.server_id!
|
||||
serverName = skip_server_obj.name!
|
||||
isConnected = true;
|
||||
isConnected = true
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func doLogin() {
|
||||
isWorking = true
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
|
||||
var deviceName = UIDevice.current.name;
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
var deviceName = UIDevice.current.name
|
||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
|
||||
|
||||
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
|
||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
|
||||
|
||||
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""
|
||||
print(authHeader)
|
||||
|
||||
|
||||
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
|
||||
|
||||
|
||||
let x: AuthenticateUserByName = AuthenticateUserByName(username: username, pw: password, password: nil)
|
||||
|
||||
|
||||
UserAPI.authenticateUserByName(authenticateUserByName: x)
|
||||
.sink(receiveCompletion: { completion in
|
||||
isWorking = false
|
||||
|
@ -106,29 +106,29 @@ struct ConnectToServerView: View {
|
|||
} catch _ as NSError {
|
||||
|
||||
}
|
||||
|
||||
|
||||
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
||||
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
||||
|
||||
do {
|
||||
try viewContext.execute(deleteRequest2)
|
||||
} catch _ as NSError {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
let newServer = Server(context: viewContext)
|
||||
newServer.baseURI = uri
|
||||
newServer.name = serverName
|
||||
newServer.server_id = server_id
|
||||
|
||||
|
||||
let newUser = SignedInUser(context: viewContext)
|
||||
newUser.device_uuid = userUUID.uuidString
|
||||
newUser.username = username
|
||||
newUser.user_id = response.user!.id!
|
||||
|
||||
|
||||
let keychain = KeychainSwift()
|
||||
keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)")
|
||||
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
DispatchQueue.main.async { [self] in
|
||||
|
@ -144,30 +144,30 @@ struct ConnectToServerView: View {
|
|||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if(!isConnected) {
|
||||
if !isConnected {
|
||||
Section(header: Text("Server Information")) {
|
||||
TextField("Jellyfin Server URL", text: $uri)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
Button {
|
||||
isWorking = true;
|
||||
if(!uri.contains("http")) {
|
||||
uri = "http://" + uri;
|
||||
isWorking = true
|
||||
if !uri.contains("http") {
|
||||
uri = "http://" + uri
|
||||
}
|
||||
if(uri.last == "/") {
|
||||
if uri.last == "/" {
|
||||
uri = String(uri.dropLast())
|
||||
}
|
||||
|
||||
|
||||
JellyfinAPI.basePath = uri
|
||||
SystemAPI.getPublicSystemInfo()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure(_):
|
||||
case .failure:
|
||||
isErrored = true
|
||||
isWorking = false
|
||||
break
|
||||
|
@ -176,15 +176,15 @@ struct ConnectToServerView: View {
|
|||
let server = response
|
||||
serverName = server.serverName!
|
||||
server_id = server.id!
|
||||
if(server.startupWizardCompleted!) {
|
||||
isConnected = true;
|
||||
|
||||
if server.startupWizardCompleted! {
|
||||
isConnected = true
|
||||
|
||||
UserAPI.getPublicUsers()
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure(_):
|
||||
case .failure:
|
||||
isErrored = true
|
||||
isWorking = false
|
||||
break
|
||||
|
@ -201,7 +201,7 @@ struct ConnectToServerView: View {
|
|||
HStack {
|
||||
Text("Connect")
|
||||
Spacer()
|
||||
if(isWorking == true) {
|
||||
if isWorking == true {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
@ -210,7 +210,7 @@ struct ConnectToServerView: View {
|
|||
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
|
||||
}
|
||||
} else {
|
||||
if(publicUsers.count == 0) {
|
||||
if publicUsers.count == 0 {
|
||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||
TextField("Username", text: $username)
|
||||
.disableAutocorrection(true)
|
||||
|
@ -225,7 +225,7 @@ struct ConnectToServerView: View {
|
|||
HStack {
|
||||
Text("Login")
|
||||
Spacer()
|
||||
if(isWorking) {
|
||||
if isWorking {
|
||||
ProgressView()
|
||||
}
|
||||
}
|
||||
|
@ -234,9 +234,9 @@ struct ConnectToServerView: View {
|
|||
Alert(title: Text("Error"), message: Text("Invalid credentials"), dismissButton: .default(Text("Back")))
|
||||
}
|
||||
}
|
||||
|
||||
if(serverSkipped) {
|
||||
Section() {
|
||||
|
||||
if serverSkipped {
|
||||
Section {
|
||||
Button {
|
||||
serverSkippedAlert = false
|
||||
server_id = ""
|
||||
|
@ -244,8 +244,8 @@ struct ConnectToServerView: View {
|
|||
isConnected = false
|
||||
serverSkipped = false
|
||||
} label: {
|
||||
HStack() {
|
||||
HStack() {
|
||||
HStack {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Change Server")
|
||||
}
|
||||
|
@ -254,13 +254,13 @@ struct ConnectToServerView: View {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Section() {
|
||||
Section {
|
||||
Button {
|
||||
publicUsers = lastPublicUsers
|
||||
usernameDisabled = false;
|
||||
usernameDisabled = false
|
||||
} label: {
|
||||
HStack() {
|
||||
HStack() {
|
||||
HStack {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back")
|
||||
}
|
||||
|
@ -272,9 +272,9 @@ struct ConnectToServerView: View {
|
|||
} else {
|
||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||
ForEach(publicUsers, id: \.id) { publicUser in
|
||||
HStack() {
|
||||
HStack {
|
||||
Button() {
|
||||
if(publicUser.hasPassword!) {
|
||||
if publicUser.hasPassword! {
|
||||
lastPublicUsers = publicUsers
|
||||
username = publicUser.name!
|
||||
usernameDisabled = true
|
||||
|
@ -286,10 +286,10 @@ struct ConnectToServerView: View {
|
|||
doLogin()
|
||||
}
|
||||
} label: {
|
||||
HStack() {
|
||||
HStack {
|
||||
Text(publicUser.name!).font(.subheadline).fontWeight(.semibold)
|
||||
Spacer()
|
||||
if(publicUser.primaryImageTag != "") {
|
||||
if publicUser.primaryImageTag != "" {
|
||||
LazyImage(source: URL(string: "\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)"))
|
||||
.contentMode(.aspectFill)
|
||||
.frame(width: 60, height: 60)
|
||||
|
@ -309,14 +309,14 @@ struct ConnectToServerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section() {
|
||||
|
||||
Section {
|
||||
Button() {
|
||||
lastPublicUsers = publicUsers
|
||||
publicUsers = []
|
||||
username = ""
|
||||
} label: {
|
||||
HStack() {
|
||||
HStack {
|
||||
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
||||
Spacer()
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
|
|
|
@ -17,7 +17,7 @@ struct ContentView: View {
|
|||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
|
||||
|
||||
@StateObject private var globalData = GlobalData()
|
||||
|
||||
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
|
||||
|
@ -36,16 +36,16 @@ struct ContentView: View {
|
|||
@State private var showSettingsPopover: Bool = false
|
||||
@State private var viewDidLoad: Bool = false
|
||||
@State private var loadState: Int = 2
|
||||
|
||||
|
||||
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
|
||||
|
||||
func startup() {
|
||||
if(viewDidLoad == true) {
|
||||
if viewDidLoad == true {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
viewDidLoad = true
|
||||
|
||||
|
||||
let size = UIScreen.main.bounds.size
|
||||
if size.width < size.height {
|
||||
orientationInfo.orientation = .portrait
|
||||
|
@ -72,21 +72,21 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
var deviceName = UIDevice.current.name;
|
||||
var deviceName = UIDevice.current.name
|
||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
|
||||
|
||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
|
||||
|
||||
var header = "MediaBrowser "
|
||||
header.append("Client=\"SwiftFin\", ")
|
||||
header.append("Device=\"\(deviceName)\", ")
|
||||
header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ")
|
||||
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
|
||||
header.append("Token=\"\(globalData.authToken)\"")
|
||||
|
||||
|
||||
globalData.authHeader = header
|
||||
JellyfinAPI.basePath = globalData.server.baseURI ?? ""
|
||||
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UserAPI.getCurrentUser()
|
||||
.sink(receiveCompletion: { completion in
|
||||
|
@ -97,13 +97,13 @@ struct ContentView: View {
|
|||
librariesShowRecentlyAdded = libraries.filter { element in
|
||||
return !(response.configuration?.latestItemsExcludes?.contains(element))!
|
||||
}
|
||||
|
||||
if(loadState == 1) {
|
||||
|
||||
if loadState == 1 {
|
||||
isLoading = false
|
||||
}
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
|
||||
|
||||
UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "")
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
|
@ -112,14 +112,14 @@ struct ContentView: View {
|
|||
response.items?.forEach({ item in
|
||||
library_names[item.id ?? ""] = item.name
|
||||
})
|
||||
|
||||
if(loadState == 1) {
|
||||
|
||||
if loadState == 1 {
|
||||
isLoading = false
|
||||
}
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
|
||||
|
||||
let defaults = UserDefaults.standard
|
||||
if defaults.integer(forKey: "InNetworkBandwidth") == 0 {
|
||||
defaults.setValue(40_000_000, forKey: "InNetworkBandwidth")
|
||||
|
@ -132,13 +132,13 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
if (needsToSelectServer == true) {
|
||||
if needsToSelectServer == true {
|
||||
NavigationView {
|
||||
ConnectToServerView(isActive: $needsToSelectServer)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(globalData)
|
||||
} else if (globalData.expiredCredentials == true) {
|
||||
} else if globalData.expiredCredentials == true {
|
||||
NavigationView {
|
||||
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
||||
reauth_deviceId: globalData.user.device_uuid ?? "", isActive: $globalData.expiredCredentials)
|
||||
|
@ -148,8 +148,8 @@ struct ContentView: View {
|
|||
} else {
|
||||
if !jsi.did {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
VStack() {
|
||||
if(loadState == 0) {
|
||||
VStack {
|
||||
if loadState == 0 {
|
||||
TabView(selection: $tabSelection) {
|
||||
NavigationView {
|
||||
VStack(alignment: .leading) {
|
||||
|
|
|
@ -12,33 +12,32 @@ import JellyfinAPI
|
|||
struct ProgressBar: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
|
||||
|
||||
let tl = CGPoint(x: rect.minX, y: rect.minY)
|
||||
let tr = CGPoint(x: rect.maxX, y: rect.minY)
|
||||
let br = CGPoint(x: rect.maxX, y: rect.maxY)
|
||||
let bls = CGPoint(x: rect.minX + 10, y: rect.maxY)
|
||||
let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10)
|
||||
|
||||
|
||||
path.move(to: tl)
|
||||
path.addLine(to: tr)
|
||||
path.addLine(to: br)
|
||||
path.addLine(to: bls)
|
||||
path.addRelativeArc(center: blc, radius: 10,
|
||||
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
|
||||
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct ContinueWatchingView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
|
||||
|
||||
@State private var items: [BaseItemDto] = []
|
||||
|
||||
|
||||
func onAppear() {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary,.backdrop,.thumb])
|
||||
ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
|
@ -47,12 +46,12 @@ struct ContinueWatchingView: View {
|
|||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
if(items.count > 0) {
|
||||
LazyHStack() {
|
||||
Spacer().frame(width:14)
|
||||
if items.count > 0 {
|
||||
LazyHStack {
|
||||
Spacer().frame(width: 14)
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: ItemView(item: item)) {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -70,7 +69,7 @@ struct ContinueWatchingView: View {
|
|||
.cornerRadius(10)
|
||||
.overlay(
|
||||
Group {
|
||||
if(item.type == "Episode") {
|
||||
if item.type == "Episode" {
|
||||
Text("\(item.name!)")
|
||||
.font(.caption)
|
||||
.padding(6)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
//lol can someone buy me a coffee this took forever :|
|
||||
// lol can someone buy me a coffee this took forever :|
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
@ -34,65 +34,65 @@ enum CPUModel {
|
|||
}
|
||||
|
||||
class DeviceProfileBuilder {
|
||||
public var bitrate: Int = 0;
|
||||
|
||||
public var bitrate: Int = 0
|
||||
|
||||
public func setMaxBitrate(bitrate: Int) {
|
||||
self.bitrate = bitrate
|
||||
}
|
||||
|
||||
|
||||
public func buildProfile() -> DeviceProfile {
|
||||
let maxStreamingBitrate = bitrate;
|
||||
let maxStaticBitrate = bitrate;
|
||||
let musicStreamingTranscodingBitrate = 384000;
|
||||
|
||||
//Build direct play profiles
|
||||
var directPlayProfiles: [DirectPlayProfile] = [];
|
||||
let maxStreamingBitrate = bitrate
|
||||
let maxStaticBitrate = bitrate
|
||||
let musicStreamingTranscodingBitrate = 384000
|
||||
|
||||
// Build direct play profiles
|
||||
var directPlayProfiles: [DirectPlayProfile] = []
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav", videoCodec: "h264", type: .video)]
|
||||
|
||||
//Device supports Dolby Digital (AC3, EAC3)
|
||||
if(supportsFeature(minimumSupported: .A8X)) {
|
||||
if(supportsFeature(minimumSupported: .A10)) {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1", type: .video)] //HEVC/H.264 with Dolby Digital
|
||||
|
||||
// Device supports Dolby Digital (AC3, EAC3)
|
||||
if supportsFeature(minimumSupported: .A8X) {
|
||||
if supportsFeature(minimumSupported: .A10) {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1", type: .video)] // HEVC/H.264 with Dolby Digital
|
||||
} else {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264", type: .video)] //H.264 with Dolby Digital
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264", type: .video)] // H.264 with Dolby Digital
|
||||
}
|
||||
}
|
||||
|
||||
//Device supports Dolby Vision?
|
||||
if(supportsFeature(minimumSupported: .A10X)) {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital - No Atmos - Vision
|
||||
|
||||
// Device supports Dolby Vision?
|
||||
if supportsFeature(minimumSupported: .A10X) {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] // H.264/HEVC with Dolby Digital - No Atmos - Vision
|
||||
}
|
||||
|
||||
//Device supports Dolby Atmos?
|
||||
if(supportsFeature(minimumSupported: .A12)) {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital & Atmos - Vision
|
||||
|
||||
// Device supports Dolby Atmos?
|
||||
if supportsFeature(minimumSupported: .A12) {
|
||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] // H.264/HEVC with Dolby Digital & Atmos - Vision
|
||||
}
|
||||
|
||||
//Build transcoding profiles
|
||||
var transcodingProfiles: [TranscodingProfile] = [];
|
||||
|
||||
// Build transcoding profiles
|
||||
var transcodingProfiles: [TranscodingProfile] = []
|
||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav")]
|
||||
|
||||
//Device supports Dolby Digital (AC3, EAC3)
|
||||
if(supportsFeature(minimumSupported: .A8X)) {
|
||||
if(supportsFeature(minimumSupported: .A10)) {
|
||||
|
||||
// Device supports Dolby Digital (AC3, EAC3)
|
||||
if supportsFeature(minimumSupported: .A8X) {
|
||||
if supportsFeature(minimumSupported: .A10) {
|
||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "aac,mp3,wav,eac3,ac3,flac,opus", audioCodec: "h264,hevc,hev1", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||
} else {
|
||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||
}
|
||||
}
|
||||
|
||||
//Device supports Dolby Vision?
|
||||
if(supportsFeature(minimumSupported: .A10X)) {
|
||||
|
||||
// Device supports Dolby Vision?
|
||||
if supportsFeature(minimumSupported: .A10X) {
|
||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||
}
|
||||
|
||||
//Device supports Dolby Atmos?
|
||||
if(supportsFeature(minimumSupported: .A12)) {
|
||||
|
||||
// Device supports Dolby Atmos?
|
||||
if supportsFeature(minimumSupported: .A12) {
|
||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
|
||||
}
|
||||
|
||||
|
||||
var codecProfiles: [CodecProfile] = []
|
||||
|
||||
|
||||
let h264CodecConditions: [ProfileCondition] = [
|
||||
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
|
||||
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline", isRequired: false),
|
||||
|
@ -103,13 +103,13 @@ class DeviceProfileBuilder {
|
|||
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "main|main 10", isRequired: false),
|
||||
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "160", isRequired: false),
|
||||
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)]
|
||||
|
||||
|
||||
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
|
||||
|
||||
if(supportsFeature(minimumSupported: .A10)) {
|
||||
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions,codec: "hevc"))
|
||||
|
||||
if supportsFeature(minimumSupported: .A10) {
|
||||
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc"))
|
||||
}
|
||||
|
||||
|
||||
var subtitleProfiles: [SubtitleProfile] = []
|
||||
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
|
||||
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
|
||||
|
@ -121,12 +121,12 @@ class DeviceProfileBuilder {
|
|||
subtitleProfiles.append(SubtitleProfile(format: "pgs", method: .embed))
|
||||
|
||||
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
|
||||
|
||||
|
||||
let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate, musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate, directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [], codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles)
|
||||
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
|
||||
private func supportsFeature(minimumSupported: CPUModel) -> Bool {
|
||||
let intValues: [CPUModel: Int] = [.A4: 1, .A5: 2, .A5X: 3, .A6: 4, .A6X: 5, .A7: 6, .A7X: 7, .A8: 8, .A8X: 9, .A9: 10, .A9X: 11, .A10: 12, .A10X: 13, .A11: 14, .A12: 15, .A12X: 16, .A12Z: 16, .A13: 17, .A14: 18, .A99: 99]
|
||||
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0
|
||||
|
@ -147,7 +147,7 @@ class DeviceProfileBuilder {
|
|||
uname(&systemInfo)
|
||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
||||
guard let value = element.value as? Int8 , value != 0 else { return identifier }
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
#endif
|
||||
|
@ -189,10 +189,10 @@ class DeviceProfileBuilder {
|
|||
case "iPad7,1", "iPad7,2": return .A10X
|
||||
case "iPad7,3", "iPad7,4": return .A10X
|
||||
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
|
||||
case "iPad8,1", "iPad8,2" ,"iPad8,3", "iPad8,4": return .A12X
|
||||
case "iPad8,5", "iPad8,6" ,"iPad8,7", "iPad8,8": return .A12X
|
||||
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return .A12X
|
||||
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return .A12X
|
||||
case "iPad8,9", "iPad8,10", "iPad8,11", "iPad8,12": return .A12Z
|
||||
case "iPad11,3", "iPad11,4" ,"iPad11,6", "iPad11,7": return .A12
|
||||
case "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12
|
||||
case "iPad13,1", "iPad13,2": return .A14
|
||||
case "AppleTV5,3": return .A8
|
||||
case "AppleTV6,2": return .A10X
|
||||
|
|
|
@ -13,7 +13,7 @@ struct EpisodeItemView: View {
|
|||
@EnvironmentObject private var globalData: GlobalData
|
||||
@EnvironmentObject private var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
||||
|
||||
|
||||
var item: BaseItemDto
|
||||
|
||||
@State private var settingState: Bool = true
|
||||
|
@ -200,7 +200,7 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(item.people!, id: \.self) { person in
|
||||
if(person.type! == "Actor") {
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(withPerson: person)
|
||||
}) {
|
||||
|
@ -399,7 +399,7 @@ struct EpisodeItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(item.people!, id: \.self) { person in
|
||||
if(person.type! == "Actor") {
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(withPerson: person)
|
||||
}) {
|
||||
|
|
|
@ -10,26 +10,26 @@ import Introspect
|
|||
import JellyfinAPI
|
||||
|
||||
class VideoPlayerItem: ObservableObject {
|
||||
@Published var shouldShowPlayer: Bool = false;
|
||||
@Published var itemToPlay: BaseItemDto = BaseItemDto();
|
||||
@Published var shouldShowPlayer: Bool = false
|
||||
@Published var itemToPlay: BaseItemDto = BaseItemDto()
|
||||
}
|
||||
|
||||
struct ItemView: View {
|
||||
@EnvironmentObject private var globalData: GlobalData
|
||||
private var item: BaseItemDto;
|
||||
|
||||
private var item: BaseItemDto
|
||||
|
||||
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
|
||||
@State private var videoIsLoading: Bool = false; //This variable is only changed by the underlying VLC view.
|
||||
@State private var isLoading: Bool = false;
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
@State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view.
|
||||
@State private var isLoading: Bool = false
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if(videoPlayerItem.shouldShowPlayer) {
|
||||
if videoPlayerItem.shouldShowPlayer {
|
||||
LoadingViewNoBlur(isShowing: $videoIsLoading) {
|
||||
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
||||
}.navigationBarHidden(true)
|
||||
|
@ -42,13 +42,13 @@ struct ItemView: View {
|
|||
.supportedOrientations(.landscape)
|
||||
} else {
|
||||
VStack {
|
||||
if(item.type == "Movie") {
|
||||
if item.type == "Movie" {
|
||||
MovieItemView(item: item)
|
||||
} else if(item.type == "Season") {
|
||||
} else if item.type == "Season" {
|
||||
SeasonItemView(item: item)
|
||||
} else if(item.type == "Series") {
|
||||
} else if item.type == "Series" {
|
||||
SeriesItemView(item: item)
|
||||
} else if(item.type == "Episode") {
|
||||
} else if item.type == "Episode" {
|
||||
EpisodeItemView(item: item)
|
||||
} else {
|
||||
Text("Type: \(item.type ?? "") not implemented yet :(")
|
||||
|
|
|
@ -19,11 +19,11 @@ class OrientationInfo: ObservableObject {
|
|||
case portrait
|
||||
case landscape
|
||||
}
|
||||
|
||||
@Published var orientation: Orientation = .portrait;
|
||||
|
||||
|
||||
@Published var orientation: Orientation = .portrait
|
||||
|
||||
private var _observer: NSObjectProtocol?
|
||||
|
||||
|
||||
init() {
|
||||
_observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [weak self] note in
|
||||
guard let device = note.object as? UIDevice else {
|
||||
|
@ -31,13 +31,12 @@ class OrientationInfo: ObservableObject {
|
|||
}
|
||||
if device.orientation.isPortrait {
|
||||
self?.orientation = .portrait
|
||||
}
|
||||
else if device.orientation.isLandscape {
|
||||
} else if device.orientation.isLandscape {
|
||||
self?.orientation = .landscape
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
if let observer = _observer {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
|
@ -52,7 +51,7 @@ extension View {
|
|||
}
|
||||
|
||||
struct HostingWindowFinder: UIViewRepresentable {
|
||||
var callback: (UIWindow?) -> ()
|
||||
var callback: (UIWindow?) -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
|
@ -89,7 +88,7 @@ struct ViewPreferenceKey: PreferenceKey {
|
|||
struct SupportedOrientationsPreferenceKey: PreferenceKey {
|
||||
typealias Value = UIInterfaceOrientationMask
|
||||
static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown
|
||||
|
||||
|
||||
static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) {
|
||||
// use the most restrictive set from the stack
|
||||
value.formIntersection(nextValue())
|
||||
|
@ -129,27 +128,27 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
|||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
_prefersHomeIndicatorAutoHidden
|
||||
}
|
||||
|
||||
|
||||
// MARK: Lock orientation
|
||||
|
||||
|
||||
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
|
||||
didSet {
|
||||
UIViewController.attemptRotationToDeviceOrientation();
|
||||
if(_orientations == .landscape) {
|
||||
let value = UIInterfaceOrientation.landscapeRight.rawValue;
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
if _orientations == .landscape {
|
||||
let value = UIInterfaceOrientation.landscapeRight.rawValue
|
||||
UIDevice.current.setValue(value, forKey: "orientation")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
_orientations
|
||||
}
|
||||
|
||||
|
||||
public var _viewPreference: UIUserInterfaceStyle = .unspecified {
|
||||
didSet {
|
||||
overrideUserInterfaceStyle = _viewPreference
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
@ -157,12 +156,12 @@ extension View {
|
|||
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View {
|
||||
preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value)
|
||||
}
|
||||
|
||||
|
||||
func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View {
|
||||
// When rendered, export the requested orientations upward to Root
|
||||
preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations)
|
||||
}
|
||||
|
||||
|
||||
func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View {
|
||||
// When rendered, export the requested orientations upward to Root
|
||||
preference(key: ViewPreferenceKey.self, value: viewPreference)
|
||||
|
@ -173,14 +172,14 @@ extension View {
|
|||
struct JellyfinPlayerApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
@StateObject private var jsi = justSignedIn()
|
||||
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.environmentObject(OrientationInfo())
|
||||
.environmentObject(jsi)
|
||||
.withHostingWindow() { window in
|
||||
.withHostingWindow { window in
|
||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(OrientationInfo()).environmentObject(jsi))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,23 +11,23 @@ import JellyfinAPI
|
|||
|
||||
struct LatestMediaView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
|
||||
|
||||
@State var items: [BaseItemDto] = []
|
||||
private var library_id: String = "";
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
private var library_id: String = ""
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
init(usingParentID: String) {
|
||||
library_id = usingParentID;
|
||||
library_id = usingParentID
|
||||
}
|
||||
|
||||
|
||||
func onAppear() {
|
||||
if(viewDidLoad == true) {
|
||||
if viewDidLoad == true {
|
||||
return
|
||||
}
|
||||
viewDidLoad = true;
|
||||
|
||||
viewDidLoad = true
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], enableUserData: true, limit: 12)
|
||||
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
|
@ -36,16 +36,16 @@ struct LatestMediaView: View {
|
|||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack() {
|
||||
Spacer().frame(width:16)
|
||||
LazyHStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(items, id: \.id) { item in
|
||||
if(item.type == "Series" || item.type == "Movie") {
|
||||
if item.type == "Series" || item.type == "Movie" {
|
||||
NavigationLink(destination: ItemView(item: item)) {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer().frame(height:10)
|
||||
Spacer().frame(height: 10)
|
||||
LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
|
||||
.placeholderAndFailure {
|
||||
Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
|
||||
|
@ -55,7 +55,7 @@ struct LatestMediaView: View {
|
|||
}
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
Spacer().frame(height:5)
|
||||
Spacer().frame(height: 5)
|
||||
Text(item.seriesName ?? item.name ?? "")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
|
|
|
@ -10,26 +10,26 @@ import SwiftUI
|
|||
|
||||
struct LibraryListView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
|
||||
|
||||
@State var library_ids: [String] = ["favorites", "genres"]
|
||||
@State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"]
|
||||
var libraries: [String: String] = [:] //input libraries
|
||||
var libraries: [String: String] = [:] // input libraries
|
||||
var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [.descending], sortBy: ["SortName"])
|
||||
|
||||
|
||||
init(libraries: [String: String]) {
|
||||
self.libraries = libraries
|
||||
}
|
||||
|
||||
|
||||
func onAppear() {
|
||||
if(library_ids.count == 2) {
|
||||
libraries.forEach() { k,v in
|
||||
if library_ids.count == 2 {
|
||||
libraries.forEach { k, v in
|
||||
print("\(k): \(v)")
|
||||
_library_ids.wrappedValue.append(k)
|
||||
_library_names.wrappedValue[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
List(library_ids, id: \.self) { key in
|
||||
switch key {
|
||||
|
|
|
@ -12,13 +12,13 @@ import JellyfinAPI
|
|||
struct LibrarySearchView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
|
||||
|
||||
@State private var items: [BaseItemDto] = []
|
||||
@State private var searchQuery: String = ""
|
||||
@State private var isLoading: Bool = false
|
||||
private var usingParentID: String = ""
|
||||
@State private var lastSearchTime: Double = CACurrentMediaTime()
|
||||
|
||||
|
||||
init(usingParentID: String) {
|
||||
self.usingParentID = usingParentID
|
||||
}
|
||||
|
@ -27,12 +27,12 @@ struct LibrarySearchView: View {
|
|||
recalcTracks()
|
||||
requestSearch(query: "")
|
||||
}
|
||||
|
||||
|
||||
func requestSearch(query: String) {
|
||||
isLoading = true
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], includeItemTypes: ["Movie","Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
|
||||
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
|
@ -42,8 +42,8 @@ struct LibrarySearchView: View {
|
|||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: tracks for grid
|
||||
|
||||
// MARK: tracks for grid
|
||||
@State private var tracks: [GridItem] = []
|
||||
func recalcTracks() {
|
||||
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||
|
@ -57,12 +57,12 @@ struct LibrarySearchView: View {
|
|||
VStack {
|
||||
Spacer().frame(height: 6)
|
||||
SearchBar(text: $searchQuery)
|
||||
if(isLoading == true) {
|
||||
if isLoading == true {
|
||||
Spacer()
|
||||
ProgressView()
|
||||
Spacer()
|
||||
} else {
|
||||
if(!items.isEmpty) {
|
||||
if !items.isEmpty {
|
||||
ScrollView(.vertical) {
|
||||
Spacer().frame(height: 16)
|
||||
LazyVGrid(columns: tracks) {
|
||||
|
@ -105,7 +105,7 @@ struct LibrarySearchView: View {
|
|||
.onAppear(perform: onAppear)
|
||||
.navigationBarTitle("Search", displayMode: .inline)
|
||||
.onChange(of: searchQuery) { query in
|
||||
if(CACurrentMediaTime() - lastSearchTime > 0.5) {
|
||||
if CACurrentMediaTime() - lastSearchTime > 0.5 {
|
||||
lastSearchTime = CACurrentMediaTime()
|
||||
requestSearch(query: query)
|
||||
}
|
||||
|
@ -113,4 +113,4 @@ struct LibrarySearchView: View {
|
|||
}
|
||||
}
|
||||
|
||||
//stream NM5 by nicki!
|
||||
// stream NM5 by nicki!
|
||||
|
|
|
@ -13,10 +13,10 @@ import JellyfinAPI
|
|||
struct LibraryView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
|
||||
|
||||
@State private var items: [BaseItemDto] = []
|
||||
@State private var isLoading: Bool = false
|
||||
|
||||
|
||||
var usingParentID: String = ""
|
||||
var title: String = ""
|
||||
var filters: LibraryFilters = LibraryFilters()
|
||||
|
@ -24,52 +24,52 @@ struct LibraryView: View {
|
|||
var genre: String = ""
|
||||
var studio: String = ""
|
||||
|
||||
@State private var totalPages: Int = 0;
|
||||
@State private var currentPage: Int = 0;
|
||||
@State private var isSearching: String? = "";
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
@State private var totalPages: Int = 0
|
||||
@State private var currentPage: Int = 0
|
||||
@State private var isSearching: String? = ""
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
init(usingParentID: String, title: String) {
|
||||
self.usingParentID = usingParentID
|
||||
self.title = title
|
||||
}
|
||||
|
||||
|
||||
init(usingParentID: String, title: String, usingFilters: LibraryFilters) {
|
||||
self.usingParentID = usingParentID
|
||||
self.title = title
|
||||
self.filters = usingFilters
|
||||
}
|
||||
|
||||
|
||||
init(withPerson: BaseItemPerson) {
|
||||
self.usingParentID = ""
|
||||
self.title = withPerson.name ?? ""
|
||||
self.personId = withPerson.id!
|
||||
}
|
||||
|
||||
|
||||
init(withGenre: NameGuidPair) {
|
||||
self.usingParentID = ""
|
||||
self.title = withGenre.name ?? ""
|
||||
self.genre = withGenre.id ?? ""
|
||||
}
|
||||
|
||||
|
||||
init(withStudio: NameGuidPair) {
|
||||
self.usingParentID = ""
|
||||
self.title = withStudio.name ?? ""
|
||||
self.studio = withStudio.id ?? ""
|
||||
}
|
||||
|
||||
|
||||
func onAppear() {
|
||||
recalcTracks()
|
||||
|
||||
if(viewDidLoad) {
|
||||
|
||||
if viewDidLoad {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
isLoading = true
|
||||
items = []
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], includeItemTypes: ["Movie","Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
|
||||
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
isLoading = false
|
||||
|
@ -83,8 +83,8 @@ struct LibraryView: View {
|
|||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: tracks for grid
|
||||
|
||||
// MARK: tracks for grid
|
||||
@State private var tracks: [GridItem] = []
|
||||
func recalcTracks() {
|
||||
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||
|
@ -96,10 +96,10 @@ struct LibraryView: View {
|
|||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if(isLoading == true) {
|
||||
if isLoading == true {
|
||||
ProgressView()
|
||||
} else {
|
||||
if(!items.isEmpty) {
|
||||
if !items.isEmpty {
|
||||
VStack {
|
||||
ScrollView(.vertical) {
|
||||
Spacer().frame(height: 16)
|
||||
|
@ -132,10 +132,10 @@ struct LibraryView: View {
|
|||
}.onChange(of: orientationInfo.orientation) { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
if(totalPages > 1) {
|
||||
HStack() {
|
||||
if totalPages > 1 {
|
||||
HStack {
|
||||
Spacer()
|
||||
HStack() {
|
||||
HStack {
|
||||
Button {
|
||||
currentPage = currentPage - 1
|
||||
onAppear()
|
||||
|
@ -169,7 +169,7 @@ struct LibraryView: View {
|
|||
.navigationBarTitle(title, displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
if(currentPage > 0) {
|
||||
if currentPage > 0 {
|
||||
Button {
|
||||
currentPage = currentPage - 1
|
||||
onAppear()
|
||||
|
@ -177,7 +177,7 @@ struct LibraryView: View {
|
|||
Image(systemName: "chevron.left")
|
||||
}
|
||||
}
|
||||
if(currentPage < totalPages - 1) {
|
||||
if currentPage < totalPages - 1 {
|
||||
Button {
|
||||
currentPage = currentPage + 1
|
||||
onAppear()
|
||||
|
@ -185,7 +185,7 @@ struct LibraryView: View {
|
|||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
if(usingParentID != "") {
|
||||
if usingParentID != "" {
|
||||
NavigationLink(destination: LibrarySearchView(usingParentID: usingParentID)) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
|
@ -195,4 +195,4 @@ struct LibraryView: View {
|
|||
}
|
||||
}
|
||||
|
||||
//stream BM^S by nicki!
|
||||
// stream BM^S by nicki!
|
||||
|
|
|
@ -12,15 +12,15 @@ struct LoadingView<Content>: View where Content: View {
|
|||
@Binding var isShowing: Bool // should the modal be visible?
|
||||
var content: () -> Content
|
||||
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
GeometryReader { _ in
|
||||
ZStack(alignment: .center) {
|
||||
// the content to display - if the modal is showing, we'll blur it
|
||||
content()
|
||||
.disabled(isShowing)
|
||||
.blur(radius: isShowing ? 2 : 0)
|
||||
|
||||
|
||||
// all contents inside here will only be shown when isShowing is true
|
||||
if isShowing {
|
||||
// this Rectangle is a semi-transparent black overlay
|
||||
|
@ -30,7 +30,7 @@ struct LoadingView<Content>: View where Content: View {
|
|||
|
||||
// the magic bit - our ProgressView just displays an activity
|
||||
// indicator, with some text underneath showing what we are doing
|
||||
HStack() {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Spacer()
|
||||
|
@ -51,14 +51,14 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
|||
@Binding var isShowing: Bool // should the modal be visible?
|
||||
var content: () -> Content
|
||||
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
GeometryReader { _ in
|
||||
ZStack(alignment: .center) {
|
||||
// the content to display - if the modal is showing, we'll blur it
|
||||
content()
|
||||
.disabled(isShowing)
|
||||
|
||||
|
||||
// all contents inside here will only be shown when isShowing is true
|
||||
if isShowing {
|
||||
// this Rectangle is a semi-transparent black overlay
|
||||
|
@ -68,7 +68,7 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
|||
|
||||
// the magic bit - our ProgressView just displays an activity
|
||||
// indicator, with some text underneath showing what we are doing
|
||||
HStack() {
|
||||
HStack {
|
||||
ProgressView()
|
||||
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||
Spacer()
|
||||
|
@ -83,4 +83,3 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -200,7 +200,7 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(item.people!, id: \.self) { person in
|
||||
if(person.type! == "Actor") {
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(withPerson: person)
|
||||
}) {
|
||||
|
@ -399,7 +399,7 @@ struct MovieItemView: View {
|
|||
HStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(item.people!, id: \.self) { person in
|
||||
if(person.type! == "Actor") {
|
||||
if person.type! == "Actor" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(withPerson: person)
|
||||
}) {
|
||||
|
|
|
@ -11,18 +11,18 @@ import JellyfinAPI
|
|||
|
||||
struct NextUpView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
|
||||
|
||||
@State private var items: [BaseItemDto] = []
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
func onAppear() {
|
||||
if(viewDidLoad == true) {
|
||||
if viewDidLoad == true {
|
||||
return
|
||||
}
|
||||
viewDidLoad = true;
|
||||
|
||||
viewDidLoad = true
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people])
|
||||
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
|
@ -31,17 +31,17 @@ struct NextUpView: View {
|
|||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if(items.count != 0) {
|
||||
if items.count != 0 {
|
||||
Text("Next Up")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack() {
|
||||
Spacer().frame(width:16)
|
||||
LazyHStack {
|
||||
Spacer().frame(width: 16)
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: ItemView(item: item)) {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -54,7 +54,7 @@ struct NextUpView: View {
|
|||
}
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
Spacer().frame(height:5)
|
||||
Spacer().frame(height: 5)
|
||||
Text(item.seriesName!)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
|
@ -66,7 +66,7 @@ struct NextUpView: View {
|
|||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}.frame(width: 100)
|
||||
Spacer().frame(width:16)
|
||||
Spacer().frame(width: 16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ struct PersistenceController {
|
|||
let result = PersistenceController(inMemory: true)
|
||||
let viewContext = result.container.viewContext
|
||||
|
||||
|
||||
do {
|
||||
try viewContext.save()
|
||||
} catch {
|
||||
|
@ -32,11 +31,11 @@ struct PersistenceController {
|
|||
container = NSPersistentCloudKitContainer(name: "Model")
|
||||
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: "group.me.vigue.jellyfin.mobileclient")!.appendingPathComponent("\(container.name).sqlite"))]
|
||||
|
||||
|
||||
if inMemory {
|
||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||
}
|
||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||
container.loadPersistentStores(completionHandler: { (_, error) in
|
||||
if let error = error as NSError? {
|
||||
// Replace this implementation with code to handle the error appropriately.
|
||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||
|
|
|
@ -8,15 +8,15 @@
|
|||
*/
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
struct SearchBar: View {
|
||||
@Binding var text: String
|
||||
|
||||
|
||||
@State private var isEditing = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
|
||||
|
||||
TextField("Search ...", text: $text)
|
||||
.padding(7)
|
||||
.padding(.horizontal, 25)
|
||||
|
@ -26,12 +26,12 @@ struct SearchBar: View {
|
|||
.onTapGesture {
|
||||
self.isEditing = true
|
||||
}
|
||||
|
||||
|
||||
if isEditing {
|
||||
Button(action: {
|
||||
self.isEditing = false
|
||||
self.text = ""
|
||||
|
||||
|
||||
}) {
|
||||
Text("Cancel")
|
||||
}
|
||||
|
|
|
@ -12,24 +12,24 @@ import JellyfinAPI
|
|||
struct SeasonItemView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
|
||||
|
||||
var item: BaseItemDto = BaseItemDto()
|
||||
@State private var episodes: [BaseItemDto] = []
|
||||
|
||||
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
|
||||
func onAppear() {
|
||||
if(viewDidLoad) {
|
||||
if viewDidLoad {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], seasonId: item.id ?? "")
|
||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
isLoading = false
|
||||
|
|
|
@ -12,23 +12,23 @@ import JellyfinAPI
|
|||
struct SeriesItemView: View {
|
||||
@EnvironmentObject private var globalData: GlobalData
|
||||
@EnvironmentObject private var orientationInfo: OrientationInfo
|
||||
|
||||
var item: BaseItemDto;
|
||||
|
||||
@State private var seasons: [BaseItemDto] = [];
|
||||
@State private var isLoading: Bool = true;
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
|
||||
var item: BaseItemDto
|
||||
|
||||
@State private var seasons: [BaseItemDto] = []
|
||||
@State private var isLoading: Bool = true
|
||||
@State private var viewDidLoad: Bool = false
|
||||
|
||||
func onAppear() {
|
||||
recalcTracks()
|
||||
if(viewDidLoad) {
|
||||
return;
|
||||
if viewDidLoad {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
isLoading = true
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], isMissing: false)
|
||||
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], isMissing: false)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
|
@ -40,17 +40,16 @@ struct SeriesItemView: View {
|
|||
}
|
||||
}
|
||||
|
||||
//MARK: Grid tracks
|
||||
// MARK: Grid tracks
|
||||
func recalcTracks() {
|
||||
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125));
|
||||
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||
tracks = []
|
||||
for _ in (0..<trkCnt)
|
||||
{
|
||||
for _ in (0..<trkCnt) {
|
||||
tracks.append(GridItem.init(.flexible()))
|
||||
}
|
||||
}
|
||||
@State private var tracks: [GridItem] = []
|
||||
|
||||
|
||||
var body: some View {
|
||||
LoadingView(isShowing: $isLoading) {
|
||||
ScrollView(.vertical) {
|
||||
|
@ -66,7 +65,7 @@ struct SeriesItemView: View {
|
|||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.frame(width:100, height: 150)
|
||||
.frame(width: 100, height: 150)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
Text(season.name ?? "")
|
||||
|
@ -74,7 +73,7 @@ struct SeriesItemView: View {
|
|||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
if(season.productionYear != nil) {
|
||||
if season.productionYear != nil {
|
||||
Text(String(season.productionYear!))
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
|
@ -84,7 +83,7 @@ struct SeriesItemView: View {
|
|||
}
|
||||
}
|
||||
Spacer().frame(height: 2)
|
||||
}.onChange(of: orientationInfo.orientation) { ip in
|
||||
}.onChange(of: orientationInfo.orientation) { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,10 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var jsi: justSignedIn
|
||||
|
||||
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Binding var close: Bool
|
||||
|
|
|
@ -11,21 +11,21 @@ import JellyfinAPI
|
|||
import MediaPlayer
|
||||
|
||||
struct Subtitle {
|
||||
var name: String;
|
||||
var id: Int32;
|
||||
var url: URL;
|
||||
var delivery: SubtitleDeliveryMethod;
|
||||
var codec: String;
|
||||
var name: String
|
||||
var id: Int32
|
||||
var url: URL
|
||||
var delivery: SubtitleDeliveryMethod
|
||||
var codec: String
|
||||
}
|
||||
|
||||
struct AudioTrack {
|
||||
var name: String;
|
||||
var id: Int32;
|
||||
var name: String
|
||||
var id: Int32
|
||||
}
|
||||
|
||||
class PlaybackItem: ObservableObject {
|
||||
@Published var videoType: PlayMethod = .directPlay;
|
||||
@Published var videoUrl: URL = URL(string: "https://example.com")!;
|
||||
@Published var videoType: PlayMethod = .directPlay
|
||||
@Published var videoUrl: URL = URL(string: "https://example.com")!
|
||||
}
|
||||
|
||||
protocol PlayerViewControllerDelegate: AnyObject {
|
||||
|
@ -37,10 +37,10 @@ protocol PlayerViewControllerDelegate: AnyObject {
|
|||
class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate {
|
||||
|
||||
weak var delegate: PlayerViewControllerDelegate?
|
||||
|
||||
|
||||
var mediaPlayer = VLCMediaPlayer()
|
||||
var globalData = GlobalData()
|
||||
|
||||
|
||||
@IBOutlet weak var timeText: UILabel!
|
||||
@IBOutlet weak var videoContentView: UIView!
|
||||
@IBOutlet weak var videoControlsView: UIView!
|
||||
|
@ -49,121 +49,120 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
|||
@IBOutlet weak var jumpBackButton: UIButton!
|
||||
@IBOutlet weak var jumpForwardButton: UIButton!
|
||||
@IBOutlet weak var playerSettingsButton: UIButton!
|
||||
|
||||
var shouldShowLoadingScreen: Bool = false;
|
||||
var ssTargetValueOffset: Int = 0;
|
||||
var ssStartValue: Int = 0;
|
||||
var optionsVC: VideoPlayerSettingsView?;
|
||||
|
||||
var paused: Bool = true;
|
||||
var lastTime: Float = 0.0;
|
||||
var startTime: Int = 0;
|
||||
var controlsAppearTime: Double = 0;
|
||||
|
||||
|
||||
var shouldShowLoadingScreen: Bool = false
|
||||
var ssTargetValueOffset: Int = 0
|
||||
var ssStartValue: Int = 0
|
||||
var optionsVC: VideoPlayerSettingsView?
|
||||
|
||||
var paused: Bool = true
|
||||
var lastTime: Float = 0.0
|
||||
var startTime: Int = 0
|
||||
var controlsAppearTime: Double = 0
|
||||
|
||||
var selectedAudioTrack: Int32 = -1 {
|
||||
didSet {
|
||||
print(selectedAudioTrack)
|
||||
}
|
||||
};
|
||||
}
|
||||
var selectedCaptionTrack: Int32 = -1 {
|
||||
didSet {
|
||||
print(selectedCaptionTrack)
|
||||
}
|
||||
}
|
||||
var playSessionId: String = "";
|
||||
var lastProgressReportTime: Double = 0;
|
||||
|
||||
var subtitleTrackArray: [Subtitle] = [];
|
||||
var audioTrackArray: [AudioTrack] = [];
|
||||
|
||||
var manifest: BaseItemDto = BaseItemDto();
|
||||
var playbackItem = PlaybackItem();
|
||||
var playSessionId: String = ""
|
||||
var lastProgressReportTime: Double = 0
|
||||
|
||||
var subtitleTrackArray: [Subtitle] = []
|
||||
var audioTrackArray: [AudioTrack] = []
|
||||
|
||||
var manifest: BaseItemDto = BaseItemDto()
|
||||
var playbackItem = PlaybackItem()
|
||||
|
||||
@IBAction func seekSliderStart(_ sender: Any) {
|
||||
sendProgressReport(eventName: "pause")
|
||||
mediaPlayer.pause()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func seekSliderValueChanged(_ sender: Any) {
|
||||
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))/1000
|
||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration);
|
||||
let scrubRemaining = videoDuration - secondsScrubbedTo;
|
||||
let remainingTime = scrubRemaining;
|
||||
let hours = floor(remainingTime / 3600);
|
||||
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60;
|
||||
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60);
|
||||
if(hours != 0) {
|
||||
timeText.text = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||
let scrubRemaining = videoDuration - secondsScrubbedTo
|
||||
let remainingTime = scrubRemaining
|
||||
let hours = floor(remainingTime / 3600)
|
||||
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60
|
||||
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60)
|
||||
if hours != 0 {
|
||||
timeText.text = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||
} else {
|
||||
timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||
timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func seekSliderEnd(_ sender: Any) {
|
||||
print("ss end")
|
||||
let videoPosition = Double(mediaPlayer.time.intValue)
|
||||
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))
|
||||
//Scrub is value from 0..1 - find position in video and add / or remove.
|
||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration);
|
||||
let offset = secondsScrubbedTo - videoPosition;
|
||||
// Scrub is value from 0..1 - find position in video and add / or remove.
|
||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||
let offset = secondsScrubbedTo - videoPosition
|
||||
mediaPlayer.play()
|
||||
if(offset > 0) {
|
||||
mediaPlayer.jumpForward(Int32(offset)/1000);
|
||||
if offset > 0 {
|
||||
mediaPlayer.jumpForward(Int32(offset)/1000)
|
||||
} else {
|
||||
mediaPlayer.jumpBackward(Int32(abs(offset))/1000);
|
||||
mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
|
||||
}
|
||||
sendProgressReport(eventName: "unpause")
|
||||
}
|
||||
|
||||
|
||||
@IBAction func exitButtonPressed(_ sender: Any) {
|
||||
sendStopReport()
|
||||
mediaPlayer.stop()
|
||||
delegate?.exitPlayer(self)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func controlViewTapped(_ sender: Any) {
|
||||
videoControlsView.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
@IBAction func contentViewTapped(_ sender: Any) {
|
||||
videoControlsView.isHidden = false
|
||||
controlsAppearTime = CACurrentMediaTime()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func jumpBackTapped(_ sender: Any) {
|
||||
if(paused == false) {
|
||||
if paused == false {
|
||||
mediaPlayer.jumpBackward(15)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func jumpForwardTapped(_ sender: Any) {
|
||||
if(paused == false) {
|
||||
if paused == false {
|
||||
mediaPlayer.jumpForward(30)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBOutlet weak var mainActionButton: UIButton!
|
||||
@IBAction func mainActionButtonPressed(_ sender: Any) {
|
||||
print(mediaPlayer.state.rawValue)
|
||||
if(paused) {
|
||||
if paused {
|
||||
mediaPlayer.play()
|
||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||
paused = false;
|
||||
paused = false
|
||||
} else {
|
||||
mediaPlayer.pause()
|
||||
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
||||
paused = true;
|
||||
paused = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func settingsButtonTapped(_ sender: UIButton) {
|
||||
optionsVC = VideoPlayerSettingsView()
|
||||
optionsVC?.delegate = self
|
||||
|
||||
optionsVC?.modalPresentationStyle = .popover
|
||||
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
|
||||
|
||||
|
||||
|
||||
// Present the view controller (in a popover).
|
||||
self.present(optionsVC!, animated: true) {
|
||||
print("popover visible, pause playback")
|
||||
|
@ -171,122 +170,121 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
|||
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func settingsPopoverDismissed() {
|
||||
optionsVC?.dismiss(animated: true, completion: nil)
|
||||
self.mediaPlayer.play()
|
||||
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||
}
|
||||
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .landscape
|
||||
}
|
||||
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func setupNowPlayingCC() {
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
commandCenter.playCommand.isEnabled = true;
|
||||
commandCenter.pauseCommand.isEnabled = true;
|
||||
commandCenter.seekForwardCommand.isEnabled = true;
|
||||
commandCenter.seekBackwardCommand.isEnabled = true;
|
||||
commandCenter.playCommand.isEnabled = true
|
||||
commandCenter.pauseCommand.isEnabled = true
|
||||
commandCenter.seekForwardCommand.isEnabled = true
|
||||
commandCenter.seekBackwardCommand.isEnabled = true
|
||||
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
||||
|
||||
// Add handler for Pause Command
|
||||
commandCenter.pauseCommand.addTarget{ event in
|
||||
commandCenter.pauseCommand.addTarget { _ in
|
||||
self.mediaPlayer.pause()
|
||||
self.sendProgressReport(eventName: "pause")
|
||||
return .success
|
||||
}
|
||||
|
||||
//Add handler for Play command
|
||||
commandCenter.playCommand.addTarget{ event in
|
||||
|
||||
// Add handler for Play command
|
||||
commandCenter.playCommand.addTarget { _ in
|
||||
self.mediaPlayer.play()
|
||||
self.sendProgressReport(eventName: "unpause")
|
||||
return .success
|
||||
}
|
||||
|
||||
//Add handler for FF command
|
||||
commandCenter.seekForwardCommand.addTarget{ event in
|
||||
|
||||
// Add handler for FF command
|
||||
commandCenter.seekForwardCommand.addTarget { _ in
|
||||
self.mediaPlayer.jumpForward(30)
|
||||
self.sendProgressReport(eventName: "timeupdate")
|
||||
return .success
|
||||
}
|
||||
|
||||
//Add handler for RW command
|
||||
commandCenter.seekBackwardCommand.addTarget{ event in
|
||||
|
||||
// Add handler for RW command
|
||||
commandCenter.seekBackwardCommand.addTarget { _ in
|
||||
self.mediaPlayer.jumpBackward(15)
|
||||
self.sendProgressReport(eventName: "timeupdate")
|
||||
return .success
|
||||
}
|
||||
|
||||
//Scrubber
|
||||
|
||||
// Scrubber
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
|
||||
guard let self = self else {return .commandFailed}
|
||||
|
||||
|
||||
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
|
||||
let targetSeconds = event.positionTime
|
||||
|
||||
|
||||
let videoPosition = Double(self.mediaPlayer.time.intValue)
|
||||
let offset = targetSeconds - videoPosition;
|
||||
if(offset > 0) {
|
||||
self.mediaPlayer.jumpForward(Int32(offset)/1000);
|
||||
let offset = targetSeconds - videoPosition
|
||||
if offset > 0 {
|
||||
self.mediaPlayer.jumpForward(Int32(offset)/1000)
|
||||
} else {
|
||||
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000);
|
||||
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
|
||||
}
|
||||
self.sendProgressReport(eventName: "unpause")
|
||||
|
||||
|
||||
return .success
|
||||
} else {
|
||||
return .commandFailed
|
||||
}
|
||||
}
|
||||
|
||||
var nowPlayingInfo = [String : Any]()
|
||||
|
||||
var nowPlayingInfo = [String: Any]()
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? ""
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
|
||||
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
}
|
||||
|
||||
|
||||
override func remoteControlReceived(with event: UIEvent?) {
|
||||
dump(event)
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
//View has loaded.
|
||||
|
||||
//Rotate to landscape only if necessary
|
||||
UIViewController.attemptRotationToDeviceOrientation();
|
||||
|
||||
// View has loaded.
|
||||
|
||||
// Rotate to landscape only if necessary
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
|
||||
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||
//mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||
|
||||
|
||||
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||
|
||||
mediaPlayer.delegate = self
|
||||
mediaPlayer.drawable = videoContentView
|
||||
|
||||
if(manifest.type == "Movie") {
|
||||
|
||||
if manifest.type == "Movie" {
|
||||
titleLabel.text = manifest.name
|
||||
} else {
|
||||
titleLabel.text = "S\(String(manifest.parentIndexNumber!)):E\(String(manifest.indexNumber!)) “\(manifest.name!)”"
|
||||
}
|
||||
|
||||
//Fetch max bitrate from UserDefaults depending on current connection mode
|
||||
|
||||
// Fetch max bitrate from UserDefaults depending on current connection mode
|
||||
let defaults = UserDefaults.standard
|
||||
let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
|
||||
|
||||
//Build a device profile
|
||||
|
||||
// Build a device profile
|
||||
let builder = DeviceProfileBuilder()
|
||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||
let profile = builder.buildProfile()
|
||||
|
||||
|
||||
let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
||||
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
||||
.sink(receiveCompletion: { completion in
|
||||
|
@ -294,147 +292,146 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
|||
}, receiveValue: { [self] response in
|
||||
playSessionId = response.playSessionId!
|
||||
let mediaSource = response.mediaSources!.first.self!
|
||||
if(mediaSource.transcodingUrl != nil) {
|
||||
//Item is being transcoded by request of server
|
||||
if mediaSource.transcodingUrl != nil {
|
||||
// Item is being transcoded by request of server
|
||||
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
|
||||
let item = PlaybackItem()
|
||||
item.videoType = .transcode
|
||||
item.videoUrl = streamURL!
|
||||
|
||||
|
||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
||||
subtitleTrackArray.append(disableSubtitleTrack);
|
||||
|
||||
//Loop through media streams and add to array
|
||||
subtitleTrackArray.append(disableSubtitleTrack)
|
||||
|
||||
// Loop through media streams and add to array
|
||||
for stream in mediaSource.mediaStreams! {
|
||||
if(stream.type == .subtitle) {
|
||||
if stream.type == .subtitle {
|
||||
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
|
||||
let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
|
||||
subtitleTrackArray.append(subtitle);
|
||||
subtitleTrackArray.append(subtitle)
|
||||
}
|
||||
|
||||
if(stream.type == .audio) {
|
||||
|
||||
if stream.type == .audio {
|
||||
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
||||
if(stream.isDefault! == true) {
|
||||
selectedAudioTrack = Int32(stream.index!);
|
||||
if stream.isDefault! == true {
|
||||
selectedAudioTrack = Int32(stream.index!)
|
||||
}
|
||||
audioTrackArray.append(subtitle);
|
||||
audioTrackArray.append(subtitle)
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedAudioTrack == -1) {
|
||||
if(audioTrackArray.count > 0) {
|
||||
selectedAudioTrack = audioTrackArray[0].id;
|
||||
|
||||
if selectedAudioTrack == -1 {
|
||||
if audioTrackArray.count > 0 {
|
||||
selectedAudioTrack = audioTrackArray[0].id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.sendPlayReport()
|
||||
playbackItem = item;
|
||||
playbackItem = item
|
||||
} else {
|
||||
//Item will be directly played by the client.
|
||||
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!;
|
||||
|
||||
// Item will be directly played by the client.
|
||||
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!
|
||||
|
||||
let item = PlaybackItem()
|
||||
item.videoUrl = streamURL
|
||||
item.videoType = .directPlay
|
||||
|
||||
|
||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
||||
subtitleTrackArray.append(disableSubtitleTrack);
|
||||
|
||||
//Loop through media streams and add to array
|
||||
subtitleTrackArray.append(disableSubtitleTrack)
|
||||
|
||||
// Loop through media streams and add to array
|
||||
for stream in mediaSource.mediaStreams! {
|
||||
if(stream.type == .subtitle) {
|
||||
if stream.type == .subtitle {
|
||||
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
|
||||
let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
|
||||
subtitleTrackArray.append(subtitle);
|
||||
subtitleTrackArray.append(subtitle)
|
||||
}
|
||||
|
||||
if(stream.type == .audio) {
|
||||
|
||||
if stream.type == .audio {
|
||||
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
||||
if(stream.isDefault! == true) {
|
||||
selectedAudioTrack = Int32(stream.index!);
|
||||
if stream.isDefault! == true {
|
||||
selectedAudioTrack = Int32(stream.index!)
|
||||
}
|
||||
audioTrackArray.append(subtitle);
|
||||
audioTrackArray.append(subtitle)
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedAudioTrack == -1) {
|
||||
if(audioTrackArray.count > 0) {
|
||||
selectedAudioTrack = audioTrackArray[0].id;
|
||||
|
||||
if selectedAudioTrack == -1 {
|
||||
if audioTrackArray.count > 0 {
|
||||
selectedAudioTrack = audioTrackArray[0].id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.sendPlayReport()
|
||||
playbackItem = item;
|
||||
playbackItem = item
|
||||
}
|
||||
|
||||
|
||||
self.setupNowPlayingCC()
|
||||
|
||||
|
||||
mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl)
|
||||
mediaPlayer.play()
|
||||
print(manifest.userData?.playbackPositionTicks ?? 0)
|
||||
mediaPlayer.jumpForward(Int32(manifest.userData?.playbackPositionTicks ?? 0/10000000))
|
||||
mediaPlayer.pause()
|
||||
subtitleTrackArray.forEach() { sub in
|
||||
if(sub.id != -1 && sub.delivery == .external && sub.codec != "subrip") {
|
||||
subtitleTrackArray.forEach { sub in
|
||||
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
|
||||
print("adding subs for id: \(sub.id) w/ url: \(sub.url)")
|
||||
mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false)
|
||||
}
|
||||
}
|
||||
delegate?.showLoadingView(self)
|
||||
while(mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1) {}
|
||||
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack;
|
||||
while mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1 {}
|
||||
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack
|
||||
mediaPlayer.pause()
|
||||
mediaPlayer.play()
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.tabBarController?.tabBar.isHidden = true;
|
||||
self.tabBarController?.tabBar.isHidden = true
|
||||
}
|
||||
|
||||
//MARK: VideoPlayerSettings Delegate
|
||||
|
||||
// MARK: VideoPlayerSettings Delegate
|
||||
func subtitleTrackChanged(newTrackID: Int32) {
|
||||
selectedCaptionTrack = newTrackID
|
||||
mediaPlayer.currentVideoSubTitleIndex = newTrackID
|
||||
}
|
||||
|
||||
|
||||
func audioTrackChanged(newTrackID: Int32) {
|
||||
selectedAudioTrack = newTrackID
|
||||
mediaPlayer.currentAudioTrackIndex = newTrackID
|
||||
}
|
||||
|
||||
|
||||
//MARK: VLCMediaPlayer Delegates
|
||||
|
||||
// MARK: VLCMediaPlayer Delegates
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
||||
switch currentState {
|
||||
case .stopped :
|
||||
break;
|
||||
break
|
||||
case .ended :
|
||||
break;
|
||||
break
|
||||
case .playing :
|
||||
print("Video is playing")
|
||||
sendProgressReport(eventName: "unpause")
|
||||
delegate?.hideLoadingView(self)
|
||||
paused = false;
|
||||
|
||||
paused = false
|
||||
|
||||
case .paused :
|
||||
print("Video is paused)")
|
||||
paused = true;
|
||||
|
||||
paused = true
|
||||
|
||||
case .opening :
|
||||
print("Video is opening)")
|
||||
|
||||
|
||||
case .buffering :
|
||||
print("Video is buffering)")
|
||||
delegate?.showLoadingView(self)
|
||||
mediaPlayer.pause()
|
||||
usleep(10000)
|
||||
mediaPlayer.play()
|
||||
|
||||
|
||||
case .error :
|
||||
print("Video has error)")
|
||||
sendStopReport()
|
||||
|
@ -444,81 +441,81 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||
let time = mediaPlayer.position;
|
||||
if(time != lastTime) {
|
||||
paused = false;
|
||||
let time = mediaPlayer.position
|
||||
if time != lastTime {
|
||||
paused = false
|
||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||
seekSlider.setValue(mediaPlayer.position, animated: true)
|
||||
delegate?.hideLoadingView(self)
|
||||
|
||||
let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000;
|
||||
let hours = remainingTime / 3600;
|
||||
let minutes = (remainingTime % 3600) / 60;
|
||||
let seconds = (remainingTime % 3600) % 60;
|
||||
var timeTextStr = "";
|
||||
if(hours != 0) {
|
||||
timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||
|
||||
let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000
|
||||
let hours = remainingTime / 3600
|
||||
let minutes = (remainingTime % 3600) / 60
|
||||
let seconds = (remainingTime % 3600) % 60
|
||||
var timeTextStr = ""
|
||||
if hours != 0 {
|
||||
timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||
} else {
|
||||
timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||
timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||
}
|
||||
timeText.text = timeTextStr
|
||||
|
||||
if(CACurrentMediaTime() - controlsAppearTime > 5) {
|
||||
|
||||
if CACurrentMediaTime() - controlsAppearTime > 5 {
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
|
||||
self.videoControlsView.alpha = 0.0
|
||||
}, completion: { (finished: Bool) in
|
||||
self.videoControlsView.isHidden = true;
|
||||
}, completion: { (_: Bool) in
|
||||
self.videoControlsView.isHidden = true
|
||||
self.videoControlsView.alpha = 1
|
||||
})
|
||||
controlsAppearTime = 10000000000000000000000;
|
||||
controlsAppearTime = 10000000000000000000000
|
||||
}
|
||||
} else {
|
||||
paused = true;
|
||||
paused = true
|
||||
}
|
||||
lastTime = time;
|
||||
|
||||
if(CACurrentMediaTime() - lastProgressReportTime > 5) {
|
||||
lastTime = time
|
||||
|
||||
if CACurrentMediaTime() - lastProgressReportTime > 5 {
|
||||
sendProgressReport(eventName: "timeupdate")
|
||||
lastProgressReportTime = CACurrentMediaTime()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: Jellyfin Playstate updates
|
||||
|
||||
// MARK: Jellyfin Playstate updates
|
||||
func sendProgressReport(eventName: String) {
|
||||
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
||||
|
||||
|
||||
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
}, receiveValue: { _ in
|
||||
print("Playback progress report sent!")
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
|
||||
|
||||
func sendStopReport() {
|
||||
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
|
||||
|
||||
|
||||
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
}, receiveValue: { _ in
|
||||
print("Playback stop report sent!")
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
}
|
||||
|
||||
|
||||
func sendPlayReport() {
|
||||
startTime = Int(Date().timeIntervalSince1970) * 10000000
|
||||
|
||||
|
||||
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
||||
|
||||
|
||||
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
|
||||
.sink(receiveCompletion: { completion in
|
||||
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||
}, receiveValue: { response in
|
||||
}, receiveValue: { _ in
|
||||
print("Playback start report sent!")
|
||||
})
|
||||
.store(in: &globalData.pendingAPIRequests)
|
||||
|
@ -528,30 +525,30 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
|||
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||
var item: BaseItemDto
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@EnvironmentObject private var globalData: GlobalData;
|
||||
|
||||
@EnvironmentObject private var globalData: GlobalData
|
||||
|
||||
var loadBinding: Binding<Bool>
|
||||
var pBinding: Binding<Bool>
|
||||
|
||||
class Coordinator: NSObject, PlayerViewControllerDelegate {
|
||||
let loadBinding: Binding<Bool>
|
||||
let pBinding: Binding<Bool>
|
||||
|
||||
|
||||
init(loadBinding: Binding<Bool>, pBinding: Binding<Bool>) {
|
||||
self.loadBinding = loadBinding
|
||||
self.pBinding = pBinding
|
||||
}
|
||||
|
||||
|
||||
func hideLoadingView(_ viewController: PlayerViewController) {
|
||||
self.loadBinding.wrappedValue = false;
|
||||
self.loadBinding.wrappedValue = false
|
||||
}
|
||||
|
||||
|
||||
func showLoadingView(_ viewController: PlayerViewController) {
|
||||
self.loadBinding.wrappedValue = true;
|
||||
self.loadBinding.wrappedValue = true
|
||||
}
|
||||
|
||||
|
||||
func exitPlayer(_ viewController: PlayerViewController) {
|
||||
self.pBinding.wrappedValue = false;
|
||||
self.pBinding.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -559,14 +556,13 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
|||
Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding)
|
||||
}
|
||||
|
||||
|
||||
typealias UIViewControllerType = PlayerViewController
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls.UIViewControllerType {
|
||||
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
||||
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
||||
customViewController.manifest = item;
|
||||
customViewController.delegate = context.coordinator;
|
||||
customViewController.globalData = globalData;
|
||||
customViewController.manifest = item
|
||||
customViewController.delegate = context.coordinator
|
||||
customViewController.globalData = globalData
|
||||
return customViewController
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class VideoPlayerSettingsView: UIViewController {
|
|||
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
|
||||
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
|
||||
}
|
||||
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
self.delegate?.settingsPopoverDismissed()
|
||||
|
@ -30,17 +30,17 @@ class VideoPlayerSettingsView: UIViewController {
|
|||
}
|
||||
|
||||
struct VideoPlayerSettings: View {
|
||||
@State var delegate: PlayerViewController!
|
||||
@State var captionTrack: Int32 = -99;
|
||||
@State var audioTrack: Int32 = -99;
|
||||
|
||||
@State weak var delegate: PlayerViewController!
|
||||
@State var captionTrack: Int32 = -99
|
||||
@State var audioTrack: Int32 = -99
|
||||
|
||||
init(delegate: PlayerViewController) {
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView() {
|
||||
Form() {
|
||||
NavigationView {
|
||||
Form {
|
||||
Picker("Closed Captions", selection: $captionTrack) {
|
||||
ForEach(delegate.subtitleTrackArray, id: \.id) { caption in
|
||||
Text(caption.name).tag(caption.id)
|
||||
|
@ -60,11 +60,11 @@ struct VideoPlayerSettings: View {
|
|||
.navigationTitle("Audio & Captions")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
if(UIDevice.current.userInterfaceIdiom == .phone) {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
Button {
|
||||
self.delegate.settingsPopoverDismissed()
|
||||
} label: {
|
||||
HStack() {
|
||||
HStack {
|
||||
Image(systemName: "chevron.left")
|
||||
Text("Back").font(.callout)
|
||||
}
|
||||
|
|
|
@ -9,68 +9,68 @@ import Foundation
|
|||
import JellyfinAPI
|
||||
import UIKit
|
||||
|
||||
//001fC^ = dark grey plain blurhash
|
||||
// 001fC^ = dark grey plain blurhash
|
||||
|
||||
extension BaseItemDto {
|
||||
|
||||
//MARK: Images
|
||||
|
||||
// MARK: Images
|
||||
func getSeriesBackdropImageBlurHash() -> String {
|
||||
let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||
|
||||
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
|
||||
let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||
|
||||
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
|
||||
}
|
||||
|
||||
|
||||
func getSeriesPrimaryImageBlurHash() -> String {
|
||||
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||
}
|
||||
|
||||
|
||||
func getPrimaryImageBlurHash() -> String {
|
||||
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||
}
|
||||
|
||||
|
||||
func getBackdropImageBlurHash() -> String {
|
||||
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||
|
||||
if(rawImgURL.contains("Backdrop")) {
|
||||
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
|
||||
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||
|
||||
if rawImgURL.contains("Backdrop") {
|
||||
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
|
||||
} else {
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
||||
var imageType = "";
|
||||
var imageTag = "";
|
||||
|
||||
if(self.primaryImageAspectRatio ?? 0.0 < 1.0) {
|
||||
imageType = "Backdrop";
|
||||
var imageType = ""
|
||||
var imageTag = ""
|
||||
|
||||
if self.primaryImageAspectRatio ?? 0.0 < 1.0 {
|
||||
imageType = "Backdrop"
|
||||
imageTag = (self.backdropImageTags ?? [""])[0]
|
||||
} else {
|
||||
imageType = "Primary";
|
||||
imageType = "Primary"
|
||||
imageTag = self.imageTags?["Primary"] ?? ""
|
||||
}
|
||||
|
||||
if(imageTag == "") {
|
||||
imageType = "Backdrop";
|
||||
|
||||
if imageTag == "" {
|
||||
imageType = "Backdrop"
|
||||
imageTag = self.parentBackdropImageTags?[0] ?? ""
|
||||
}
|
||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||
let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||
return URL(string: urlString)!
|
||||
}
|
||||
|
||||
|
||||
func getSeriesBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
||||
let imageType = "Backdrop";
|
||||
let imageTag = (self.parentBackdropImageTags ?? [])[0];
|
||||
|
||||
let imageType = "Backdrop"
|
||||
let imageTag = (self.parentBackdropImageTags ?? [])[0]
|
||||
|
||||
print(imageType)
|
||||
print(imageTag)
|
||||
|
||||
|
@ -78,29 +78,29 @@ extension BaseItemDto {
|
|||
let urlString = "\(baseURL)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||
return URL(string: urlString)!
|
||||
}
|
||||
|
||||
|
||||
func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
||||
let imageType = "Primary";
|
||||
let imageType = "Primary"
|
||||
let imageTag = self.seriesPrimaryImageTag ?? ""
|
||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||
let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||
return URL(string: urlString)!
|
||||
}
|
||||
|
||||
|
||||
func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
||||
let imageType = "Primary";
|
||||
var imageTag = self.imageTags?["Primary"] ?? "";
|
||||
|
||||
if(imageTag == "") {
|
||||
let imageType = "Primary"
|
||||
var imageTag = self.imageTags?["Primary"] ?? ""
|
||||
|
||||
if imageTag == "" {
|
||||
imageTag = self.seriesPrimaryImageTag ?? ""
|
||||
}
|
||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||
|
||||
|
||||
let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||
return URL(string: urlString)!
|
||||
}
|
||||
|
||||
//MARK: Calculations
|
||||
|
||||
// MARK: Calculations
|
||||
func getItemRuntime() -> String {
|
||||
let seconds: Int = Int(self.runTimeTicks!) / 10_000_000
|
||||
let hours = (seconds / 3600)
|
||||
|
@ -111,12 +111,12 @@ extension BaseItemDto {
|
|||
return "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func getItemProgressString() -> String {
|
||||
if(self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0) {
|
||||
return "";
|
||||
if self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
let remainingSecs = Int(self.runTimeTicks! - (self.userData?.playbackPositionTicks!)!) / 10_000_000
|
||||
let proghours = Int(remainingSecs / 3600)
|
||||
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
|
||||
|
@ -130,19 +130,19 @@ extension BaseItemDto {
|
|||
|
||||
extension BaseItemPerson {
|
||||
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
||||
let imageType = "Primary";
|
||||
let imageType = "Primary"
|
||||
let imageTag = self.primaryImageTag ?? ""
|
||||
|
||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||
|
||||
|
||||
let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||
return URL(string: urlString)!
|
||||
}
|
||||
|
||||
|
||||
func getBlurHash() -> String {
|
||||
let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString;
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
||||
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
||||
let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString
|
||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||
|
||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,14 +115,12 @@ private func signPow(_ value: Float, _ exp: Float) -> Float {
|
|||
|
||||
private func linearTosRGB(_ value: Float) -> Int {
|
||||
let v = max(0, min(1, value))
|
||||
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
|
||||
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
||||
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
||||
}
|
||||
|
||||
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
||||
let v = Float(Int64(value)) / 255
|
||||
if v <= 0.04045 { return v / 12.92 }
|
||||
else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||
if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||
}
|
||||
|
||||
private let encodeCharacters: [String] = {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
|
||||
public extension Collection {
|
||||
|
||||
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.
|
||||
|
|
|
@ -15,11 +15,11 @@ func HandleAPIRequestCompletion(globalData: GlobalData, completion: Subscribers.
|
|||
break
|
||||
case .failure(let error):
|
||||
if let err = error as? ErrorResponse {
|
||||
switch(err){
|
||||
switch err {
|
||||
case .error(401, _, _, _):
|
||||
globalData.expiredCredentials = true;
|
||||
case .error(_, _, _, _):
|
||||
globalData.networkError = true;
|
||||
globalData.expiredCredentials = true
|
||||
case .error:
|
||||
globalData.networkError = true
|
||||
}
|
||||
}
|
||||
break
|
||||
|
|
|
@ -19,8 +19,7 @@ struct ParallaxHeaderScrollView<Header: View, StaticOverlayView: View, Content:
|
|||
staticOverlayView: StaticOverlayView,
|
||||
overlayAlignment: Alignment = .center,
|
||||
headerHeight: CGFloat,
|
||||
content: @escaping () -> Content)
|
||||
{
|
||||
content: @escaping () -> Content) {
|
||||
self.header = header
|
||||
self.staticOverlayView = staticOverlayView
|
||||
self.overlayAlignment = overlayAlignment
|
||||
|
|
|
@ -15,7 +15,7 @@ extension String {
|
|||
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
|
||||
} catch { return self }
|
||||
}
|
||||
|
||||
|
||||
func leftPad(toWidth width: Int, withString string: String?) -> String {
|
||||
let paddingString = string ?? " "
|
||||
|
||||
|
@ -32,4 +32,3 @@ extension String {
|
|||
return "\(padString)\(self)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,14 +32,14 @@ class GlobalData: ObservableObject {
|
|||
@Published var authToken: String = ""
|
||||
@Published var server: Server!
|
||||
@Published var authHeader: String = ""
|
||||
@Published var isInNetwork: Bool = true;
|
||||
@Published var networkError: Bool = false;
|
||||
@Published var expiredCredentials: Bool = false;
|
||||
var pendingAPIRequests = Set<AnyCancellable>();
|
||||
@Published var isInNetwork: Bool = true
|
||||
@Published var networkError: Bool = false
|
||||
@Published var expiredCredentials: Bool = false
|
||||
var pendingAPIRequests = Set<AnyCancellable>()
|
||||
}
|
||||
|
||||
extension GlobalData: Equatable {
|
||||
|
||||
|
||||
static func == (lhs: GlobalData, rhs: GlobalData) -> Bool {
|
||||
lhs.user == rhs.user
|
||||
&& lhs.authToken == rhs.authToken
|
||||
|
|
|
@ -10,12 +10,12 @@
|
|||
import Foundation
|
||||
|
||||
struct UserSettings: Decodable {
|
||||
var LocalMaxBitrate: Int;
|
||||
var RemoteMaxBitrate: Int;
|
||||
var AutoSelectSubtitles: Bool;
|
||||
var AutoSelectSubtitlesLangcode: String;
|
||||
var SubtitlePositionOffset: Int;
|
||||
var SubtitleFontName: String;
|
||||
var LocalMaxBitrate: Int
|
||||
var RemoteMaxBitrate: Int
|
||||
var AutoSelectSubtitles: Bool
|
||||
var AutoSelectSubtitlesLangcode: String
|
||||
var SubtitlePositionOffset: Int
|
||||
var SubtitleFontName: String
|
||||
}
|
||||
|
||||
struct Bitrates: Codable, Hashable {
|
||||
|
|
|
@ -380,7 +380,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
|
@ -391,7 +391,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
|
@ -406,7 +406,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
|
@ -418,7 +418,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
|
@ -431,7 +431,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
NextUpEntryView(entry: .init(date: Date(),
|
||||
items: [
|
||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
|
@ -441,7 +441,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
|
|
|
@ -14,21 +14,21 @@ import UIKit
|
|||
|
||||
final class WidgetEnvironment {
|
||||
static let shared = WidgetEnvironment()
|
||||
|
||||
|
||||
var server: Server?
|
||||
var user: SignedInUser?
|
||||
var header: String?
|
||||
|
||||
|
||||
init() {
|
||||
update()
|
||||
}
|
||||
|
||||
|
||||
func update() {
|
||||
let serverRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Server")
|
||||
let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server]
|
||||
let savedUserRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "SignedInUser")
|
||||
let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) as? [SignedInUser]
|
||||
|
||||
|
||||
server = servers?.first
|
||||
user = savedUsers?.first
|
||||
|
||||
|
|
Loading…
Reference in New Issue