[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 {
|
if inMemory {
|
||||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
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? {
|
if let error = error as NSError? {
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
// 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.
|
// 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.
|
||||||
|
|
|
@ -16,28 +16,28 @@ struct ConnectToServerView: View {
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
@EnvironmentObject var jsi: justSignedIn
|
@EnvironmentObject var jsi: justSignedIn
|
||||||
|
|
||||||
@State private var uri = "";
|
@State private var uri = ""
|
||||||
@State private var isWorking = false;
|
@State private var isWorking = false
|
||||||
@State private var isErrored = false;
|
@State private var isErrored = false
|
||||||
@State private var isDone = false;
|
@State private var isDone = false
|
||||||
@State private var isSignInErrored = false;
|
@State private var isSignInErrored = false
|
||||||
@State private var isConnected = false;
|
@State private var isConnected = false
|
||||||
@State private var serverName = "";
|
@State private var serverName = ""
|
||||||
@State private var usernameDisabled: Bool = false;
|
@State private var usernameDisabled: Bool = false
|
||||||
@State private var publicUsers: [UserDto] = [];
|
@State private var publicUsers: [UserDto] = []
|
||||||
@State private var lastPublicUsers: [UserDto] = [];
|
@State private var lastPublicUsers: [UserDto] = []
|
||||||
@State private var username = "";
|
@State private var username = ""
|
||||||
@State private var password = "";
|
@State private var password = ""
|
||||||
@State private var server_id = "";
|
@State private var server_id = ""
|
||||||
@State private var serverSkipped: Bool = false;
|
@State private var serverSkipped: Bool = false
|
||||||
@State private var serverSkippedAlert: Bool = false;
|
@State private var serverSkippedAlert: Bool = false
|
||||||
@State private var skip_server_bool: Bool = false;
|
@State private var skip_server_bool: Bool = false
|
||||||
@State private var skip_server_obj: Server!;
|
@State private var skip_server_obj: Server!
|
||||||
|
|
||||||
@Binding var rootIsActive: Bool
|
@Binding var rootIsActive: Bool
|
||||||
|
|
||||||
private var reauthDeviceID: String = "";
|
private var reauthDeviceID: String = ""
|
||||||
private let userUUID = UUID();
|
private let userUUID = UUID()
|
||||||
|
|
||||||
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
|
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
|
||||||
_rootIsActive = isActive
|
_rootIsActive = isActive
|
||||||
|
@ -51,7 +51,7 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
if(skip_server_bool) {
|
if skip_server_bool {
|
||||||
uri = skip_server_obj.baseURI!
|
uri = skip_server_obj.baseURI!
|
||||||
|
|
||||||
UserAPI.getPublicUsers()
|
UserAPI.getPublicUsers()
|
||||||
|
@ -59,19 +59,19 @@ struct ConnectToServerView: View {
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
case .failure(_):
|
case .failure:
|
||||||
skip_server_bool = false;
|
skip_server_bool = false
|
||||||
skip_server_obj = Server();
|
skip_server_obj = Server()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
publicUsers = response
|
publicUsers = response
|
||||||
|
|
||||||
serverSkipped = true;
|
serverSkipped = true
|
||||||
serverSkippedAlert = true;
|
serverSkippedAlert = true
|
||||||
server_id = skip_server_obj.server_id!
|
server_id = skip_server_obj.server_id!
|
||||||
serverName = skip_server_obj.name!
|
serverName = skip_server_obj.name!
|
||||||
isConnected = true;
|
isConnected = true
|
||||||
})
|
})
|
||||||
.store(in: &globalData.pendingAPIRequests)
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
}
|
}
|
||||||
|
@ -80,12 +80,12 @@ struct ConnectToServerView: View {
|
||||||
func doLogin() {
|
func doLogin() {
|
||||||
isWorking = true
|
isWorking = true
|
||||||
|
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
|
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.folding(options: .diacriticInsensitive, locale: .current)
|
||||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
|
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
|
||||||
|
|
||||||
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
|
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""
|
||||||
print(authHeader)
|
print(authHeader)
|
||||||
|
|
||||||
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
|
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
|
||||||
|
@ -147,17 +147,17 @@ struct ConnectToServerView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
if(!isConnected) {
|
if !isConnected {
|
||||||
Section(header: Text("Server Information")) {
|
Section(header: Text("Server Information")) {
|
||||||
TextField("Jellyfin Server URL", text: $uri)
|
TextField("Jellyfin Server URL", text: $uri)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
Button {
|
Button {
|
||||||
isWorking = true;
|
isWorking = true
|
||||||
if(!uri.contains("http")) {
|
if !uri.contains("http") {
|
||||||
uri = "http://" + uri;
|
uri = "http://" + uri
|
||||||
}
|
}
|
||||||
if(uri.last == "/") {
|
if uri.last == "/" {
|
||||||
uri = String(uri.dropLast())
|
uri = String(uri.dropLast())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ struct ConnectToServerView: View {
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
case .failure(_):
|
case .failure:
|
||||||
isErrored = true
|
isErrored = true
|
||||||
isWorking = false
|
isWorking = false
|
||||||
break
|
break
|
||||||
|
@ -176,15 +176,15 @@ struct ConnectToServerView: View {
|
||||||
let server = response
|
let server = response
|
||||||
serverName = server.serverName!
|
serverName = server.serverName!
|
||||||
server_id = server.id!
|
server_id = server.id!
|
||||||
if(server.startupWizardCompleted!) {
|
if server.startupWizardCompleted! {
|
||||||
isConnected = true;
|
isConnected = true
|
||||||
|
|
||||||
UserAPI.getPublicUsers()
|
UserAPI.getPublicUsers()
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
case .failure(_):
|
case .failure:
|
||||||
isErrored = true
|
isErrored = true
|
||||||
isWorking = false
|
isWorking = false
|
||||||
break
|
break
|
||||||
|
@ -201,7 +201,7 @@ struct ConnectToServerView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Connect")
|
Text("Connect")
|
||||||
Spacer()
|
Spacer()
|
||||||
if(isWorking == true) {
|
if isWorking == true {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,7 +210,7 @@ struct ConnectToServerView: View {
|
||||||
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
|
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(publicUsers.count == 0) {
|
if publicUsers.count == 0 {
|
||||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||||
TextField("Username", text: $username)
|
TextField("Username", text: $username)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
|
@ -225,7 +225,7 @@ struct ConnectToServerView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Login")
|
Text("Login")
|
||||||
Spacer()
|
Spacer()
|
||||||
if(isWorking) {
|
if isWorking {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,8 +235,8 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(serverSkipped) {
|
if serverSkipped {
|
||||||
Section() {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
serverSkippedAlert = false
|
serverSkippedAlert = false
|
||||||
server_id = ""
|
server_id = ""
|
||||||
|
@ -244,8 +244,8 @@ struct ConnectToServerView: View {
|
||||||
isConnected = false
|
isConnected = false
|
||||||
serverSkipped = false
|
serverSkipped = false
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack {
|
||||||
HStack() {
|
HStack {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
Text("Change Server")
|
Text("Change Server")
|
||||||
}
|
}
|
||||||
|
@ -254,13 +254,13 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Section() {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
publicUsers = lastPublicUsers
|
publicUsers = lastPublicUsers
|
||||||
usernameDisabled = false;
|
usernameDisabled = false
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack {
|
||||||
HStack() {
|
HStack {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
Text("Back")
|
Text("Back")
|
||||||
}
|
}
|
||||||
|
@ -272,9 +272,9 @@ struct ConnectToServerView: View {
|
||||||
} else {
|
} else {
|
||||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
||||||
ForEach(publicUsers, id: \.id) { publicUser in
|
ForEach(publicUsers, id: \.id) { publicUser in
|
||||||
HStack() {
|
HStack {
|
||||||
Button() {
|
Button() {
|
||||||
if(publicUser.hasPassword!) {
|
if publicUser.hasPassword! {
|
||||||
lastPublicUsers = publicUsers
|
lastPublicUsers = publicUsers
|
||||||
username = publicUser.name!
|
username = publicUser.name!
|
||||||
usernameDisabled = true
|
usernameDisabled = true
|
||||||
|
@ -286,10 +286,10 @@ struct ConnectToServerView: View {
|
||||||
doLogin()
|
doLogin()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack {
|
||||||
Text(publicUser.name!).font(.subheadline).fontWeight(.semibold)
|
Text(publicUser.name!).font(.subheadline).fontWeight(.semibold)
|
||||||
Spacer()
|
Spacer()
|
||||||
if(publicUser.primaryImageTag != "") {
|
if publicUser.primaryImageTag != "" {
|
||||||
LazyImage(source: URL(string: "\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)"))
|
LazyImage(source: URL(string: "\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)"))
|
||||||
.contentMode(.aspectFill)
|
.contentMode(.aspectFill)
|
||||||
.frame(width: 60, height: 60)
|
.frame(width: 60, height: 60)
|
||||||
|
@ -310,13 +310,13 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section() {
|
Section {
|
||||||
Button() {
|
Button() {
|
||||||
lastPublicUsers = publicUsers
|
lastPublicUsers = publicUsers
|
||||||
publicUsers = []
|
publicUsers = []
|
||||||
username = ""
|
username = ""
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack {
|
||||||
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
||||||
Spacer()
|
Spacer()
|
||||||
Image(systemName: "person.fill.questionmark")
|
Image(systemName: "person.fill.questionmark")
|
||||||
|
|
|
@ -40,7 +40,7 @@ struct ContentView: View {
|
||||||
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
|
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
|
||||||
|
|
||||||
func startup() {
|
func startup() {
|
||||||
if(viewDidLoad == true) {
|
if viewDidLoad == true {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,9 +72,9 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
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.folding(options: .diacriticInsensitive, locale: .current)
|
||||||
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
|
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
|
||||||
|
|
||||||
var header = "MediaBrowser "
|
var header = "MediaBrowser "
|
||||||
header.append("Client=\"SwiftFin\", ")
|
header.append("Client=\"SwiftFin\", ")
|
||||||
|
@ -98,7 +98,7 @@ struct ContentView: View {
|
||||||
return !(response.configuration?.latestItemsExcludes?.contains(element))!
|
return !(response.configuration?.latestItemsExcludes?.contains(element))!
|
||||||
}
|
}
|
||||||
|
|
||||||
if(loadState == 1) {
|
if loadState == 1 {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -113,7 +113,7 @@ struct ContentView: View {
|
||||||
library_names[item.id ?? ""] = item.name
|
library_names[item.id ?? ""] = item.name
|
||||||
})
|
})
|
||||||
|
|
||||||
if(loadState == 1) {
|
if loadState == 1 {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -132,13 +132,13 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if (needsToSelectServer == true) {
|
if needsToSelectServer == true {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ConnectToServerView(isActive: $needsToSelectServer)
|
ConnectToServerView(isActive: $needsToSelectServer)
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
.environmentObject(globalData)
|
.environmentObject(globalData)
|
||||||
} else if (globalData.expiredCredentials == true) {
|
} else if globalData.expiredCredentials == true {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
|
||||||
reauth_deviceId: globalData.user.device_uuid ?? "", isActive: $globalData.expiredCredentials)
|
reauth_deviceId: globalData.user.device_uuid ?? "", isActive: $globalData.expiredCredentials)
|
||||||
|
@ -148,8 +148,8 @@ struct ContentView: View {
|
||||||
} else {
|
} else {
|
||||||
if !jsi.did {
|
if !jsi.did {
|
||||||
LoadingView(isShowing: $isLoading) {
|
LoadingView(isShowing: $isLoading) {
|
||||||
VStack() {
|
VStack {
|
||||||
if(loadState == 0) {
|
if loadState == 0 {
|
||||||
TabView(selection: $tabSelection) {
|
TabView(selection: $tabSelection) {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
|
@ -30,7 +30,6 @@ struct ProgressBar: Shape {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
struct ContinueWatchingView: View {
|
struct ContinueWatchingView: View {
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
|
@ -38,7 +37,7 @@ struct ContinueWatchingView: View {
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
@ -50,9 +49,9 @@ struct ContinueWatchingView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
if(items.count > 0) {
|
if items.count > 0 {
|
||||||
LazyHStack() {
|
LazyHStack {
|
||||||
Spacer().frame(width:14)
|
Spacer().frame(width: 14)
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
NavigationLink(destination: ItemView(item: item)) {
|
NavigationLink(destination: ItemView(item: item)) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -70,7 +69,7 @@ struct ContinueWatchingView: View {
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.overlay(
|
.overlay(
|
||||||
Group {
|
Group {
|
||||||
if(item.type == "Episode") {
|
if item.type == "Episode" {
|
||||||
Text("\(item.name!)")
|
Text("\(item.name!)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(6)
|
.padding(6)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* 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 Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
@ -34,60 +34,60 @@ enum CPUModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeviceProfileBuilder {
|
class DeviceProfileBuilder {
|
||||||
public var bitrate: Int = 0;
|
public var bitrate: Int = 0
|
||||||
|
|
||||||
public func setMaxBitrate(bitrate: Int) {
|
public func setMaxBitrate(bitrate: Int) {
|
||||||
self.bitrate = bitrate
|
self.bitrate = bitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
public func buildProfile() -> DeviceProfile {
|
public func buildProfile() -> DeviceProfile {
|
||||||
let maxStreamingBitrate = bitrate;
|
let maxStreamingBitrate = bitrate
|
||||||
let maxStaticBitrate = bitrate;
|
let maxStaticBitrate = bitrate
|
||||||
let musicStreamingTranscodingBitrate = 384000;
|
let musicStreamingTranscodingBitrate = 384000
|
||||||
|
|
||||||
//Build direct play profiles
|
// Build direct play profiles
|
||||||
var directPlayProfiles: [DirectPlayProfile] = [];
|
var directPlayProfiles: [DirectPlayProfile] = []
|
||||||
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav", videoCodec: "h264", type: .video)]
|
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav", videoCodec: "h264", type: .video)]
|
||||||
|
|
||||||
//Device supports Dolby Digital (AC3, EAC3)
|
// Device supports Dolby Digital (AC3, EAC3)
|
||||||
if(supportsFeature(minimumSupported: .A8X)) {
|
if supportsFeature(minimumSupported: .A8X) {
|
||||||
if(supportsFeature(minimumSupported: .A10)) {
|
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
|
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 {
|
} 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?
|
// Device supports Dolby Vision?
|
||||||
if(supportsFeature(minimumSupported: .A10X)) {
|
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
|
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?
|
// Device supports Dolby Atmos?
|
||||||
if(supportsFeature(minimumSupported: .A12)) {
|
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
|
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
|
// Build transcoding profiles
|
||||||
var transcodingProfiles: [TranscodingProfile] = [];
|
var transcodingProfiles: [TranscodingProfile] = []
|
||||||
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav")]
|
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav")]
|
||||||
|
|
||||||
//Device supports Dolby Digital (AC3, EAC3)
|
// Device supports Dolby Digital (AC3, EAC3)
|
||||||
if(supportsFeature(minimumSupported: .A8X)) {
|
if supportsFeature(minimumSupported: .A8X) {
|
||||||
if(supportsFeature(minimumSupported: .A10)) {
|
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)]
|
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 {
|
} 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)]
|
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?
|
// Device supports Dolby Vision?
|
||||||
if(supportsFeature(minimumSupported: .A10X)) {
|
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)]
|
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?
|
// Device supports Dolby Atmos?
|
||||||
if(supportsFeature(minimumSupported: .A12)) {
|
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)]
|
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)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,8 +106,8 @@ class DeviceProfileBuilder {
|
||||||
|
|
||||||
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
|
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
|
||||||
|
|
||||||
if(supportsFeature(minimumSupported: .A10)) {
|
if supportsFeature(minimumSupported: .A10) {
|
||||||
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions,codec: "hevc"))
|
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitleProfiles: [SubtitleProfile] = []
|
var subtitleProfiles: [SubtitleProfile] = []
|
||||||
|
@ -147,7 +147,7 @@ class DeviceProfileBuilder {
|
||||||
uname(&systemInfo)
|
uname(&systemInfo)
|
||||||
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
let machineMirror = Mirror(reflecting: systemInfo.machine)
|
||||||
let identifier = machineMirror.children.reduce("") { identifier, element in
|
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)))
|
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@ -189,10 +189,10 @@ class DeviceProfileBuilder {
|
||||||
case "iPad7,1", "iPad7,2": return .A10X
|
case "iPad7,1", "iPad7,2": return .A10X
|
||||||
case "iPad7,3", "iPad7,4": return .A10X
|
case "iPad7,3", "iPad7,4": return .A10X
|
||||||
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
|
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
|
||||||
case "iPad8,1", "iPad8,2" ,"iPad8,3", "iPad8,4": 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,5", "iPad8,6", "iPad8,7", "iPad8,8": return .A12X
|
||||||
case "iPad8,9", "iPad8,10", "iPad8,11", "iPad8,12": return .A12Z
|
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 "iPad13,1", "iPad13,2": return .A14
|
||||||
case "AppleTV5,3": return .A8
|
case "AppleTV5,3": return .A8
|
||||||
case "AppleTV6,2": return .A10X
|
case "AppleTV6,2": return .A10X
|
||||||
|
|
|
@ -200,7 +200,7 @@ struct EpisodeItemView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer().frame(width: 16)
|
Spacer().frame(width: 16)
|
||||||
ForEach(item.people!, id: \.self) { person in
|
ForEach(item.people!, id: \.self) { person in
|
||||||
if(person.type! == "Actor") {
|
if person.type! == "Actor" {
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
LibraryView(withPerson: person)
|
LibraryView(withPerson: person)
|
||||||
}) {
|
}) {
|
||||||
|
@ -399,7 +399,7 @@ struct EpisodeItemView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer().frame(width: 16)
|
Spacer().frame(width: 16)
|
||||||
ForEach(item.people!, id: \.self) { person in
|
ForEach(item.people!, id: \.self) { person in
|
||||||
if(person.type! == "Actor") {
|
if person.type! == "Actor" {
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
LibraryView(withPerson: person)
|
LibraryView(withPerson: person)
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -10,18 +10,18 @@ import Introspect
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
class VideoPlayerItem: ObservableObject {
|
class VideoPlayerItem: ObservableObject {
|
||||||
@Published var shouldShowPlayer: Bool = false;
|
@Published var shouldShowPlayer: Bool = false
|
||||||
@Published var itemToPlay: BaseItemDto = BaseItemDto();
|
@Published var itemToPlay: BaseItemDto = BaseItemDto()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemView: View {
|
struct ItemView: View {
|
||||||
@EnvironmentObject private var globalData: GlobalData
|
@EnvironmentObject private var globalData: GlobalData
|
||||||
private var item: BaseItemDto;
|
private var item: BaseItemDto
|
||||||
|
|
||||||
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
|
@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 videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view.
|
||||||
@State private var isLoading: Bool = false;
|
@State private var isLoading: Bool = false
|
||||||
@State private var viewDidLoad: Bool = false;
|
@State private var viewDidLoad: Bool = false
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
self.item = item
|
self.item = item
|
||||||
|
@ -29,7 +29,7 @@ struct ItemView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if(videoPlayerItem.shouldShowPlayer) {
|
if videoPlayerItem.shouldShowPlayer {
|
||||||
LoadingViewNoBlur(isShowing: $videoIsLoading) {
|
LoadingViewNoBlur(isShowing: $videoIsLoading) {
|
||||||
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
||||||
}.navigationBarHidden(true)
|
}.navigationBarHidden(true)
|
||||||
|
@ -42,13 +42,13 @@ struct ItemView: View {
|
||||||
.supportedOrientations(.landscape)
|
.supportedOrientations(.landscape)
|
||||||
} else {
|
} else {
|
||||||
VStack {
|
VStack {
|
||||||
if(item.type == "Movie") {
|
if item.type == "Movie" {
|
||||||
MovieItemView(item: item)
|
MovieItemView(item: item)
|
||||||
} else if(item.type == "Season") {
|
} else if item.type == "Season" {
|
||||||
SeasonItemView(item: item)
|
SeasonItemView(item: item)
|
||||||
} else if(item.type == "Series") {
|
} else if item.type == "Series" {
|
||||||
SeriesItemView(item: item)
|
SeriesItemView(item: item)
|
||||||
} else if(item.type == "Episode") {
|
} else if item.type == "Episode" {
|
||||||
EpisodeItemView(item: item)
|
EpisodeItemView(item: item)
|
||||||
} else {
|
} else {
|
||||||
Text("Type: \(item.type ?? "") not implemented yet :(")
|
Text("Type: \(item.type ?? "") not implemented yet :(")
|
||||||
|
|
|
@ -20,7 +20,7 @@ class OrientationInfo: ObservableObject {
|
||||||
case landscape
|
case landscape
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published var orientation: Orientation = .portrait;
|
@Published var orientation: Orientation = .portrait
|
||||||
|
|
||||||
private var _observer: NSObjectProtocol?
|
private var _observer: NSObjectProtocol?
|
||||||
|
|
||||||
|
@ -31,8 +31,7 @@ class OrientationInfo: ObservableObject {
|
||||||
}
|
}
|
||||||
if device.orientation.isPortrait {
|
if device.orientation.isPortrait {
|
||||||
self?.orientation = .portrait
|
self?.orientation = .portrait
|
||||||
}
|
} else if device.orientation.isLandscape {
|
||||||
else if device.orientation.isLandscape {
|
|
||||||
self?.orientation = .landscape
|
self?.orientation = .landscape
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +51,7 @@ extension View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HostingWindowFinder: UIViewRepresentable {
|
struct HostingWindowFinder: UIViewRepresentable {
|
||||||
var callback: (UIWindow?) -> ()
|
var callback: (UIWindow?) -> Void
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
|
@ -134,13 +133,13 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
|
|
||||||
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
|
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
|
||||||
didSet {
|
didSet {
|
||||||
UIViewController.attemptRotationToDeviceOrientation();
|
UIViewController.attemptRotationToDeviceOrientation()
|
||||||
if(_orientations == .landscape) {
|
if _orientations == .landscape {
|
||||||
let value = UIInterfaceOrientation.landscapeRight.rawValue;
|
let value = UIInterfaceOrientation.landscapeRight.rawValue
|
||||||
UIDevice.current.setValue(value, forKey: "orientation")
|
UIDevice.current.setValue(value, forKey: "orientation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
_orientations
|
_orientations
|
||||||
}
|
}
|
||||||
|
@ -149,7 +148,7 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
didSet {
|
didSet {
|
||||||
overrideUserInterfaceStyle = _viewPreference
|
overrideUserInterfaceStyle = _viewPreference
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
|
@ -180,7 +179,7 @@ struct JellyfinPlayerApp: App {
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environmentObject(OrientationInfo())
|
.environmentObject(OrientationInfo())
|
||||||
.environmentObject(jsi)
|
.environmentObject(jsi)
|
||||||
.withHostingWindow() { window in
|
.withHostingWindow { window in
|
||||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(OrientationInfo()).environmentObject(jsi))
|
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(OrientationInfo()).environmentObject(jsi))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,21 +13,21 @@ struct LatestMediaView: View {
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
@State var items: [BaseItemDto] = []
|
@State var items: [BaseItemDto] = []
|
||||||
private var library_id: String = "";
|
private var library_id: String = ""
|
||||||
@State private var viewDidLoad: Bool = false;
|
@State private var viewDidLoad: Bool = false
|
||||||
|
|
||||||
init(usingParentID: String) {
|
init(usingParentID: String) {
|
||||||
library_id = usingParentID;
|
library_id = usingParentID
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(viewDidLoad == true) {
|
if viewDidLoad == true {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewDidLoad = true;
|
viewDidLoad = true
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
@ -39,13 +39,13 @@ struct LatestMediaView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack() {
|
LazyHStack {
|
||||||
Spacer().frame(width:16)
|
Spacer().frame(width: 16)
|
||||||
ForEach(items, id: \.id) { item in
|
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)) {
|
NavigationLink(destination: ItemView(item: item)) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Spacer().frame(height:10)
|
Spacer().frame(height: 10)
|
||||||
LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
|
LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
|
||||||
.placeholderAndFailure {
|
.placeholderAndFailure {
|
||||||
Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
|
Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
|
||||||
|
@ -55,7 +55,7 @@ struct LatestMediaView: View {
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
Spacer().frame(height:5)
|
Spacer().frame(height: 5)
|
||||||
Text(item.seriesName ?? item.name ?? "")
|
Text(item.seriesName ?? item.name ?? "")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
|
@ -13,7 +13,7 @@ struct LibraryListView: View {
|
||||||
|
|
||||||
@State var library_ids: [String] = ["favorites", "genres"]
|
@State var library_ids: [String] = ["favorites", "genres"]
|
||||||
@State var library_names: [String: String] = ["favorites": "Favorites", "genres": "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"])
|
var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [.descending], sortBy: ["SortName"])
|
||||||
|
|
||||||
init(libraries: [String: String]) {
|
init(libraries: [String: String]) {
|
||||||
|
@ -21,8 +21,8 @@ struct LibraryListView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(library_ids.count == 2) {
|
if library_ids.count == 2 {
|
||||||
libraries.forEach() { k,v in
|
libraries.forEach { k, v in
|
||||||
print("\(k): \(v)")
|
print("\(k): \(v)")
|
||||||
_library_ids.wrappedValue.append(k)
|
_library_ids.wrappedValue.append(k)
|
||||||
_library_names.wrappedValue[k] = v
|
_library_names.wrappedValue[k] = v
|
||||||
|
|
|
@ -32,7 +32,7 @@ struct LibrarySearchView: View {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
@ -43,7 +43,7 @@ struct LibrarySearchView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: tracks for grid
|
// MARK: tracks for grid
|
||||||
@State private var tracks: [GridItem] = []
|
@State private var tracks: [GridItem] = []
|
||||||
func recalcTracks() {
|
func recalcTracks() {
|
||||||
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||||
|
@ -57,12 +57,12 @@ struct LibrarySearchView: View {
|
||||||
VStack {
|
VStack {
|
||||||
Spacer().frame(height: 6)
|
Spacer().frame(height: 6)
|
||||||
SearchBar(text: $searchQuery)
|
SearchBar(text: $searchQuery)
|
||||||
if(isLoading == true) {
|
if isLoading == true {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView()
|
ProgressView()
|
||||||
Spacer()
|
Spacer()
|
||||||
} else {
|
} else {
|
||||||
if(!items.isEmpty) {
|
if !items.isEmpty {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
Spacer().frame(height: 16)
|
Spacer().frame(height: 16)
|
||||||
LazyVGrid(columns: tracks) {
|
LazyVGrid(columns: tracks) {
|
||||||
|
@ -105,7 +105,7 @@ struct LibrarySearchView: View {
|
||||||
.onAppear(perform: onAppear)
|
.onAppear(perform: onAppear)
|
||||||
.navigationBarTitle("Search", displayMode: .inline)
|
.navigationBarTitle("Search", displayMode: .inline)
|
||||||
.onChange(of: searchQuery) { query in
|
.onChange(of: searchQuery) { query in
|
||||||
if(CACurrentMediaTime() - lastSearchTime > 0.5) {
|
if CACurrentMediaTime() - lastSearchTime > 0.5 {
|
||||||
lastSearchTime = CACurrentMediaTime()
|
lastSearchTime = CACurrentMediaTime()
|
||||||
requestSearch(query: query)
|
requestSearch(query: query)
|
||||||
}
|
}
|
||||||
|
@ -113,4 +113,4 @@ struct LibrarySearchView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//stream NM5 by nicki!
|
// stream NM5 by nicki!
|
||||||
|
|
|
@ -24,10 +24,10 @@ struct LibraryView: View {
|
||||||
var genre: String = ""
|
var genre: String = ""
|
||||||
var studio: String = ""
|
var studio: String = ""
|
||||||
|
|
||||||
@State private var totalPages: Int = 0;
|
@State private var totalPages: Int = 0
|
||||||
@State private var currentPage: Int = 0;
|
@State private var currentPage: Int = 0
|
||||||
@State private var isSearching: String? = "";
|
@State private var isSearching: String? = ""
|
||||||
@State private var viewDidLoad: Bool = false;
|
@State private var viewDidLoad: Bool = false
|
||||||
|
|
||||||
init(usingParentID: String, title: String) {
|
init(usingParentID: String, title: String) {
|
||||||
self.usingParentID = usingParentID
|
self.usingParentID = usingParentID
|
||||||
|
@ -61,7 +61,7 @@ struct LibraryView: View {
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
recalcTracks()
|
recalcTracks()
|
||||||
|
|
||||||
if(viewDidLoad) {
|
if viewDidLoad {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ struct LibraryView: View {
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
@ -84,7 +84,7 @@ struct LibraryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: tracks for grid
|
// MARK: tracks for grid
|
||||||
@State private var tracks: [GridItem] = []
|
@State private var tracks: [GridItem] = []
|
||||||
func recalcTracks() {
|
func recalcTracks() {
|
||||||
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
|
||||||
|
@ -96,10 +96,10 @@ struct LibraryView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if(isLoading == true) {
|
if isLoading == true {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else {
|
} else {
|
||||||
if(!items.isEmpty) {
|
if !items.isEmpty {
|
||||||
VStack {
|
VStack {
|
||||||
ScrollView(.vertical) {
|
ScrollView(.vertical) {
|
||||||
Spacer().frame(height: 16)
|
Spacer().frame(height: 16)
|
||||||
|
@ -132,10 +132,10 @@ struct LibraryView: View {
|
||||||
}.onChange(of: orientationInfo.orientation) { _ in
|
}.onChange(of: orientationInfo.orientation) { _ in
|
||||||
recalcTracks()
|
recalcTracks()
|
||||||
}
|
}
|
||||||
if(totalPages > 1) {
|
if totalPages > 1 {
|
||||||
HStack() {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
HStack() {
|
HStack {
|
||||||
Button {
|
Button {
|
||||||
currentPage = currentPage - 1
|
currentPage = currentPage - 1
|
||||||
onAppear()
|
onAppear()
|
||||||
|
@ -169,7 +169,7 @@ struct LibraryView: View {
|
||||||
.navigationBarTitle(title, displayMode: .inline)
|
.navigationBarTitle(title, displayMode: .inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
if(currentPage > 0) {
|
if currentPage > 0 {
|
||||||
Button {
|
Button {
|
||||||
currentPage = currentPage - 1
|
currentPage = currentPage - 1
|
||||||
onAppear()
|
onAppear()
|
||||||
|
@ -177,7 +177,7 @@ struct LibraryView: View {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(currentPage < totalPages - 1) {
|
if currentPage < totalPages - 1 {
|
||||||
Button {
|
Button {
|
||||||
currentPage = currentPage + 1
|
currentPage = currentPage + 1
|
||||||
onAppear()
|
onAppear()
|
||||||
|
@ -185,7 +185,7 @@ struct LibraryView: View {
|
||||||
Image(systemName: "chevron.right")
|
Image(systemName: "chevron.right")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(usingParentID != "") {
|
if usingParentID != "" {
|
||||||
NavigationLink(destination: LibrarySearchView(usingParentID: usingParentID)) {
|
NavigationLink(destination: LibrarySearchView(usingParentID: usingParentID)) {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
}
|
}
|
||||||
|
@ -195,4 +195,4 @@ struct LibraryView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//stream BM^S by nicki!
|
// stream BM^S by nicki!
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct LoadingView<Content>: View where Content: View {
|
||||||
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { _ in
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
// the content to display - if the modal is showing, we'll blur it
|
// the content to display - if the modal is showing, we'll blur it
|
||||||
content()
|
content()
|
||||||
|
@ -30,7 +30,7 @@ struct LoadingView<Content>: View where Content: View {
|
||||||
|
|
||||||
// the magic bit - our ProgressView just displays an activity
|
// the magic bit - our ProgressView just displays an activity
|
||||||
// indicator, with some text underneath showing what we are doing
|
// indicator, with some text underneath showing what we are doing
|
||||||
HStack() {
|
HStack {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -53,7 +53,7 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
||||||
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { _ in
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
// the content to display - if the modal is showing, we'll blur it
|
// the content to display - if the modal is showing, we'll blur it
|
||||||
content()
|
content()
|
||||||
|
@ -68,7 +68,7 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
||||||
|
|
||||||
// the magic bit - our ProgressView just displays an activity
|
// the magic bit - our ProgressView just displays an activity
|
||||||
// indicator, with some text underneath showing what we are doing
|
// indicator, with some text underneath showing what we are doing
|
||||||
HStack() {
|
HStack {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -83,4 +83,3 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -200,7 +200,7 @@ struct MovieItemView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer().frame(width: 16)
|
Spacer().frame(width: 16)
|
||||||
ForEach(item.people!, id: \.self) { person in
|
ForEach(item.people!, id: \.self) { person in
|
||||||
if(person.type! == "Actor") {
|
if person.type! == "Actor" {
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
LibraryView(withPerson: person)
|
LibraryView(withPerson: person)
|
||||||
}) {
|
}) {
|
||||||
|
@ -399,7 +399,7 @@ struct MovieItemView: View {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer().frame(width: 16)
|
Spacer().frame(width: 16)
|
||||||
ForEach(item.people!, id: \.self) { person in
|
ForEach(item.people!, id: \.self) { person in
|
||||||
if(person.type! == "Actor") {
|
if person.type! == "Actor" {
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
LibraryView(withPerson: person)
|
LibraryView(withPerson: person)
|
||||||
}) {
|
}) {
|
||||||
|
|
|
@ -13,16 +13,16 @@ struct NextUpView: View {
|
||||||
@EnvironmentObject var globalData: GlobalData
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
@State private var items: [BaseItemDto] = []
|
@State private var items: [BaseItemDto] = []
|
||||||
@State private var viewDidLoad: Bool = false;
|
@State private var viewDidLoad: Bool = false
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(viewDidLoad == true) {
|
if viewDidLoad == true {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewDidLoad = true;
|
viewDidLoad = true
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
@ -34,14 +34,14 @@ struct NextUpView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if(items.count != 0) {
|
if items.count != 0 {
|
||||||
Text("Next Up")
|
Text("Next Up")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack() {
|
LazyHStack {
|
||||||
Spacer().frame(width:16)
|
Spacer().frame(width: 16)
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
NavigationLink(destination: ItemView(item: item)) {
|
NavigationLink(destination: ItemView(item: item)) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -54,7 +54,7 @@ struct NextUpView: View {
|
||||||
}
|
}
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
Spacer().frame(height:5)
|
Spacer().frame(height: 5)
|
||||||
Text(item.seriesName!)
|
Text(item.seriesName!)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
@ -66,7 +66,7 @@ struct NextUpView: View {
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}.frame(width: 100)
|
}.frame(width: 100)
|
||||||
Spacer().frame(width:16)
|
Spacer().frame(width: 16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ struct PersistenceController {
|
||||||
let result = PersistenceController(inMemory: true)
|
let result = PersistenceController(inMemory: true)
|
||||||
let viewContext = result.container.viewContext
|
let viewContext = result.container.viewContext
|
||||||
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try viewContext.save()
|
try viewContext.save()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -36,7 +35,7 @@ struct PersistenceController {
|
||||||
if inMemory {
|
if inMemory {
|
||||||
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
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? {
|
if let error = error as NSError? {
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
// 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.
|
// 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.
|
||||||
|
|
|
@ -24,12 +24,12 @@ struct SeasonItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
if(viewDidLoad) {
|
if viewDidLoad {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
isLoading = false
|
isLoading = false
|
||||||
|
|
|
@ -13,22 +13,22 @@ struct SeriesItemView: View {
|
||||||
@EnvironmentObject private var globalData: GlobalData
|
@EnvironmentObject private var globalData: GlobalData
|
||||||
@EnvironmentObject private var orientationInfo: OrientationInfo
|
@EnvironmentObject private var orientationInfo: OrientationInfo
|
||||||
|
|
||||||
var item: BaseItemDto;
|
var item: BaseItemDto
|
||||||
|
|
||||||
@State private var seasons: [BaseItemDto] = [];
|
@State private var seasons: [BaseItemDto] = []
|
||||||
@State private var isLoading: Bool = true;
|
@State private var isLoading: Bool = true
|
||||||
@State private var viewDidLoad: Bool = false;
|
@State private var viewDidLoad: Bool = false
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
recalcTracks()
|
recalcTracks()
|
||||||
if(viewDidLoad) {
|
if viewDidLoad {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
|
@ -40,12 +40,11 @@ struct SeriesItemView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: Grid tracks
|
// MARK: Grid tracks
|
||||||
func recalcTracks() {
|
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 = []
|
tracks = []
|
||||||
for _ in (0..<trkCnt)
|
for _ in (0..<trkCnt) {
|
||||||
{
|
|
||||||
tracks.append(GridItem.init(.flexible()))
|
tracks.append(GridItem.init(.flexible()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,7 @@ struct SeriesItemView: View {
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}
|
}
|
||||||
.frame(width:100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 5)
|
.shadow(radius: 5)
|
||||||
Text(season.name ?? "")
|
Text(season.name ?? "")
|
||||||
|
@ -74,7 +73,7 @@ struct SeriesItemView: View {
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
if(season.productionYear != nil) {
|
if season.productionYear != nil {
|
||||||
Text(String(season.productionYear!))
|
Text(String(season.productionYear!))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
@ -84,7 +83,7 @@ struct SeriesItemView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer().frame(height: 2)
|
Spacer().frame(height: 2)
|
||||||
}.onChange(of: orientationInfo.orientation) { ip in
|
}.onChange(of: orientationInfo.orientation) { _ in
|
||||||
recalcTracks()
|
recalcTracks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,21 +11,21 @@ import JellyfinAPI
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
|
|
||||||
struct Subtitle {
|
struct Subtitle {
|
||||||
var name: String;
|
var name: String
|
||||||
var id: Int32;
|
var id: Int32
|
||||||
var url: URL;
|
var url: URL
|
||||||
var delivery: SubtitleDeliveryMethod;
|
var delivery: SubtitleDeliveryMethod
|
||||||
var codec: String;
|
var codec: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AudioTrack {
|
struct AudioTrack {
|
||||||
var name: String;
|
var name: String
|
||||||
var id: Int32;
|
var id: Int32
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaybackItem: ObservableObject {
|
class PlaybackItem: ObservableObject {
|
||||||
@Published var videoType: PlayMethod = .directPlay;
|
@Published var videoType: PlayMethod = .directPlay
|
||||||
@Published var videoUrl: URL = URL(string: "https://example.com")!;
|
@Published var videoUrl: URL = URL(string: "https://example.com")!
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol PlayerViewControllerDelegate: AnyObject {
|
protocol PlayerViewControllerDelegate: AnyObject {
|
||||||
|
@ -50,34 +50,34 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
@IBOutlet weak var jumpForwardButton: UIButton!
|
@IBOutlet weak var jumpForwardButton: UIButton!
|
||||||
@IBOutlet weak var playerSettingsButton: UIButton!
|
@IBOutlet weak var playerSettingsButton: UIButton!
|
||||||
|
|
||||||
var shouldShowLoadingScreen: Bool = false;
|
var shouldShowLoadingScreen: Bool = false
|
||||||
var ssTargetValueOffset: Int = 0;
|
var ssTargetValueOffset: Int = 0
|
||||||
var ssStartValue: Int = 0;
|
var ssStartValue: Int = 0
|
||||||
var optionsVC: VideoPlayerSettingsView?;
|
var optionsVC: VideoPlayerSettingsView?
|
||||||
|
|
||||||
var paused: Bool = true;
|
var paused: Bool = true
|
||||||
var lastTime: Float = 0.0;
|
var lastTime: Float = 0.0
|
||||||
var startTime: Int = 0;
|
var startTime: Int = 0
|
||||||
var controlsAppearTime: Double = 0;
|
var controlsAppearTime: Double = 0
|
||||||
|
|
||||||
var selectedAudioTrack: Int32 = -1 {
|
var selectedAudioTrack: Int32 = -1 {
|
||||||
didSet {
|
didSet {
|
||||||
print(selectedAudioTrack)
|
print(selectedAudioTrack)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
var selectedCaptionTrack: Int32 = -1 {
|
var selectedCaptionTrack: Int32 = -1 {
|
||||||
didSet {
|
didSet {
|
||||||
print(selectedCaptionTrack)
|
print(selectedCaptionTrack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var playSessionId: String = "";
|
var playSessionId: String = ""
|
||||||
var lastProgressReportTime: Double = 0;
|
var lastProgressReportTime: Double = 0
|
||||||
|
|
||||||
var subtitleTrackArray: [Subtitle] = [];
|
var subtitleTrackArray: [Subtitle] = []
|
||||||
var audioTrackArray: [AudioTrack] = [];
|
var audioTrackArray: [AudioTrack] = []
|
||||||
|
|
||||||
var manifest: BaseItemDto = BaseItemDto();
|
var manifest: BaseItemDto = BaseItemDto()
|
||||||
var playbackItem = PlaybackItem();
|
var playbackItem = PlaybackItem()
|
||||||
|
|
||||||
@IBAction func seekSliderStart(_ sender: Any) {
|
@IBAction func seekSliderStart(_ sender: Any) {
|
||||||
sendProgressReport(eventName: "pause")
|
sendProgressReport(eventName: "pause")
|
||||||
|
@ -86,16 +86,16 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
|
|
||||||
@IBAction func seekSliderValueChanged(_ sender: Any) {
|
@IBAction func seekSliderValueChanged(_ sender: Any) {
|
||||||
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))/1000
|
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))/1000
|
||||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration);
|
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||||
let scrubRemaining = videoDuration - secondsScrubbedTo;
|
let scrubRemaining = videoDuration - secondsScrubbedTo
|
||||||
let remainingTime = scrubRemaining;
|
let remainingTime = scrubRemaining
|
||||||
let hours = floor(remainingTime / 3600);
|
let hours = floor(remainingTime / 3600)
|
||||||
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60;
|
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60
|
||||||
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60);
|
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60)
|
||||||
if(hours != 0) {
|
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"))";
|
timeText.text = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||||
} else {
|
} 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"))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,14 +103,14 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
print("ss end")
|
print("ss end")
|
||||||
let videoPosition = Double(mediaPlayer.time.intValue)
|
let videoPosition = Double(mediaPlayer.time.intValue)
|
||||||
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.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.
|
// Scrub is value from 0..1 - find position in video and add / or remove.
|
||||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration);
|
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||||
let offset = secondsScrubbedTo - videoPosition;
|
let offset = secondsScrubbedTo - videoPosition
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
if(offset > 0) {
|
if offset > 0 {
|
||||||
mediaPlayer.jumpForward(Int32(offset)/1000);
|
mediaPlayer.jumpForward(Int32(offset)/1000)
|
||||||
} else {
|
} else {
|
||||||
mediaPlayer.jumpBackward(Int32(abs(offset))/1000);
|
mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
|
||||||
}
|
}
|
||||||
sendProgressReport(eventName: "unpause")
|
sendProgressReport(eventName: "unpause")
|
||||||
}
|
}
|
||||||
|
@ -131,13 +131,13 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func jumpBackTapped(_ sender: Any) {
|
@IBAction func jumpBackTapped(_ sender: Any) {
|
||||||
if(paused == false) {
|
if paused == false {
|
||||||
mediaPlayer.jumpBackward(15)
|
mediaPlayer.jumpBackward(15)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func jumpForwardTapped(_ sender: Any) {
|
@IBAction func jumpForwardTapped(_ sender: Any) {
|
||||||
if(paused == false) {
|
if paused == false {
|
||||||
mediaPlayer.jumpForward(30)
|
mediaPlayer.jumpForward(30)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,14 +145,14 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
@IBOutlet weak var mainActionButton: UIButton!
|
@IBOutlet weak var mainActionButton: UIButton!
|
||||||
@IBAction func mainActionButtonPressed(_ sender: Any) {
|
@IBAction func mainActionButtonPressed(_ sender: Any) {
|
||||||
print(mediaPlayer.state.rawValue)
|
print(mediaPlayer.state.rawValue)
|
||||||
if(paused) {
|
if paused {
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||||
paused = false;
|
paused = false
|
||||||
} else {
|
} else {
|
||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
||||||
paused = true;
|
paused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +163,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
optionsVC?.modalPresentationStyle = .popover
|
optionsVC?.modalPresentationStyle = .popover
|
||||||
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
|
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
|
||||||
|
|
||||||
|
|
||||||
// Present the view controller (in a popover).
|
// Present the view controller (in a popover).
|
||||||
self.present(optionsVC!, animated: true) {
|
self.present(optionsVC!, animated: true) {
|
||||||
print("popover visible, pause playback")
|
print("popover visible, pause playback")
|
||||||
|
@ -188,41 +187,41 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
|
|
||||||
func setupNowPlayingCC() {
|
func setupNowPlayingCC() {
|
||||||
let commandCenter = MPRemoteCommandCenter.shared()
|
let commandCenter = MPRemoteCommandCenter.shared()
|
||||||
commandCenter.playCommand.isEnabled = true;
|
commandCenter.playCommand.isEnabled = true
|
||||||
commandCenter.pauseCommand.isEnabled = true;
|
commandCenter.pauseCommand.isEnabled = true
|
||||||
commandCenter.seekForwardCommand.isEnabled = true;
|
commandCenter.seekForwardCommand.isEnabled = true
|
||||||
commandCenter.seekBackwardCommand.isEnabled = true;
|
commandCenter.seekBackwardCommand.isEnabled = true
|
||||||
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
commandCenter.changePlaybackPositionCommand.isEnabled = true
|
||||||
|
|
||||||
// Add handler for Pause Command
|
// Add handler for Pause Command
|
||||||
commandCenter.pauseCommand.addTarget{ event in
|
commandCenter.pauseCommand.addTarget { _ in
|
||||||
self.mediaPlayer.pause()
|
self.mediaPlayer.pause()
|
||||||
self.sendProgressReport(eventName: "pause")
|
self.sendProgressReport(eventName: "pause")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add handler for Play command
|
// Add handler for Play command
|
||||||
commandCenter.playCommand.addTarget{ event in
|
commandCenter.playCommand.addTarget { _ in
|
||||||
self.mediaPlayer.play()
|
self.mediaPlayer.play()
|
||||||
self.sendProgressReport(eventName: "unpause")
|
self.sendProgressReport(eventName: "unpause")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add handler for FF command
|
// Add handler for FF command
|
||||||
commandCenter.seekForwardCommand.addTarget{ event in
|
commandCenter.seekForwardCommand.addTarget { _ in
|
||||||
self.mediaPlayer.jumpForward(30)
|
self.mediaPlayer.jumpForward(30)
|
||||||
self.sendProgressReport(eventName: "timeupdate")
|
self.sendProgressReport(eventName: "timeupdate")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add handler for RW command
|
// Add handler for RW command
|
||||||
commandCenter.seekBackwardCommand.addTarget{ event in
|
commandCenter.seekBackwardCommand.addTarget { _ in
|
||||||
self.mediaPlayer.jumpBackward(15)
|
self.mediaPlayer.jumpBackward(15)
|
||||||
self.sendProgressReport(eventName: "timeupdate")
|
self.sendProgressReport(eventName: "timeupdate")
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
//Scrubber
|
// Scrubber
|
||||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
|
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
|
||||||
guard let self = self else {return .commandFailed}
|
guard let self = self else {return .commandFailed}
|
||||||
|
|
||||||
|
@ -230,11 +229,11 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
let targetSeconds = event.positionTime
|
let targetSeconds = event.positionTime
|
||||||
|
|
||||||
let videoPosition = Double(self.mediaPlayer.time.intValue)
|
let videoPosition = Double(self.mediaPlayer.time.intValue)
|
||||||
let offset = targetSeconds - videoPosition;
|
let offset = targetSeconds - videoPosition
|
||||||
if(offset > 0) {
|
if offset > 0 {
|
||||||
self.mediaPlayer.jumpForward(Int32(offset)/1000);
|
self.mediaPlayer.jumpForward(Int32(offset)/1000)
|
||||||
} else {
|
} else {
|
||||||
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000);
|
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
|
||||||
}
|
}
|
||||||
self.sendProgressReport(eventName: "unpause")
|
self.sendProgressReport(eventName: "unpause")
|
||||||
|
|
||||||
|
@ -244,7 +243,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nowPlayingInfo = [String : Any]()
|
var nowPlayingInfo = [String: Any]()
|
||||||
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? ""
|
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? ""
|
||||||
|
|
||||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||||
|
@ -258,29 +257,28 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
//View has loaded.
|
// View has loaded.
|
||||||
|
|
||||||
//Rotate to landscape only if necessary
|
// Rotate to landscape only if necessary
|
||||||
UIViewController.attemptRotationToDeviceOrientation();
|
UIViewController.attemptRotationToDeviceOrientation()
|
||||||
|
|
||||||
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||||
//mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||||
|
|
||||||
|
|
||||||
mediaPlayer.delegate = self
|
mediaPlayer.delegate = self
|
||||||
mediaPlayer.drawable = videoContentView
|
mediaPlayer.drawable = videoContentView
|
||||||
|
|
||||||
if(manifest.type == "Movie") {
|
if manifest.type == "Movie" {
|
||||||
titleLabel.text = manifest.name
|
titleLabel.text = manifest.name
|
||||||
} else {
|
} else {
|
||||||
titleLabel.text = "S\(String(manifest.parentIndexNumber!)):E\(String(manifest.indexNumber!)) “\(manifest.name!)”"
|
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 defaults = UserDefaults.standard
|
||||||
let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
|
let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
|
||||||
|
|
||||||
//Build a device profile
|
// Build a device profile
|
||||||
let builder = DeviceProfileBuilder()
|
let builder = DeviceProfileBuilder()
|
||||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||||
let profile = builder.buildProfile()
|
let profile = builder.buildProfile()
|
||||||
|
@ -294,77 +292,77 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
}, receiveValue: { [self] response in
|
}, receiveValue: { [self] response in
|
||||||
playSessionId = response.playSessionId!
|
playSessionId = response.playSessionId!
|
||||||
let mediaSource = response.mediaSources!.first.self!
|
let mediaSource = response.mediaSources!.first.self!
|
||||||
if(mediaSource.transcodingUrl != nil) {
|
if mediaSource.transcodingUrl != nil {
|
||||||
//Item is being transcoded by request of server
|
// Item is being transcoded by request of server
|
||||||
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
|
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
|
||||||
let item = PlaybackItem()
|
let item = PlaybackItem()
|
||||||
item.videoType = .transcode
|
item.videoType = .transcode
|
||||||
item.videoUrl = streamURL!
|
item.videoUrl = streamURL!
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack);
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
//Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
for stream in mediaSource.mediaStreams! {
|
for stream in mediaSource.mediaStreams! {
|
||||||
if(stream.type == .subtitle) {
|
if stream.type == .subtitle {
|
||||||
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
|
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!)
|
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!))
|
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
||||||
if(stream.isDefault! == true) {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!);
|
selectedAudioTrack = Int32(stream.index!)
|
||||||
}
|
}
|
||||||
audioTrackArray.append(subtitle);
|
audioTrackArray.append(subtitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(selectedAudioTrack == -1) {
|
if selectedAudioTrack == -1 {
|
||||||
if(audioTrackArray.count > 0) {
|
if audioTrackArray.count > 0 {
|
||||||
selectedAudioTrack = audioTrackArray[0].id;
|
selectedAudioTrack = audioTrackArray[0].id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sendPlayReport()
|
self.sendPlayReport()
|
||||||
playbackItem = item;
|
playbackItem = item
|
||||||
} else {
|
} else {
|
||||||
//Item will be directly played by the client.
|
// 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 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()
|
let item = PlaybackItem()
|
||||||
item.videoUrl = streamURL
|
item.videoUrl = streamURL
|
||||||
item.videoType = .directPlay
|
item.videoType = .directPlay
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack);
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
//Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
for stream in mediaSource.mediaStreams! {
|
for stream in mediaSource.mediaStreams! {
|
||||||
if(stream.type == .subtitle) {
|
if stream.type == .subtitle {
|
||||||
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
|
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!)
|
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!))
|
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
|
||||||
if(stream.isDefault! == true) {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!);
|
selectedAudioTrack = Int32(stream.index!)
|
||||||
}
|
}
|
||||||
audioTrackArray.append(subtitle);
|
audioTrackArray.append(subtitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(selectedAudioTrack == -1) {
|
if selectedAudioTrack == -1 {
|
||||||
if(audioTrackArray.count > 0) {
|
if audioTrackArray.count > 0 {
|
||||||
selectedAudioTrack = audioTrackArray[0].id;
|
selectedAudioTrack = audioTrackArray[0].id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sendPlayReport()
|
self.sendPlayReport()
|
||||||
playbackItem = item;
|
playbackItem = item
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setupNowPlayingCC()
|
self.setupNowPlayingCC()
|
||||||
|
@ -374,15 +372,15 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
print(manifest.userData?.playbackPositionTicks ?? 0)
|
print(manifest.userData?.playbackPositionTicks ?? 0)
|
||||||
mediaPlayer.jumpForward(Int32(manifest.userData?.playbackPositionTicks ?? 0/10000000))
|
mediaPlayer.jumpForward(Int32(manifest.userData?.playbackPositionTicks ?? 0/10000000))
|
||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
subtitleTrackArray.forEach() { sub in
|
subtitleTrackArray.forEach { sub in
|
||||||
if(sub.id != -1 && sub.delivery == .external && sub.codec != "subrip") {
|
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
|
||||||
print("adding subs for id: \(sub.id) w/ url: \(sub.url)")
|
print("adding subs for id: \(sub.id) w/ url: \(sub.url)")
|
||||||
mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false)
|
mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delegate?.showLoadingView(self)
|
delegate?.showLoadingView(self)
|
||||||
while(mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1) {}
|
while mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1 {}
|
||||||
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack;
|
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack
|
||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
})
|
})
|
||||||
|
@ -392,10 +390,10 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
self.tabBarController?.tabBar.isHidden = true;
|
self.tabBarController?.tabBar.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: VideoPlayerSettings Delegate
|
// MARK: VideoPlayerSettings Delegate
|
||||||
func subtitleTrackChanged(newTrackID: Int32) {
|
func subtitleTrackChanged(newTrackID: Int32) {
|
||||||
selectedCaptionTrack = newTrackID
|
selectedCaptionTrack = newTrackID
|
||||||
mediaPlayer.currentVideoSubTitleIndex = newTrackID
|
mediaPlayer.currentVideoSubTitleIndex = newTrackID
|
||||||
|
@ -406,24 +404,23 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
mediaPlayer.currentAudioTrackIndex = newTrackID
|
mediaPlayer.currentAudioTrackIndex = newTrackID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: VLCMediaPlayer Delegates
|
||||||
//MARK: VLCMediaPlayer Delegates
|
|
||||||
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||||
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .stopped :
|
case .stopped :
|
||||||
break;
|
break
|
||||||
case .ended :
|
case .ended :
|
||||||
break;
|
break
|
||||||
case .playing :
|
case .playing :
|
||||||
print("Video is playing")
|
print("Video is playing")
|
||||||
sendProgressReport(eventName: "unpause")
|
sendProgressReport(eventName: "unpause")
|
||||||
delegate?.hideLoadingView(self)
|
delegate?.hideLoadingView(self)
|
||||||
paused = false;
|
paused = false
|
||||||
|
|
||||||
case .paused :
|
case .paused :
|
||||||
print("Video is paused)")
|
print("Video is paused)")
|
||||||
paused = true;
|
paused = true
|
||||||
|
|
||||||
case .opening :
|
case .opening :
|
||||||
print("Video is opening)")
|
print("Video is opening)")
|
||||||
|
@ -446,53 +443,53 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||||
let time = mediaPlayer.position;
|
let time = mediaPlayer.position
|
||||||
if(time != lastTime) {
|
if time != lastTime {
|
||||||
paused = false;
|
paused = false
|
||||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||||
seekSlider.setValue(mediaPlayer.position, animated: true)
|
seekSlider.setValue(mediaPlayer.position, animated: true)
|
||||||
delegate?.hideLoadingView(self)
|
delegate?.hideLoadingView(self)
|
||||||
|
|
||||||
let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000;
|
let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000
|
||||||
let hours = remainingTime / 3600;
|
let hours = remainingTime / 3600
|
||||||
let minutes = (remainingTime % 3600) / 60;
|
let minutes = (remainingTime % 3600) / 60
|
||||||
let seconds = (remainingTime % 3600) % 60;
|
let seconds = (remainingTime % 3600) % 60
|
||||||
var timeTextStr = "";
|
var timeTextStr = ""
|
||||||
if(hours != 0) {
|
if hours != 0 {
|
||||||
timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
|
timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||||
} else {
|
} 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
|
timeText.text = timeTextStr
|
||||||
|
|
||||||
if(CACurrentMediaTime() - controlsAppearTime > 5) {
|
if CACurrentMediaTime() - controlsAppearTime > 5 {
|
||||||
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
|
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
|
||||||
self.videoControlsView.alpha = 0.0
|
self.videoControlsView.alpha = 0.0
|
||||||
}, completion: { (finished: Bool) in
|
}, completion: { (_: Bool) in
|
||||||
self.videoControlsView.isHidden = true;
|
self.videoControlsView.isHidden = true
|
||||||
self.videoControlsView.alpha = 1
|
self.videoControlsView.alpha = 1
|
||||||
})
|
})
|
||||||
controlsAppearTime = 10000000000000000000000;
|
controlsAppearTime = 10000000000000000000000
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
paused = true;
|
paused = true
|
||||||
}
|
}
|
||||||
lastTime = time;
|
lastTime = time
|
||||||
|
|
||||||
if(CACurrentMediaTime() - lastProgressReportTime > 5) {
|
if CACurrentMediaTime() - lastProgressReportTime > 5 {
|
||||||
sendProgressReport(eventName: "timeupdate")
|
sendProgressReport(eventName: "timeupdate")
|
||||||
lastProgressReportTime = CACurrentMediaTime()
|
lastProgressReportTime = CACurrentMediaTime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: Jellyfin Playstate updates
|
// MARK: Jellyfin Playstate updates
|
||||||
func sendProgressReport(eventName: String) {
|
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")
|
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)
|
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { _ in
|
||||||
print("Playback progress report sent!")
|
print("Playback progress report sent!")
|
||||||
})
|
})
|
||||||
.store(in: &globalData.pendingAPIRequests)
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
|
@ -504,7 +501,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { _ in
|
||||||
print("Playback stop report sent!")
|
print("Playback stop report sent!")
|
||||||
})
|
})
|
||||||
.store(in: &globalData.pendingAPIRequests)
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
|
@ -518,7 +515,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
|
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { _ in
|
||||||
print("Playback start report sent!")
|
print("Playback start report sent!")
|
||||||
})
|
})
|
||||||
.store(in: &globalData.pendingAPIRequests)
|
.store(in: &globalData.pendingAPIRequests)
|
||||||
|
@ -528,7 +525,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
|
||||||
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@EnvironmentObject private var globalData: GlobalData;
|
@EnvironmentObject private var globalData: GlobalData
|
||||||
|
|
||||||
var loadBinding: Binding<Bool>
|
var loadBinding: Binding<Bool>
|
||||||
var pBinding: Binding<Bool>
|
var pBinding: Binding<Bool>
|
||||||
|
@ -543,15 +540,15 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||||
}
|
}
|
||||||
|
|
||||||
func hideLoadingView(_ viewController: PlayerViewController) {
|
func hideLoadingView(_ viewController: PlayerViewController) {
|
||||||
self.loadBinding.wrappedValue = false;
|
self.loadBinding.wrappedValue = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func showLoadingView(_ viewController: PlayerViewController) {
|
func showLoadingView(_ viewController: PlayerViewController) {
|
||||||
self.loadBinding.wrappedValue = true;
|
self.loadBinding.wrappedValue = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitPlayer(_ viewController: PlayerViewController) {
|
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)
|
Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
typealias UIViewControllerType = PlayerViewController
|
typealias UIViewControllerType = PlayerViewController
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls.UIViewControllerType {
|
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls.UIViewControllerType {
|
||||||
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
||||||
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
||||||
customViewController.manifest = item;
|
customViewController.manifest = item
|
||||||
customViewController.delegate = context.coordinator;
|
customViewController.delegate = context.coordinator
|
||||||
customViewController.globalData = globalData;
|
customViewController.globalData = globalData
|
||||||
return customViewController
|
return customViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,17 +30,17 @@ class VideoPlayerSettingsView: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPlayerSettings: View {
|
struct VideoPlayerSettings: View {
|
||||||
@State var delegate: PlayerViewController!
|
@State weak var delegate: PlayerViewController!
|
||||||
@State var captionTrack: Int32 = -99;
|
@State var captionTrack: Int32 = -99
|
||||||
@State var audioTrack: Int32 = -99;
|
@State var audioTrack: Int32 = -99
|
||||||
|
|
||||||
init(delegate: PlayerViewController) {
|
init(delegate: PlayerViewController) {
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView() {
|
NavigationView {
|
||||||
Form() {
|
Form {
|
||||||
Picker("Closed Captions", selection: $captionTrack) {
|
Picker("Closed Captions", selection: $captionTrack) {
|
||||||
ForEach(delegate.subtitleTrackArray, id: \.id) { caption in
|
ForEach(delegate.subtitleTrackArray, id: \.id) { caption in
|
||||||
Text(caption.name).tag(caption.id)
|
Text(caption.name).tag(caption.id)
|
||||||
|
@ -60,11 +60,11 @@ struct VideoPlayerSettings: View {
|
||||||
.navigationTitle("Audio & Captions")
|
.navigationTitle("Audio & Captions")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
if(UIDevice.current.userInterfaceIdiom == .phone) {
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
Button {
|
Button {
|
||||||
self.delegate.settingsPopoverDismissed()
|
self.delegate.settingsPopoverDismissed()
|
||||||
} label: {
|
} label: {
|
||||||
HStack() {
|
HStack {
|
||||||
Image(systemName: "chevron.left")
|
Image(systemName: "chevron.left")
|
||||||
Text("Back").font(.callout)
|
Text("Back").font(.callout)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,57 +9,57 @@ import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
//001fC^ = dark grey plain blurhash
|
// 001fC^ = dark grey plain blurhash
|
||||||
|
|
||||||
extension BaseItemDto {
|
extension BaseItemDto {
|
||||||
|
|
||||||
//MARK: Images
|
// MARK: Images
|
||||||
func getSeriesBackdropImageBlurHash() -> String {
|
func getSeriesBackdropImageBlurHash() -> String {
|
||||||
let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
|
let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString
|
||||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||||
|
|
||||||
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
|
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSeriesPrimaryImageBlurHash() -> String {
|
func getSeriesPrimaryImageBlurHash() -> String {
|
||||||
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
|
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString
|
||||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||||
|
|
||||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPrimaryImageBlurHash() -> String {
|
func getPrimaryImageBlurHash() -> String {
|
||||||
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
|
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString
|
||||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||||
|
|
||||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBackdropImageBlurHash() -> String {
|
func getBackdropImageBlurHash() -> String {
|
||||||
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
|
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString
|
||||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||||
|
|
||||||
if(rawImgURL.contains("Backdrop")) {
|
if rawImgURL.contains("Backdrop") {
|
||||||
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
|
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
|
||||||
} else {
|
} else {
|
||||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
func getBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
var imageType = "";
|
var imageType = ""
|
||||||
var imageTag = "";
|
var imageTag = ""
|
||||||
|
|
||||||
if(self.primaryImageAspectRatio ?? 0.0 < 1.0) {
|
if self.primaryImageAspectRatio ?? 0.0 < 1.0 {
|
||||||
imageType = "Backdrop";
|
imageType = "Backdrop"
|
||||||
imageTag = (self.backdropImageTags ?? [""])[0]
|
imageTag = (self.backdropImageTags ?? [""])[0]
|
||||||
} else {
|
} else {
|
||||||
imageType = "Primary";
|
imageType = "Primary"
|
||||||
imageTag = self.imageTags?["Primary"] ?? ""
|
imageTag = self.imageTags?["Primary"] ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if(imageTag == "") {
|
if imageTag == "" {
|
||||||
imageType = "Backdrop";
|
imageType = "Backdrop"
|
||||||
imageTag = self.parentBackdropImageTags?[0] ?? ""
|
imageTag = self.parentBackdropImageTags?[0] ?? ""
|
||||||
}
|
}
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
@ -68,8 +68,8 @@ extension BaseItemDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSeriesBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
func getSeriesBackdropImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
let imageType = "Backdrop";
|
let imageType = "Backdrop"
|
||||||
let imageTag = (self.parentBackdropImageTags ?? [])[0];
|
let imageTag = (self.parentBackdropImageTags ?? [])[0]
|
||||||
|
|
||||||
print(imageType)
|
print(imageType)
|
||||||
print(imageTag)
|
print(imageTag)
|
||||||
|
@ -80,7 +80,7 @@ extension BaseItemDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
let imageType = "Primary";
|
let imageType = "Primary"
|
||||||
let imageTag = self.seriesPrimaryImageTag ?? ""
|
let imageTag = self.seriesPrimaryImageTag ?? ""
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
|
||||||
|
@ -88,10 +88,10 @@ extension BaseItemDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
let imageType = "Primary";
|
let imageType = "Primary"
|
||||||
var imageTag = self.imageTags?["Primary"] ?? "";
|
var imageTag = self.imageTags?["Primary"] ?? ""
|
||||||
|
|
||||||
if(imageTag == "") {
|
if imageTag == "" {
|
||||||
imageTag = self.seriesPrimaryImageTag ?? ""
|
imageTag = self.seriesPrimaryImageTag ?? ""
|
||||||
}
|
}
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
@ -100,7 +100,7 @@ extension BaseItemDto {
|
||||||
return URL(string: urlString)!
|
return URL(string: urlString)!
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: Calculations
|
// MARK: Calculations
|
||||||
func getItemRuntime() -> String {
|
func getItemRuntime() -> String {
|
||||||
let seconds: Int = Int(self.runTimeTicks!) / 10_000_000
|
let seconds: Int = Int(self.runTimeTicks!) / 10_000_000
|
||||||
let hours = (seconds / 3600)
|
let hours = (seconds / 3600)
|
||||||
|
@ -113,8 +113,8 @@ extension BaseItemDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getItemProgressString() -> String {
|
func getItemProgressString() -> String {
|
||||||
if(self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0) {
|
if self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0 {
|
||||||
return "";
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
let remainingSecs = Int(self.runTimeTicks! - (self.userData?.playbackPositionTicks!)!) / 10_000_000
|
let remainingSecs = Int(self.runTimeTicks! - (self.userData?.playbackPositionTicks!)!) / 10_000_000
|
||||||
|
@ -130,7 +130,7 @@ extension BaseItemDto {
|
||||||
|
|
||||||
extension BaseItemPerson {
|
extension BaseItemPerson {
|
||||||
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
func getImage(baseURL: String, maxWidth: Int) -> URL {
|
||||||
let imageType = "Primary";
|
let imageType = "Primary"
|
||||||
let imageTag = self.primaryImageTag ?? ""
|
let imageTag = self.primaryImageTag ?? ""
|
||||||
|
|
||||||
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
|
||||||
|
@ -140,9 +140,9 @@ extension BaseItemPerson {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBlurHash() -> String {
|
func getBlurHash() -> String {
|
||||||
let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString;
|
let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString
|
||||||
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
|
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
|
||||||
|
|
||||||
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
|
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,14 +115,12 @@ private func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||||
|
|
||||||
private func linearTosRGB(_ value: Float) -> Int {
|
private func linearTosRGB(_ value: Float) -> Int {
|
||||||
let v = max(0, min(1, value))
|
let v = max(0, min(1, value))
|
||||||
if v <= 0.0031308 { return Int(v * 12.92 * 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) }
|
||||||
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
||||||
let v = Float(Int64(value)) / 255
|
let v = Float(Int64(value)) / 255
|
||||||
if v <= 0.04045 { return v / 12.92 }
|
if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||||
else { return pow((v + 0.055) / 1.055, 2.4) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private let encodeCharacters: [String] = {
|
private let encodeCharacters: [String] = {
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
public extension Collection {
|
public extension Collection {
|
||||||
|
|
||||||
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.
|
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.
|
||||||
|
|
|
@ -15,11 +15,11 @@ func HandleAPIRequestCompletion(globalData: GlobalData, completion: Subscribers.
|
||||||
break
|
break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
if let err = error as? ErrorResponse {
|
if let err = error as? ErrorResponse {
|
||||||
switch(err){
|
switch err {
|
||||||
case .error(401, _, _, _):
|
case .error(401, _, _, _):
|
||||||
globalData.expiredCredentials = true;
|
globalData.expiredCredentials = true
|
||||||
case .error(_, _, _, _):
|
case .error:
|
||||||
globalData.networkError = true;
|
globalData.networkError = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
|
@ -19,8 +19,7 @@ struct ParallaxHeaderScrollView<Header: View, StaticOverlayView: View, Content:
|
||||||
staticOverlayView: StaticOverlayView,
|
staticOverlayView: StaticOverlayView,
|
||||||
overlayAlignment: Alignment = .center,
|
overlayAlignment: Alignment = .center,
|
||||||
headerHeight: CGFloat,
|
headerHeight: CGFloat,
|
||||||
content: @escaping () -> Content)
|
content: @escaping () -> Content) {
|
||||||
{
|
|
||||||
self.header = header
|
self.header = header
|
||||||
self.staticOverlayView = staticOverlayView
|
self.staticOverlayView = staticOverlayView
|
||||||
self.overlayAlignment = overlayAlignment
|
self.overlayAlignment = overlayAlignment
|
||||||
|
|
|
@ -32,4 +32,3 @@ extension String {
|
||||||
return "\(padString)\(self)"
|
return "\(padString)\(self)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,10 @@ class GlobalData: ObservableObject {
|
||||||
@Published var authToken: String = ""
|
@Published var authToken: String = ""
|
||||||
@Published var server: Server!
|
@Published var server: Server!
|
||||||
@Published var authHeader: String = ""
|
@Published var authHeader: String = ""
|
||||||
@Published var isInNetwork: Bool = true;
|
@Published var isInNetwork: Bool = true
|
||||||
@Published var networkError: Bool = false;
|
@Published var networkError: Bool = false
|
||||||
@Published var expiredCredentials: Bool = false;
|
@Published var expiredCredentials: Bool = false
|
||||||
var pendingAPIRequests = Set<AnyCancellable>();
|
var pendingAPIRequests = Set<AnyCancellable>()
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GlobalData: Equatable {
|
extension GlobalData: Equatable {
|
||||||
|
|
|
@ -10,12 +10,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct UserSettings: Decodable {
|
struct UserSettings: Decodable {
|
||||||
var LocalMaxBitrate: Int;
|
var LocalMaxBitrate: Int
|
||||||
var RemoteMaxBitrate: Int;
|
var RemoteMaxBitrate: Int
|
||||||
var AutoSelectSubtitles: Bool;
|
var AutoSelectSubtitles: Bool
|
||||||
var AutoSelectSubtitlesLangcode: String;
|
var AutoSelectSubtitlesLangcode: String
|
||||||
var SubtitlePositionOffset: Int;
|
var SubtitlePositionOffset: Int
|
||||||
var SubtitleFontName: String;
|
var SubtitleFontName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Bitrates: Codable, Hashable {
|
struct Bitrates: Codable, Hashable {
|
||||||
|
|
|
@ -380,7 +380,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -391,7 +391,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
@ -406,7 +406,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -418,7 +418,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
@ -431,7 +431,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
NextUpEntryView(entry: .init(date: Date(),
|
NextUpEntryView(entry: .init(date: Date(),
|
||||||
items: [
|
items: [
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -441,7 +441,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol"))
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
|
Loading…
Reference in New Issue