[create-pull-request] automated change (#47)

Co-authored-by: acvigue <acvigue@users.noreply.github.com>
This commit is contained in:
github-actions[bot] 2021-06-10 09:28:21 -07:00 committed by GitHub
parent 9ad53092a4
commit b26f81247a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 631 additions and 645 deletions

View File

@ -35,7 +35,7 @@ struct PersistenceController {
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

View File

@ -15,83 +15,83 @@ struct ConnectToServerView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var jsi: justSignedIn
@State private var uri = "";
@State private var isWorking = false;
@State private var isErrored = false;
@State private var isDone = false;
@State private var isSignInErrored = false;
@State private var isConnected = false;
@State private var serverName = "";
@State private var usernameDisabled: Bool = false;
@State private var publicUsers: [UserDto] = [];
@State private var lastPublicUsers: [UserDto] = [];
@State private var username = "";
@State private var password = "";
@State private var server_id = "";
@State private var serverSkipped: Bool = false;
@State private var serverSkippedAlert: Bool = false;
@State private var skip_server_bool: Bool = false;
@State private var skip_server_obj: Server!;
@State private var uri = ""
@State private var isWorking = false
@State private var isErrored = false
@State private var isDone = false
@State private var isSignInErrored = false
@State private var isConnected = false
@State private var serverName = ""
@State private var usernameDisabled: Bool = false
@State private var publicUsers: [UserDto] = []
@State private var lastPublicUsers: [UserDto] = []
@State private var username = ""
@State private var password = ""
@State private var server_id = ""
@State private var serverSkipped: Bool = false
@State private var serverSkippedAlert: Bool = false
@State private var skip_server_bool: Bool = false
@State private var skip_server_obj: Server!
@Binding var rootIsActive: Bool
private var reauthDeviceID: String = "";
private let userUUID = UUID();
private var reauthDeviceID: String = ""
private let userUUID = UUID()
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
_rootIsActive = isActive
skip_server_bool = skip_server
skip_server_obj = skip_server_prefill
reauthDeviceID = reauth_deviceId
}
init(isActive: Binding<Bool>) {
_rootIsActive = isActive
}
func start() {
if(skip_server_bool) {
if skip_server_bool {
uri = skip_server_obj.baseURI!
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(_):
skip_server_bool = false;
skip_server_obj = Server();
case .failure:
skip_server_bool = false
skip_server_obj = Server()
break
}
}, receiveValue: { response in
publicUsers = response
serverSkipped = true;
serverSkippedAlert = true;
serverSkipped = true
serverSkippedAlert = true
server_id = skip_server_obj.server_id!
serverName = skip_server_obj.name!
isConnected = true;
isConnected = true
})
.store(in: &globalData.pendingAPIRequests)
}
}
func doLogin() {
isWorking = true
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
var deviceName = UIDevice.current.name;
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""
print(authHeader)
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
let x: AuthenticateUserByName = AuthenticateUserByName(username: username, pw: password, password: nil)
UserAPI.authenticateUserByName(authenticateUserByName: x)
.sink(receiveCompletion: { completion in
isWorking = false
@ -106,29 +106,29 @@ struct ConnectToServerView: View {
} catch _ as NSError {
}
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
do {
try viewContext.execute(deleteRequest2)
} catch _ as NSError {
}
let newServer = Server(context: viewContext)
newServer.baseURI = uri
newServer.name = serverName
newServer.server_id = server_id
let newUser = SignedInUser(context: viewContext)
newUser.device_uuid = userUUID.uuidString
newUser.username = username
newUser.user_id = response.user!.id!
let keychain = KeychainSwift()
keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)")
do {
try viewContext.save()
DispatchQueue.main.async { [self] in
@ -144,30 +144,30 @@ struct ConnectToServerView: View {
})
.store(in: &globalData.pendingAPIRequests)
}
var body: some View {
Form {
if(!isConnected) {
if !isConnected {
Section(header: Text("Server Information")) {
TextField("Jellyfin Server URL", text: $uri)
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
isWorking = true;
if(!uri.contains("http")) {
uri = "http://" + uri;
isWorking = true
if !uri.contains("http") {
uri = "http://" + uri
}
if(uri.last == "/") {
if uri.last == "/" {
uri = String(uri.dropLast())
}
JellyfinAPI.basePath = uri
SystemAPI.getPublicSystemInfo()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(_):
case .failure:
isErrored = true
isWorking = false
break
@ -176,15 +176,15 @@ struct ConnectToServerView: View {
let server = response
serverName = server.serverName!
server_id = server.id!
if(server.startupWizardCompleted!) {
isConnected = true;
if server.startupWizardCompleted! {
isConnected = true
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(_):
case .failure:
isErrored = true
isWorking = false
break
@ -201,7 +201,7 @@ struct ConnectToServerView: View {
HStack {
Text("Connect")
Spacer()
if(isWorking == true) {
if isWorking == true {
ProgressView()
}
}
@ -210,7 +210,7 @@ struct ConnectToServerView: View {
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
}
} else {
if(publicUsers.count == 0) {
if publicUsers.count == 0 {
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
TextField("Username", text: $username)
.disableAutocorrection(true)
@ -225,7 +225,7 @@ struct ConnectToServerView: View {
HStack {
Text("Login")
Spacer()
if(isWorking) {
if isWorking {
ProgressView()
}
}
@ -234,9 +234,9 @@ struct ConnectToServerView: View {
Alert(title: Text("Error"), message: Text("Invalid credentials"), dismissButton: .default(Text("Back")))
}
}
if(serverSkipped) {
Section() {
if serverSkipped {
Section {
Button {
serverSkippedAlert = false
server_id = ""
@ -244,8 +244,8 @@ struct ConnectToServerView: View {
isConnected = false
serverSkipped = false
} label: {
HStack() {
HStack() {
HStack {
HStack {
Image(systemName: "chevron.left")
Text("Change Server")
}
@ -254,13 +254,13 @@ struct ConnectToServerView: View {
}
}
} else {
Section() {
Section {
Button {
publicUsers = lastPublicUsers
usernameDisabled = false;
usernameDisabled = false
} label: {
HStack() {
HStack() {
HStack {
HStack {
Image(systemName: "chevron.left")
Text("Back")
}
@ -272,9 +272,9 @@ struct ConnectToServerView: View {
} else {
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
ForEach(publicUsers, id: \.id) { publicUser in
HStack() {
HStack {
Button() {
if(publicUser.hasPassword!) {
if publicUser.hasPassword! {
lastPublicUsers = publicUsers
username = publicUser.name!
usernameDisabled = true
@ -286,10 +286,10 @@ struct ConnectToServerView: View {
doLogin()
}
} label: {
HStack() {
HStack {
Text(publicUser.name!).font(.subheadline).fontWeight(.semibold)
Spacer()
if(publicUser.primaryImageTag != "") {
if publicUser.primaryImageTag != "" {
LazyImage(source: URL(string: "\(uri)/Users/\(publicUser.id!)/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)"))
.contentMode(.aspectFill)
.frame(width: 60, height: 60)
@ -309,14 +309,14 @@ struct ConnectToServerView: View {
}
}
}
Section() {
Section {
Button() {
lastPublicUsers = publicUsers
publicUsers = []
username = ""
} label: {
HStack() {
HStack {
Text("Other User").font(.subheadline).fontWeight(.semibold)
Spacer()
Image(systemName: "person.fill.questionmark")

View File

@ -17,7 +17,7 @@ struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var orientationInfo: OrientationInfo
@EnvironmentObject var jsi: justSignedIn
@StateObject private var globalData = GlobalData()
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
@ -36,16 +36,16 @@ struct ContentView: View {
@State private var showSettingsPopover: Bool = false
@State private var viewDidLoad: Bool = false
@State private var loadState: Int = 2
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
func startup() {
if(viewDidLoad == true) {
if viewDidLoad == true {
return
}
viewDidLoad = true
let size = UIScreen.main.bounds.size
if size.width < size.height {
orientationInfo.orientation = .portrait
@ -72,21 +72,21 @@ struct ContentView: View {
}
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name;
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
var header = "MediaBrowser "
header.append("Client=\"SwiftFin\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
header.append("Token=\"\(globalData.authToken)\"")
globalData.authHeader = header
JellyfinAPI.basePath = globalData.server.baseURI ?? ""
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
DispatchQueue.global(qos: .userInitiated).async {
UserAPI.getCurrentUser()
.sink(receiveCompletion: { completion in
@ -97,13 +97,13 @@ struct ContentView: View {
librariesShowRecentlyAdded = libraries.filter { element in
return !(response.configuration?.latestItemsExcludes?.contains(element))!
}
if(loadState == 1) {
if loadState == 1 {
isLoading = false
}
})
.store(in: &globalData.pendingAPIRequests)
UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
@ -112,14 +112,14 @@ struct ContentView: View {
response.items?.forEach({ item in
library_names[item.id ?? ""] = item.name
})
if(loadState == 1) {
if loadState == 1 {
isLoading = false
}
})
.store(in: &globalData.pendingAPIRequests)
}
let defaults = UserDefaults.standard
if defaults.integer(forKey: "InNetworkBandwidth") == 0 {
defaults.setValue(40_000_000, forKey: "InNetworkBandwidth")
@ -132,13 +132,13 @@ struct ContentView: View {
}
var body: some View {
if (needsToSelectServer == true) {
if needsToSelectServer == true {
NavigationView {
ConnectToServerView(isActive: $needsToSelectServer)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(globalData)
} else if (globalData.expiredCredentials == true) {
} else if globalData.expiredCredentials == true {
NavigationView {
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
reauth_deviceId: globalData.user.device_uuid ?? "", isActive: $globalData.expiredCredentials)
@ -148,8 +148,8 @@ struct ContentView: View {
} else {
if !jsi.did {
LoadingView(isShowing: $isLoading) {
VStack() {
if(loadState == 0) {
VStack {
if loadState == 0 {
TabView(selection: $tabSelection) {
NavigationView {
VStack(alignment: .leading) {

View File

@ -12,33 +12,32 @@ import JellyfinAPI
struct ProgressBar: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let tl = CGPoint(x: rect.minX, y: rect.minY)
let tr = CGPoint(x: rect.maxX, y: rect.minY)
let br = CGPoint(x: rect.maxX, y: rect.maxY)
let bls = CGPoint(x: rect.minX + 10, y: rect.maxY)
let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10)
path.move(to: tl)
path.addLine(to: tr)
path.addLine(to: br)
path.addLine(to: bls)
path.addRelativeArc(center: blc, radius: 10,
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
return path
}
}
struct ContinueWatchingView: View {
@EnvironmentObject var globalData: GlobalData
@State private var items: [BaseItemDto] = []
func onAppear() {
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary,.backdrop,.thumb])
ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
@ -47,12 +46,12 @@ struct ContinueWatchingView: View {
.store(in: &globalData.pendingAPIRequests)
}
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
if(items.count > 0) {
LazyHStack() {
Spacer().frame(width:14)
if items.count > 0 {
LazyHStack {
Spacer().frame(width: 14)
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
@ -70,7 +69,7 @@ struct ContinueWatchingView: View {
.cornerRadius(10)
.overlay(
Group {
if(item.type == "Episode") {
if item.type == "Episode" {
Text("\(item.name!)")
.font(.caption)
.padding(6)

View File

@ -5,7 +5,7 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
//lol can someone buy me a coffee this took forever :|
// lol can someone buy me a coffee this took forever :|
import Foundation
import JellyfinAPI
@ -34,65 +34,65 @@ enum CPUModel {
}
class DeviceProfileBuilder {
public var bitrate: Int = 0;
public var bitrate: Int = 0
public func setMaxBitrate(bitrate: Int) {
self.bitrate = bitrate
}
public func buildProfile() -> DeviceProfile {
let maxStreamingBitrate = bitrate;
let maxStaticBitrate = bitrate;
let musicStreamingTranscodingBitrate = 384000;
//Build direct play profiles
var directPlayProfiles: [DirectPlayProfile] = [];
let maxStreamingBitrate = bitrate
let maxStaticBitrate = bitrate
let musicStreamingTranscodingBitrate = 384000
// Build direct play profiles
var directPlayProfiles: [DirectPlayProfile] = []
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav", videoCodec: "h264", type: .video)]
//Device supports Dolby Digital (AC3, EAC3)
if(supportsFeature(minimumSupported: .A8X)) {
if(supportsFeature(minimumSupported: .A10)) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1", type: .video)] //HEVC/H.264 with Dolby Digital
// Device supports Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) {
if supportsFeature(minimumSupported: .A10) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "hevc,h264,hev1", type: .video)] // HEVC/H.264 with Dolby Digital
} else {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264", type: .video)] //H.264 with Dolby Digital
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "ac3,eac3,aac,mp3,wav,opus", videoCodec: "h264", type: .video)] // H.264 with Dolby Digital
}
}
//Device supports Dolby Vision?
if(supportsFeature(minimumSupported: .A10X)) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital - No Atmos - Vision
// Device supports Dolby Vision?
if supportsFeature(minimumSupported: .A10X) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", videoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] // H.264/HEVC with Dolby Digital - No Atmos - Vision
}
//Device supports Dolby Atmos?
if(supportsFeature(minimumSupported: .A12)) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] //H.264/HEVC with Dolby Digital & Atmos - Vision
// Device supports Dolby Atmos?
if supportsFeature(minimumSupported: .A12) {
directPlayProfiles = [DirectPlayProfile(container: "mov,mp4,mkv", audioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", videoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1", type: .video)] // H.264/HEVC with Dolby Digital & Atmos - Vision
}
//Build transcoding profiles
var transcodingProfiles: [TranscodingProfile] = [];
// Build transcoding profiles
var transcodingProfiles: [TranscodingProfile] = []
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav")]
//Device supports Dolby Digital (AC3, EAC3)
if(supportsFeature(minimumSupported: .A8X)) {
if(supportsFeature(minimumSupported: .A10)) {
// Device supports Dolby Digital (AC3, EAC3)
if supportsFeature(minimumSupported: .A8X) {
if supportsFeature(minimumSupported: .A10) {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "aac,mp3,wav,eac3,ac3,flac,opus", audioCodec: "h264,hevc,hev1", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
} else {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "h264", audioCodec: "aac,mp3,wav,eac3,ac3,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
}
}
//Device supports Dolby Vision?
if(supportsFeature(minimumSupported: .A10X)) {
// Device supports Dolby Vision?
if supportsFeature(minimumSupported: .A10X) {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
}
//Device supports Dolby Atmos?
if(supportsFeature(minimumSupported: .A12)) {
// Device supports Dolby Atmos?
if supportsFeature(minimumSupported: .A12) {
transcodingProfiles = [TranscodingProfile(container: "ts", type: .video, videoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", audioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", _protocol: "hls", context: .streaming, maxAudioChannels: "6", minSegments: 2, breakOnNonKeyFrames: true)]
}
var codecProfiles: [CodecProfile] = []
let h264CodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "high|main|baseline|constrained baseline", isRequired: false),
@ -103,13 +103,13 @@ class DeviceProfileBuilder {
ProfileCondition(condition: .equalsAny, property: .videoProfile, value: "main|main 10", isRequired: false),
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "160", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)]
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
if(supportsFeature(minimumSupported: .A10)) {
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions,codec: "hevc"))
if supportsFeature(minimumSupported: .A10) {
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions, codec: "hevc"))
}
var subtitleProfiles: [SubtitleProfile] = []
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
@ -121,12 +121,12 @@ class DeviceProfileBuilder {
subtitleProfiles.append(SubtitleProfile(format: "pgs", method: .embed))
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate, musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate, directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [], codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles)
return profile
}
private func supportsFeature(minimumSupported: CPUModel) -> Bool {
let intValues: [CPUModel: Int] = [.A4: 1, .A5: 2, .A5X: 3, .A6: 4, .A6X: 5, .A7: 6, .A7X: 7, .A8: 8, .A8X: 9, .A9: 10, .A9X: 11, .A10: 12, .A10X: 13, .A11: 14, .A12: 15, .A12X: 16, .A12Z: 16, .A13: 17, .A14: 18, .A99: 99]
return intValues[CPUinfo()] ?? 0 >= intValues[minimumSupported] ?? 0
@ -147,7 +147,7 @@ class DeviceProfileBuilder {
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8 , value != 0 else { return identifier }
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
#endif
@ -189,10 +189,10 @@ class DeviceProfileBuilder {
case "iPad7,1", "iPad7,2": return .A10X
case "iPad7,3", "iPad7,4": return .A10X
case "iPad7,5", "iPad7,6", "iPad7,11", "iPad7,12": return .A10
case "iPad8,1", "iPad8,2" ,"iPad8,3", "iPad8,4": return .A12X
case "iPad8,5", "iPad8,6" ,"iPad8,7", "iPad8,8": return .A12X
case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return .A12X
case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return .A12X
case "iPad8,9", "iPad8,10", "iPad8,11", "iPad8,12": return .A12Z
case "iPad11,3", "iPad11,4" ,"iPad11,6", "iPad11,7": return .A12
case "iPad11,3", "iPad11,4", "iPad11,6", "iPad11,7": return .A12
case "iPad13,1", "iPad13,2": return .A14
case "AppleTV5,3": return .A8
case "AppleTV6,2": return .A10X

View File

@ -13,7 +13,7 @@ struct EpisodeItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
@EnvironmentObject private var playbackInfo: VideoPlayerItem
var item: BaseItemDto
@State private var settingState: Bool = true
@ -200,7 +200,7 @@ struct EpisodeItemView: View {
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
if(person.type! == "Actor") {
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
}) {
@ -399,7 +399,7 @@ struct EpisodeItemView: View {
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
if(person.type! == "Actor") {
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
}) {

View File

@ -10,26 +10,26 @@ import Introspect
import JellyfinAPI
class VideoPlayerItem: ObservableObject {
@Published var shouldShowPlayer: Bool = false;
@Published var itemToPlay: BaseItemDto = BaseItemDto();
@Published var shouldShowPlayer: Bool = false
@Published var itemToPlay: BaseItemDto = BaseItemDto()
}
struct ItemView: View {
@EnvironmentObject private var globalData: GlobalData
private var item: BaseItemDto;
private var item: BaseItemDto
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
@State private var videoIsLoading: Bool = false; //This variable is only changed by the underlying VLC view.
@State private var isLoading: Bool = false;
@State private var viewDidLoad: Bool = false;
@State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view.
@State private var isLoading: Bool = false
@State private var viewDidLoad: Bool = false
init(item: BaseItemDto) {
self.item = item
}
var body: some View {
VStack {
if(videoPlayerItem.shouldShowPlayer) {
if videoPlayerItem.shouldShowPlayer {
LoadingViewNoBlur(isShowing: $videoIsLoading) {
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay, loadBinding: $videoIsLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
}.navigationBarHidden(true)
@ -42,13 +42,13 @@ struct ItemView: View {
.supportedOrientations(.landscape)
} else {
VStack {
if(item.type == "Movie") {
if item.type == "Movie" {
MovieItemView(item: item)
} else if(item.type == "Season") {
} else if item.type == "Season" {
SeasonItemView(item: item)
} else if(item.type == "Series") {
} else if item.type == "Series" {
SeriesItemView(item: item)
} else if(item.type == "Episode") {
} else if item.type == "Episode" {
EpisodeItemView(item: item)
} else {
Text("Type: \(item.type ?? "") not implemented yet :(")

View File

@ -19,11 +19,11 @@ class OrientationInfo: ObservableObject {
case portrait
case landscape
}
@Published var orientation: Orientation = .portrait;
@Published var orientation: Orientation = .portrait
private var _observer: NSObjectProtocol?
init() {
_observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [weak self] note in
guard let device = note.object as? UIDevice else {
@ -31,13 +31,12 @@ class OrientationInfo: ObservableObject {
}
if device.orientation.isPortrait {
self?.orientation = .portrait
}
else if device.orientation.isLandscape {
} else if device.orientation.isLandscape {
self?.orientation = .landscape
}
}
}
deinit {
if let observer = _observer {
NotificationCenter.default.removeObserver(observer)
@ -52,7 +51,7 @@ extension View {
}
struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
var callback: (UIWindow?) -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()
@ -89,7 +88,7 @@ struct ViewPreferenceKey: PreferenceKey {
struct SupportedOrientationsPreferenceKey: PreferenceKey {
typealias Value = UIInterfaceOrientationMask
static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown
static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) {
// use the most restrictive set from the stack
value.formIntersection(nextValue())
@ -129,27 +128,27 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
override var prefersHomeIndicatorAutoHidden: Bool {
_prefersHomeIndicatorAutoHidden
}
// MARK: Lock orientation
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
didSet {
UIViewController.attemptRotationToDeviceOrientation();
if(_orientations == .landscape) {
let value = UIInterfaceOrientation.landscapeRight.rawValue;
UIViewController.attemptRotationToDeviceOrientation()
if _orientations == .landscape {
let value = UIInterfaceOrientation.landscapeRight.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
}
};
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
_orientations
}
public var _viewPreference: UIUserInterfaceStyle = .unspecified {
didSet {
overrideUserInterfaceStyle = _viewPreference
}
};
}
}
extension View {
@ -157,12 +156,12 @@ extension View {
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View {
preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value)
}
func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View {
// When rendered, export the requested orientations upward to Root
preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations)
}
func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View {
// When rendered, export the requested orientations upward to Root
preference(key: ViewPreferenceKey.self, value: viewPreference)
@ -173,14 +172,14 @@ extension View {
struct JellyfinPlayerApp: App {
let persistenceController = PersistenceController.shared
@StateObject private var jsi = justSignedIn()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(OrientationInfo())
.environmentObject(jsi)
.withHostingWindow() { window in
.withHostingWindow { window in
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(OrientationInfo()).environmentObject(jsi))
}
}

View File

@ -11,23 +11,23 @@ import JellyfinAPI
struct LatestMediaView: View {
@EnvironmentObject var globalData: GlobalData
@State var items: [BaseItemDto] = []
private var library_id: String = "";
@State private var viewDidLoad: Bool = false;
private var library_id: String = ""
@State private var viewDidLoad: Bool = false
init(usingParentID: String) {
library_id = usingParentID;
library_id = usingParentID
}
func onAppear() {
if(viewDidLoad == true) {
if viewDidLoad == true {
return
}
viewDidLoad = true;
viewDidLoad = true
DispatchQueue.global(qos: .userInitiated).async {
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], enableUserData: true, limit: 12)
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
@ -36,16 +36,16 @@ struct LatestMediaView: View {
.store(in: &globalData.pendingAPIRequests)
}
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack() {
Spacer().frame(width:16)
LazyHStack {
Spacer().frame(width: 16)
ForEach(items, id: \.id) { item in
if(item.type == "Series" || item.type == "Movie") {
if item.type == "Series" || item.type == "Movie" {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height:10)
Spacer().frame(height: 10)
LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
@ -55,7 +55,7 @@ struct LatestMediaView: View {
}
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height:5)
Spacer().frame(height: 5)
Text(item.seriesName ?? item.name ?? "")
.font(.caption)
.fontWeight(.semibold)

View File

@ -10,26 +10,26 @@ import SwiftUI
struct LibraryListView: View {
@EnvironmentObject var globalData: GlobalData
@State var library_ids: [String] = ["favorites", "genres"]
@State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"]
var libraries: [String: String] = [:] //input libraries
var libraries: [String: String] = [:] // input libraries
var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [.descending], sortBy: ["SortName"])
init(libraries: [String: String]) {
self.libraries = libraries
}
func onAppear() {
if(library_ids.count == 2) {
libraries.forEach() { k,v in
if library_ids.count == 2 {
libraries.forEach { k, v in
print("\(k): \(v)")
_library_ids.wrappedValue.append(k)
_library_names.wrappedValue[k] = v
}
}
}
var body: some View {
List(library_ids, id: \.self) { key in
switch key {

View File

@ -12,13 +12,13 @@ import JellyfinAPI
struct LibrarySearchView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@State private var items: [BaseItemDto] = []
@State private var searchQuery: String = ""
@State private var isLoading: Bool = false
private var usingParentID: String = ""
@State private var lastSearchTime: Double = CACurrentMediaTime()
init(usingParentID: String) {
self.usingParentID = usingParentID
}
@ -27,12 +27,12 @@ struct LibrarySearchView: View {
recalcTracks()
requestSearch(query: "")
}
func requestSearch(query: String) {
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], includeItemTypes: ["Movie","Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
@ -42,8 +42,8 @@ struct LibrarySearchView: View {
.store(in: &globalData.pendingAPIRequests)
}
}
//MARK: tracks for grid
// MARK: tracks for grid
@State private var tracks: [GridItem] = []
func recalcTracks() {
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
@ -57,12 +57,12 @@ struct LibrarySearchView: View {
VStack {
Spacer().frame(height: 6)
SearchBar(text: $searchQuery)
if(isLoading == true) {
if isLoading == true {
Spacer()
ProgressView()
Spacer()
} else {
if(!items.isEmpty) {
if !items.isEmpty {
ScrollView(.vertical) {
Spacer().frame(height: 16)
LazyVGrid(columns: tracks) {
@ -105,7 +105,7 @@ struct LibrarySearchView: View {
.onAppear(perform: onAppear)
.navigationBarTitle("Search", displayMode: .inline)
.onChange(of: searchQuery) { query in
if(CACurrentMediaTime() - lastSearchTime > 0.5) {
if CACurrentMediaTime() - lastSearchTime > 0.5 {
lastSearchTime = CACurrentMediaTime()
requestSearch(query: query)
}
@ -113,4 +113,4 @@ struct LibrarySearchView: View {
}
}
//stream NM5 by nicki!
// stream NM5 by nicki!

View File

@ -13,10 +13,10 @@ import JellyfinAPI
struct LibraryView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@State private var items: [BaseItemDto] = []
@State private var isLoading: Bool = false
var usingParentID: String = ""
var title: String = ""
var filters: LibraryFilters = LibraryFilters()
@ -24,52 +24,52 @@ struct LibraryView: View {
var genre: String = ""
var studio: String = ""
@State private var totalPages: Int = 0;
@State private var currentPage: Int = 0;
@State private var isSearching: String? = "";
@State private var viewDidLoad: Bool = false;
@State private var totalPages: Int = 0
@State private var currentPage: Int = 0
@State private var isSearching: String? = ""
@State private var viewDidLoad: Bool = false
init(usingParentID: String, title: String) {
self.usingParentID = usingParentID
self.title = title
}
init(usingParentID: String, title: String, usingFilters: LibraryFilters) {
self.usingParentID = usingParentID
self.title = title
self.filters = usingFilters
}
init(withPerson: BaseItemPerson) {
self.usingParentID = ""
self.title = withPerson.name ?? ""
self.personId = withPerson.id!
}
init(withGenre: NameGuidPair) {
self.usingParentID = ""
self.title = withGenre.name ?? ""
self.genre = withGenre.id ?? ""
}
init(withStudio: NameGuidPair) {
self.usingParentID = ""
self.title = withStudio.name ?? ""
self.studio = withStudio.id ?? ""
}
func onAppear() {
recalcTracks()
if(viewDidLoad) {
if viewDidLoad {
return
}
isLoading = true
items = []
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], includeItemTypes: ["Movie","Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
isLoading = false
@ -83,8 +83,8 @@ struct LibraryView: View {
.store(in: &globalData.pendingAPIRequests)
}
}
//MARK: tracks for grid
// MARK: tracks for grid
@State private var tracks: [GridItem] = []
func recalcTracks() {
let trkCnt = Int(floor(UIScreen.main.bounds.size.width / 125))
@ -96,10 +96,10 @@ struct LibraryView: View {
var body: some View {
ZStack {
if(isLoading == true) {
if isLoading == true {
ProgressView()
} else {
if(!items.isEmpty) {
if !items.isEmpty {
VStack {
ScrollView(.vertical) {
Spacer().frame(height: 16)
@ -132,10 +132,10 @@ struct LibraryView: View {
}.onChange(of: orientationInfo.orientation) { _ in
recalcTracks()
}
if(totalPages > 1) {
HStack() {
if totalPages > 1 {
HStack {
Spacer()
HStack() {
HStack {
Button {
currentPage = currentPage - 1
onAppear()
@ -169,7 +169,7 @@ struct LibraryView: View {
.navigationBarTitle(title, displayMode: .inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if(currentPage > 0) {
if currentPage > 0 {
Button {
currentPage = currentPage - 1
onAppear()
@ -177,7 +177,7 @@ struct LibraryView: View {
Image(systemName: "chevron.left")
}
}
if(currentPage < totalPages - 1) {
if currentPage < totalPages - 1 {
Button {
currentPage = currentPage + 1
onAppear()
@ -185,7 +185,7 @@ struct LibraryView: View {
Image(systemName: "chevron.right")
}
}
if(usingParentID != "") {
if usingParentID != "" {
NavigationLink(destination: LibrarySearchView(usingParentID: usingParentID)) {
Image(systemName: "magnifyingglass")
}
@ -195,4 +195,4 @@ struct LibraryView: View {
}
}
//stream BM^S by nicki!
// stream BM^S by nicki!

View File

@ -12,15 +12,15 @@ struct LoadingView<Content>: View where Content: View {
@Binding var isShowing: Bool // should the modal be visible?
var content: () -> Content
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
var body: some View {
GeometryReader { geometry in
GeometryReader { _ in
ZStack(alignment: .center) {
// the content to display - if the modal is showing, we'll blur it
content()
.disabled(isShowing)
.blur(radius: isShowing ? 2 : 0)
// all contents inside here will only be shown when isShowing is true
if isShowing {
// this Rectangle is a semi-transparent black overlay
@ -30,7 +30,7 @@ struct LoadingView<Content>: View where Content: View {
// the magic bit - our ProgressView just displays an activity
// indicator, with some text underneath showing what we are doing
HStack() {
HStack {
ProgressView()
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
Spacer()
@ -51,14 +51,14 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
@Binding var isShowing: Bool // should the modal be visible?
var content: () -> Content
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
var body: some View {
GeometryReader { geometry in
GeometryReader { _ in
ZStack(alignment: .center) {
// the content to display - if the modal is showing, we'll blur it
content()
.disabled(isShowing)
// all contents inside here will only be shown when isShowing is true
if isShowing {
// this Rectangle is a semi-transparent black overlay
@ -68,7 +68,7 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
// the magic bit - our ProgressView just displays an activity
// indicator, with some text underneath showing what we are doing
HStack() {
HStack {
ProgressView()
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
Spacer()
@ -83,4 +83,3 @@ struct LoadingViewNoBlur<Content>: View where Content: View {
}
}
}

View File

@ -200,7 +200,7 @@ struct MovieItemView: View {
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
if(person.type! == "Actor") {
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
}) {
@ -399,7 +399,7 @@ struct MovieItemView: View {
HStack {
Spacer().frame(width: 16)
ForEach(item.people!, id: \.self) { person in
if(person.type! == "Actor") {
if person.type! == "Actor" {
NavigationLink(destination: LazyView {
LibraryView(withPerson: person)
}) {

View File

@ -11,18 +11,18 @@ import JellyfinAPI
struct NextUpView: View {
@EnvironmentObject var globalData: GlobalData
@State private var items: [BaseItemDto] = []
@State private var viewDidLoad: Bool = false;
@State private var viewDidLoad: Bool = false
func onAppear() {
if(viewDidLoad == true) {
if viewDidLoad == true {
return
}
viewDidLoad = true;
viewDidLoad = true
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people])
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
@ -31,17 +31,17 @@ struct NextUpView: View {
.store(in: &globalData.pendingAPIRequests)
}
}
var body: some View {
VStack(alignment: .leading) {
if(items.count != 0) {
if items.count != 0 {
Text("Next Up")
.font(.title2)
.fontWeight(.bold)
.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack() {
Spacer().frame(width:16)
LazyHStack {
Spacer().frame(width: 16)
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
@ -54,7 +54,7 @@ struct NextUpView: View {
}
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height:5)
Spacer().frame(height: 5)
Text(item.seriesName!)
.font(.caption)
.fontWeight(.semibold)
@ -66,7 +66,7 @@ struct NextUpView: View {
.foregroundColor(.secondary)
.lineLimit(1)
}.frame(width: 100)
Spacer().frame(width:16)
Spacer().frame(width: 16)
}
}
}

View File

@ -14,7 +14,6 @@ struct PersistenceController {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
do {
try viewContext.save()
} catch {
@ -32,11 +31,11 @@ struct PersistenceController {
container = NSPersistentCloudKitContainer(name: "Model")
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.me.vigue.jellyfin.mobileclient")!.appendingPathComponent("\(container.name).sqlite"))]
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

View File

@ -8,15 +8,15 @@
*/
import SwiftUI
struct SearchBar: View {
@Binding var text: String
@State private var isEditing = false
var body: some View {
HStack {
TextField("Search ...", text: $text)
.padding(7)
.padding(.horizontal, 25)
@ -26,12 +26,12 @@ struct SearchBar: View {
.onTapGesture {
self.isEditing = true
}
if isEditing {
Button(action: {
self.isEditing = false
self.text = ""
}) {
Text("Cancel")
}

View File

@ -12,24 +12,24 @@ import JellyfinAPI
struct SeasonItemView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
var item: BaseItemDto = BaseItemDto()
@State private var episodes: [BaseItemDto] = []
@State private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false
init(item: BaseItemDto) {
self.item = item
}
func onAppear() {
if(viewDidLoad) {
if viewDidLoad {
return
}
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], seasonId: item.id ?? "")
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
isLoading = false

View File

@ -12,23 +12,23 @@ import JellyfinAPI
struct SeriesItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
var item: BaseItemDto;
@State private var seasons: [BaseItemDto] = [];
@State private var isLoading: Bool = true;
@State private var viewDidLoad: Bool = false;
var item: BaseItemDto
@State private var seasons: [BaseItemDto] = []
@State private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false
func onAppear() {
recalcTracks()
if(viewDidLoad) {
return;
if viewDidLoad {
return
}
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio,.seriesPrimaryImage,.seasonUserData,.overview,.genres,.people], isMissing: false)
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], isMissing: false)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
@ -40,17 +40,16 @@ struct SeriesItemView: View {
}
}
//MARK: Grid tracks
// MARK: Grid tracks
func recalcTracks() {
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125));
let trkCnt: Int = Int(floor(UIScreen.main.bounds.size.width / 125))
tracks = []
for _ in (0..<trkCnt)
{
for _ in (0..<trkCnt) {
tracks.append(GridItem.init(.flexible()))
}
}
@State private var tracks: [GridItem] = []
var body: some View {
LoadingView(isShowing: $isLoading) {
ScrollView(.vertical) {
@ -66,7 +65,7 @@ struct SeriesItemView: View {
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width:100, height: 150)
.frame(width: 100, height: 150)
.cornerRadius(10)
.shadow(radius: 5)
Text(season.name ?? "")
@ -74,7 +73,7 @@ struct SeriesItemView: View {
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
if(season.productionYear != nil) {
if season.productionYear != nil {
Text(String(season.productionYear!))
.foregroundColor(.secondary)
.font(.caption)
@ -84,7 +83,7 @@ struct SeriesItemView: View {
}
}
Spacer().frame(height: 2)
}.onChange(of: orientationInfo.orientation) { ip in
}.onChange(of: orientationInfo.orientation) { _ in
recalcTracks()
}
}

View File

@ -10,10 +10,10 @@ import SwiftUI
struct SettingsView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var jsi: justSignedIn
@ObservedObject var viewModel: SettingsViewModel
@Binding var close: Bool

View File

@ -11,21 +11,21 @@ import JellyfinAPI
import MediaPlayer
struct Subtitle {
var name: String;
var id: Int32;
var url: URL;
var delivery: SubtitleDeliveryMethod;
var codec: String;
var name: String
var id: Int32
var url: URL
var delivery: SubtitleDeliveryMethod
var codec: String
}
struct AudioTrack {
var name: String;
var id: Int32;
var name: String
var id: Int32
}
class PlaybackItem: ObservableObject {
@Published var videoType: PlayMethod = .directPlay;
@Published var videoUrl: URL = URL(string: "https://example.com")!;
@Published var videoType: PlayMethod = .directPlay
@Published var videoUrl: URL = URL(string: "https://example.com")!
}
protocol PlayerViewControllerDelegate: AnyObject {
@ -37,10 +37,10 @@ protocol PlayerViewControllerDelegate: AnyObject {
class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate {
weak var delegate: PlayerViewControllerDelegate?
var mediaPlayer = VLCMediaPlayer()
var globalData = GlobalData()
@IBOutlet weak var timeText: UILabel!
@IBOutlet weak var videoContentView: UIView!
@IBOutlet weak var videoControlsView: UIView!
@ -49,121 +49,120 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
@IBOutlet weak var jumpBackButton: UIButton!
@IBOutlet weak var jumpForwardButton: UIButton!
@IBOutlet weak var playerSettingsButton: UIButton!
var shouldShowLoadingScreen: Bool = false;
var ssTargetValueOffset: Int = 0;
var ssStartValue: Int = 0;
var optionsVC: VideoPlayerSettingsView?;
var paused: Bool = true;
var lastTime: Float = 0.0;
var startTime: Int = 0;
var controlsAppearTime: Double = 0;
var shouldShowLoadingScreen: Bool = false
var ssTargetValueOffset: Int = 0
var ssStartValue: Int = 0
var optionsVC: VideoPlayerSettingsView?
var paused: Bool = true
var lastTime: Float = 0.0
var startTime: Int = 0
var controlsAppearTime: Double = 0
var selectedAudioTrack: Int32 = -1 {
didSet {
print(selectedAudioTrack)
}
};
}
var selectedCaptionTrack: Int32 = -1 {
didSet {
print(selectedCaptionTrack)
}
}
var playSessionId: String = "";
var lastProgressReportTime: Double = 0;
var subtitleTrackArray: [Subtitle] = [];
var audioTrackArray: [AudioTrack] = [];
var manifest: BaseItemDto = BaseItemDto();
var playbackItem = PlaybackItem();
var playSessionId: String = ""
var lastProgressReportTime: Double = 0
var subtitleTrackArray: [Subtitle] = []
var audioTrackArray: [AudioTrack] = []
var manifest: BaseItemDto = BaseItemDto()
var playbackItem = PlaybackItem()
@IBAction func seekSliderStart(_ sender: Any) {
sendProgressReport(eventName: "pause")
mediaPlayer.pause()
}
@IBAction func seekSliderValueChanged(_ sender: Any) {
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))/1000
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration);
let scrubRemaining = videoDuration - secondsScrubbedTo;
let remainingTime = scrubRemaining;
let hours = floor(remainingTime / 3600);
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60;
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60);
if(hours != 0) {
timeText.text = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))";
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let scrubRemaining = videoDuration - secondsScrubbedTo
let remainingTime = scrubRemaining
let hours = floor(remainingTime / 3600)
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60)
if hours != 0 {
timeText.text = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
} else {
timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))";
timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
}
}
@IBAction func seekSliderEnd(_ sender: Any) {
print("ss end")
let videoPosition = Double(mediaPlayer.time.intValue)
let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))
//Scrub is value from 0..1 - find position in video and add / or remove.
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration);
let offset = secondsScrubbedTo - videoPosition;
// Scrub is value from 0..1 - find position in video and add / or remove.
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let offset = secondsScrubbedTo - videoPosition
mediaPlayer.play()
if(offset > 0) {
mediaPlayer.jumpForward(Int32(offset)/1000);
if offset > 0 {
mediaPlayer.jumpForward(Int32(offset)/1000)
} else {
mediaPlayer.jumpBackward(Int32(abs(offset))/1000);
mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
}
sendProgressReport(eventName: "unpause")
}
@IBAction func exitButtonPressed(_ sender: Any) {
sendStopReport()
mediaPlayer.stop()
delegate?.exitPlayer(self)
}
@IBAction func controlViewTapped(_ sender: Any) {
videoControlsView.isHidden = true
}
@IBAction func contentViewTapped(_ sender: Any) {
videoControlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime()
}
@IBAction func jumpBackTapped(_ sender: Any) {
if(paused == false) {
if paused == false {
mediaPlayer.jumpBackward(15)
}
}
@IBAction func jumpForwardTapped(_ sender: Any) {
if(paused == false) {
if paused == false {
mediaPlayer.jumpForward(30)
}
}
@IBOutlet weak var mainActionButton: UIButton!
@IBAction func mainActionButtonPressed(_ sender: Any) {
print(mediaPlayer.state.rawValue)
if(paused) {
if paused {
mediaPlayer.play()
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false;
paused = false
} else {
mediaPlayer.pause()
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
paused = true;
paused = true
}
}
@IBAction func settingsButtonTapped(_ sender: UIButton) {
optionsVC = VideoPlayerSettingsView()
optionsVC?.delegate = self
optionsVC?.modalPresentationStyle = .popover
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
// Present the view controller (in a popover).
self.present(optionsVC!, animated: true) {
print("popover visible, pause playback")
@ -171,122 +170,121 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
}
}
func settingsPopoverDismissed() {
optionsVC?.dismiss(animated: true, completion: nil)
self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
override var shouldAutorotate: Bool {
return true
}
func setupNowPlayingCC() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true;
commandCenter.pauseCommand.isEnabled = true;
commandCenter.seekForwardCommand.isEnabled = true;
commandCenter.seekBackwardCommand.isEnabled = true;
commandCenter.playCommand.isEnabled = true
commandCenter.pauseCommand.isEnabled = true
commandCenter.seekForwardCommand.isEnabled = true
commandCenter.seekBackwardCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.isEnabled = true
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget{ event in
commandCenter.pauseCommand.addTarget { _ in
self.mediaPlayer.pause()
self.sendProgressReport(eventName: "pause")
return .success
}
//Add handler for Play command
commandCenter.playCommand.addTarget{ event in
// Add handler for Play command
commandCenter.playCommand.addTarget { _ in
self.mediaPlayer.play()
self.sendProgressReport(eventName: "unpause")
return .success
}
//Add handler for FF command
commandCenter.seekForwardCommand.addTarget{ event in
// Add handler for FF command
commandCenter.seekForwardCommand.addTarget { _ in
self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate")
return .success
}
//Add handler for RW command
commandCenter.seekBackwardCommand.addTarget{ event in
// Add handler for RW command
commandCenter.seekBackwardCommand.addTarget { _ in
self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate")
return .success
}
//Scrubber
// Scrubber
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
guard let self = self else {return .commandFailed}
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
let targetSeconds = event.positionTime
let videoPosition = Double(self.mediaPlayer.time.intValue)
let offset = targetSeconds - videoPosition;
if(offset > 0) {
self.mediaPlayer.jumpForward(Int32(offset)/1000);
let offset = targetSeconds - videoPosition
if offset > 0 {
self.mediaPlayer.jumpForward(Int32(offset)/1000)
} else {
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000);
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
}
self.sendProgressReport(eventName: "unpause")
return .success
} else {
return .commandFailed
}
}
var nowPlayingInfo = [String : Any]()
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? ""
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
UIApplication.shared.beginReceivingRemoteControlEvents()
}
override func remoteControlReceived(with event: UIEvent?) {
dump(event)
}
override func viewDidLoad() {
super.viewDidLoad()
//View has loaded.
//Rotate to landscape only if necessary
UIViewController.attemptRotationToDeviceOrientation();
// View has loaded.
// Rotate to landscape only if necessary
UIViewController.attemptRotationToDeviceOrientation()
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
//mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
mediaPlayer.delegate = self
mediaPlayer.drawable = videoContentView
if(manifest.type == "Movie") {
if manifest.type == "Movie" {
titleLabel.text = manifest.name
} else {
titleLabel.text = "S\(String(manifest.parentIndexNumber!)):E\(String(manifest.indexNumber!))\(manifest.name!)"
}
//Fetch max bitrate from UserDefaults depending on current connection mode
// Fetch max bitrate from UserDefaults depending on current connection mode
let defaults = UserDefaults.standard
let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
//Build a device profile
// Build a device profile
let builder = DeviceProfileBuilder()
builder.setMaxBitrate(bitrate: maxBitrate)
let profile = builder.buildProfile()
let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
DispatchQueue.global(qos: .userInitiated).async { [self] in
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { completion in
@ -294,147 +292,146 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
}, receiveValue: { [self] response in
playSessionId = response.playSessionId!
let mediaSource = response.mediaSources!.first.self!
if(mediaSource.transcodingUrl != nil) {
//Item is being transcoded by request of server
if mediaSource.transcodingUrl != nil {
// Item is being transcoded by request of server
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
let item = PlaybackItem()
item.videoType = .transcode
item.videoUrl = streamURL!
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
subtitleTrackArray.append(disableSubtitleTrack);
//Loop through media streams and add to array
subtitleTrackArray.append(disableSubtitleTrack)
// Loop through media streams and add to array
for stream in mediaSource.mediaStreams! {
if(stream.type == .subtitle) {
if stream.type == .subtitle {
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
subtitleTrackArray.append(subtitle);
subtitleTrackArray.append(subtitle)
}
if(stream.type == .audio) {
if stream.type == .audio {
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
if(stream.isDefault! == true) {
selectedAudioTrack = Int32(stream.index!);
if stream.isDefault! == true {
selectedAudioTrack = Int32(stream.index!)
}
audioTrackArray.append(subtitle);
audioTrackArray.append(subtitle)
}
}
if(selectedAudioTrack == -1) {
if(audioTrackArray.count > 0) {
selectedAudioTrack = audioTrackArray[0].id;
if selectedAudioTrack == -1 {
if audioTrackArray.count > 0 {
selectedAudioTrack = audioTrackArray[0].id
}
}
self.sendPlayReport()
playbackItem = item;
playbackItem = item
} else {
//Item will be directly played by the client.
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!;
// Item will be directly played by the client.
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!
let item = PlaybackItem()
item.videoUrl = streamURL
item.videoType = .directPlay
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: .embed, codec: "")
subtitleTrackArray.append(disableSubtitleTrack);
//Loop through media streams and add to array
subtitleTrackArray.append(disableSubtitleTrack)
// Loop through media streams and add to array
for stream in mediaSource.mediaStreams! {
if(stream.type == .subtitle) {
if stream.type == .subtitle {
let deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
let subtitle = Subtitle(name: stream.displayTitle!, id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!)
subtitleTrackArray.append(subtitle);
subtitleTrackArray.append(subtitle)
}
if(stream.type == .audio) {
if stream.type == .audio {
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
if(stream.isDefault! == true) {
selectedAudioTrack = Int32(stream.index!);
if stream.isDefault! == true {
selectedAudioTrack = Int32(stream.index!)
}
audioTrackArray.append(subtitle);
audioTrackArray.append(subtitle)
}
}
if(selectedAudioTrack == -1) {
if(audioTrackArray.count > 0) {
selectedAudioTrack = audioTrackArray[0].id;
if selectedAudioTrack == -1 {
if audioTrackArray.count > 0 {
selectedAudioTrack = audioTrackArray[0].id
}
}
self.sendPlayReport()
playbackItem = item;
playbackItem = item
}
self.setupNowPlayingCC()
mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl)
mediaPlayer.play()
print(manifest.userData?.playbackPositionTicks ?? 0)
mediaPlayer.jumpForward(Int32(manifest.userData?.playbackPositionTicks ?? 0/10000000))
mediaPlayer.pause()
subtitleTrackArray.forEach() { sub in
if(sub.id != -1 && sub.delivery == .external && sub.codec != "subrip") {
subtitleTrackArray.forEach { sub in
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
print("adding subs for id: \(sub.id) w/ url: \(sub.url)")
mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false)
}
}
delegate?.showLoadingView(self)
while(mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1) {}
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack;
while mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1 {}
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack
mediaPlayer.pause()
mediaPlayer.play()
})
.store(in: &globalData.pendingAPIRequests)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = true;
self.tabBarController?.tabBar.isHidden = true
}
//MARK: VideoPlayerSettings Delegate
// MARK: VideoPlayerSettings Delegate
func subtitleTrackChanged(newTrackID: Int32) {
selectedCaptionTrack = newTrackID
mediaPlayer.currentVideoSubTitleIndex = newTrackID
}
func audioTrackChanged(newTrackID: Int32) {
selectedAudioTrack = newTrackID
mediaPlayer.currentAudioTrackIndex = newTrackID
}
//MARK: VLCMediaPlayer Delegates
// MARK: VLCMediaPlayer Delegates
func mediaPlayerStateChanged(_ aNotification: Notification!) {
let currentState: VLCMediaPlayerState = mediaPlayer.state
switch currentState {
case .stopped :
break;
break
case .ended :
break;
break
case .playing :
print("Video is playing")
sendProgressReport(eventName: "unpause")
delegate?.hideLoadingView(self)
paused = false;
paused = false
case .paused :
print("Video is paused)")
paused = true;
paused = true
case .opening :
print("Video is opening)")
case .buffering :
print("Video is buffering)")
delegate?.showLoadingView(self)
mediaPlayer.pause()
usleep(10000)
mediaPlayer.play()
case .error :
print("Video has error)")
sendStopReport()
@ -444,81 +441,81 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
break
}
}
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
let time = mediaPlayer.position;
if(time != lastTime) {
paused = false;
let time = mediaPlayer.position
if time != lastTime {
paused = false
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
seekSlider.setValue(mediaPlayer.position, animated: true)
delegate?.hideLoadingView(self)
let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000;
let hours = remainingTime / 3600;
let minutes = (remainingTime % 3600) / 60;
let seconds = (remainingTime % 3600) % 60;
var timeTextStr = "";
if(hours != 0) {
timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000
let hours = remainingTime / 3600
let minutes = (remainingTime % 3600) / 60
let seconds = (remainingTime % 3600) % 60
var timeTextStr = ""
if hours != 0 {
timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"
} else {
timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"
}
timeText.text = timeTextStr
if(CACurrentMediaTime() - controlsAppearTime > 5) {
if CACurrentMediaTime() - controlsAppearTime > 5 {
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
self.videoControlsView.alpha = 0.0
}, completion: { (finished: Bool) in
self.videoControlsView.isHidden = true;
}, completion: { (_: Bool) in
self.videoControlsView.isHidden = true
self.videoControlsView.alpha = 1
})
controlsAppearTime = 10000000000000000000000;
controlsAppearTime = 10000000000000000000000
}
} else {
paused = true;
paused = true
}
lastTime = time;
if(CACurrentMediaTime() - lastProgressReportTime > 5) {
lastTime = time
if CACurrentMediaTime() - lastProgressReportTime > 5 {
sendProgressReport(eventName: "timeupdate")
lastProgressReportTime = CACurrentMediaTime()
}
}
//MARK: Jellyfin Playstate updates
// MARK: Jellyfin Playstate updates
func sendProgressReport(eventName: String) {
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, receiveValue: { response in
}, receiveValue: { _ in
print("Playback progress report sent!")
})
.store(in: &globalData.pendingAPIRequests)
}
func sendStopReport() {
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, receiveValue: { response in
}, receiveValue: { _ in
print("Playback stop report sent!")
})
.store(in: &globalData.pendingAPIRequests)
}
func sendPlayReport() {
startTime = Int(Date().timeIntervalSince1970) * 10000000
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, receiveValue: { response in
}, receiveValue: { _ in
print("Playback start report sent!")
})
.store(in: &globalData.pendingAPIRequests)
@ -528,30 +525,30 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: BaseItemDto
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject private var globalData: GlobalData;
@EnvironmentObject private var globalData: GlobalData
var loadBinding: Binding<Bool>
var pBinding: Binding<Bool>
class Coordinator: NSObject, PlayerViewControllerDelegate {
let loadBinding: Binding<Bool>
let pBinding: Binding<Bool>
init(loadBinding: Binding<Bool>, pBinding: Binding<Bool>) {
self.loadBinding = loadBinding
self.pBinding = pBinding
}
func hideLoadingView(_ viewController: PlayerViewController) {
self.loadBinding.wrappedValue = false;
self.loadBinding.wrappedValue = false
}
func showLoadingView(_ viewController: PlayerViewController) {
self.loadBinding.wrappedValue = true;
self.loadBinding.wrappedValue = true
}
func exitPlayer(_ viewController: PlayerViewController) {
self.pBinding.wrappedValue = false;
self.pBinding.wrappedValue = false
}
}
@ -559,14 +556,13 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding)
}
typealias UIViewControllerType = PlayerViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls.UIViewControllerType {
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
customViewController.manifest = item;
customViewController.delegate = context.coordinator;
customViewController.globalData = globalData;
customViewController.manifest = item
customViewController.delegate = context.coordinator
customViewController.globalData = globalData
return customViewController
}

View File

@ -22,7 +22,7 @@ class VideoPlayerSettingsView: UIViewController {
contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.delegate?.settingsPopoverDismissed()
@ -30,17 +30,17 @@ class VideoPlayerSettingsView: UIViewController {
}
struct VideoPlayerSettings: View {
@State var delegate: PlayerViewController!
@State var captionTrack: Int32 = -99;
@State var audioTrack: Int32 = -99;
@State weak var delegate: PlayerViewController!
@State var captionTrack: Int32 = -99
@State var audioTrack: Int32 = -99
init(delegate: PlayerViewController) {
self.delegate = delegate
}
var body: some View {
NavigationView() {
Form() {
NavigationView {
Form {
Picker("Closed Captions", selection: $captionTrack) {
ForEach(delegate.subtitleTrackArray, id: \.id) { caption in
Text(caption.name).tag(caption.id)
@ -60,11 +60,11 @@ struct VideoPlayerSettings: View {
.navigationTitle("Audio & Captions")
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
if(UIDevice.current.userInterfaceIdiom == .phone) {
if UIDevice.current.userInterfaceIdiom == .phone {
Button {
self.delegate.settingsPopoverDismissed()
} label: {
HStack() {
HStack {
Image(systemName: "chevron.left")
Text("Back").font(.callout)
}

View File

@ -9,68 +9,68 @@ import Foundation
import JellyfinAPI
import UIKit
//001fC^ = dark grey plain blurhash
// 001fC^ = dark grey plain blurhash
extension BaseItemDto {
//MARK: Images
// MARK: Images
func getSeriesBackdropImageBlurHash() -> String {
let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
let rawImgURL = self.getSeriesBackdropImage(baseURL: "", maxWidth: 1).absoluteString
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
}
func getSeriesPrimaryImageBlurHash() -> String {
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
let rawImgURL = self.getSeriesPrimaryImage(baseURL: "", maxWidth: 1).absoluteString
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
func getPrimaryImageBlurHash() -> String {
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString;
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
let rawImgURL = self.getPrimaryImage(baseURL: "", maxWidth: 1).absoluteString
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
func getBackdropImageBlurHash() -> String {
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString;
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
if(rawImgURL.contains("Backdrop")) {
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^";
let rawImgURL = self.getBackdropImage(baseURL: "", maxWidth: 1).absoluteString
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
if rawImgURL.contains("Backdrop") {
return self.imageBlurHashes?.backdrop?[imgTag] ?? "001fC^"
} else {
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
}
func getBackdropImage(baseURL: String, maxWidth: Int) -> URL {
var imageType = "";
var imageTag = "";
if(self.primaryImageAspectRatio ?? 0.0 < 1.0) {
imageType = "Backdrop";
var imageType = ""
var imageTag = ""
if self.primaryImageAspectRatio ?? 0.0 < 1.0 {
imageType = "Backdrop"
imageTag = (self.backdropImageTags ?? [""])[0]
} else {
imageType = "Primary";
imageType = "Primary"
imageTag = self.imageTags?["Primary"] ?? ""
}
if(imageTag == "") {
imageType = "Backdrop";
if imageTag == "" {
imageType = "Backdrop"
imageTag = self.parentBackdropImageTags?[0] ?? ""
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
return URL(string: urlString)!
}
func getSeriesBackdropImage(baseURL: String, maxWidth: Int) -> URL {
let imageType = "Backdrop";
let imageTag = (self.parentBackdropImageTags ?? [])[0];
let imageType = "Backdrop"
let imageTag = (self.parentBackdropImageTags ?? [])[0]
print(imageType)
print(imageTag)
@ -78,29 +78,29 @@ extension BaseItemDto {
let urlString = "\(baseURL)/Items/\(self.parentBackdropItemId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
return URL(string: urlString)!
}
func getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
let imageType = "Primary";
let imageType = "Primary"
let imageTag = self.seriesPrimaryImageTag ?? ""
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = "\(baseURL)/Items/\(self.seriesId ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
return URL(string: urlString)!
}
func getPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
let imageType = "Primary";
var imageTag = self.imageTags?["Primary"] ?? "";
if(imageTag == "") {
let imageType = "Primary"
var imageTag = self.imageTags?["Primary"] ?? ""
if imageTag == "" {
imageTag = self.seriesPrimaryImageTag ?? ""
}
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
return URL(string: urlString)!
}
//MARK: Calculations
// MARK: Calculations
func getItemRuntime() -> String {
let seconds: Int = Int(self.runTimeTicks!) / 10_000_000
let hours = (seconds / 3600)
@ -111,12 +111,12 @@ extension BaseItemDto {
return "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m"
}
}
func getItemProgressString() -> String {
if(self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0) {
return "";
if self.userData?.playbackPositionTicks == nil || self.userData?.playbackPositionTicks == 0 {
return ""
}
let remainingSecs = Int(self.runTimeTicks! - (self.userData?.playbackPositionTicks!)!) / 10_000_000
let proghours = Int(remainingSecs / 3600)
let progminutes = Int((Int(remainingSecs) - (proghours * 3600)) / 60)
@ -130,19 +130,19 @@ extension BaseItemDto {
extension BaseItemPerson {
func getImage(baseURL: String, maxWidth: Int) -> URL {
let imageType = "Primary";
let imageType = "Primary"
let imageTag = self.primaryImageTag ?? ""
let x = UIScreen.main.nativeScale * CGFloat(maxWidth)
let urlString = "\(baseURL)/Items/\(self.id ?? "")/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=85&tag=\(imageTag)"
return URL(string: urlString)!
}
func getBlurHash() -> String {
let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString;
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1];
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^";
let rawImgURL = self.getImage(baseURL: "", maxWidth: 1).absoluteString
let imgTag = rawImgURL.components(separatedBy: "&tag=")[1]
return self.imageBlurHashes?.primary?[imgTag] ?? "001fC^"
}
}

View File

@ -115,14 +115,12 @@ private func signPow(_ value: Float, _ exp: Float) -> Float {
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
if v <= 0.04045 { return v / 12.92 } else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {

View File

@ -7,7 +7,6 @@
import Foundation
public extension Collection {
/// SwifterSwift: Safe protects the array from out of bounds by use of optional.

View File

@ -15,11 +15,11 @@ func HandleAPIRequestCompletion(globalData: GlobalData, completion: Subscribers.
break
case .failure(let error):
if let err = error as? ErrorResponse {
switch(err){
switch err {
case .error(401, _, _, _):
globalData.expiredCredentials = true;
case .error(_, _, _, _):
globalData.networkError = true;
globalData.expiredCredentials = true
case .error:
globalData.networkError = true
}
}
break

View File

@ -19,8 +19,7 @@ struct ParallaxHeaderScrollView<Header: View, StaticOverlayView: View, Content:
staticOverlayView: StaticOverlayView,
overlayAlignment: Alignment = .center,
headerHeight: CGFloat,
content: @escaping () -> Content)
{
content: @escaping () -> Content) {
self.header = header
self.staticOverlayView = staticOverlayView
self.overlayAlignment = overlayAlignment

View File

@ -15,7 +15,7 @@ extension String {
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
} catch { return self }
}
func leftPad(toWidth width: Int, withString string: String?) -> String {
let paddingString = string ?? " "
@ -32,4 +32,3 @@ extension String {
return "\(padString)\(self)"
}
}

View File

@ -32,14 +32,14 @@ class GlobalData: ObservableObject {
@Published var authToken: String = ""
@Published var server: Server!
@Published var authHeader: String = ""
@Published var isInNetwork: Bool = true;
@Published var networkError: Bool = false;
@Published var expiredCredentials: Bool = false;
var pendingAPIRequests = Set<AnyCancellable>();
@Published var isInNetwork: Bool = true
@Published var networkError: Bool = false
@Published var expiredCredentials: Bool = false
var pendingAPIRequests = Set<AnyCancellable>()
}
extension GlobalData: Equatable {
static func == (lhs: GlobalData, rhs: GlobalData) -> Bool {
lhs.user == rhs.user
&& lhs.authToken == rhs.authToken

View File

@ -10,12 +10,12 @@
import Foundation
struct UserSettings: Decodable {
var LocalMaxBitrate: Int;
var RemoteMaxBitrate: Int;
var AutoSelectSubtitles: Bool;
var AutoSelectSubtitlesLangcode: String;
var SubtitlePositionOffset: Int;
var SubtitleFontName: String;
var LocalMaxBitrate: Int
var RemoteMaxBitrate: Int
var AutoSelectSubtitles: Bool
var AutoSelectSubtitlesLangcode: String
var SubtitlePositionOffset: Int
var SubtitleFontName: String
}
struct Bitrates: Codable, Hashable {

View File

@ -380,7 +380,7 @@ struct NextUpWidget_Previews: PreviewProvider {
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
UIImage(named: "WidgetHeaderSymbol")),
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
UIImage(named: "WidgetHeaderSymbol")),
UIImage(named: "WidgetHeaderSymbol"))
],
error: nil))
.previewContext(WidgetPreviewContext(family: .systemMedium))
@ -391,7 +391,7 @@ struct NextUpWidget_Previews: PreviewProvider {
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
UIImage(named: "WidgetHeaderSymbol")),
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
UIImage(named: "WidgetHeaderSymbol")),
UIImage(named: "WidgetHeaderSymbol"))
],
error: nil))
.previewContext(WidgetPreviewContext(family: .systemLarge))
@ -406,7 +406,7 @@ struct NextUpWidget_Previews: PreviewProvider {
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
UIImage(named: "WidgetHeaderSymbol")),
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
UIImage(named: "WidgetHeaderSymbol")),
UIImage(named: "WidgetHeaderSymbol"))
],
error: nil))
.previewContext(WidgetPreviewContext(family: .systemMedium))
@ -418,7 +418,7 @@ struct NextUpWidget_Previews: PreviewProvider {
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
UIImage(named: "WidgetHeaderSymbol")),
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
UIImage(named: "WidgetHeaderSymbol")),
UIImage(named: "WidgetHeaderSymbol"))
],
error: nil))
.previewContext(WidgetPreviewContext(family: .systemLarge))
@ -431,7 +431,7 @@ struct NextUpWidget_Previews: PreviewProvider {
NextUpEntryView(entry: .init(date: Date(),
items: [
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
UIImage(named: "WidgetHeaderSymbol")),
UIImage(named: "WidgetHeaderSymbol"))
],
error: nil))
.previewContext(WidgetPreviewContext(family: .systemMedium))
@ -441,7 +441,7 @@ struct NextUpWidget_Previews: PreviewProvider {
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
UIImage(named: "WidgetHeaderSymbol")),
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
UIImage(named: "WidgetHeaderSymbol")),
UIImage(named: "WidgetHeaderSymbol"))
],
error: nil))
.previewContext(WidgetPreviewContext(family: .systemLarge))

View File

@ -14,21 +14,21 @@ import UIKit
final class WidgetEnvironment {
static let shared = WidgetEnvironment()
var server: Server?
var user: SignedInUser?
var header: String?
init() {
update()
}
func update() {
let serverRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Server")
let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server]
let savedUserRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "SignedInUser")
let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) as? [SignedInUser]
server = servers?.first
user = savedUsers?.first