Start moving to generated client
This commit is contained in:
parent
a7147e7e7c
commit
007930ec06
|
@ -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 */,
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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-" :
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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-" :
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)!
|
||||
}
|
||||
}
|
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"/>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue