Start moving to generated client

This commit is contained in:
Aiden Vigue 2021-06-06 21:54:07 -07:00
parent a7147e7e7c
commit 007930ec06
26 changed files with 659 additions and 1021 deletions

View File

@ -34,13 +34,15 @@
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
535870A72669D8AE00D05A09 /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; };
535870A82669D8AE00D05A09 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String.swift */; };
535870A92669D8AE00D05A09 /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage.swift */; };
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; };
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* Typings.swift */; };
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; };
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; };
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; };
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
@ -50,7 +52,6 @@
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276D263C25100035E14B /* ContinueWatchingView.swift */; };
53892770263C25230035E14B /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389276F263C25230035E14B /* NextUpView.swift */; };
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892771263C8C6F0035E14B /* LoadingView.swift */; };
53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53892776263CBB000035E14B /* JellyApiTypings.swift */; };
5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 53892779263CBFE70035E14B /* SwiftyJSON */; };
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5389277B263CC3DB0035E14B /* BlurHashDecode.swift */; };
53987CA426572C1300E7EA70 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; };
@ -66,17 +67,17 @@
53D5E3DE264B47EE00BADDC8 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; };
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; };
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelectorView.swift */; };
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; };
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */; };
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388D265F777C00A81A2A /* LibraryViewModel.swift */; };
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; };
621338932660107500A81A2A /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* String.swift */; };
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
62133895266096EF00A81A2A /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62133894266096EF00A81A2A /* LibraryListViewModel.swift */; };
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
621C638226676728004216EA /* LazyImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* LazyImage.swift */; };
621C638226676728004216EA /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621C638126676728004216EA /* NukeExtensions.swift */; };
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
6273DD43265F4195009C1D0B /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD42265F4195009C1D0B /* Moya */; };
6273DD45265F4195009C1D0B /* CombineMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 6273DD44265F4195009C1D0B /* CombineMoya */; };
@ -138,6 +139,7 @@
535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = "<group>"; };
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = "<group>"; };
5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = "<group>"; };
5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -149,7 +151,6 @@
5389276D263C25100035E14B /* ContinueWatchingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContinueWatchingView.swift; sourceTree = "<group>"; };
5389276F263C25230035E14B /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = "<group>"; };
53892771263C8C6F0035E14B /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
53892776263CBB000035E14B /* JellyApiTypings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyApiTypings.swift; sourceTree = "<group>"; };
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
53987CA326572C1300E7EA70 /* SeasonItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeasonItemView.swift; sourceTree = "<group>"; };
53987CA526572F0700E7EA70 /* SeriesItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriesItemView.swift; sourceTree = "<group>"; };
@ -161,16 +162,16 @@
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Carthage/Build/MobileVLCKit.xcframework; sourceTree = "<group>"; };
53DF641D263D9C0600A7CD1A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryFilterView.swift; sourceTree = "<group>"; };
53E4E648263F725B00F67C6B /* MultiSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelector.swift; sourceTree = "<group>"; };
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSelectorView.swift; sourceTree = "<group>"; };
53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = "<group>"; };
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSettingsView.swift; sourceTree = "<group>"; };
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
6213388D265F777C00A81A2A /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = "<group>"; };
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
621338922660107500A81A2A /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
62133894266096EF00A81A2A /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
621C638126676728004216EA /* LazyImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyImage.swift; sourceTree = "<group>"; };
621C638126676728004216EA /* NukeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = "<group>"; };
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIOld.swift; sourceTree = "<group>"; };
6273DD4D265F47B2009C1D0B /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
@ -331,12 +332,13 @@
isa = PBXGroup;
children = (
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */,
53E4E648263F725B00F67C6B /* MultiSelector.swift */,
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
621338B22660A07800A81A2A /* LazyView.swift */,
621338922660107500A81A2A /* String.swift */,
621338922660107500A81A2A /* StringExtensions.swift */,
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
621C638126676728004216EA /* LazyImage.swift */,
621C638126676728004216EA /* NukeExtensions.swift */,
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */,
5364F454266CA0DC0026ECBA /* APIExtensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -344,7 +346,6 @@
6273DD46265F419B009C1D0B /* APIs */ = {
isa = PBXGroup;
children = (
53892776263CBB000035E14B /* JellyApiTypings.swift */,
6273DD47265F41B3009C1D0B /* JellyfinAPIOld.swift */,
);
path = APIs;
@ -529,10 +530,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
535870A82669D8AE00D05A09 /* String.swift in Sources */,
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */,
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
535870A72669D8AE00D05A09 /* MultiSelector.swift in Sources */,
535870A92669D8AE00D05A09 /* LazyImage.swift in Sources */,
535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */,
535870A92669D8AE00D05A09 /* NukeExtensions.swift in Sources */,
5358706C2669D21700D05A09 /* Persistence.swift in Sources */,
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
535870652669D21600D05A09 /* ContentView.swift in Sources */,
@ -540,6 +541,7 @@
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */,
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -548,8 +550,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
621338932660107500A81A2A /* String.swift in Sources */,
621C638226676728004216EA /* LazyImage.swift in Sources */,
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
621C638226676728004216EA /* NukeExtensions.swift in Sources */,
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
@ -565,7 +568,7 @@
5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */,
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */,
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */,
6213388E265F777C00A81A2A /* LibraryViewModel.swift in Sources */,
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
@ -573,7 +576,6 @@
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
6273DD48265F41B3009C1D0B /* JellyfinAPIOld.swift in Sources */,
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */,
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */,
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,

View File

@ -1,79 +0,0 @@
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import SwiftUI
extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
extension View {
func ifVisible(in rect: CGRect, in space: CoordinateSpace, execute: @escaping (CGRect) -> Void) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let frame = geometry.frame(in: space)
if frame.intersects(rect) {
execute(frame)
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
struct ServerPublicInfoResponse: Codable {
var LocalAddress: String
var ServerName: String
var Version: String
var ProductName: String
var OperatingSystem: String
var Id: String
var StartupWizardCompleted: Bool
}
struct ServerUserResponse: Codable {
var Name: String
var Id: String
var PrimaryImageTag: String
}
struct ServerAuthByNameResponse: Codable {
var User: ServerUserResponse
var AccessToken: String
}
struct ResumeItem {
var Name: String = "";
var Id: String = "";
var IndexNumber: Int? = nil;
var ParentIndexNumber: Int? = nil;
var Image: String = "";
var ImageType: String = "";
var BlurHash: String = "";
var `Type`: String = "";
var SeasonId: String? = nil;
var SeriesId: String? = nil;
var SeriesName: String? = nil;
var ItemProgress: Double = 0;
var SeasonImage: String? = nil;
var SeasonImageType: String? = nil;
var SeasonImageBlurHash: String? = nil;
var ItemBadge: Int? = 0;
var ProductionYear: Int = 1999;
var Watched: Bool = false;
}
struct ServerMeResponse: Codable {
}

View File

@ -88,7 +88,7 @@ extension JellyfinAPIOld: TargetType {
switch self {
case let .items(global, _, _),
let .search(global, _, _, _):
return URL(string: global.server?.baseURI ?? "")!
return URL(string: global.server.baseURI ?? "")!
}
}
@ -96,7 +96,7 @@ extension JellyfinAPIOld: TargetType {
switch self {
case let .items(global, _, _),
let .search(global, _, _, _):
return "/Users/\(global.user?.user_id ?? "")/Items"
return "/Users/\(global.user.user_id ?? "")/Items"
}
}

View File

@ -5,26 +5,19 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
//MARK: refactor this file! it's the first swift file I ever wrote and it clearly shows.
import SwiftyRequest
import SwiftyJSON
import SwiftUI
import CoreData
import KeychainSwift
import NukeUI
import JellyfinAPI
class publicUser: ObservableObject {
@Published var username: String = "";
@Published var hasPassword: Bool = true;
@Published var primaryImageTag: String = "";
@Published var id: String = "";
}
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;
@ -33,27 +26,26 @@ struct ConnectToServerView: View {
@State private var isConnected = false;
@State private var serverName = "";
@State private var usernameDisabled: Bool = false;
@State private var publicUsers: [publicUser] = [];
@State private var lastPublicUsers: [publicUser] = [];
@Binding var rootIsActive : Bool
let userUUID = UUID();
@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;
private var reauthDeviceID: String = "";
private var skip_server_bool: Bool = false;
private var skip_server_obj: Server?
@State private var skip_server_bool: Bool = false;
@State private var skip_server_obj: Server = Server();
init(skip_server: Bool, skip_server_prefill: Server?, reauth_deviceId: String, isActive: Binding<Bool>) {
@Binding var rootIsActive: Bool
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
_rootIsActive = isActive
}
init(isActive: Binding<Bool>) {
@ -62,111 +54,91 @@ struct ConnectToServerView: View {
func start() {
if(skip_server_bool) {
_uri.wrappedValue = skip_server_obj?.baseURI ?? ""
let request = RestRequest(method: .get, url: uri + "/users/public")
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
do {
let body = response.body;
let json = try JSON(data: body);
for (_,publicUserDto):(String, JSON) in json {
let newPublicUser = publicUser()
newPublicUser.username = publicUserDto["Name"].string ?? ""
newPublicUser.hasPassword = publicUserDto["HasPassword"].bool ?? true
newPublicUser.primaryImageTag = publicUserDto["PrimaryImageTag"].string ?? ""
newPublicUser.id = publicUserDto["Id"].string ?? ""
_publicUsers.wrappedValue.append(newPublicUser)
}
} catch(_) {
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();
break
}
_serverSkipped.wrappedValue = true;
_serverSkippedAlert.wrappedValue = true;
_server_id.wrappedValue = skip_server_obj?.server_id ?? ""
_serverName.wrappedValue = skip_server_obj?.name ?? ""
_isConnected.wrappedValue = true;
break
case .failure(_):
_serverSkipped.wrappedValue = true;
_serverSkippedAlert.wrappedValue = true;
_server_id.wrappedValue = skip_server_obj?.server_id ?? ""
_serverName.wrappedValue = skip_server_obj?.name ?? ""
_isConnected.wrappedValue = true;
break
}
}
}, receiveValue: { response in
publicUsers = response
serverSkipped = true;
serverSkippedAlert = true;
server_id = skip_server_obj.server_id!
serverName = skip_server_obj.name!
isConnected = true;
})
.store(in: &globalData.pendingAPIRequests)
}
}
func doLogin() {
_isWorking.wrappedValue = true
let authJson: [String: Any] = ["Username": _username.wrappedValue, "Pw": _password.wrappedValue]
let request = RestRequest(method: .post, url: uri + "/Users/authenticatebyname")
isWorking = true
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(UIDevice.current.name)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
var deviceName = UIDevice.current.name;
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]");
request.headerParameters["X-Emby-Authorization"] = authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.messageBodyDictionary = authJson
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\"";
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
UserAPI.authenticateUser(userId: username, pw: password)
.sink(receiveCompletion: { completion in
isWorking = false
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
let json = try JSON(data: response.body)
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try viewContext.execute(deleteRequest)
} catch _ as NSError {
do {
try viewContext.execute(deleteRequest)
} catch _ as NSError {
// TODO: handle the error
}
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
}
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
do {
try viewContext.execute(deleteRequest2)
} catch _ as NSError {
// TODO: handle the error
}
let newServer = Server(context: viewContext)
newServer.baseURI = _uri.wrappedValue
newServer.name = _serverName.wrappedValue
newServer.server_id = _server_id.wrappedValue
let newUser = SignedInUser(context: viewContext)
newUser.device_uuid = userUUID.uuidString
newUser.username = _username.wrappedValue
newUser.user_id = json["User"]["Id"].string ?? ""
let keychain = KeychainSwift()
keychain.set(json["AccessToken"].string ?? "", forKey: "AccessToken_\(json["User"]["Id"].string ?? "")")
do {
try viewContext.save()
DispatchQueue.main.async { [self] in
globalData.authHeader = authHeader
_rootIsActive.wrappedValue = false
jsi.did = true
}
} catch {
}
} catch {
do {
try viewContext.execute(deleteRequest2)
} catch _ as NSError {
}
case .failure(_):
_isSignInErrored.wrappedValue = true;
}
_isWorking.wrappedValue = false;
}
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
globalData.authHeader = authHeader
_rootIsActive.wrappedValue = false
jsi.did = true
}
} catch {
print("Couldn't store objects to CoreData")
}
})
.store(in: &globalData.pendingAPIRequests)
}
var body: some View {
@ -177,56 +149,50 @@ struct ConnectToServerView: View {
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
_isWorking.wrappedValue = true;
if(!_uri.wrappedValue.contains("http")) {
_uri.wrappedValue = "http://" + _uri.wrappedValue;
isWorking = true;
if(!uri.contains("http")) {
uri = "http://" + uri;
}
if(_uri.wrappedValue.last == "/") {
_uri.wrappedValue = String(_uri.wrappedValue.dropLast())
if(uri.last == "/") {
uri = String(uri.dropLast())
}
let request = RestRequest(method: .get, url: uri + "/System/Info/Public")
request.responseObject() { (result: Result<RestResponse<ServerPublicInfoResponse>, RestError>) in
switch result {
case .success(let response):
let server = response.body
_serverName.wrappedValue = server.ServerName
_server_id.wrappedValue = server.Id
if(server.StartupWizardCompleted) {
_isConnected.wrappedValue = true;
}
let request2 = RestRequest(method: .get, url: uri + "/users/public")
request2.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
do {
let body = response.body;
let json = try JSON(data: body);
for (_,publicUserDto):(String, JSON) in json {
let newPublicUser = publicUser()
newPublicUser.username = publicUserDto["Name"].string ?? ""
newPublicUser.hasPassword = publicUserDto["HasPassword"].bool ?? true
newPublicUser.primaryImageTag = publicUserDto["PrimaryImageTag"].string ?? ""
newPublicUser.id = publicUserDto["Id"].string ?? ""
_publicUsers.wrappedValue.append(newPublicUser)
}
} catch(_) {
}
_isWorking.wrappedValue = false;
JellyfinAPI.basePath = uri
SystemAPI.getPublicSystemInfo()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(_):
_isErrored.wrappedValue = true;
_isWorking.wrappedValue = false;
isErrored = true
isWorking = false
break
}
}
case .failure(_):
_isErrored.wrappedValue = true;
_isWorking.wrappedValue = false;
}
}
}, receiveValue: { response in
let server = response
serverName = server.serverName!
server_id = server.id!
if(server.startupWizardCompleted!) {
isConnected = true;
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(_):
isErrored = true
isWorking = false
break
}
}, receiveValue: { response in
publicUsers = response
isWorking = false
})
.store(in: &globalData.pendingAPIRequests)
}
})
.store(in: &globalData.pendingAPIRequests)
} label: {
HStack {
Text("Connect")
@ -240,7 +206,7 @@ struct ConnectToServerView: View {
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
}
} else {
if(_publicUsers.wrappedValue.count == 0) {
if(publicUsers.count == 0) {
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
TextField("Username", text: $username)
.disableAutocorrection(true)
@ -268,11 +234,11 @@ struct ConnectToServerView: View {
if(serverSkipped) {
Section() {
Button {
_serverSkippedAlert.wrappedValue = false;
_server_id.wrappedValue = ""
_serverName.wrappedValue = ""
_isConnected.wrappedValue = false;
_serverSkipped.wrappedValue = false;
serverSkippedAlert = false
server_id = ""
serverName = ""
isConnected = false
serverSkipped = false
} label: {
HStack() {
HStack() {
@ -286,8 +252,8 @@ struct ConnectToServerView: View {
} else {
Section() {
Button {
_publicUsers.wrappedValue = _lastPublicUsers.wrappedValue
_usernameDisabled.wrappedValue = false;
publicUsers = lastPublicUsers
usernameDisabled = false;
} label: {
HStack() {
HStack() {
@ -301,26 +267,26 @@ struct ConnectToServerView: View {
}
} else {
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
ForEach(publicUsers, id: \.id) { pubuser in
ForEach(publicUsers, id: \.id) { publicUser in
HStack() {
Button() {
if(pubuser.hasPassword) {
_lastPublicUsers.wrappedValue = _publicUsers.wrappedValue
_username.wrappedValue = pubuser.username
_usernameDisabled.wrappedValue = true;
_publicUsers.wrappedValue = []
if(publicUser.hasPassword!) {
lastPublicUsers = publicUsers
username = publicUser.name!
usernameDisabled = true
publicUsers = []
} else {
_publicUsers.wrappedValue = []
_password.wrappedValue = "";
_username.wrappedValue = pubuser.username
publicUsers = []
password = ""
username = publicUser.name!
doLogin()
}
} label: {
HStack() {
Text(pubuser.username).font(.subheadline).fontWeight(.semibold)
Text(publicUser.name!).font(.subheadline).fontWeight(.semibold)
Spacer()
if(pubuser.primaryImageTag != "") {
LazyImage(source: URL(string: "\(uri)/Users/\(pubuser.id)/Images/Primary?width=200&quality=80&tag=\(pubuser.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)
.cornerRadius(30.0)
@ -342,9 +308,9 @@ struct ConnectToServerView: View {
Section() {
Button() {
_lastPublicUsers.wrappedValue = _publicUsers.wrappedValue;
_publicUsers.wrappedValue = []
_username.wrappedValue = ""
lastPublicUsers = publicUsers
publicUsers = []
username = ""
} label: {
HStack() {
Text("Other User").font(.subheadline).fontWeight(.semibold)

View File

@ -8,51 +8,40 @@
import SwiftUI
import KeychainSwift
import SwiftyJSON
import SwiftyRequest
import Nuke
import Combine
import JellyfinAPI
struct ContentView: View {
@Environment(\.managedObjectContext)
private var viewContext
@EnvironmentObject
var orientationInfo: OrientationInfo
@StateObject
private var globalData = GlobalData()
@EnvironmentObject
var jsi: justSignedIn
@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)])
private var servers: FetchedResults<Server>
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
private var servers: FetchedResults<Server>
@FetchRequest(entity: SignedInUser.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username,
ascending: true)])
private var savedUsers: FetchedResults<SignedInUser>
@FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)])
private var savedUsers: FetchedResults<SignedInUser>
@State
private var needsToSelectServer = false
@State
private var isLoading = false
@State
private var tabSelection: String = "Home"
@State
private var libraries: [String] = []
@State
private var library_names: [String: String] = [:]
@State
private var librariesShowRecentlyAdded: [String] = []
@State
private var libraryPrefillID: String = ""
@State
private var showSettingsPopover: Bool = false
@State
private var viewDidLoad: Bool = false
@State private var needsToSelectServer = false
@State private var isLoading = false
@State private var tabSelection: String = "Home"
@State private var libraries: [String] = []
@State private var library_names: [String: String] = [:]
@State private var librariesShowRecentlyAdded: [String] = []
@State private var libraryPrefillID: String = ""
@State private var showSettingsPopover: Bool = false
@State private var viewDidLoad: Bool = false
func startup() {
if(viewDidLoad == true) {
return
}
viewDidLoad = true
let size = UIScreen.main.bounds.size
if size.width < size.height {
orientationInfo.orientation = .portrait
@ -60,12 +49,6 @@ struct ContentView: View {
orientationInfo.orientation = .landscape
}
if viewDidLoad {
return
}
viewDidLoad = true
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
@ -91,19 +74,18 @@ struct ContentView: View {
var header = "MediaBrowser "
header.append("Client=\"SwiftFin\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(globalData.user?.device_uuid ?? "")\", ")
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.basePath = globalData.server.baseURI ?? ""
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
UserAPI.getCurrentUser()
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
//Get all libraries
libraries = response.configuration?.orderedViews ?? []
librariesShowRecentlyAdded = libraries.filter { element in
return !(response.configuration?.latestItemsExcludes?.contains(element))!
@ -111,11 +93,10 @@ struct ContentView: View {
})
.store(in: &globalData.pendingAPIRequests)
UserViewsAPI.getUserViews(userId: globalData.user?.user_id ?? "")
UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
//Get all libraries
response.items?.forEach({ item in
library_names[item.id ?? ""] = item.name
})
@ -144,7 +125,7 @@ struct ContentView: View {
} else if (globalData.expiredCredentials == true) {
NavigationView {
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)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(globalData)
@ -155,9 +136,9 @@ struct ContentView: View {
NavigationView {
VStack(alignment: .leading) {
ScrollView {
Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 15)
Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 16)
ContinueWatchingView()
NextUpView().padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
NextUpView()
ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in
VStack(alignment: .leading) {
HStack {
@ -171,7 +152,7 @@ struct ContentView: View {
Text("See All").font(.subheadline).fontWeight(.bold)
}
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
LatestMediaView(library: library_id)
LatestMediaView(usingLibraryID: library_id)
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
@ -211,10 +192,10 @@ struct ContentView: View {
.environmentObject(globalData)
.onAppear(perform: startup)
.alert(isPresented: $globalData.networkError) {
Alert(title: Text("Network Error"), message: Text("Couldn't connect to Jellyfin"), dismissButton: .default(Text("Ok")))
Alert(title: Text("Network Error"), message: Text("An error occured while performing a network request"), dismissButton: .default(Text("Ok")))
}
} else {
Text("Signing in...")
Text("Please wait...")
.onAppear(perform: {
DispatchQueue.main.async { [self] in
_viewDidLoad.wrappedValue = false

View File

@ -6,27 +6,24 @@
*/
import SwiftUI
import SwiftyRequest
import SwiftyJSON
import NukeUI
import JellyfinAPI
struct CustomShape: Shape {
let radius: CGFloat
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 + radius, y: rect.maxY)
let blc = CGPoint(x: rect.minX + radius, y: rect.maxY - radius)
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: radius,
path.addRelativeArc(center: blc, radius: 10,
startAngle: Angle.degrees(90), delta: Angle.degrees(90))
return path
@ -35,147 +32,77 @@ struct CustomShape: Shape {
struct ContinueWatchingView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@State var resumeItems: [ResumeItem] = []
@State private var viewDidLoad: Int = 0;
@State private var isLoading: Bool = true;
@State private var items: [BaseItemDto] = []
func onAppear() {
if(globalData.server?.baseURI == "") {
return
}
if(viewDidLoad == 1) {
return
}
_viewDidLoad.wrappedValue = 1;
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Items/Resume?Limit=12&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&MediaTypes=Video")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
let body = response.body
do {
let json = try JSON(data: body)
for (_,item):(String, JSON) in json["Items"] {
// Do something you want
var itemObj = ResumeItem()
if(item["PrimaryImageAspectRatio"].double ?? 0.0 < 1.0) {
//portrait; use backdrop instead
itemObj.Image = item["BackdropImageTags"][0].string ?? ""
itemObj.ImageType = "Backdrop"
if(itemObj.Image == "") {
itemObj.Image = item["ParentBackdropImageTags"][0].string ?? ""
}
itemObj.BlurHash = item["ImageBlurHashes"]["Backdrop"][itemObj.Image].string ?? ""
} else {
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
}
itemObj.Name = item["Name"].string ?? ""
itemObj.Type = item["Type"].string ?? ""
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
itemObj.Id = item["Id"].string ?? ""
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
itemObj.SeasonId = item["SeasonId"].string ?? nil
itemObj.SeriesId = item["SeriesId"].string ?? nil
itemObj.SeriesName = item["SeriesName"].string ?? nil
itemObj.ItemProgress = item["UserData"]["PlayedPercentage"].double ?? 0.00
_resumeItems.wrappedValue.append(itemObj)
}
_isLoading.wrappedValue = false;
} catch {
}
break
case .failure(let error):
_viewDidLoad.wrappedValue = 0;
debugPrint(error)
break
}
}
ItemsAPI.getResumeItems(userId: globalData.user.user_id ?? "", limit: 12, fields: [.primaryImageAspectRatio], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary,.backdrop,.thumb])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
items = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
if(_resumeItems.wrappedValue.count > 0) {
if(items.count > 0) {
LazyHStack() {
Spacer().frame(width:12)
ForEach(resumeItems, id: \.Id) { item in
NavigationLink(destination: ItemView(item: item)) {
Spacer().frame(width:14)
ForEach(items, id: \.id) { item in
NavigationLink(destination: EmptyView()) {
VStack(alignment: .leading) {
Spacer().frame(height: 10)
if(item.Type == "Episode") {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!)
.resizable()
.frame(width: 320, height: 180)
.cornerRadius(10)
}
.frame(width: 320, height: 180)
.cornerRadius(10)
.overlay(
ZStack {
Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0)) - \(item.Name)")
LazyImage(source: item.getBackdropImage(baseURL: globalData.server.baseURI ?? "", maxWidth: 320))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item.getBackdropImageBlurHash(), size: CGSize(width: 48, height: 32))!)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 320, height: 180)
.cornerRadius(10)
}
.aspectRatio(contentMode: .fill)
.frame(width: 320, height: 180)
.cornerRadius(10)
.overlay(
Group {
if(item.type == "Episode") {
Text("\(item.name!)")
.font(.caption)
.padding(6)
.foregroundColor(.white)
}.background(Color.black)
.opacity(0.8)
.cornerRadius(10.0)
.padding(6), alignment: .topTrailing
)
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(CustomShape(radius: 10))
.frame(width: CGFloat((item.ItemProgress/100)*320), height: 7)
.padding(0), alignment: .bottomLeading
)
} else {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!)
.resizable()
.frame(width: 320, height: 180)
.cornerRadius(10)
}
.frame(width: 320, height: 180)
.cornerRadius(10)
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(CustomShape(radius: 10))
.frame(width: CGFloat((item.ItemProgress/100)*320), height: 7)
}
}.background(Color.black)
.opacity(0.8)
.cornerRadius(10.0)
.padding(6), alignment: .topTrailing
)
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(ProgressBar())
.frame(width: CGFloat(item.userData!.playedPercentage!*3.2), height: 7)
.padding(0), alignment: .bottomLeading
)
}
Text("\(item.Type == "Episode" ? item.SeriesName ?? "" : item.Name)")
)
Text(item.seriesName ?? item.name ?? "")
.font(.callout)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
.frame(width: 320, alignment: .leading)
Spacer().frame(height: 5)
}.padding(.trailing, 5)
}
}
Spacer().frame(width: 16)
}
Spacer().frame(width: 2)
}.frame(height: 215)
.padding(.bottom, 10)
} else {
EmptyView()
}
}.onAppear(perform: onAppear)
.padding(.bottom, 10)
}
}

View File

@ -8,7 +8,7 @@
//lol can someone buy me a coffee this took forever :|
import Foundation
import SwiftyJSON
import JellyfinAPI
enum CPUModel {
case A4
@ -33,65 +33,6 @@ enum CPUModel {
case A99
}
struct _AVDirectProfile: Codable {
var Container: String;
var `Type`: String;
var AudioCodec: String = "";
var VideoCodec: String = "";
}
struct _AVTranscodingProfile: Codable {
var Container: String;
var `Type`: String;
var AudioCodec: String = "";
var VideoCodec: String = "";
var Context: String = "";
var `Protocol`: String = "hls";
var MaxAudioChannels: String = "6";
var MinSegments: String = "2";
var BreakOnNonKeyFrames: Bool = true;
}
struct _AVCodecCondition: Codable {
var Condition: String;
var Property: String;
var Value: String;
var IsRequired: Bool;
}
struct _AVCodecProfile: Codable {
var `Type`: String;
var Codec: String = "";
var Conditions: [_AVCodecCondition] = [];
}
struct _AVSubtitleProfile: Codable {
var Format: String;
var Method: String;
}
struct _AVResponseProfile: Codable {
var `Type`: String;
var Container: String;
var MimeType: String;
}
struct DeviceProfile: Codable {
var MaxStreamingBitrate: Int;
var MaxStaticBitrate: Int;
var MusicStreamingTranscodingBitrate: Int;
var DirectPlayProfiles: [_AVDirectProfile] = [];
var TranscodingProfiles: [_AVTranscodingProfile] = [];
var ContainerProfiles: [_AVDirectProfile] = [];
var CodecProfiles: [_AVCodecProfile] = [];
var SubtitleProfiles: [_AVSubtitleProfile] = [];
var ResponseProfiles: [_AVResponseProfile] = [];
}
struct DeviceProfileRoot: Codable {
var DeviceProfile: DeviceProfile;
}
class DeviceProfileBuilder {
public var bitrate: Int = 0;
@ -99,92 +40,91 @@ class DeviceProfileBuilder {
self.bitrate = bitrate
}
public func buildProfile() -> DeviceProfileRoot {
print(CPUinfo())
let MaxStreamingBitrate = bitrate;
let MaxStaticBitrate = bitrate;
let MusicStreamingTranscodingBitrate = 384000;
public func buildProfile() -> DeviceProfile {
let maxStreamingBitrate = bitrate;
let maxStaticBitrate = bitrate;
let musicStreamingTranscodingBitrate = 384000;
//Build direct play profiles
var DirectPlayProfiles: [_AVDirectProfile] = [];
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav", VideoCodec: "h264")]
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 = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "hevc,h264,hev1")] //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 {
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "ac3,eac3,aac,mp3,wav,opus", VideoCodec: "h264")] //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 = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "dvhe,dvh1,dva1,dvav,h264,hevc,hev1")] //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?
if(supportsFeature(minimumSupported: .A12)) {
DirectPlayProfiles = [_AVDirectProfile(Container: "mov,mp4,mkv", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,truehd,dts,dca,opus", VideoCodec: "h264,hevc,dvhe,dvh1,dva1,dvav,h264,hevc,hev1")] //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
var TranscodingProfiles: [_AVTranscodingProfile] = [];
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav", VideoCodec: "h264", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "2", MinSegments: "2", BreakOnNonKeyFrames: true)]
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)) {
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,eac3,ac3,flac,opus", VideoCodec: "h264,hevc,hev1", Context: "Streaming", Protocol: "hls", 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 {
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,eac3,ac3,opus", VideoCodec: "h264", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "2", 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?
if(supportsFeature(minimumSupported: .A10X)) {
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,opus", VideoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", Context: "Streaming", Protocol: "hls", 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?
if(supportsFeature(minimumSupported: .A12)) {
TranscodingProfiles = [_AVTranscodingProfile(Container: "ts", Type: "Video", AudioCodec: "aac,mp3,wav,ac3,eac3,flac,dts,truehd,dca,opus", VideoCodec: "dva1,dvav,dvhe,dvh1,hevc,h264,hev1", Context: "Streaming", Protocol: "hls", MaxAudioChannels: "9", 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)]
}
var CodecProfiles: [_AVCodecProfile] = []
var codecProfiles: [CodecProfile] = []
let h264CodecConditions: [_AVCodecCondition] = [
_AVCodecCondition(Condition: "NotEquals", Property: "IsAnamorphic", Value: "true", IsRequired: false),
_AVCodecCondition(Condition: "EqualsAny", Property: "VideoProfile", Value: "high|main|baseline|constrained baseline", IsRequired: false),
_AVCodecCondition(Condition: "LessThanEqual", Property: "VideoLevel", Value: "60", IsRequired: false),
_AVCodecCondition(Condition: "NotEquals", Property: "IsInterlaced", Value: "true", IsRequired: false)]
let hevcCodecConditions: [_AVCodecCondition] = [
_AVCodecCondition(Condition: "NotEquals", Property: "IsAnamorphic", Value: "true", IsRequired: false),
_AVCodecCondition(Condition: "EqualsAny", Property: "VideoProfile", Value: "main|main 10", IsRequired: false),
_AVCodecCondition(Condition: "LessThanEqual", Property: "VideoLevel", Value: "160", IsRequired: false),
_AVCodecCondition(Condition: "NotEquals", Property: "IsInterlaced", Value: "true", IsRequired: false)]
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),
ProfileCondition(condition: .lessThanEqual, property: .videoLevel, value: "60", isRequired: false),
ProfileCondition(condition: .notEquals, property: .isInterlaced, value: "true", isRequired: false)]
let hevcCodecConditions: [ProfileCondition] = [
ProfileCondition(condition: .notEquals, property: .isAnamorphic, value: "true", isRequired: false),
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(_AVCodecProfile(Type: "Video", Codec: "h264", Conditions: h264CodecConditions))
codecProfiles.append(CodecProfile(type: .video, applyConditions: h264CodecConditions, codec: "h264"))
if(supportsFeature(minimumSupported: .A10)) {
CodecProfiles.append(_AVCodecProfile(Type: "Video", Codec: "hevc", Conditions: hevcCodecConditions))
codecProfiles.append(CodecProfile(type: .video, applyConditions: hevcCodecConditions,codec: "hevc"))
}
var SubtitleProfiles: [_AVSubtitleProfile] = []
SubtitleProfiles.append(_AVSubtitleProfile(Format: "vtt", Method: "External"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "ass", Method: "External"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "ssa", Method: "External"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "pgssub", Method: "Embed"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "sub", Method: "Embed"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "rip", Method: "Embed"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "srt", Method: "Embed"))
SubtitleProfiles.append(_AVSubtitleProfile(Format: "pgs", Method: "Embed"))
var subtitleProfiles: [SubtitleProfile] = []
subtitleProfiles.append(SubtitleProfile(format: "vtt", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ass", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "ssa", method: .external))
subtitleProfiles.append(SubtitleProfile(format: "pgssub", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "sub", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "rip", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "srt", method: .embed))
subtitleProfiles.append(SubtitleProfile(format: "pgs", method: .embed))
let ResponseProfiles: [_AVResponseProfile] = [_AVResponseProfile(Type: "Video", Container: "m4v", MimeType: "video/mp4")]
let responseProfiles: [ResponseProfile] = [ResponseProfile(container: "m4v", type: .video, mimeType: "video/mp4")]
let DP = DeviceProfile(MaxStreamingBitrate: MaxStreamingBitrate, MaxStaticBitrate: MaxStaticBitrate, MusicStreamingTranscodingBitrate: MusicStreamingTranscodingBitrate, DirectPlayProfiles: DirectPlayProfiles, TranscodingProfiles: TranscodingProfiles, CodecProfiles: CodecProfiles, SubtitleProfiles: SubtitleProfiles, ResponseProfiles: ResponseProfiles)
let profile = DeviceProfile(maxStreamingBitrate: maxStreamingBitrate, maxStaticBitrate: maxStaticBitrate, musicStreamingTranscodingBitrate: musicStreamingTranscodingBitrate, directPlayProfiles: directPlayProfiles, transcodingProfiles: transcodingProfiles, containerProfiles: [], codecProfiles: codecProfiles, responseProfiles: responseProfiles, subtitleProfiles: subtitleProfiles)
return DeviceProfileRoot(DeviceProfile: DP)
return profile
}
private func supportsFeature(minimumSupported: CPUModel) -> Bool {

View File

@ -37,8 +37,8 @@ struct EpisodeItemView: View {
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let request = RestRequest(method: .post,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -47,8 +47,8 @@ struct EpisodeItemView: View {
}
} else {
let request = RestRequest(method: .delete,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -64,8 +64,8 @@ struct EpisodeItemView: View {
didSet {
if favorite == true {
let request = RestRequest(method: .post,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -74,8 +74,8 @@ struct EpisodeItemView: View {
}
} else {
let request = RestRequest(method: .delete,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -96,9 +96,9 @@ struct EpisodeItemView: View {
return
}
_viewDidLoad.wrappedValue = true
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -152,7 +152,7 @@ struct EpisodeItemView: View {
cast.Role = person["Role"].string ?? ""
cast
.Image =
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxHeight=250&quality=85&tag=\(imageTag)")!
fullItem.Cast.append(cast)
}
}
@ -203,7 +203,7 @@ struct EpisodeItemView: View {
}
var portraitHeaderView: some View {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
@ -219,7 +219,7 @@ struct EpisodeItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
@ -420,7 +420,7 @@ struct EpisodeItemView: View {
} else {
GeometryReader { geometry in
ZStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
@ -441,7 +441,7 @@ struct EpisodeItemView: View {
.blur(radius: 2)
HStack {
VStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :

View File

@ -7,25 +7,29 @@
import SwiftUI
import Introspect
import JellyfinAPI
class ItemPlayback: ObservableObject {
@Published var shouldPlay: Bool = false;
@Published var itemToPlay: DetailItem = DetailItem();
//good lord the environmental modifiers ;P
class VideoPlayerItem: ObservableObject {
@Published var shouldShowPlayer: Bool = false;
@Published var itemToPlay: BaseItemDto = BaseItemDto();
}
struct ItemView: View {
var item: ResumeItem;
@StateObject private var playback: ItemPlayback = ItemPlayback()
@State private var shouldShowLoadingView: Bool = false;
private var item: BaseItemDto;
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
init(item: ResumeItem) {
@State private var isLoading: Bool = false; //This variable is only changed by the underlying VLC view.
init(item: BaseItemDto) {
self.item = item;
}
var body: some View {
if(playback.shouldPlay) {
LoadingViewNoBlur(isShowing: $shouldShowLoadingView) {
VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $shouldShowLoadingView, pBinding: _playback.projectedValue.shouldPlay)
if(videoPlayerItem.shouldShowPlayer) {
LoadingViewNoBlur(isShowing: $isLoading) {
VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $isLoading, pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
}.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: true)
@ -34,7 +38,6 @@ struct ItemView: View {
.edgesIgnoringSafeArea(.all)
.overrideViewPreference(.unspecified)
.supportedOrientations(.landscape)
} else {
Group {
if(item.Type == "Movie") {
@ -50,7 +53,7 @@ struct ItemView: View {
}
}
.introspectTabBarController { (UITabBarController) in
UITabBarController.tabBar.isHidden = false
UITabBarController.tabBar.isHidden = false
}
.navigationBarHidden(false)
.navigationBarBackButtonHidden(false)

View File

@ -169,24 +169,6 @@ extension View {
}
}
extension String {
public func leftPad(toWidth width: Int, withString string: String?) -> String {
let paddingString = string ?? " "
if self.count >= width {
return self
}
let remainingLength: Int = width - self.count
var padString = String()
for _ in 0 ..< remainingLength {
padString += paddingString
}
return "\(padString)\(self)"
}
}
@main
struct JellyfinPlayerApp: App {
let persistenceController = PersistenceController.shared

View File

@ -6,148 +6,68 @@
*/
import SwiftUI
import SwiftyRequest
import SwiftyJSON
import NukeUI
import JellyfinAPI
struct LatestMediaView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@State var resumeItems: [ResumeItem] = []
@State var items: [BaseItemDto] = []
private var library_id: String = "";
@State private var viewDidLoad: Int = 0;
@State private var viewDidLoad: Bool = false;
init(library: String) {
library_id = library;
}
init() {
library_id = "";
init(usingLibraryID: String) {
library_id = usingLibraryID;
}
func onAppear() {
if(globalData.server?.baseURI == "") {
if(viewDidLoad == true) {
return
}
if(viewDidLoad == 1) {
return
}
_viewDidLoad.wrappedValue = 1;
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Items/Latest?Limit=12&IncludeItemTypes=Movie%2CSeries&Limit=16&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo%2CPath&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&ParentId=\(library_id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
viewDidLoad = true;
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
let body = response.body
do {
let json = try JSON(data: body)
for (_,item):(String, JSON) in json {
// Do something you want
var itemObj = ResumeItem()
itemObj.Image = item["ImageTags"]["Primary"].string ?? ""
itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
itemObj.Name = item["Name"].string ?? ""
itemObj.Type = item["Type"].string ?? ""
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
itemObj.Id = item["Id"].string ?? ""
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
itemObj.SeasonId = item["SeasonId"].string ?? nil
itemObj.SeriesId = item["SeriesId"].string ?? nil
itemObj.SeriesName = item["SeriesName"].string ?? nil
itemObj.Watched = item["UserData"]["Played"].bool ?? false
if(itemObj.Type == "Series") {
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
}
if(itemObj.Type != "Episode") {
_resumeItems.wrappedValue.append(itemObj)
}
}
//print("latestmediaview done https")
} catch {
}
break
case .failure(let error):
debugPrint(error)
_viewDidLoad.wrappedValue = 0;
break
}
}
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio,.seriesPrimaryImage], enableUserData: true, limit: 12)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
items = response
})
.store(in: &globalData.pendingAPIRequests)
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack() {
Spacer().frame(width:14)
ForEach(resumeItems, id: \.Id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
if(item.Type == "Series") {
Spacer().frame(width:16)
ForEach(items, id: \.id) { item in
if(item.type == "Series" || item.type == "Movie") {
NavigationLink(destination: EmptyView()) {
VStack(alignment: .leading) {
Spacer().frame(height:10)
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
LazyImage(source: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!)
Image(uiImage: UIImage(blurHash: item.getPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
.resizable()
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width: 100, height: 150)
.cornerRadius(10)
.overlay(
ZStack {
if(item.ItemBadge == 0) {
Image(systemName: "checkmark")
.font(.caption)
.padding(3)
.foregroundColor(.white)
} else {
Text("\(String(item.ItemBadge ?? 0))")
.font(.caption)
.padding(3)
.foregroundColor(.white)
}
}.background(Color.black)
.opacity(0.8)
.cornerRadius(10.0)
.padding(3), alignment: .topTrailing
)
} else {
Spacer().frame(height:10)
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!)
.resizable()
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width: 100, height: 150)
.cornerRadius(10)
}
Text(item.Name)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
Spacer().frame(height:5)
}.frame(width: 100)
Spacer().frame(height:5)
Text(item.seasonName ?? item.name ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
}.frame(width: 100)
Spacer().frame(width: 15)
}
}
Spacer().frame(width: 14)
}
}.frame(height: 190)
}.onAppear(perform: onAppear).padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
}
}
struct LatestMediaView_Previews: PreviewProvider {
static var previews: some View {
LatestMediaView()
}
.frame(height: 190)
}
.onAppear(perform: onAppear)
.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 190)
}
}

View File

@ -65,8 +65,8 @@ struct LibraryFilterView: View {
_sortOrder.wrappedValue = filter.asc?.rawValue ?? sortOrder
_allGenres.wrappedValue = []
let url = "/Items/Filters?UserId=\(globalData.user?.user_id ?? "")&ParentId=\(library)"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
let url = "/Items/Filters?UserId=\(globalData.user.user_id ?? "")&ParentId=\(library)"
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"

View File

@ -87,7 +87,7 @@ struct ResumeItemGridCell: View {
var body: some View {
VStack(alignment: .leading) {
if item.Type == "Movie" {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
@ -100,7 +100,7 @@ struct ResumeItemGridCell: View {
.frame(width: 100, height: 150)
.cornerRadius(10)
} else {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item

View File

@ -136,7 +136,7 @@ extension LibraryView {
var body: some View {
VStack(alignment: .leading) {
if item.Type == "Movie" {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
@ -149,7 +149,7 @@ extension LibraryView {
.frame(width: 100, height: 150)
.cornerRadius(10)
} else {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item

View File

@ -125,11 +125,11 @@ struct MovieItemView: View {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
print((globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
print((globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
let request = RestRequest(method: .post,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)?DatePlayed=\(formatter.string(from: date).replacingOccurrences(of: ":", with: "%3A"))")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -138,8 +138,8 @@ struct MovieItemView: View {
}
} else {
let request = RestRequest(method: .delete,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/PlayedItems/\(fullItem.Id)")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/PlayedItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -155,8 +155,8 @@ struct MovieItemView: View {
didSet {
if favorite == true {
let request = RestRequest(method: .post,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -165,8 +165,8 @@ struct MovieItemView: View {
}
} else {
let request = RestRequest(method: .delete,
url: (globalData.server?.baseURI ?? "") +
"/Users/\(globalData.user?.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
url: (globalData.server.baseURI ?? "") +
"/Users/\(globalData.user.user_id ?? "")/FavoriteItems/\(fullItem.Id)")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -187,9 +187,9 @@ struct MovieItemView: View {
return
}
_viewDidLoad.wrappedValue = true
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -242,7 +242,7 @@ struct MovieItemView: View {
cast.Role = person["Role"].string ?? ""
cast
.Image =
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")!
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=250&quality=85&tag=\(imageTag)")!
fullItem.Cast.append(cast)
}
}
@ -293,7 +293,7 @@ struct MovieItemView: View {
}
var portraitHeaderView: some View {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
@ -309,7 +309,7 @@ struct MovieItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
@ -510,7 +510,7 @@ struct MovieItemView: View {
} else {
GeometryReader { geometry in
ZStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(fullItem.Backdrop)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
@ -531,7 +531,7 @@ struct MovieItemView: View {
.blur(radius: 2)
HStack {
VStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :

View File

@ -6,109 +6,73 @@
*/
import SwiftUI
import SwiftyRequest
import SwiftyJSON
import NukeUI
import JellyfinAPI
struct NextUpView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@State var resumeItems: [ResumeItem] = []
@State private var viewDidLoad: Int = 0;
@State private var isLoading: Bool = false;
@State private var items: [BaseItemDto] = []
@State private var viewDidLoad: Bool = false;
func onAppear() {
if(globalData.server?.baseURI == "") {
if(viewDidLoad == true) {
return
}
if(viewDidLoad == 1) {
return
}
_viewDidLoad.wrappedValue = 1;
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Shows/NextUp?Limit=12&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb&MediaTypes=Video&UserId=\(globalData.user?.user_id ?? "")")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
viewDidLoad = true;
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
let body = response.body
do {
let json = try JSON(data: body)
for (_,item):(String, JSON) in json["Items"] {
// Do something you want
var itemObj = ResumeItem()
itemObj.Image = item["SeriesPrimaryImageTag"].string ?? ""
itemObj.ImageType = "Primary"
itemObj.BlurHash = item["ImageBlurHashes"]["Primary"][itemObj.Image].string ?? ""
itemObj.Name = item["Name"].string ?? ""
itemObj.Type = item["Type"].string ?? ""
itemObj.IndexNumber = item["IndexNumber"].int ?? nil
itemObj.Id = item["Id"].string ?? ""
itemObj.ParentIndexNumber = item["ParentIndexNumber"].int ?? nil
itemObj.SeasonId = item["SeasonId"].string ?? nil
itemObj.SeriesId = item["SeriesId"].string ?? nil
itemObj.SeriesName = item["SeriesName"].string ?? nil
_resumeItems.wrappedValue.append(itemObj)
}
_isLoading.wrappedValue = false;
} catch {
}
break
case .failure(let error):
debugPrint(error)
_viewDidLoad.wrappedValue = 0;
break
}
}
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
items = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
}
var body: some View {
VStack(alignment: .leading) {
if(resumeItems.count != 0) {
Text("Next Up").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
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() {
if(isLoading == false) {
Spacer().frame(width:14)
ForEach(resumeItems, id: \.Id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height:10)
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!)
.resizable()
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.SeriesName ?? "")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0))")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
Spacer().frame(height:5)
}
.frame(width: 100)
Spacer().frame(width:12)
}
Spacer().frame(width: 10)
Spacer().frame(width:16)
ForEach(items, id: \.id) { item in
NavigationLink(destination: EmptyView()) {
VStack(alignment: .leading) {
LazyImage(source: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item.getSeriesPrimaryImageBlurHash(), size: CGSize(width: 16, height: 20))!)
.resizable()
.frame(width: 100, height: 150)
.cornerRadius(10)
}
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height:5)
Text(item.seriesName!)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0)")
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.secondary)
.lineLimit(1)
}.frame(width: 100)
Spacer().frame(width:16)
}
}
}.frame(height: 200)
}.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0)).frame(height: 200)
}
}
.frame(height: 200)
}
}.onAppear(perform: onAppear).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
}
.onAppear(perform: onAppear)
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
}

View File

@ -35,9 +35,9 @@ struct SeasonItemView: View {
if hasAppearedOnce {
return
}
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
let url = "/Users/\(globalData.user.user_id ?? "")/Items/\(item.Id)"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -84,7 +84,7 @@ struct SeasonItemView: View {
cast.Role = person["Role"].string ?? ""
cast
.Image =
URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")!
URL(string: "\(globalData.server.baseURI ?? "")/Items/\(cast.Id)/Images/Primary?maxWidth=2000&quality=90&tag=\(imageTag)")!
responseItem.Cast.append(cast)
}
}
@ -92,8 +92,8 @@ struct SeasonItemView: View {
_fullItem.wrappedValue = responseItem
let url2 =
"/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user?.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview"
let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url2)
"/Shows/\(fullItem.SeriesId ?? "")/Episodes?SeasonId=\(item.Id)&UserId=\(globalData.user.user_id ?? "")&Fields=ItemCounts%2CPrimaryImageAspectRatio%2CBasicSyncInfo%2CCanDelete%2CMediaSourceCount%2COverview"
let request2 = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url2)
request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request2.contentType = "application/json"
request2.acceptType = "application/json"
@ -193,7 +193,7 @@ struct SeasonItemView: View {
if isLoading {
EmptyView()
} else {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=550&quality=90&tag=\(item.SeasonImage ?? "")"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
@ -209,7 +209,7 @@ struct SeasonItemView: View {
var portraitHeaderOverlayView: some View {
HStack(alignment: .bottom, spacing: 12) {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem
@ -258,7 +258,7 @@ struct SeasonItemView: View {
ForEach(episodes, id: \.Id) { episode in
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
HStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: episode
.PosterBlurHash == "" ?
@ -276,7 +276,7 @@ struct SeasonItemView: View {
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(CustomShape(radius: 10))
.mask(ProgressBar())
.frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7)
.padding(0), alignment: .bottomLeading
)
@ -333,7 +333,7 @@ struct SeasonItemView: View {
} else {
GeometryReader { geometry in
ZStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.SeriesId ?? "")/Images/Backdrop?maxWidth=\(String(Int(geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing)))&quality=80&tag=\(item.SeasonImage ?? "")"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: item
.SeasonImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item
@ -355,7 +355,7 @@ struct SeasonItemView: View {
HStack {
VStack(alignment: .leading) {
Spacer().frame(height: 16)
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?maxWidth=250&quality=90&tag=\(fullItem.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: fullItem
.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" :
@ -392,7 +392,7 @@ struct SeasonItemView: View {
ForEach(episodes, id: \.Id) { episode in
NavigationLink(destination: ItemView(item: episode.ResumeItem ?? ResumeItem())) {
HStack {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(episode.Id)/Images/Primary?maxWidth=300&quality=90&tag=\(episode.Poster)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: episode
.PosterBlurHash == "" ?
@ -410,7 +410,7 @@ struct SeasonItemView: View {
.overlay(
Rectangle()
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
.mask(CustomShape(radius: 10))
.mask(ProgressBar())
.frame(width: CGFloat((episode.Progress / Double(episode.RuntimeTicks)) * 150), height: 7)
.padding(0), alignment: .bottomLeading
)

View File

@ -22,9 +22,9 @@ struct SeriesItemView: View {
return;
}
_isLoading.wrappedValue = true;
let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user?.user_id ?? "")&Fields=ItemCount"
let url = "/Shows/\(item.Id )/Seasons?userId=\(globalData.user.user_id ?? "")&Fields=ItemCount"
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + url)
let request = RestRequest(method: .get, url: (globalData.server.baseURI ?? "") + url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
@ -97,7 +97,7 @@ struct SeriesItemView: View {
ForEach(items, id: \.Id) { item in
NavigationLink(destination: ItemView(item: item )) {
VStack(alignment: .leading) {
LazyImage(source: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)"))
LazyImage(source: URL(string: "\(globalData.server.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=90&tag=\(item.Image)"))
.placeholderAndFailure {
Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 32, height: 32))!)
.resizable()

View File

@ -32,7 +32,7 @@ struct SettingsView: View {
private var autoSelectSubtitlesLangcode: String = "none"
func onAppear() {
_username.wrappedValue = globalData.user?.username ?? ""
_username.wrappedValue = globalData.user.username ?? ""
let defaults = UserDefaults.standard
_inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth")
_outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth")
@ -94,8 +94,8 @@ struct SettingsView: View {
// TODO: handle the error
}
globalData.server = nil
globalData.user = nil
globalData.server = Server()
globalData.user = SignedInUser()
globalData.authToken = ""
globalData.authHeader = ""
jsi.did = true

View File

@ -7,19 +7,13 @@
import SwiftUI
import MobileVLCKit
import SwiftyJSON
import SwiftyRequest
enum VideoType {
case hls;
case direct;
}
import JellyfinAPI
struct Subtitle {
var name: String;
var id: Int32;
var url: URL;
var delivery: String;
var delivery: SubtitleDeliveryMethod;
var codec: String;
}
@ -29,7 +23,7 @@ struct AudioTrack {
}
class PlaybackItem: ObservableObject {
@Published var videoType: VideoType = .hls;
@Published var videoType: PlayMethod = .directPlay;
@Published var videoUrl: URL = URL(string: "https://example.com")!;
}
@ -81,7 +75,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
var subtitleTrackArray: [Subtitle] = [];
var audioTrackArray: [AudioTrack] = [];
var manifest: DetailItem = DetailItem();
var manifest: BaseItemDto = BaseItemDto();
var playbackItem = PlaybackItem();
@IBAction func seekSliderStart(_ sender: Any) {
@ -199,8 +193,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
//Rotate to landscape only if necessary
UIViewController.attemptRotationToDeviceOrientation();
//Show loading screen
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
//mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
@ -208,7 +200,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
mediaPlayer.delegate = self
mediaPlayer.drawable = videoContentView
titleLabel.text = manifest.Name
titleLabel.text = manifest.name
//Fetch max bitrate from UserDefaults depending on current connection mode
let defaults = UserDefaults.standard
@ -219,118 +211,88 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
builder.setMaxBitrate(bitrate: maxBitrate)
let profile = builder.buildProfile()
let jsonEncoder = JSONEncoder()
let jsonData = try! jsonEncoder.encode(profile)
let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
let url = (globalData.server?.baseURI ?? "") + "/Items/\(manifest.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=\(Int(manifest.Progress))&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=\(profile.DeviceProfile.MaxStreamingBitrate)";
let request = RestRequest(method: .post, url: url)
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.messageBody = jsonData
request.responseData() { [self] (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let response):
let body = response.body
do {
let json = try JSON(data: body)
playSessionId = json["PlaySessionId"].string ?? "";
if(json["MediaSources"][0]["TranscodingUrl"].string != nil) {
let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? ""))")!
let item = PlaybackItem()
item.videoType = .hls
item.videoUrl = streamURL
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "")
subtitleTrackArray.append(disableSubtitleTrack);
for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] {
if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles
let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")!
let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "")
subtitleTrackArray.append(subtitle);
}
if(stream["Type"].string == "Audio") {
let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0))
if(stream["IsDefault"].boolValue) {
selectedAudioTrack = Int32(stream["Index"].int ?? 0);
}
audioTrackArray.append(subtitle);
}
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
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, 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
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
for stream in mediaSource.mediaStreams! {
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);
}
if(selectedAudioTrack == -1) {
if(audioTrackArray.count > 0) {
selectedAudioTrack = audioTrackArray[0].id;
if(stream.type == .audio) {
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
if(stream.isDefault! == true) {
selectedAudioTrack = Int32(stream.index!);
}
audioTrackArray.append(subtitle);
}
self.sendPlayReport()
playbackItem = item;
} else {
print("Direct playing!");
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=\(json["MediaSources"][0]["ETag"])")!;
let item = PlaybackItem()
item.videoUrl = streamURL
item.videoType = .direct
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "")
subtitleTrackArray.append(disableSubtitleTrack);
for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] {
if(stream["Type"].string == "Subtitle") {
let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")!
let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "")
subtitleTrackArray.append(subtitle);
}
if(stream["Type"].string == "Audio") {
let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0))
if(stream["IsDefault"].boolValue) {
selectedAudioTrack = Int32(stream["Index"].int ?? 0);
}
audioTrackArray.append(subtitle);
}
}
if(selectedAudioTrack == -1) {
if(audioTrackArray.count > 0) {
selectedAudioTrack = audioTrackArray[0].id;
}
}
sendPlayReport()
playbackItem = item;
}
DispatchQueue.global(qos: .background).async {
mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl)
mediaPlayer.play()
mediaPlayer.jumpForward(Int32(manifest.Progress/10000000))
mediaPlayer.pause()
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)
}
if(selectedAudioTrack == -1) {
if(audioTrackArray.count > 0) {
selectedAudioTrack = audioTrackArray[0].id;
}
delegate?.showLoadingView(self)
while(mediaPlayer.numberOfSubtitlesTracks != subtitleTrackArray.count - 1) {}
mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack;
mediaPlayer.pause()
mediaPlayer.play()
}
} catch {
self.sendPlayReport()
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!)")!;
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
for stream in mediaSource.mediaStreams! {
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);
}
if(stream.type == .audio) {
let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!))
if(stream.isDefault! == true) {
selectedAudioTrack = Int32(stream.index!);
}
audioTrackArray.append(subtitle);
}
}
if(selectedAudioTrack == -1) {
if(audioTrackArray.count > 0) {
selectedAudioTrack = audioTrackArray[0].id;
}
}
self.sendPlayReport()
playbackItem = item;
}
break
case .failure(let error):
debugPrint(error)
break
}
}
})
.store(in: &globalData.pendingAPIRequests)
}
override func viewWillAppear(_ animated: Bool) {
@ -430,74 +392,46 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
//MARK: Jellyfin Playstate updates
func sendProgressReport(eventName: String) {
var progressBody: String = "";
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(mediaPlayer.state == .paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"EventName\":\"\(eventName)\"}";
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 request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.messageBody = progressBody.data(using: .ascii);
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let resp):
print(resp.body)
break
case .failure(let error):
debugPrint(error)
break
}
}
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, receiveValue: { response in
print("Playback progress report sent!")
})
.store(in: &globalData.pendingAPIRequests)
}
func sendStopReport() {
var progressBody: String = "";
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: [])
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}";
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.messageBody = progressBody.data(using: .ascii);
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let resp):
print(resp.body)
break
case .failure(let error):
debugPrint(error)
break
}
}
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, receiveValue: { response in
print("Playback stop report sent!")
})
.store(in: &globalData.pendingAPIRequests)
}
func sendPlayReport() {
var progressBody: String = "";
startTime = Int(Date().timeIntervalSince1970) * 10000000
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(manifest.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}";
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")
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing")
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
request.contentType = "application/json"
request.acceptType = "application/json"
request.messageBody = progressBody.data(using: .ascii);
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
switch result {
case .success(let resp):
print(resp.body)
break
case .failure(let error):
debugPrint(error)
break
}
}
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
}, receiveValue: { response in
print("Playback start report sent!")
})
.store(in: &globalData.pendingAPIRequests)
}
}
struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: DetailItem
var item: BaseItemDto
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject private var globalData: GlobalData;

View File

@ -0,0 +1,81 @@
/* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
import UIKit
//001fC^ = dark grey plain blurhash
extension BaseItemDto {
func getSeriesPrimaryImageBlurHash() -> String {
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^";
}
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^";
} else {
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";
imageTag = (self.backdropImageTags ?? [""])[0]
} else {
imageType = "Primary";
imageTag = self.imageTags?["Primary"] ?? ""
}
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 getSeriesPrimaryImage(baseURL: String, maxWidth: Int) -> URL {
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 == "") {
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)!
}
}

View File

@ -15,4 +15,21 @@ 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 ?? " "
if self.count >= width {
return self
}
let remainingLength: Int = width - self.count
var padString = String()
for _ in 0 ..< remainingLength {
padString += paddingString
}
return "\(padString)\(self)"
}
}

View File

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E232" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E241" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="Server" representedClassName="Server" syncable="YES" codeGenerationType="class">
<attribute name="baseURI" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="server_id" optional="YES" attributeType="String"/>
<attribute name="baseURI" attributeType="String" defaultValueString=""/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="server_id" attributeType="String" defaultValueString=""/>
</entity>
<entity name="SignedInUser" representedClassName="SignedInUser" syncable="YES" codeGenerationType="class">
<attribute name="device_uuid" optional="YES" attributeType="String"/>
<attribute name="user_id" optional="YES" attributeType="String"/>
<attribute name="username" optional="YES" attributeType="String"/>
<attribute name="device_uuid" attributeType="String" defaultValueString=""/>
<attribute name="user_id" attributeType="String" defaultValueString=""/>
<attribute name="username" attributeType="String" defaultValueString=""/>
</entity>
<elements>
<element name="Server" positionX="-63" positionY="-9" width="128" height="74"/>

View File

@ -13,9 +13,9 @@ class justSignedIn: ObservableObject {
}
class GlobalData: ObservableObject {
@Published var user: SignedInUser?
@Published var user: SignedInUser = SignedInUser()
@Published var authToken: String = ""
@Published var server: Server?
@Published var server: Server = Server()
@Published var authHeader: String = ""
@Published var isInNetwork: Bool = true;
@Published var networkError: Bool = false;