Upload all files
This commit is contained in:
parent
7232b8d54b
commit
52cac0ab6f
|
@ -0,0 +1 @@
|
||||||
|
binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.3.0
|
|
@ -0,0 +1 @@
|
||||||
|
binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" "3.3.16"
|
Binary file not shown.
|
@ -0,0 +1,613 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 52;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; };
|
||||||
|
5338F751263B62E80014BF09 /* HidingViews in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F750263B62E80014BF09 /* HidingViews */; };
|
||||||
|
5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F753263B65E10014BF09 /* SwiftyRequest */; };
|
||||||
|
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; };
|
||||||
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; };
|
||||||
|
535BAEA32649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */; };
|
||||||
|
535BAEA5264A151C005FA86D /* VLCPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VLCPlayer.swift */; };
|
||||||
|
535BAEA7264A18AA005FA86D /* PlayerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA6264A18AA005FA86D /* PlayerDemo.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 */; };
|
||||||
|
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
|
||||||
|
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||||
|
5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
||||||
|
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 */; };
|
||||||
|
53892782263CC8770035E14B /* URLImage in Frameworks */ = {isa = PBXBuildFile; productRef = 53892781263CC8770035E14B /* URLImage */; };
|
||||||
|
538CD954263E3DC100BB5AF0 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */; };
|
||||||
|
538CD957263E441500BB5AF0 /* ExyteGrid in Frameworks */ = {isa = PBXBuildFile; productRef = 538CD956263E441500BB5AF0 /* ExyteGrid */; };
|
||||||
|
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */; };
|
||||||
|
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A089CF264DA9DA00D57806 /* MovieItemView.swift */; };
|
||||||
|
53D2F74A264C69F6005792BB /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53D2F749264C69F6005792BB /* Introspect */; };
|
||||||
|
53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; };
|
||||||
|
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 */; };
|
||||||
|
53E4E645263F6BC000F67C6B /* PartialSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 53E4E644263F6BC000F67C6B /* PartialSheet */; };
|
||||||
|
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */; };
|
||||||
|
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53E4E648263F725B00F67C6B /* MultiSelector.swift */; };
|
||||||
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
53D5E3DF264B47EE00BADDC8 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
53D5E3DE264B47EE00BADDC8 /* MobileVLCKit.xcframework in Embed Frameworks */,
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; };
|
||||||
|
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
|
||||||
|
535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinHLSResourceLoaderDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
535BAEA4264A151C005FA86D /* VLCPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayer.swift; sourceTree = "<group>"; };
|
||||||
|
535BAEA6264A18AA005FA86D /* PlayerDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerDemo.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>"; };
|
||||||
|
5377CBF8263B596B003A4E83 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
|
5377CBFD263B596B003A4E83 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
|
5377CC00263B596B003A4E83 /* JellyfinPlayer.xcdatamodel */ = {isa = PBXFileReference; explicitFileType = wrapper.xcdatamodel; path = JellyfinPlayer.xcdatamodel; sourceTree = "<group>"; };
|
||||||
|
5377CC02263B596B003A4E83 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
|
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
53A089CF264DA9DA00D57806 /* MovieItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieItemView.swift; sourceTree = "<group>"; };
|
||||||
|
53D5E3DA264B460200BADDC8 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
|
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
5377CBEE263B596A003A4E83 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
538CD954263E3DC100BB5AF0 /* SDWebImageSwiftUI in Frameworks */,
|
||||||
|
538CD957263E441500BB5AF0 /* ExyteGrid in Frameworks */,
|
||||||
|
53E4E645263F6BC000F67C6B /* PartialSheet in Frameworks */,
|
||||||
|
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||||
|
53D5E3DD264B47EE00BADDC8 /* MobileVLCKit.xcframework in Frameworks */,
|
||||||
|
5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */,
|
||||||
|
53892782263CC8770035E14B /* URLImage in Frameworks */,
|
||||||
|
53D2F74A264C69F6005792BB /* Introspect in Frameworks */,
|
||||||
|
5389277A263CBFE70035E14B /* SwiftyJSON in Frameworks */,
|
||||||
|
5338F751263B62E80014BF09 /* HidingViews in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
5377CBE8263B596A003A4E83 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
53D5E3DA264B460200BADDC8 /* Cartfile */,
|
||||||
|
5377CBF3263B596A003A4E83 /* JellyfinPlayer */,
|
||||||
|
5377CBF2263B596A003A4E83 /* Products */,
|
||||||
|
53D5E3DB264B47EE00BADDC8 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
5377CBF2263B596A003A4E83 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */,
|
||||||
|
5377CBF6263B596A003A4E83 /* ContentView.swift */,
|
||||||
|
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
||||||
|
5377CBFD263B596B003A4E83 /* PersistenceController.swift */,
|
||||||
|
5377CC02263B596B003A4E83 /* Info.plist */,
|
||||||
|
5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */,
|
||||||
|
5377CBFA263B596B003A4E83 /* Preview Content */,
|
||||||
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||||
|
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
|
||||||
|
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
||||||
|
5389276F263C25230035E14B /* NextUpView.swift */,
|
||||||
|
53892771263C8C6F0035E14B /* LoadingView.swift */,
|
||||||
|
53892776263CBB000035E14B /* JellyApiTypings.swift */,
|
||||||
|
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */,
|
||||||
|
53FF7F29263CF3F500585C35 /* LatestMediaView.swift */,
|
||||||
|
53DF641D263D9C0600A7CD1A /* LibraryView.swift */,
|
||||||
|
53E4E646263F6CF100F67C6B /* LibraryFilterView.swift */,
|
||||||
|
53E4E648263F725B00F67C6B /* MultiSelector.swift */,
|
||||||
|
535BAE9E2649E569005FA86D /* ItemView.swift */,
|
||||||
|
53A089CF264DA9DA00D57806 /* MovieItemView.swift */,
|
||||||
|
535BAEA22649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift */,
|
||||||
|
535BAEA4264A151C005FA86D /* VLCPlayer.swift */,
|
||||||
|
535BAEA6264A18AA005FA86D /* PlayerDemo.swift */,
|
||||||
|
);
|
||||||
|
path = JellyfinPlayer;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
5377CBFA263B596B003A4E83 /* Preview Content */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */,
|
||||||
|
);
|
||||||
|
path = "Preview Content";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
53D5E3DB264B47EE00BADDC8 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
5377CBF0263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer" */;
|
||||||
|
buildPhases = (
|
||||||
|
5377CBED263B596A003A4E83 /* Sources */,
|
||||||
|
5377CBEE263B596A003A4E83 /* Frameworks */,
|
||||||
|
5377CBEF263B596A003A4E83 /* Resources */,
|
||||||
|
53D5E3DF264B47EE00BADDC8 /* Embed Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = JellyfinPlayer;
|
||||||
|
packageProductDependencies = (
|
||||||
|
5338F750263B62E80014BF09 /* HidingViews */,
|
||||||
|
5338F753263B65E10014BF09 /* SwiftyRequest */,
|
||||||
|
5338F756263B7E2E0014BF09 /* KeychainSwift */,
|
||||||
|
53892779263CBFE70035E14B /* SwiftyJSON */,
|
||||||
|
53892781263CC8770035E14B /* URLImage */,
|
||||||
|
538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */,
|
||||||
|
538CD956263E441500BB5AF0 /* ExyteGrid */,
|
||||||
|
53E4E644263F6BC000F67C6B /* PartialSheet */,
|
||||||
|
53D2F749264C69F6005792BB /* Introspect */,
|
||||||
|
);
|
||||||
|
productName = JellyfinPlayer;
|
||||||
|
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
5377CBE9263B596A003A4E83 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastSwiftUpdateCheck = 1250;
|
||||||
|
LastUpgradeCheck = 1250;
|
||||||
|
TargetAttributes = {
|
||||||
|
5377CBF0263B596A003A4E83 = {
|
||||||
|
CreatedOnToolsVersion = 12.5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 5377CBEC263B596A003A4E83 /* Build configuration list for PBXProject "JellyfinPlayer" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 5377CBE8263B596A003A4E83;
|
||||||
|
packageReferences = (
|
||||||
|
5338F74F263B62E80014BF09 /* XCRemoteSwiftPackageReference "HidingViews" */,
|
||||||
|
5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */,
|
||||||
|
5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||||
|
53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||||
|
53892780263CC8770035E14B /* XCRemoteSwiftPackageReference "url-image" */,
|
||||||
|
538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
|
||||||
|
538CD955263E441500BB5AF0 /* XCRemoteSwiftPackageReference "Grid" */,
|
||||||
|
53E4E643263F6BC000F67C6B /* XCRemoteSwiftPackageReference "PartialSheet" */,
|
||||||
|
53D2F748264C69F6005792BB /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
|
);
|
||||||
|
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
5377CBF0263B596A003A4E83 /* JellyfinPlayer */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
5377CBEF263B596A003A4E83 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */,
|
||||||
|
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
5377CBED263B596A003A4E83 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||||
|
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||||
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||||
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||||
|
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||||
|
535BAEA5264A151C005FA86D /* VLCPlayer.swift in Sources */,
|
||||||
|
5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */,
|
||||||
|
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||||
|
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
||||||
|
53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */,
|
||||||
|
535BAEA7264A18AA005FA86D /* PlayerDemo.swift in Sources */,
|
||||||
|
53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */,
|
||||||
|
53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */,
|
||||||
|
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
||||||
|
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||||
|
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||||
|
535BAEA32649E96A005FA86D /* JellyfinHLSResourceLoaderDelegate.swift in Sources */,
|
||||||
|
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||||
|
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||||
|
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
5377CC19263B596B003A4E83 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
5377CC1A263B596B003A4E83 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
5377CC1C263B596B003A4E83 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer/Preview Content\"";
|
||||||
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
INFOPLIST_FILE = JellyfinPlayer/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = me.vigue.JellyfinPlayer;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
5377CC1D263B596B003A4E83 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer/Preview Content\"";
|
||||||
|
DEVELOPMENT_TEAM = 9R8RREG67J;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
INFOPLIST_FILE = JellyfinPlayer/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 14.5;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = me.vigue.JellyfinPlayer;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
5377CBEC263B596A003A4E83 /* Build configuration list for PBXProject "JellyfinPlayer" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
5377CC19263B596B003A4E83 /* Debug */,
|
||||||
|
5377CC1A263B596B003A4E83 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
5377CC1B263B596B003A4E83 /* Build configuration list for PBXNativeTarget "JellyfinPlayer" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
5377CC1C263B596B003A4E83 /* Debug */,
|
||||||
|
5377CC1D263B596B003A4E83 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
5338F74F263B62E80014BF09 /* XCRemoteSwiftPackageReference "HidingViews" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/GeorgeElsham/HidingViews";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.1.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/Kitura/SwiftyRequest";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 3.2.200;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/evgenyneu/keychain-swift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 19.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 5.0.1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
53892780263CC8770035E14B /* XCRemoteSwiftPackageReference "url-image" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/dmytro-anokhin/url-image";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.2.5;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
538CD955263E441500BB5AF0 /* XCRemoteSwiftPackageReference "Grid" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/exyte/Grid";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.4.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
53D2F748264C69F6005792BB /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 0.1.3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
53E4E643263F6BC000F67C6B /* XCRemoteSwiftPackageReference "PartialSheet" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/AndreaMiotto/PartialSheet";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.1.11;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
5338F750263B62E80014BF09 /* HidingViews */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 5338F74F263B62E80014BF09 /* XCRemoteSwiftPackageReference "HidingViews" */;
|
||||||
|
productName = HidingViews;
|
||||||
|
};
|
||||||
|
5338F753263B65E10014BF09 /* SwiftyRequest */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 5338F752263B65E10014BF09 /* XCRemoteSwiftPackageReference "SwiftyRequest" */;
|
||||||
|
productName = SwiftyRequest;
|
||||||
|
};
|
||||||
|
5338F756263B7E2E0014BF09 /* KeychainSwift */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||||
|
productName = KeychainSwift;
|
||||||
|
};
|
||||||
|
53892779263CBFE70035E14B /* SwiftyJSON */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 53892778263CBFE70035E14B /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
|
||||||
|
productName = SwiftyJSON;
|
||||||
|
};
|
||||||
|
53892781263CC8770035E14B /* URLImage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 53892780263CC8770035E14B /* XCRemoteSwiftPackageReference "url-image" */;
|
||||||
|
productName = URLImage;
|
||||||
|
};
|
||||||
|
538CD953263E3DC100BB5AF0 /* SDWebImageSwiftUI */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 538CD952263E3DC100BB5AF0 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||||
|
productName = SDWebImageSwiftUI;
|
||||||
|
};
|
||||||
|
538CD956263E441500BB5AF0 /* ExyteGrid */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 538CD955263E441500BB5AF0 /* XCRemoteSwiftPackageReference "Grid" */;
|
||||||
|
productName = ExyteGrid;
|
||||||
|
};
|
||||||
|
53D2F749264C69F6005792BB /* Introspect */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 53D2F748264C69F6005792BB /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||||
|
productName = Introspect;
|
||||||
|
};
|
||||||
|
53E4E644263F6BC000F67C6B /* PartialSheet */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 53E4E643263F6BC000F67C6B /* XCRemoteSwiftPackageReference "PartialSheet" */;
|
||||||
|
productName = PartialSheet;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
|
|
||||||
|
/* Begin XCVersionGroup section */
|
||||||
|
5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */ = {
|
||||||
|
isa = XCVersionGroup;
|
||||||
|
children = (
|
||||||
|
5377CC00263B596B003A4E83 /* JellyfinPlayer.xcdatamodel */,
|
||||||
|
);
|
||||||
|
currentVersion = 5377CC00263B596B003A4E83 /* JellyfinPlayer.xcdatamodel */;
|
||||||
|
path = Model.xcdatamodeld;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
versionGroupType = wrapper.xcdatamodel;
|
||||||
|
};
|
||||||
|
/* End XCVersionGroup section */
|
||||||
|
};
|
||||||
|
rootObject = 5377CBE9263B596A003A4E83 /* Project object */;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,160 @@
|
||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "async-http-client",
|
||||||
|
"repositoryURL": "https://github.com/swift-server/async-http-client.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "037b70291941fe43de668066eb6fb802c5e181d2",
|
||||||
|
"version": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "CircuitBreaker",
|
||||||
|
"repositoryURL": "https://github.com/Kitura/CircuitBreaker.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "915cd4ed17500784cf5bcbf2ef54a76830884c86",
|
||||||
|
"version": "5.0.200"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "ExyteGrid",
|
||||||
|
"repositoryURL": "https://github.com/exyte/Grid",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "585dc249126fda6ae675d78175b0c52a311f10c9",
|
||||||
|
"version": "1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "HidingViews",
|
||||||
|
"repositoryURL": "https://github.com/GeorgeElsham/HidingViews",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "7fde89eaeb2f0d3a07f8bf517507c6e27af8e4c3",
|
||||||
|
"version": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "KeychainSwift",
|
||||||
|
"repositoryURL": "https://github.com/evgenyneu/keychain-swift",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "96fb84f45a96630e7583903bd7e08cf095c7a7ef",
|
||||||
|
"version": "19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "LoggerAPI",
|
||||||
|
"repositoryURL": "https://github.com/Kitura/LoggerAPI.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "e82d34eab3f0b05391082b11ea07d3b70d2f65bb",
|
||||||
|
"version": "1.9.200"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "PartialSheet",
|
||||||
|
"repositoryURL": "https://github.com/AndreaMiotto/PartialSheet",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "936465232b6399e402e79d5b031622af9a5e9960",
|
||||||
|
"version": "2.1.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SDWebImage",
|
||||||
|
"repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "76dd4b49110b8624317fc128e7fa0d8a252018bc",
|
||||||
|
"version": "5.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SDWebImageSwiftUI",
|
||||||
|
"repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "cd8625b7cf11a97698e180d28bb7d5d357196678",
|
||||||
|
"version": "2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-log",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7",
|
||||||
|
"version": "1.4.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-nio",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "21782f3bdc9581148d38d0ccaab6ec952ccda56b",
|
||||||
|
"version": "2.28.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-nio-extras",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-nio-extras.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "de1c80ad1fdff1ba772bcef6b392c3ef735f39a6",
|
||||||
|
"version": "1.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-nio-ssl",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "3d576964a1ace80d2a3f8bab96cab03e5ee074dc",
|
||||||
|
"version": "2.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "Introspect",
|
||||||
|
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2",
|
||||||
|
"version": "0.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SwiftyJSON",
|
||||||
|
"repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "b3dcd7dbd0d488e1a7077cb33b00f2083e382f07",
|
||||||
|
"version": "5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SwiftyRequest",
|
||||||
|
"repositoryURL": "https://github.com/Kitura/SwiftyRequest",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "2c543777a5088bed811503a68551a4b4eceac198",
|
||||||
|
"version": "3.2.200"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "URLImage",
|
||||||
|
"repositoryURL": "https://github.com/dmytro-anokhin/url-image",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "ccab89ad1cedb04f25dd4df1776dd8c8583b914a",
|
||||||
|
"version": "2.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "76x76"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "83.5x83.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIImage {
|
||||||
|
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
|
||||||
|
guard blurHash.count >= 6 else { return nil }
|
||||||
|
|
||||||
|
let sizeFlag = String(blurHash[0]).decode83()
|
||||||
|
let numY = (sizeFlag / 9) + 1
|
||||||
|
let numX = (sizeFlag % 9) + 1
|
||||||
|
|
||||||
|
let quantisedMaximumValue = String(blurHash[1]).decode83()
|
||||||
|
let maximumValue = Float(quantisedMaximumValue + 1) / 166
|
||||||
|
|
||||||
|
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
|
||||||
|
|
||||||
|
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
|
||||||
|
if i == 0 {
|
||||||
|
let value = String(blurHash[2 ..< 6]).decode83()
|
||||||
|
return decodeDC(value)
|
||||||
|
} else {
|
||||||
|
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
|
||||||
|
return decodeAC(value, maximumValue: maximumValue * punch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = Int(size.width)
|
||||||
|
let height = Int(size.height)
|
||||||
|
let bytesPerRow = width * 3
|
||||||
|
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
|
||||||
|
CFDataSetLength(data, bytesPerRow * height)
|
||||||
|
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
|
||||||
|
|
||||||
|
for y in 0 ..< height {
|
||||||
|
for x in 0 ..< width {
|
||||||
|
var r: Float = 0
|
||||||
|
var g: Float = 0
|
||||||
|
var b: Float = 0
|
||||||
|
|
||||||
|
for j in 0 ..< numY {
|
||||||
|
for i in 0 ..< numX {
|
||||||
|
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
|
||||||
|
let colour = colours[i + j * numX]
|
||||||
|
r += colour.0 * basis
|
||||||
|
g += colour.1 * basis
|
||||||
|
b += colour.2 * basis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let intR = UInt8(linearTosRGB(r))
|
||||||
|
let intG = UInt8(linearTosRGB(g))
|
||||||
|
let intB = UInt8(linearTosRGB(b))
|
||||||
|
|
||||||
|
pixels[3 * x + 0 + y * bytesPerRow] = intR
|
||||||
|
pixels[3 * x + 1 + y * bytesPerRow] = intG
|
||||||
|
pixels[3 * x + 2 + y * bytesPerRow] = intB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||||
|
|
||||||
|
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||||
|
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
||||||
|
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
|
||||||
|
|
||||||
|
self.init(cgImage: cgImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
|
||||||
|
let intR = value >> 16
|
||||||
|
let intG = (value >> 8) & 255
|
||||||
|
let intB = value & 255
|
||||||
|
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
|
||||||
|
let quantR = value / (19 * 19)
|
||||||
|
let quantG = (value / 19) % 19
|
||||||
|
let quantB = value % 19
|
||||||
|
|
||||||
|
let rgb = (
|
||||||
|
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
|
||||||
|
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
|
||||||
|
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
|
||||||
|
)
|
||||||
|
|
||||||
|
return rgb
|
||||||
|
}
|
||||||
|
|
||||||
|
private func signPow(_ value: Float, _ exp: Float) -> Float {
|
||||||
|
return copysign(pow(abs(value), exp), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func linearTosRGB(_ value: Float) -> Int {
|
||||||
|
let v = max(0, min(1, value))
|
||||||
|
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
|
||||||
|
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
|
||||||
|
let v = Float(Int64(value)) / 255
|
||||||
|
if v <= 0.04045 { return v / 12.92 }
|
||||||
|
else { return pow((v + 0.055) / 1.055, 2.4) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private let encodeCharacters: [String] = {
|
||||||
|
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let decodeCharacters: [String: Int] = {
|
||||||
|
var dict: [String: Int] = [:]
|
||||||
|
for (index, character) in encodeCharacters.enumerated() {
|
||||||
|
dict[character] = index
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}()
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
func decode83() -> Int {
|
||||||
|
var value: Int = 0
|
||||||
|
for character in self {
|
||||||
|
if let digit = decodeCharacters[String(character)] {
|
||||||
|
value = value * 83 + digit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
subscript (offset: Int) -> Character {
|
||||||
|
return self[index(startIndex, offsetBy: offset)]
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript (bounds: CountableClosedRange<Int>) -> Substring {
|
||||||
|
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||||
|
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||||
|
return self[start...end]
|
||||||
|
}
|
||||||
|
|
||||||
|
subscript (bounds: CountableRange<Int>) -> Substring {
|
||||||
|
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||||
|
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||||
|
return self[start..<end]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
//
|
||||||
|
// ConnectToServerView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/29/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import HidingViews
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import CoreData
|
||||||
|
import KeychainSwift
|
||||||
|
|
||||||
|
struct ConnectToServerView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@EnvironmentObject var jsi: justSignedIn
|
||||||
|
@State private var uri = "";
|
||||||
|
@State private var isWorking = false;
|
||||||
|
@State private var isErrored = false;
|
||||||
|
@State private var isDone = false;
|
||||||
|
@State private var isSignInErrored = false;
|
||||||
|
@State private var isConnected = false;
|
||||||
|
@State private var serverName = "";
|
||||||
|
@Binding var rootIsActive : Bool
|
||||||
|
|
||||||
|
let userUUID = UUID();
|
||||||
|
|
||||||
|
@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?
|
||||||
|
|
||||||
|
init(skip_server: Bool, skip_server_prefill: Server?, reauth_deviceId: String, isActive: Binding<Bool>) {
|
||||||
|
skip_server_bool = skip_server
|
||||||
|
skip_server_obj = skip_server_prefill
|
||||||
|
reauthDeviceID = reauth_deviceId
|
||||||
|
_rootIsActive = isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
init(isActive: Binding<Bool>) {
|
||||||
|
_rootIsActive = isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
if(skip_server_bool) {
|
||||||
|
_serverSkipped.wrappedValue = true;
|
||||||
|
_serverSkippedAlert.wrappedValue = true;
|
||||||
|
_server_id.wrappedValue = skip_server_obj?.server_id ?? ""
|
||||||
|
_serverName.wrappedValue = skip_server_obj?.name ?? ""
|
||||||
|
_uri.wrappedValue = skip_server_obj?.baseURI ?? ""
|
||||||
|
_isConnected.wrappedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
if(!isConnected) {
|
||||||
|
Section(header: Text("Server Information")) {
|
||||||
|
TextField("Server URL", text: $uri)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
Button {
|
||||||
|
_isWorking.wrappedValue = true;
|
||||||
|
|
||||||
|
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
|
||||||
|
print("Found server: " + server.ServerName)
|
||||||
|
_serverName.wrappedValue = server.ServerName
|
||||||
|
_server_id.wrappedValue = server.Id
|
||||||
|
if(!server.StartupWizardCompleted) {
|
||||||
|
print("Server needs configured")
|
||||||
|
} else {
|
||||||
|
_isConnected.wrappedValue = true;
|
||||||
|
}
|
||||||
|
case .failure( _):
|
||||||
|
_isErrored.wrappedValue = true;
|
||||||
|
}
|
||||||
|
_isWorking.wrappedValue = false;
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Connect")
|
||||||
|
Spacer()
|
||||||
|
ProgressView().isHidden(!isWorking)
|
||||||
|
}
|
||||||
|
}.disabled(isWorking || uri.isEmpty)
|
||||||
|
}.alert(isPresented: $isErrored) {
|
||||||
|
Alert(title: Text("Error"), message: Text("Couldn't connect to Jellyfin server"), dismissButton: .default(Text("Try again")))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Section(header: Text("\(serverSkipped ? "re" : "")Authenticate to \(serverName)")) {
|
||||||
|
TextField("Username", text: $username)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
SecureField("Password", text: $password)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
Button {
|
||||||
|
_isWorking.wrappedValue = 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")\"";
|
||||||
|
print(authHeader)
|
||||||
|
let authJson: [String: Any] = ["Username": _username.wrappedValue, "Pw": _password.wrappedValue]
|
||||||
|
let request = RestRequest(method: .post, url: uri + "/Users/authenticatebyname")
|
||||||
|
request.headerParameters["X-Emby-Authorization"] = authHeader
|
||||||
|
request.contentType = "application/json"
|
||||||
|
request.acceptType = "application/json"
|
||||||
|
request.messageBodyDictionary = authJson
|
||||||
|
|
||||||
|
request.responseData() { (result: Result<RestResponse<Data>, RestError>) in
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
do {
|
||||||
|
let json = try JSON(data: response.body)
|
||||||
|
dump(json)
|
||||||
|
|
||||||
|
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
||||||
|
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try viewContext.execute(deleteRequest)
|
||||||
|
} catch _ as NSError {
|
||||||
|
// TODO: handle the error
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
print("Saved to Core Data Store")
|
||||||
|
jsi.did = true
|
||||||
|
_rootIsActive.wrappedValue = false
|
||||||
|
} catch {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
let nsError = error as NSError
|
||||||
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
print(error)
|
||||||
|
_isSignInErrored.wrappedValue = true;
|
||||||
|
}
|
||||||
|
_isWorking.wrappedValue = false;
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Login")
|
||||||
|
Spacer()
|
||||||
|
ProgressView().isHidden(!isWorking)
|
||||||
|
}
|
||||||
|
}.disabled(isWorking || username.isEmpty || password.isEmpty)
|
||||||
|
.alert(isPresented: $isSignInErrored) {
|
||||||
|
Alert(title: Text("Error"), message: Text("Invalid credentials"), dismissButton: .default(Text("Back")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.navigationTitle("Connect to Server")
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.alert(isPresented: $serverSkippedAlert) {
|
||||||
|
Alert(title: Text("Error"), message: Text("Credentials have expired"), dismissButton: .default(Text("Sign in again")))
|
||||||
|
}
|
||||||
|
.onAppear(perform: start)
|
||||||
|
.transition(.move(edge:.bottom))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,337 @@
|
||||||
|
//
|
||||||
|
// ContentView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/29/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import KeychainSwift
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
|
||||||
|
class GlobalData: ObservableObject {
|
||||||
|
@Published var user: SignedInUser?
|
||||||
|
@Published var authToken: String = ""
|
||||||
|
@Published var server: Server?
|
||||||
|
@Published var authHeader: String = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
|
||||||
|
self.background(HostingWindowFinder(callback: callback))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HostingWindowFinder: UIViewRepresentable {
|
||||||
|
var callback: (UIWindow?) -> ()
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
DispatchQueue.main.async { [weak view] in
|
||||||
|
self.callback(view?.window)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIView, context: Context) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
|
||||||
|
typealias Value = Bool
|
||||||
|
|
||||||
|
static var defaultValue: Value = false
|
||||||
|
|
||||||
|
static func reduce(value: inout Value, nextValue: () -> Value) {
|
||||||
|
value = nextValue() || value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ViewPreferenceKey: PreferenceKey {
|
||||||
|
typealias Value = UIUserInterfaceStyle
|
||||||
|
|
||||||
|
static var defaultValue: UIUserInterfaceStyle = .unspecified
|
||||||
|
|
||||||
|
static func reduce(value: inout UIUserInterfaceStyle, nextValue: () -> UIUserInterfaceStyle) {
|
||||||
|
value = nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SupportedOrientationsPreferenceKey: PreferenceKey {
|
||||||
|
typealias Value = UIInterfaceOrientationMask
|
||||||
|
static var defaultValue: UIInterfaceOrientationMask = .allButUpsideDown
|
||||||
|
|
||||||
|
static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) {
|
||||||
|
// use the most restrictive set from the stack
|
||||||
|
value.formIntersection(nextValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
/// Navigate to a new view.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - view: View to navigate to.
|
||||||
|
/// - binding: Only navigates when this condition is `true`.
|
||||||
|
func navigate<NewView: View>(to view: NewView, when binding: Binding<Bool>) -> some View {
|
||||||
|
NavigationView {
|
||||||
|
ZStack {
|
||||||
|
self
|
||||||
|
.navigationBarTitle("")
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
|
||||||
|
NavigationLink(
|
||||||
|
destination: view
|
||||||
|
.navigationBarTitle("")
|
||||||
|
.navigationBarHidden(true),
|
||||||
|
isActive: binding
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
|
init<V: View>(wrappedView: V) {
|
||||||
|
let box = Box()
|
||||||
|
super.init(rootView: AnyView(wrappedView
|
||||||
|
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
|
||||||
|
box.value?._prefersHomeIndicatorAutoHidden = $0
|
||||||
|
}.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
|
||||||
|
box.value?._orientations = $0
|
||||||
|
}.onPreferenceChange(ViewPreferenceKey.self) {
|
||||||
|
box.value?._viewPreference = $0
|
||||||
|
}
|
||||||
|
))
|
||||||
|
box.value = self
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc required dynamic init?(coder aDecoder: NSCoder) {
|
||||||
|
super.init(coder: aDecoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Box {
|
||||||
|
weak var value: PreferenceUIHostingController?
|
||||||
|
init() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Prefers Home Indicator Auto Hidden
|
||||||
|
|
||||||
|
public var _prefersHomeIndicatorAutoHidden = false {
|
||||||
|
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
|
||||||
|
}
|
||||||
|
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||||
|
_prefersHomeIndicatorAutoHidden
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Lock orientation
|
||||||
|
|
||||||
|
public var _orientations: UIInterfaceOrientationMask = .allButUpsideDown {
|
||||||
|
didSet { UIViewController.attemptRotationToDeviceOrientation();
|
||||||
|
if(_orientations == .landscapeRight) {
|
||||||
|
let value = UIInterfaceOrientation.landscapeRight.rawValue;
|
||||||
|
UIDevice.current.setValue(value, forKey: "orientation")
|
||||||
|
} else {
|
||||||
|
let value = UIInterfaceOrientation.portrait.rawValue;
|
||||||
|
UIDevice.current.setValue(value, forKey: "orientation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
|
_orientations
|
||||||
|
}
|
||||||
|
|
||||||
|
public var _viewPreference: UIUserInterfaceStyle = .unspecified {
|
||||||
|
didSet {
|
||||||
|
overrideUserInterfaceStyle = _viewPreference
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
// Controls the application's preferred home indicator auto-hiding when this view is shown.
|
||||||
|
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View {
|
||||||
|
preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedOrientations(_ supportedOrientations: UIInterfaceOrientationMask) -> some View {
|
||||||
|
// When rendered, export the requested orientations upward to Root
|
||||||
|
preference(key: SupportedOrientationsPreferenceKey.self, value: supportedOrientations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideViewPreference(_ viewPreference: UIUserInterfaceStyle) -> some View {
|
||||||
|
// When rendered, export the requested orientations upward to Root
|
||||||
|
preference(key: ViewPreferenceKey.self, value: viewPreference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@StateObject private var globalData = GlobalData()
|
||||||
|
@EnvironmentObject var jsi: justSignedIn
|
||||||
|
|
||||||
|
@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>
|
||||||
|
|
||||||
|
@State private var needsToSelectServer = false;
|
||||||
|
@State private var isSignInErrored = 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 = "";
|
||||||
|
|
||||||
|
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||||
|
|
||||||
|
var isPortrait: Bool {
|
||||||
|
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func startup() {
|
||||||
|
_libraries.wrappedValue = []
|
||||||
|
_library_names.wrappedValue = [:]
|
||||||
|
_librariesShowRecentlyAdded.wrappedValue = []
|
||||||
|
if(servers.isEmpty) {
|
||||||
|
_isLoading.wrappedValue = false;
|
||||||
|
_needsToSelectServer.wrappedValue = true;
|
||||||
|
} else {
|
||||||
|
_isLoading.wrappedValue = true;
|
||||||
|
let savedUser = savedUsers[0];
|
||||||
|
|
||||||
|
let keychain = KeychainSwift();
|
||||||
|
if(keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil) {
|
||||||
|
_globalData.wrappedValue.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? ""
|
||||||
|
_globalData.wrappedValue.server = servers[0]
|
||||||
|
_globalData.wrappedValue.user = savedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String;
|
||||||
|
globalData.authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(UIDevice.current.name)\", DeviceId=\"\(globalData.user?.device_uuid ?? "")\", Version=\"\(appVersion ?? "0.0.1")\", Token=\"\(globalData.authToken)\"";
|
||||||
|
let request = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/Me")
|
||||||
|
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 resp):
|
||||||
|
do {
|
||||||
|
let json = try JSON(data: resp.body)
|
||||||
|
_libraries.wrappedValue = json["Configuration"]["OrderedViews"].arrayObject as? [String] ?? [];
|
||||||
|
let array2 = json["Configuration"]["LatestItemsExcludes"].arrayObject as? [String] ?? []
|
||||||
|
_librariesShowRecentlyAdded.wrappedValue = _libraries.wrappedValue.filter { element in
|
||||||
|
return !array2.contains(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
let request2 = RestRequest(method: .get, url: (globalData.server?.baseURI ?? "") + "/Users/\(globalData.user?.user_id ?? "")/Views")
|
||||||
|
request2.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
|
request2.contentType = "application/json"
|
||||||
|
request2.acceptType = "application/json"
|
||||||
|
|
||||||
|
request2.responseData() { (result2: Result<RestResponse<Data>, RestError>) in
|
||||||
|
switch result2 {
|
||||||
|
case .success( let resp):
|
||||||
|
do {
|
||||||
|
let json2 = try JSON(data: resp.body)
|
||||||
|
for (_,item2):(String, JSON) in json2["Items"] {
|
||||||
|
_library_names.wrappedValue[item2["Id"].string ?? ""] = item2["Name"].string ?? ""
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failure( _):
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_isLoading.wrappedValue = false;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failure( _):
|
||||||
|
_isLoading.wrappedValue = false;
|
||||||
|
_isSignInErrored.wrappedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LoadingView(isShowing: $isLoading) {
|
||||||
|
TabView(selection: $tabSelection) {
|
||||||
|
NavigationView() {
|
||||||
|
VStack {
|
||||||
|
NavigationLink(destination: ConnectToServerView(isActive: $needsToSelectServer), isActive: $needsToSelectServer) {
|
||||||
|
EmptyView()
|
||||||
|
}.isDetailLink(false)
|
||||||
|
NavigationLink(destination: ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server, reauth_deviceId: globalData.user?.device_uuid ?? "", isActive: $isSignInErrored), isActive: $isSignInErrored) {
|
||||||
|
EmptyView()
|
||||||
|
}.isDetailLink(false)
|
||||||
|
if(!needsToSelectServer && !isSignInErrored) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
ScrollView() {
|
||||||
|
Spacer().frame(height: self.isPortrait ? 0 : 15)
|
||||||
|
ContinueWatchingView()
|
||||||
|
NextUpView().padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
ForEach(librariesShowRecentlyAdded, id: \.self) { library_id in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack() {
|
||||||
|
Text("Latest \(library_names[library_id] ?? "")").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
|
||||||
|
Spacer()
|
||||||
|
NavigationLink(destination: LibraryView(prefill: library_id, names: library_names, libraries: libraries, filter: "&SortBy=DateCreated&SortOrder=Descending")) {
|
||||||
|
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||||
|
}
|
||||||
|
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||||
|
LatestMediaView(library: library_id)
|
||||||
|
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
}
|
||||||
|
Spacer().frame(height: 7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Home")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
print("Settings tapped!")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "gear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabItem({
|
||||||
|
Text("Home")
|
||||||
|
Image(systemName: "house")
|
||||||
|
})
|
||||||
|
.tag("Home")
|
||||||
|
NavigationView() {
|
||||||
|
LibraryView(prefill: "", names: library_names, libraries: libraries)
|
||||||
|
.navigationTitle("Library")
|
||||||
|
}
|
||||||
|
.tabItem({
|
||||||
|
Text("All Media")
|
||||||
|
Image(systemName: "folder")
|
||||||
|
})
|
||||||
|
.tag("All Media")
|
||||||
|
}
|
||||||
|
}.environmentObject(globalData)
|
||||||
|
.onAppear(perform: startup)
|
||||||
|
.navigationViewStyle(StackNavigationViewStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
//
|
||||||
|
// NextUpView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
|
||||||
|
struct ContinueWatchingView: 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 = true;
|
||||||
|
|
||||||
|
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
|
||||||
|
let itemObj = ResumeItem()
|
||||||
|
if(item["PrimaryImageAspectRatio"].double! < 1.0) {
|
||||||
|
//portrait; use backdrop instead
|
||||||
|
itemObj.Image = item["BackdropImageTags"][0].string ?? ""
|
||||||
|
itemObj.ImageType = "Backdrop"
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack() {
|
||||||
|
if(isLoading == false) {
|
||||||
|
Spacer().frame(width:16)
|
||||||
|
ForEach(resumeItems, id: \.Id) { item in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Spacer().frame(height: 10)
|
||||||
|
if(item.Type == "Episode") {
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=560&fillHeight=315&quality=90&tag=\(item.Image)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.scaledToFit()
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width: 320, height: 180)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
Text("S\(String(item.ParentIndexNumber ?? 0)):E\(String(item.IndexNumber ?? 0)) - \(item.Name)")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(6)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}.background(Color.black)
|
||||||
|
.opacity(0.8)
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
.padding(6), alignment: .topTrailing
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .circular)
|
||||||
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255).opacity(0.4))
|
||||||
|
.frame(width: CGFloat((item.ItemProgress/100)*320), height: 180)
|
||||||
|
.padding(0), alignment: .bottomLeading
|
||||||
|
)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
} else {
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=560&fillHeight=315&quality=90&tag=\(item.Image)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.scaledToFit()
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width: 320, height: 180)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .circular)
|
||||||
|
.fill(Color(red: 172/255, green: 92/255, blue: 195/255).opacity(0.4))
|
||||||
|
.frame(width: CGFloat((item.ItemProgress/100)*320), height: 180)
|
||||||
|
.padding(0), alignment: .bottomLeading
|
||||||
|
)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
}
|
||||||
|
Text("\(item.Type == "Episode" ? item.SeriesName ?? "" : item.Name)")
|
||||||
|
.font(.callout)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer().frame(height: 5)
|
||||||
|
}.padding(.trailing, 5)
|
||||||
|
}
|
||||||
|
Spacer().frame(width:14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 200)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
}.onAppear(perform: onAppear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContinueWatchingView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ContinueWatchingView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Jellyfin iOS</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UIStatusBarHidden</key>
|
||||||
|
<true/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// ItemView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/10/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import Introspect
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
|
||||||
|
struct ItemView: View {
|
||||||
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
@State private var isLoading: Bool = false;
|
||||||
|
var item: ResumeItem;
|
||||||
|
|
||||||
|
init(item: ResumeItem) {
|
||||||
|
self.item = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if(item.Type == "Movie") {
|
||||||
|
MovieItemView(item: self.item)
|
||||||
|
} else {
|
||||||
|
Text("Type: \(item.Type) not implemented yet :(")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// JellyApiTypings.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResumeItem: ObservableObject {
|
||||||
|
@Published var Name: String = "";
|
||||||
|
@Published var Id: String = "";
|
||||||
|
@Published var IndexNumber: Int? = nil;
|
||||||
|
@Published var ParentIndexNumber: Int? = nil;
|
||||||
|
@Published var Image: String = "";
|
||||||
|
@Published var ImageType: String = "";
|
||||||
|
@Published var BlurHash: String = "";
|
||||||
|
@Published var `Type`: String = "";
|
||||||
|
@Published var SeasonId: String? = nil;
|
||||||
|
@Published var SeriesId: String? = nil;
|
||||||
|
@Published var SeriesName: String? = nil;
|
||||||
|
@Published var ItemProgress: Double = 0;
|
||||||
|
@Published var ItemBadge: Int? = 0;
|
||||||
|
@Published var ProductionYear: Int = 1999;
|
||||||
|
@Published var Watched: Bool = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServerMeResponse: Codable {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
//
|
||||||
|
// JellyfinHLSResourceLoaderDelegate.swift
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class JellyfinHLSResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
|
||||||
|
|
||||||
|
typealias Completion = (URL?) -> Void
|
||||||
|
|
||||||
|
private static let SchemeSuffix = "icpt"
|
||||||
|
|
||||||
|
// MARK: - Properties
|
||||||
|
// MARK: Public
|
||||||
|
|
||||||
|
var completion: Completion?
|
||||||
|
|
||||||
|
lazy var streamingAssetURL: URL = {
|
||||||
|
guard var components = URLComponents(url: self.url, resolvingAgainstBaseURL: false) else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
components.scheme = (components.scheme ?? "") + JellyfinHLSResourceLoaderDelegate.SchemeSuffix
|
||||||
|
guard let retURL = components.url else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
return retURL
|
||||||
|
}()
|
||||||
|
|
||||||
|
// MARK: Private
|
||||||
|
|
||||||
|
private let url: URL
|
||||||
|
private var infoResponse: URLResponse?
|
||||||
|
private var urlSession: URLSession?
|
||||||
|
private lazy var mediaData = Data()
|
||||||
|
private var loadingRequests = [AVAssetResourceLoadingRequest]()
|
||||||
|
|
||||||
|
// MARK: - Life Cycle Methods
|
||||||
|
|
||||||
|
init(withURL url: URL) {
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
func invalidate() {
|
||||||
|
self.loadingRequests.forEach { $0.finishLoading() }
|
||||||
|
self.invalidateURLSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AVAssetResourceLoaderDelegate
|
||||||
|
|
||||||
|
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
|
||||||
|
if self.urlSession == nil {
|
||||||
|
self.urlSession = self.createURLSession()
|
||||||
|
let task = self.urlSession!.dataTask(with: self.url)
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadingRequests.append(loadingRequest)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
|
||||||
|
if let index = self.loadingRequests.firstIndex(of: loadingRequest) {
|
||||||
|
self.loadingRequests.remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - URLSessionDataDelegate
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||||
|
self.infoResponse = response
|
||||||
|
self.processRequests()
|
||||||
|
|
||||||
|
completionHandler(.allow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||||
|
self.mediaData.append(data)
|
||||||
|
self.processRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
private func createURLSession() -> URLSession {
|
||||||
|
let config = URLSessionConfiguration.default
|
||||||
|
let operationQueue = OperationQueue()
|
||||||
|
operationQueue.maxConcurrentOperationCount = 1
|
||||||
|
return URLSession(configuration: config, delegate: self, delegateQueue: operationQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func invalidateURLSession() {
|
||||||
|
self.urlSession?.invalidateAndCancel()
|
||||||
|
self.urlSession = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isInfo(request: AVAssetResourceLoadingRequest) -> Bool {
|
||||||
|
return request.contentInformationRequest != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fillInfoRequest(request: inout AVAssetResourceLoadingRequest, response: URLResponse) {
|
||||||
|
request.contentInformationRequest?.isByteRangeAccessSupported = true
|
||||||
|
request.contentInformationRequest?.contentType = response.mimeType
|
||||||
|
request.contentInformationRequest?.contentLength = response.expectedContentLength
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processRequests() {
|
||||||
|
var finishedRequests = Set<AVAssetResourceLoadingRequest>()
|
||||||
|
self.loadingRequests.forEach {
|
||||||
|
var request = $0
|
||||||
|
if self.isInfo(request: request), let response = self.infoResponse {
|
||||||
|
self.fillInfoRequest(request: &request, response: response)
|
||||||
|
}
|
||||||
|
if let dataRequest = request.dataRequest, self.checkAndRespond(forRequest: dataRequest) {
|
||||||
|
finishedRequests.insert(request)
|
||||||
|
request.finishLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.loadingRequests = self.loadingRequests.filter { !finishedRequests.contains($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkAndRespond(forRequest dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
|
||||||
|
let downloadedData = self.mediaData
|
||||||
|
let downloadedDataLength = Int64(downloadedData.count)
|
||||||
|
|
||||||
|
let requestRequestedOffset = dataRequest.requestedOffset
|
||||||
|
let requestRequestedLength = Int64(dataRequest.requestedLength)
|
||||||
|
let requestCurrentOffset = dataRequest.currentOffset
|
||||||
|
|
||||||
|
if downloadedDataLength < requestCurrentOffset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let downloadedUnreadDataLength = downloadedDataLength - requestCurrentOffset
|
||||||
|
let requestUnreadDataLength = requestRequestedOffset + requestRequestedLength - requestCurrentOffset
|
||||||
|
let respondDataLength = min(requestUnreadDataLength, downloadedUnreadDataLength)
|
||||||
|
|
||||||
|
dataRequest.respond(with: downloadedData.subdata(in: Range(NSMakeRange(Int(requestCurrentOffset), Int(respondDataLength)))!))
|
||||||
|
|
||||||
|
let requestEndOffset = requestRequestedOffset + requestRequestedLength
|
||||||
|
|
||||||
|
return requestCurrentOffset >= requestEndOffset
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
//
|
||||||
|
// JellyfinPlayerApp.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/29/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class justSignedIn: ObservableObject {
|
||||||
|
@Published var did: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct JellyfinPlayerApp: App {
|
||||||
|
let persistenceController = PersistenceController.shared
|
||||||
|
@StateObject private var jsi = justSignedIn()
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
if(!jsi.did) {
|
||||||
|
ContentView()
|
||||||
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
|
.environmentObject(jsi)
|
||||||
|
.withHostingWindow() { window in
|
||||||
|
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
|
.environmentObject(jsi))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Please wait...")
|
||||||
|
.onAppear(perform: {
|
||||||
|
print("Signing in")
|
||||||
|
sleep(1)
|
||||||
|
jsi.did = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
//
|
||||||
|
// LatestMediaView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
|
||||||
|
struct LatestMediaView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
|
@State var resumeItems: [ResumeItem] = []
|
||||||
|
private var library_id: String = "";
|
||||||
|
@State private var viewDidLoad: Int = 0;
|
||||||
|
|
||||||
|
init(library: String) {
|
||||||
|
library_id = library;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
library_id = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
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/Latest?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"
|
||||||
|
|
||||||
|
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
|
||||||
|
let 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack() {
|
||||||
|
Spacer().frame(width:18)
|
||||||
|
ForEach(resumeItems, id: \.Id) { item in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if(item.Type == "Series") {
|
||||||
|
Spacer().frame(height:10)
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.scaledToFit()
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width: 100, height: 150)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
Text("\(String(item.ItemBadge ?? 0))")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(3)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}.background(Color.black)
|
||||||
|
.opacity(0.8)
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
.padding(3), alignment: .topTrailing
|
||||||
|
).shadow(radius: 6)
|
||||||
|
} else {
|
||||||
|
Spacer().frame(height:10)
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.scaledToFit()
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width: 100, height: 150)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.shadow(radius: 6)
|
||||||
|
}
|
||||||
|
Text(item.Name)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer().frame(height:5)
|
||||||
|
}.frame(width: 100)
|
||||||
|
Spacer().frame(width: 14)
|
||||||
|
}
|
||||||
|
Spacer().frame(width:18)
|
||||||
|
}
|
||||||
|
}.onAppear(perform: onAppear).padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LatestMediaView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LatestMediaView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
//
|
||||||
|
// LibraryFilterView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/2/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyJSON
|
||||||
|
import SwiftyRequest
|
||||||
|
|
||||||
|
struct Genre: Hashable, Identifiable {
|
||||||
|
var name: String
|
||||||
|
var id: String { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct LibraryFilterView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
|
||||||
|
@State var library: String;
|
||||||
|
@Binding var output: String;
|
||||||
|
@State private var isLoading: Bool = true;
|
||||||
|
@State private var onlyUnplayed: Bool = false;
|
||||||
|
@State private var allGenres: [Genre] = [];
|
||||||
|
@State private var selectedGenres: Set<Genre> = [];
|
||||||
|
|
||||||
|
@State private var allRatings: [Genre] = [];
|
||||||
|
@State private var selectedRatings: Set<Genre> = [];
|
||||||
|
@State private var sortBySelection: String = "SortName";
|
||||||
|
@State private var sortOrder: String = "Descending";
|
||||||
|
@State private var viewDidLoad: Bool = false;
|
||||||
|
@Binding var close: Bool;
|
||||||
|
|
||||||
|
func onAppear() {
|
||||||
|
if(_output.wrappedValue.contains("&Filters=IsUnplayed")) {
|
||||||
|
_onlyUnplayed.wrappedValue = true;
|
||||||
|
}
|
||||||
|
if(_output.wrappedValue.contains("&Genres=")) {
|
||||||
|
let genreString = _output.wrappedValue.components(separatedBy: "&Genres=")[1].components(separatedBy: "&")[0];
|
||||||
|
for genre in genreString.components(separatedBy: "%7C") {
|
||||||
|
_selectedGenres.wrappedValue.insert(Genre(name: genre.removingPercentEncoding ?? ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(_output.wrappedValue.contains("&OfficialRatings=")) {
|
||||||
|
let ratingString = _output.wrappedValue.components(separatedBy: "&OfficialRatings=")[1].components(separatedBy: "&")[0];
|
||||||
|
for rating in ratingString.components(separatedBy: "%7C") {
|
||||||
|
_selectedRatings.wrappedValue.insert(Genre(name: rating.removingPercentEncoding ?? ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sortBy = _output.wrappedValue.components(separatedBy: "&SortBy=")[1].components(separatedBy: "&")[0];
|
||||||
|
_sortBySelection.wrappedValue = sortBy;
|
||||||
|
let sortOrder = _output.wrappedValue.components(separatedBy: "&SortOrder=")[1].components(separatedBy: "&")[0];
|
||||||
|
_sortOrder.wrappedValue = sortOrder;
|
||||||
|
|
||||||
|
recalculateFilters()
|
||||||
|
if(_viewDidLoad.wrappedValue == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_viewDidLoad.wrappedValue = true;
|
||||||
|
_allGenres.wrappedValue = []
|
||||||
|
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"
|
||||||
|
|
||||||
|
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)
|
||||||
|
let arr = json["Genres"].arrayObject as? [String] ?? []
|
||||||
|
for genreName in arr {
|
||||||
|
//print(genreName)
|
||||||
|
let genre = Genre(name: genreName)
|
||||||
|
allGenres.append(genre)
|
||||||
|
}
|
||||||
|
|
||||||
|
let arr2 = json["OfficialRatings"].arrayObject as? [String] ?? []
|
||||||
|
for genreName in arr2 {
|
||||||
|
//print(genreName)
|
||||||
|
let genre = Genre(name: genreName)
|
||||||
|
allRatings.append(genre)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recalculateFilters() {
|
||||||
|
output = "";
|
||||||
|
if(_onlyUnplayed.wrappedValue) {
|
||||||
|
output = "&Filters=IsUnPlayed";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectedGenres.count != 0) {
|
||||||
|
output += "&Genres="
|
||||||
|
var genres: [String] = []
|
||||||
|
for genre in selectedGenres {
|
||||||
|
genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||||
|
}
|
||||||
|
output += genres.joined(separator: "%7C")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(selectedRatings.count != 0) {
|
||||||
|
output += "&OfficialRatings="
|
||||||
|
var genres: [String] = []
|
||||||
|
for genre in selectedRatings {
|
||||||
|
genres.append(genre.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")
|
||||||
|
}
|
||||||
|
output += genres.joined(separator: "%7C")
|
||||||
|
}
|
||||||
|
output += "&SortBy=\(sortBySelection)&SortOrder=\(sortOrder)"
|
||||||
|
//print(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView() {
|
||||||
|
LoadingView(isShowing: $isLoading) {
|
||||||
|
Form {
|
||||||
|
Toggle("Only show unplayed items", isOn: $onlyUnplayed)
|
||||||
|
.onChange(of: onlyUnplayed) { tag in
|
||||||
|
recalculateFilters()
|
||||||
|
}
|
||||||
|
MultiSelector(
|
||||||
|
label: "Genres",
|
||||||
|
options: allGenres,
|
||||||
|
optionToString: { $0.name },
|
||||||
|
selected: $selectedGenres
|
||||||
|
).onChange(of: selectedGenres) { tag in
|
||||||
|
recalculateFilters()
|
||||||
|
}
|
||||||
|
MultiSelector(
|
||||||
|
label: "Parental Ratings",
|
||||||
|
options: allRatings,
|
||||||
|
optionToString: { $0.name },
|
||||||
|
selected: $selectedRatings
|
||||||
|
).onChange(of: selectedRatings) { tag in
|
||||||
|
recalculateFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Sort settings")) {
|
||||||
|
Picker("Sort by", selection: $sortBySelection) {
|
||||||
|
Text("Name").tag("SortName")
|
||||||
|
Text("Date Added").tag("DateCreated")
|
||||||
|
Text("Date Played").tag("DatePlayed")
|
||||||
|
Text("Date Released").tag("PremiereDate")
|
||||||
|
Text("Runtime").tag("Runtime")
|
||||||
|
}.onChange(of: sortBySelection) { tag in
|
||||||
|
recalculateFilters()
|
||||||
|
}
|
||||||
|
Picker("Sort order", selection: $sortOrder) {
|
||||||
|
Text("Ascending").tag("Ascending")
|
||||||
|
Text("Descending").tag("Descending")
|
||||||
|
}.onChange(of: sortOrder) { tag in
|
||||||
|
recalculateFilters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onAppear(perform: onAppear)
|
||||||
|
.navigationBarTitle("Filters", displayMode: .inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
close = false
|
||||||
|
} label: {
|
||||||
|
HStack() {
|
||||||
|
Text("Back").font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
//
|
||||||
|
// LibraryView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/1/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import ExyteGrid
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
|
||||||
|
struct LibraryView: View {
|
||||||
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
@State private var prefill_id: String = "";
|
||||||
|
@State private var library_names: [String: String] = [:]
|
||||||
|
@State private var library_ids: [String] = []
|
||||||
|
@State private var selected_library_id: String = "";
|
||||||
|
@State private var isLoading: Bool = true;
|
||||||
|
|
||||||
|
@State private var startIndex: Int = 0;
|
||||||
|
@State private var endIndex: Int = 60;
|
||||||
|
@State private var totalItems: Int = 0;
|
||||||
|
|
||||||
|
@State private var viewDidLoad: Bool = false;
|
||||||
|
@State private var filterString: String = "&SortBy=SortName&SortOrder=Descending";
|
||||||
|
@State private var showFiltersPopover: Bool = false
|
||||||
|
|
||||||
|
|
||||||
|
var gridItems: [GridItem] = [GridItem(.adaptive(minimum: 150, maximum: 400))]
|
||||||
|
|
||||||
|
init(prefill: String?, names: [String: String], libraries: [String]) {
|
||||||
|
_prefill_id = State(wrappedValue: prefill ?? "")
|
||||||
|
_library_names = State(wrappedValue: names)
|
||||||
|
_library_ids = State(wrappedValue: libraries)
|
||||||
|
//print("prefilling w/ \(prefill ?? "") aka \(names[prefill ?? ""] ?? "nil")")
|
||||||
|
}
|
||||||
|
|
||||||
|
init(prefill: String?, names: [String: String], libraries: [String], filter: String) {
|
||||||
|
_prefill_id = State(wrappedValue: prefill ?? "")
|
||||||
|
_library_names = State(wrappedValue: names)
|
||||||
|
_library_ids = State(wrappedValue: libraries)
|
||||||
|
_filterString = State(wrappedValue: filter);
|
||||||
|
//print("prefilling w/ \(prefill ?? "") aka \(names[prefill ?? ""] ?? "nil")")
|
||||||
|
}
|
||||||
|
|
||||||
|
@State var items: [ResumeItem] = []
|
||||||
|
|
||||||
|
func listOnAppear() {
|
||||||
|
if(_viewDidLoad.wrappedValue == false) {
|
||||||
|
//print("running VDL")
|
||||||
|
_viewDidLoad.wrappedValue = true;
|
||||||
|
_library_ids.wrappedValue.append("favorites")
|
||||||
|
_library_names.wrappedValue["favorites"] = "Favorites"
|
||||||
|
|
||||||
|
_library_ids.wrappedValue.append("genres")
|
||||||
|
_library_names.wrappedValue["genres"] = "Genres"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadItems() {
|
||||||
|
_isLoading.wrappedValue = true;
|
||||||
|
let url = "/Users/\(globalData.user?.user_id ?? "")/Items?Limit=\(endIndex)&StartIndex=\(startIndex)&Recursive=true&Fields=PrimaryImageAspectRatio%2CBasicSyncInfo&ImageTypeLimit=1&EnableImageTypes=Primary%2CBackdrop%2CThumb%2CBanner&IncludeItemTypes=Movie,Series\(selected_library_id == "favorites" ? "&Filters=IsFavorite" : "&ParentId=" + selected_library_id)\(filterString)"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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)
|
||||||
|
_totalItems.wrappedValue = json["TotalRecordCount"].int ?? 0;
|
||||||
|
for (_,item):(String, JSON) in json["Items"] {
|
||||||
|
// Do something you want
|
||||||
|
let itemObj = ResumeItem()
|
||||||
|
itemObj.Type = item["Type"].string ?? ""
|
||||||
|
if(itemObj.Type == "Series") {
|
||||||
|
itemObj.ItemBadge = item["UserData"]["UnplayedItemCount"].int ?? 0
|
||||||
|
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 = nil
|
||||||
|
itemObj.Id = item["Id"].string ?? ""
|
||||||
|
itemObj.ParentIndexNumber = nil
|
||||||
|
itemObj.SeasonId = nil
|
||||||
|
itemObj.SeriesId = nil
|
||||||
|
itemObj.SeriesName = nil
|
||||||
|
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||||
|
} else {
|
||||||
|
itemObj.ProductionYear = item["ProductionYear"].int ?? 0
|
||||||
|
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
|
||||||
|
|
||||||
|
_items.wrappedValue.append(itemObj)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_isLoading.wrappedValue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onAppear() {
|
||||||
|
if(_prefill_id.wrappedValue != "") {
|
||||||
|
_selected_library_id.wrappedValue = _prefill_id.wrappedValue;
|
||||||
|
}
|
||||||
|
if(_items.wrappedValue.count == 0) {
|
||||||
|
loadItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
|
||||||
|
|
||||||
|
var isPortrait: Bool {
|
||||||
|
let result = verticalSizeClass == .regular && horizontalSizeClass == .compact
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracks: [GridTrack] {
|
||||||
|
self.isPortrait ? 3 : 6
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if(prefill_id != "") {
|
||||||
|
LoadingView(isShowing: $isLoading) {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Grid(tracks: self.tracks, spacing: GridSpacing(horizontal: 0, vertical: 20)) {
|
||||||
|
ForEach(items, id: \.Id) { item in
|
||||||
|
NavigationLink(destination: ItemView(item: item )) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if(item.Type == "Movie") {
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||||
|
.resizable()
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.frame(width: 100, height: 150)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width:100, height: 150)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
} else {
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)"))
|
||||||
|
.resizable()
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.frame(width: 100, height: 150)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width:100, height: 150)
|
||||||
|
.cornerRadius(10).overlay(
|
||||||
|
ZStack {
|
||||||
|
Text("\(String(item.ItemBadge ?? 0))")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(3)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}.background(Color.black)
|
||||||
|
.opacity(0.8)
|
||||||
|
.cornerRadius(10.0)
|
||||||
|
.padding(3), alignment: .topTrailing
|
||||||
|
)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
}
|
||||||
|
Text(item.Name)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(String(item.ProductionYear))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}.frame(width: 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(startIndex + endIndex < totalItems) {
|
||||||
|
HStack() {
|
||||||
|
Spacer()
|
||||||
|
Button() {
|
||||||
|
startIndex += endIndex;
|
||||||
|
loadItems()
|
||||||
|
} label: {
|
||||||
|
HStack() {
|
||||||
|
Text("Load more").font(.callout)
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}.gridSpan(column: self.isPortrait ? 3 : 6)
|
||||||
|
}
|
||||||
|
Spacer().frame(height: 2).gridSpan(column: self.isPortrait ? 3 : 6)
|
||||||
|
}.gridContentMode(.scroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overrideViewPreference(.unspecified)
|
||||||
|
.onAppear(perform: onAppear)
|
||||||
|
.onChange(of: filterString) { tag in
|
||||||
|
isLoading = true;
|
||||||
|
startIndex = 0;
|
||||||
|
totalItems = 0;
|
||||||
|
items = [];
|
||||||
|
loadItems();
|
||||||
|
}
|
||||||
|
.navigationTitle(library_names[prefill_id] ?? "Library")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
showFiltersPopover = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "line.horizontal.3.decrease")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.popover( isPresented: self.$showFiltersPopover, arrowEdge: .bottom) { LibraryFilterView(library: selected_library_id, output: $filterString, close: $showFiltersPopover) }
|
||||||
|
} else {
|
||||||
|
List(library_ids, id:\.self) { id in
|
||||||
|
if(id != "genres") {
|
||||||
|
NavigationLink(destination: LibraryView(prefill: id, names: library_names, libraries: library_ids)) {
|
||||||
|
Text(library_names[id] ?? "").foregroundColor(Color.primary)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NavigationLink(destination: LibraryView(prefill: id, names: library_names, libraries: library_ids)) {
|
||||||
|
Text(library_names[id] ?? "").foregroundColor(Color.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onAppear(perform: listOnAppear).overrideViewPreference(.unspecified).navigationTitle("All Media")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
print("Search tapped!")
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LoadingView<Content>: View where Content: View {
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
@Binding var isShowing: Bool // should the modal be visible?
|
||||||
|
var content: () -> Content
|
||||||
|
var text: String? // the text to display under the ProgressView - defaults to "Loading..."
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack(alignment: .center) {
|
||||||
|
// the content to display - if the modal is showing, we'll blur it
|
||||||
|
content()
|
||||||
|
.disabled(isShowing)
|
||||||
|
.blur(radius: isShowing ? 2 : 0)
|
||||||
|
|
||||||
|
// all contents inside here will only be shown when isShowing is true
|
||||||
|
if isShowing {
|
||||||
|
// this Rectangle is a semi-transparent black overlay
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.black).opacity(isShowing ? 0.6 : 0)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
|
||||||
|
// the magic bit - our ProgressView just displays an activity
|
||||||
|
// indicator, with some text underneath showing what we are doing
|
||||||
|
HStack() {
|
||||||
|
ProgressView()
|
||||||
|
Text(text ?? "Loading").fontWeight(.semibold).font(.callout).offset(x: 60)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 10))
|
||||||
|
.frame(width: 250)
|
||||||
|
.background(colorScheme == .dark ? Color(UIColor.systemGray6) : Color.white)
|
||||||
|
.foregroundColor(Color.primary)
|
||||||
|
.cornerRadius(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>_XCCurrentVersionName</key>
|
||||||
|
<string>JellyfinPlayer.xcdatamodel</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?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="">
|
||||||
|
<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"/>
|
||||||
|
</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"/>
|
||||||
|
</entity>
|
||||||
|
<elements>
|
||||||
|
<element name="Server" positionX="-63" positionY="-9" width="128" height="74"/>
|
||||||
|
<element name="SignedInUser" positionX="-63" positionY="9" width="128" height="74"/>
|
||||||
|
</elements>
|
||||||
|
</model>
|
|
@ -0,0 +1,266 @@
|
||||||
|
//
|
||||||
|
// MovieItemView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/13/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import Introspect
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
|
||||||
|
class DetailItem: ObservableObject {
|
||||||
|
@Published var Name: String = "";
|
||||||
|
@Published var Id: String = "";
|
||||||
|
@Published var IndexNumber: Int? = nil;
|
||||||
|
@Published var ParentIndexNumber: Int? = nil;
|
||||||
|
@Published var Poster: String = "";
|
||||||
|
@Published var Backdrop: String = ""
|
||||||
|
@Published var PosterBlurHash: String = "";
|
||||||
|
@Published var BackdropBlurHash: String = "";
|
||||||
|
@Published var `Type`: String = "";
|
||||||
|
@Published var SeasonId: String? = nil;
|
||||||
|
@Published var SeriesId: String? = nil;
|
||||||
|
@Published var SeriesName: String? = nil;
|
||||||
|
@Published var ItemProgress: Double = 0;
|
||||||
|
@Published var ItemBadge: Int? = 0;
|
||||||
|
@Published var ProductionYear: Int = 1999;
|
||||||
|
@Published var Runtime: String = "";
|
||||||
|
@Published var RuntimeTicks: Int = 0;
|
||||||
|
@Published var Cast: [CastMember] = [];
|
||||||
|
@Published var OfficialRating: String = "";
|
||||||
|
@Published var Progress: Double = 0;
|
||||||
|
@Published var Watched: Bool = false;
|
||||||
|
@Published var Overview: String = "";
|
||||||
|
@Published var Tagline: String = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
class CastMember: ObservableObject {
|
||||||
|
@Published var Name: String = "";
|
||||||
|
@Published var Role: String = "";
|
||||||
|
@Published var ImageBlurHash: String = "";
|
||||||
|
@Published var Id: String = "";
|
||||||
|
@Published var Image: URL = URL(string: "https://example.com")!;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MovieItemView: View {
|
||||||
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
@State private var isLoading: Bool = true;
|
||||||
|
var item: ResumeItem;
|
||||||
|
var fullItem: DetailItem;
|
||||||
|
@State private var playing: Bool = false;
|
||||||
|
@State private var vc: PreferenceUIHostingController? = nil;
|
||||||
|
@State private var progressString: String = "";
|
||||||
|
@State private var watched: Bool = false;
|
||||||
|
@State private var favorite: Bool = false;
|
||||||
|
|
||||||
|
init(item: ResumeItem) {
|
||||||
|
self.item = item;
|
||||||
|
self.fullItem = DetailItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
func lockOrientations() {
|
||||||
|
if(_vc.wrappedValue != nil) {
|
||||||
|
_vc.wrappedValue?._prefersHomeIndicatorAutoHidden = true;
|
||||||
|
_vc.wrappedValue?._orientations = .landscapeRight;
|
||||||
|
_vc.wrappedValue?._viewPreference = .dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadData() {
|
||||||
|
if(_vc.wrappedValue != nil) {
|
||||||
|
_vc.wrappedValue?._prefersHomeIndicatorAutoHidden = false;
|
||||||
|
_vc.wrappedValue?._orientations = .allButUpsideDown;
|
||||||
|
_vc.wrappedValue?._viewPreference = .unspecified;
|
||||||
|
}
|
||||||
|
let url = "/Users/\(globalData.user?.user_id ?? "")/Items/\(item.Id)"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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)
|
||||||
|
fullItem.ProductionYear = json["ProductionYear"].int ?? 0
|
||||||
|
fullItem.Poster = json["ImageTags"]["Primary"].string ?? ""
|
||||||
|
fullItem.PosterBlurHash = json["ImageBlurHashes"]["Primary"][fullItem.Poster].string ?? ""
|
||||||
|
fullItem.Backdrop = json["BackdropImageTags"][0].string ?? ""
|
||||||
|
fullItem.BackdropBlurHash = json["ImageBlurHashes"]["Backdrop"][fullItem.Backdrop].string ?? ""
|
||||||
|
fullItem.Name = json["Name"].string ?? ""
|
||||||
|
fullItem.Type = json["Type"].string ?? ""
|
||||||
|
fullItem.IndexNumber = json["IndexNumber"].int ?? nil
|
||||||
|
fullItem.Id = json["Id"].string ?? ""
|
||||||
|
fullItem.ParentIndexNumber = json["ParentIndexNumber"].int ?? nil
|
||||||
|
fullItem.SeasonId = json["SeasonId"].string ?? nil
|
||||||
|
fullItem.SeriesId = json["SeriesId"].string ?? nil
|
||||||
|
fullItem.Overview = json["Overview"].string ?? ""
|
||||||
|
fullItem.Tagline = json["Taglines"][0].string ?? ""
|
||||||
|
fullItem.SeriesName = json["SeriesName"].string ?? nil
|
||||||
|
fullItem.Progress = Double(json["UserData"]["PlaybackPositionTicks"].int ?? 0)
|
||||||
|
fullItem.OfficialRating = json["OfficialRating"].string ?? "PG-13"
|
||||||
|
fullItem.Watched = json["UserData"]["Played"].bool ?? false;
|
||||||
|
_watched.wrappedValue = fullItem.Watched
|
||||||
|
_favorite.wrappedValue = json["UserData"]["IsFavorite"].bool ?? false;
|
||||||
|
|
||||||
|
//Process runtime
|
||||||
|
let seconds: Int = ((json["RunTimeTicks"].int ?? 0)/10000000)
|
||||||
|
fullItem.RuntimeTicks = json["RunTimeTicks"].int ?? 0;
|
||||||
|
let hours = (seconds/3600)
|
||||||
|
let minutes = ((seconds - (hours * 3600))/60)
|
||||||
|
if(hours != 0) {
|
||||||
|
fullItem.Runtime = "\(hours):\(String(minutes).leftPad(toWidth: 2, withString: "0"))"
|
||||||
|
} else {
|
||||||
|
fullItem.Runtime = "\(String(minutes).leftPad(toWidth: 2, withString: "0"))m"
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fullItem.Progress != 0) {
|
||||||
|
let remainingSecs = (Double(json["RunTimeTicks"].int ?? 0) - fullItem.Progress)/10000000
|
||||||
|
let proghours = Int(remainingSecs/3600)
|
||||||
|
let progminutes = Int((Int(remainingSecs) - (proghours * 3600))/60)
|
||||||
|
if(proghours != 0) {
|
||||||
|
_progressString.wrappedValue = "\(proghours):\(String(progminutes).leftPad(toWidth: 2, withString: "0"))"
|
||||||
|
} else {
|
||||||
|
_progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_isLoading.wrappedValue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if(playing) {
|
||||||
|
PlayerDemo(item: fullItem, playing: $playing).onAppear(perform: lockOrientations)
|
||||||
|
} else {
|
||||||
|
LoadingView(isShowing: $isLoading) {
|
||||||
|
ScrollView() {
|
||||||
|
VStack(alignment:.leading) {
|
||||||
|
if(!isLoading) {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
VStack() {
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=3840&quality=90&tag=\(fullItem.Backdrop)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625)
|
||||||
|
}
|
||||||
|
.opacity(0.3)
|
||||||
|
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing, height: (geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) * 0.5625)
|
||||||
|
.shadow(radius: 5)
|
||||||
|
.overlay(
|
||||||
|
HStack() {
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Primary?fillWidth=300&fillHeight=450&quality=90&tag=\(fullItem.Poster)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
Image(uiImage: UIImage(blurHash: (fullItem.PosterBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.PosterBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 120, height: 180)
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width: 120, height: 180)
|
||||||
|
.cornerRadius(10)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Spacer()
|
||||||
|
Text(fullItem.Name).font(.headline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.offset(y: -4)
|
||||||
|
HStack() {
|
||||||
|
Text(String(fullItem.ProductionYear)).font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(fullItem.Runtime).font(.subheadline)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(fullItem.OfficialRating).font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.stroke(Color.secondary, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}.offset(x: 0, y: -46)
|
||||||
|
}.offset(x: 16, y: 40)
|
||||||
|
, alignment: .bottomLeading)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack() {
|
||||||
|
//Play button
|
||||||
|
Button() {
|
||||||
|
playing = true;
|
||||||
|
} label: {
|
||||||
|
HStack() {
|
||||||
|
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||||
|
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||||
|
}
|
||||||
|
.frame(width: 120, height: 35)
|
||||||
|
.background(Color(UIColor.systemBlue))
|
||||||
|
.cornerRadius(10)
|
||||||
|
}.buttonStyle(PlainButtonStyle())
|
||||||
|
.frame(width: 120, height: 25)
|
||||||
|
Spacer()
|
||||||
|
HStack() {
|
||||||
|
Button() {
|
||||||
|
favorite.toggle()
|
||||||
|
} label: {
|
||||||
|
if(!favorite) {
|
||||||
|
Image(systemName: "heart").foregroundColor(Color.primary).font(.system(size: 20))
|
||||||
|
} else {
|
||||||
|
Image(systemName: "heart.fill").foregroundColor(Color(UIColor.systemRed)).font(.system(size: 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button() {
|
||||||
|
watched.toggle()
|
||||||
|
} label: {
|
||||||
|
if(watched) {
|
||||||
|
Image(systemName: "checkmark.rectangle.fill").foregroundColor(Color.primary).font(.system(size: 20))
|
||||||
|
} else {
|
||||||
|
Image(systemName: "xmark.rectangle").foregroundColor(Color.primary).font(.system(size: 20))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(fullItem.Tagline).font(.body).italic().padding(.top, 7).fixedSize(horizontal: false, vertical: true)
|
||||||
|
Text(fullItem.Overview).font(.footnote).padding(.top, 3).fixedSize(horizontal: false, vertical: true)
|
||||||
|
}.padding(EdgeInsets(top: 24, leading: 16, bottom: 0, trailing: 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationTitle("Details")
|
||||||
|
.supportedOrientations(.allButUpsideDown)
|
||||||
|
.prefersHomeIndicatorAutoHidden(false)
|
||||||
|
.withHostingWindow() { window in
|
||||||
|
let rootVC = window?.rootViewController;
|
||||||
|
let UIHostingcontroller: PreferenceUIHostingController = rootVC as! PreferenceUIHostingController;
|
||||||
|
vc = UIHostingcontroller;
|
||||||
|
}
|
||||||
|
.introspectTabBarController { (UITabBarController) in
|
||||||
|
UITabBarController.tabBar.isHidden = false
|
||||||
|
}
|
||||||
|
}.onAppear(perform: loadData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
//
|
||||||
|
// MultiSelector.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/2/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
private struct MultiSelectionView<Selectable: Identifiable & Hashable>: View {
|
||||||
|
let options: [Selectable]
|
||||||
|
let optionToString: (Selectable) -> String
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
@Binding var selected: Set<Selectable>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(options) { selectable in
|
||||||
|
Button(action: { toggleSelection(selectable: selectable) }) {
|
||||||
|
HStack {
|
||||||
|
Text(optionToString(selectable)).foregroundColor(Color.primary)
|
||||||
|
Spacer()
|
||||||
|
if selected.contains { $0.id == selectable.id } {
|
||||||
|
Image(systemName: "checkmark").foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.tag(selectable.id)
|
||||||
|
}
|
||||||
|
}.listStyle(GroupedListStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func toggleSelection(selectable: Selectable) {
|
||||||
|
if let existingIndex = selected.firstIndex(where: { $0.id == selectable.id }) {
|
||||||
|
selected.remove(at: existingIndex)
|
||||||
|
} else {
|
||||||
|
selected.insert(selectable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MultiSelector<Selectable: Identifiable & Hashable>: View {
|
||||||
|
let label: String
|
||||||
|
let options: [Selectable]
|
||||||
|
let optionToString: (Selectable) -> String
|
||||||
|
|
||||||
|
var selected: Binding<Set<Selectable>>
|
||||||
|
|
||||||
|
private var formattedSelectedListString: String {
|
||||||
|
ListFormatter.localizedString(byJoining: selected.wrappedValue.map { optionToString($0) })
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink(destination: multiSelectionView()) {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
Spacer()
|
||||||
|
Text(formattedSelectedListString)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func multiSelectionView() -> some View {
|
||||||
|
MultiSelectionView(
|
||||||
|
options: options,
|
||||||
|
optionToString: optionToString,
|
||||||
|
label: self.label,
|
||||||
|
selected: selected
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
//
|
||||||
|
// NextUpView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/30/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyRequest
|
||||||
|
import SwiftyJSON
|
||||||
|
import SDWebImageSwiftUI
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
func onAppear() {
|
||||||
|
if(globalData.server?.baseURI == "") {
|
||||||
|
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"
|
||||||
|
|
||||||
|
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
|
||||||
|
let 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Next Up").font(.title2).fontWeight(.bold).padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack() {
|
||||||
|
if(isLoading == false) {
|
||||||
|
Spacer().frame(width:18)
|
||||||
|
ForEach(resumeItems, id: \.Id) { item in
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Spacer().frame(height:10)
|
||||||
|
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?fillWidth=300&fillHeight=450&quality=90&tag=\(item.Image)")!)
|
||||||
|
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||||
|
.placeholder {
|
||||||
|
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()
|
||||||
|
.scaledToFit()
|
||||||
|
.cornerRadius(10)
|
||||||
|
}
|
||||||
|
.frame(width: 100, height: 150)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.shadow(radius: 6)
|
||||||
|
Text(item.SeriesName ?? "")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Spacer().frame(height:5)
|
||||||
|
}
|
||||||
|
.frame(width: 100)
|
||||||
|
Spacer().frame(width:12)
|
||||||
|
}
|
||||||
|
Spacer().frame(width:18)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding(EdgeInsets(top: -2, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
}.onAppear(perform: onAppear).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// Persistence.swift
|
||||||
|
// JFPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/29/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
struct PersistenceController {
|
||||||
|
static let shared = PersistenceController()
|
||||||
|
|
||||||
|
static var preview: PersistenceController = {
|
||||||
|
let result = PersistenceController(inMemory: true)
|
||||||
|
let viewContext = result.container.viewContext
|
||||||
|
|
||||||
|
|
||||||
|
do {
|
||||||
|
try viewContext.save()
|
||||||
|
} catch {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
let nsError = error as NSError
|
||||||
|
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}()
|
||||||
|
|
||||||
|
let container: NSPersistentCloudKitContainer
|
||||||
|
|
||||||
|
init(inMemory: Bool = false) {
|
||||||
|
container = NSPersistentCloudKitContainer(name: "Model")
|
||||||
|
if inMemory {
|
||||||
|
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
|
||||||
|
}
|
||||||
|
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||||
|
if let error = error as NSError? {
|
||||||
|
// Replace this implementation with code to handle the error appropriately.
|
||||||
|
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
||||||
|
|
||||||
|
/*
|
||||||
|
Typical reasons for an error here include:
|
||||||
|
* The parent directory does not exist, cannot be created, or disallows writing.
|
||||||
|
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
|
||||||
|
* The device is out of space.
|
||||||
|
* The store could not be migrated to the current model version.
|
||||||
|
Check the error message to determine what the actual problem was.
|
||||||
|
*/
|
||||||
|
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,436 @@
|
||||||
|
//
|
||||||
|
// PlayerDemo.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/10/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftyJSON
|
||||||
|
import SwiftyRequest
|
||||||
|
import AVKit
|
||||||
|
import MobileVLCKit
|
||||||
|
|
||||||
|
struct Subtitle {
|
||||||
|
var name: String;
|
||||||
|
var id: Int32;
|
||||||
|
var url: URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
struct PlayerDemo: View {
|
||||||
|
@EnvironmentObject var globalData: GlobalData
|
||||||
|
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
|
||||||
|
var item: DetailItem;
|
||||||
|
@State private var pbitem: PlaybackItem = PlaybackItem(videoType: VideoType.direct, videoUrl: URL(string: "https://example.com")!, subtitles: []);
|
||||||
|
@State private var streamLoading = false;
|
||||||
|
@State private var vlcplayer: VLCMediaPlayer = VLCMediaPlayer(options: ["-vv", "--sub-margin=-50", "--network-caching=10000"]);
|
||||||
|
@State private var isPlaying = false;
|
||||||
|
@State private var subtitles: [Subtitle] = [];
|
||||||
|
@State private var inactivity: Bool = true;
|
||||||
|
@State private var lastActivityTime: Double = 0;
|
||||||
|
@State private var scrub: Double = 0;
|
||||||
|
@State private var timeText: String = "-:--:--";
|
||||||
|
@State private var playPauseButtonSystemName: String = "pause";
|
||||||
|
@State private var playSessionId: String = "";
|
||||||
|
@State private var lastPosition: Double = 0;
|
||||||
|
@State private var iterations: Int = 0;
|
||||||
|
@State private var captionConfiguration: Bool = false {
|
||||||
|
didSet {
|
||||||
|
if(captionConfiguration == false) {
|
||||||
|
vlcplayer.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@State private var selectedCaptionTrack: Int32 = -1;
|
||||||
|
var playing: Binding<Bool>;
|
||||||
|
|
||||||
|
init(item: DetailItem, playing: Binding<Bool>) {
|
||||||
|
self.item = item;
|
||||||
|
self.playing = playing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@State var lastProgressReportSent: Double = CACurrentMediaTime()
|
||||||
|
|
||||||
|
func keepUpWithPlayerState() {
|
||||||
|
if(!vlcplayer.isPlaying) {
|
||||||
|
while(!vlcplayer.isPlaying) {}
|
||||||
|
}
|
||||||
|
while(vlcplayer.state != VLCMediaPlayerState.stopped) {
|
||||||
|
_streamLoading.wrappedValue = false;
|
||||||
|
while(vlcplayer.isPlaying) {
|
||||||
|
vlcplayer.currentVideoSubTitleIndex = _selectedCaptionTrack.wrappedValue;
|
||||||
|
usleep(500000)
|
||||||
|
if(CACurrentMediaTime() - lastProgressReportSent > 10) {
|
||||||
|
sendProgressReport()
|
||||||
|
_lastProgressReportSent.wrappedValue = CACurrentMediaTime()
|
||||||
|
}
|
||||||
|
if(vlcplayer.time.intValue != 0) {
|
||||||
|
_scrub.wrappedValue = Double(Double(vlcplayer.time.intValue) / Double(vlcplayer.time.intValue + abs(vlcplayer.remainingTime.intValue)));
|
||||||
|
|
||||||
|
//Turn remainingTime into text
|
||||||
|
let remainingTime = abs(vlcplayer.remainingTime.intValue)/1000;
|
||||||
|
let hours = remainingTime / 3600;
|
||||||
|
let minutes = (remainingTime % 3600) / 60;
|
||||||
|
let seconds = (remainingTime % 3600) % 60;
|
||||||
|
if(hours != 0) {
|
||||||
|
timeText = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||||
|
} else {
|
||||||
|
timeText = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(CACurrentMediaTime() - _lastActivityTime.wrappedValue > 5 && vlcplayer.state != VLCMediaPlayerState.paused) {
|
||||||
|
_inactivity.wrappedValue = true
|
||||||
|
}
|
||||||
|
if((lastPosition == Double(vlcplayer.position) && vlcplayer.state != VLCMediaPlayerState.paused)) {
|
||||||
|
if(iterations > 3) {
|
||||||
|
_iterations.wrappedValue = 0;
|
||||||
|
_streamLoading.wrappedValue = true;
|
||||||
|
print("Buffering")
|
||||||
|
}
|
||||||
|
_iterations.wrappedValue+=1;
|
||||||
|
} else {
|
||||||
|
_iterations.wrappedValue = 0;
|
||||||
|
print("Not Buffering")
|
||||||
|
_streamLoading.wrappedValue = false;
|
||||||
|
}
|
||||||
|
if(vlcplayer.state == VLCMediaPlayerState.error) {
|
||||||
|
playing.wrappedValue = false;
|
||||||
|
}
|
||||||
|
_lastPosition.wrappedValue = Double(vlcplayer.position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendProgressReport() {
|
||||||
|
var progressBody: String = "";
|
||||||
|
if(pbitem.videoType == VideoType.direct) {
|
||||||
|
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(vlcplayer.state == VLCMediaPlayerState.paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":140000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":16209515560670000,\"AudioStreamIndex\":1,\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(pbitem.videoType == VideoType.direct ? "DirectStream" : "Transcode")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem1\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"EventName\":\"timeupdate\"}";
|
||||||
|
} else {
|
||||||
|
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(vlcplayer.state == VLCMediaPlayerState.paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":140000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":16209515560670000,\"AudioStreamIndex\":1,\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem1\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"EventName\":\"timeupdate\"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 response):
|
||||||
|
let body = response.body
|
||||||
|
print(body)
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendStopReport() {
|
||||||
|
var progressBody: String = "";
|
||||||
|
if(pbitem.videoType == VideoType.direct) {
|
||||||
|
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":140000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":16209515560670000,\"AudioStreamIndex\":1,\"BufferedRanges\":[],\"PlayMethod\":\"\(pbitem.videoType == VideoType.direct ? "DirectStream" : "Transcode")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem1\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem1\"}]}";
|
||||||
|
} else {
|
||||||
|
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":140000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":16209515560670000,\"AudioStreamIndex\":1,\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem1\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem1\"}]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 response):
|
||||||
|
let body = response.body
|
||||||
|
print(body)
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendPlayReport() {
|
||||||
|
var progressBody: String = "";
|
||||||
|
if(pbitem.videoType == VideoType.hls) {
|
||||||
|
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":140000000,\"PositionTicks\":0,\"PlaybackStartTimeTicks\":16209515560670000,\"AudioStreamIndex\":1,\"BufferedRanges\":[],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem1\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem1\"}]}";
|
||||||
|
} else {
|
||||||
|
progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":140000000,\"PositionTicks\":0,\"PlaybackStartTimeTicks\":16209515560670000,\"AudioStreamIndex\":1,\"BufferedRanges\":[],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem1\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem1\"}]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 response):
|
||||||
|
let body = response.body
|
||||||
|
print(body)
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startStream() {
|
||||||
|
_streamLoading.wrappedValue = true;
|
||||||
|
//print((globalData.server?.baseURI ?? "") + "/Items/\(item)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=60000000")
|
||||||
|
let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Items/\(item.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=0&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=60000000")
|
||||||
|
request.headerParameters["X-Emby-Authorization"] = globalData.authHeader
|
||||||
|
request.contentType = "application/json"
|
||||||
|
request.acceptType = "application/json"
|
||||||
|
request.messageBody = "{\"DeviceProfile\":{\"MaxStreamingBitrate\":120000000,\"MaxStaticBitrate\":100000000,\"MusicStreamingTranscodingBitrate\":384000,\"DirectPlayProfiles\":[{\"Container\":\"webm\",\"Type\":\"Video\",\"VideoCodec\":\"vp8,vp9\",\"AudioCodec\":\"vorbis\"},{\"Container\":\"mp4,m4v,mkv\",\"Type\":\"Video\",\"VideoCodec\":\"hevc,h264,vp8,vp9\",\"AudioCodec\":\"aac,mp3,ac3,eac3,flac,alac,vorbis,dts\"},{\"Container\":\"mov\",\"Type\":\"Video\",\"VideoCodec\":\"h264\",\"AudioCodec\":\"aac,mp3,ac3,eac3,flac,alac,vorbis\"},{\"Container\":\"mp3\",\"Type\":\"Audio\"},{\"Container\":\"aac\",\"Type\":\"Audio\"},{\"Container\":\"m4a\",\"AudioCodec\":\"aac\",\"Type\":\"Audio\"},{\"Container\":\"m4b\",\"AudioCodec\":\"aac\",\"Type\":\"Audio\"},{\"Container\":\"flac\",\"Type\":\"Audio\"},{\"Container\":\"alac\",\"Type\":\"Audio\"},{\"Container\":\"m4a\",\"AudioCodec\":\"alac\",\"Type\":\"Audio\"},{\"Container\":\"m4b\",\"AudioCodec\":\"alac\",\"Type\":\"Audio\"},{\"Container\":\"webma\",\"Type\":\"Audio\"},{\"Container\":\"webm\",\"AudioCodec\":\"webma\",\"Type\":\"Audio\"},{\"Container\":\"wav\",\"Type\":\"Audio\"}],\"TranscodingProfiles\":[{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\",\"Context\":\"Streaming\",\"Protocol\":\"hls\",\"MaxAudioChannels\":\"6\",\"MinSegments\":\"2\",\"BreakOnNonKeyFrames\":true},{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"mp3\",\"Type\":\"Audio\",\"AudioCodec\":\"mp3\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"wav\",\"Type\":\"Audio\",\"AudioCodec\":\"wav\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"mp3\",\"Type\":\"Audio\",\"AudioCodec\":\"mp3\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"aac\",\"Type\":\"Audio\",\"AudioCodec\":\"aac\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"wav\",\"Type\":\"Audio\",\"AudioCodec\":\"wav\",\"Context\":\"Static\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"ts\",\"Type\":\"Video\",\"AudioCodec\":\"aac,mp3,ac3,eac3\",\"VideoCodec\":\"h264\",\"Context\":\"Streaming\",\"Protocol\":\"hls\",\"MaxAudioChannels\":\"6\",\"MinSegments\":\"2\",\"BreakOnNonKeyFrames\":true},{\"Container\":\"webm\",\"Type\":\"Video\",\"AudioCodec\":\"vorbis\",\"VideoCodec\":\"vpx\",\"Context\":\"Streaming\",\"Protocol\":\"http\",\"MaxAudioChannels\":\"6\"},{\"Container\":\"mp4\",\"Type\":\"Video\",\"AudioCodec\":\"aac,mp3,ac3,eac3,flac,alac,vorbis\",\"VideoCodec\":\"h264\",\"Context\":\"Static\",\"Protocol\":\"http\"}],\"ContainerProfiles\":[],\"CodecProfiles\":[{\"Type\":\"Video\",\"Codec\":\"h264\",\"Conditions\":[{\"Condition\":\"NotEquals\",\"Property\":\"IsAnamorphic\",\"Value\":\"true\",\"IsRequired\":false},{\"Condition\":\"EqualsAny\",\"Property\":\"VideoProfile\",\"Value\":\"high|main|baseline|constrained baseline\",\"IsRequired\":false},{\"Condition\":\"LessThanEqual\",\"Property\":\"VideoLevel\",\"Value\":\"80\",\"IsRequired\":false},{\"Condition\":\"NotEquals\",\"Property\":\"IsInterlaced\",\"Value\":\"true\",\"IsRequired\":false}]},{\"Type\":\"Video\",\"Codec\":\"hevc\",\"Conditions\":[{\"Condition\":\"NotEquals\",\"Property\":\"IsAnamorphic\",\"Value\":\"true\",\"IsRequired\":false},{\"Condition\":\"EqualsAny\",\"Property\":\"VideoProfile\",\"Value\":\"main|main 10\",\"IsRequired\":false},{\"Condition\":\"LessThanEqual\",\"Property\":\"VideoLevel\",\"Value\":\"190\",\"IsRequired\":false},{\"Condition\":\"NotEquals\",\"Property\":\"IsInterlaced\",\"Value\":\"true\",\"IsRequired\":false}]}],\"SubtitleProfiles\":[{\"Format\":\"vtt\",\"Method\":\"External\"},{\"Format\":\"ass\",\"Method\":\"External\"},{\"Format\":\"ssa\",\"Method\":\"External\"}],\"ResponseProfiles\":[{\"Type\":\"Video\",\"Container\":\"m4v\",\"MimeType\":\"video/mp4\"}]}}".data(using: .ascii);
|
||||||
|
|
||||||
|
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)
|
||||||
|
_playSessionId.wrappedValue = json["PlaySessionId"].string ?? "";
|
||||||
|
if(json["MediaSources"][0]["TranscodingUrl"].string != nil) {
|
||||||
|
//Video is transcoded due to TranscodingReason - also may just be remuxed
|
||||||
|
for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] {
|
||||||
|
if(stream["Type"].string == "Subtitle") {
|
||||||
|
print("Found subtitle track: \(stream["DeliveryUrl"].string ?? "")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? "").replacingOccurrences(of: "master.m3u8", with: "main.m3u8"))")!
|
||||||
|
print(streamURL);
|
||||||
|
let item = PlaybackItem(videoType: VideoType.hls, videoUrl: streamURL, subtitles: [])
|
||||||
|
var SubIndex: Int32 = 2;
|
||||||
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!)
|
||||||
|
_subtitles.wrappedValue.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: SubIndex, url: deliveryUrl)
|
||||||
|
SubIndex+=1;
|
||||||
|
_subtitles.wrappedValue.append(subtitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendPlayReport();
|
||||||
|
pbitem = item;
|
||||||
|
pbitem.subtitles = subtitles;
|
||||||
|
_isPlaying.wrappedValue = true;
|
||||||
|
} else {
|
||||||
|
print("Direct play of item \(item.Name)")
|
||||||
|
let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")/Videos/\(item.Id)/stream.mp4?Static=true&mediaSourceId=\(item.Id)&deviceId=\(globalData.user?.device_uuid ?? "")&api_key=\(globalData.authToken)&Tag=\(json["MediaSources"][0]["ETag"])")!;
|
||||||
|
let item = PlaybackItem(videoType: VideoType.direct, videoUrl: streamURL, subtitles: [])
|
||||||
|
var SubIndex: Int32 = 2;
|
||||||
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!)
|
||||||
|
_subtitles.wrappedValue.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: SubIndex, url: deliveryUrl)
|
||||||
|
SubIndex+=1;
|
||||||
|
_subtitles.wrappedValue.append(subtitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pbitem = item;
|
||||||
|
pbitem.subtitles = subtitles;
|
||||||
|
_isPlaying.wrappedValue = true;
|
||||||
|
}
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||||
|
self.keepUpWithPlayerState()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
debugPrint(error)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func processScrubbingState() {
|
||||||
|
let videoDuration = Double(vlcplayer.time.intValue + abs(vlcplayer.remainingTime.intValue))/1000
|
||||||
|
while(vlcplayer.state != VLCMediaPlayerState.paused) {}
|
||||||
|
while(vlcplayer.state == VLCMediaPlayerState.paused) {
|
||||||
|
let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration);
|
||||||
|
let scrubRemaining = videoDuration - secondsScrubbedTo;
|
||||||
|
usleep(10000)
|
||||||
|
|
||||||
|
let remainingTime = scrubRemaining;
|
||||||
|
let hours = floor(remainingTime / 3600);
|
||||||
|
let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60;
|
||||||
|
let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60);
|
||||||
|
if(hours != 0) {
|
||||||
|
timeText = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||||
|
} else {
|
||||||
|
timeText = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetTimer() {
|
||||||
|
if(_inactivity.wrappedValue == false) {
|
||||||
|
_inactivity.wrappedValue = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastActivityTime.wrappedValue = CACurrentMediaTime()
|
||||||
|
_inactivity.wrappedValue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LoadingView(isShowing: ($streamLoading)) {
|
||||||
|
ZStack() {
|
||||||
|
VLCPlayer(url: $pbitem, player: $vlcplayer, startTime: Int(item.Progress)).onDisappear(perform: {
|
||||||
|
_isPlaying.wrappedValue = false;
|
||||||
|
vlcplayer.stop()
|
||||||
|
})
|
||||||
|
VStack() {
|
||||||
|
HStack() {
|
||||||
|
HStack() {
|
||||||
|
Button() {
|
||||||
|
self.playing.wrappedValue = false;
|
||||||
|
} label: {
|
||||||
|
HStack() {
|
||||||
|
Image(systemName: "chevron.left").font(.system(size: 20)).foregroundColor(.white)
|
||||||
|
}
|
||||||
|
}.frame(width: 20)
|
||||||
|
Spacer()
|
||||||
|
Text(item.Name).font(.headline).fontWeight(.semibold).foregroundColor(.white).offset(x:-4)
|
||||||
|
Spacer()
|
||||||
|
Button() {
|
||||||
|
vlcplayer.pause()
|
||||||
|
self.captionConfiguration = true;
|
||||||
|
} label: {
|
||||||
|
HStack() {
|
||||||
|
Image(systemName: "captions.bubble").font(.system(size: 20)).foregroundColor(.white)
|
||||||
|
}
|
||||||
|
}.frame(width: 20)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}.padding(EdgeInsets(top: 55, leading: 40, bottom: 0, trailing: 40))
|
||||||
|
Spacer()
|
||||||
|
HStack() {
|
||||||
|
Spacer()
|
||||||
|
Button() {
|
||||||
|
vlcplayer.jumpBackward(15)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "gobackward.15").font(.system(size: 40)).foregroundColor(.white)
|
||||||
|
}.padding(20)
|
||||||
|
Spacer()
|
||||||
|
Button() {
|
||||||
|
if(vlcplayer.state != VLCMediaPlayerState.paused) {
|
||||||
|
vlcplayer.pause()
|
||||||
|
playPauseButtonSystemName = "play"
|
||||||
|
sendProgressReport()
|
||||||
|
} else {
|
||||||
|
vlcplayer.play()
|
||||||
|
playPauseButtonSystemName = "pause"
|
||||||
|
sendProgressReport()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: playPauseButtonSystemName).font(.system(size: 55)).foregroundColor(.white)
|
||||||
|
}.padding(20)
|
||||||
|
Spacer()
|
||||||
|
Button() {
|
||||||
|
vlcplayer.jumpForward(15)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "goforward.15").font(.system(size: 40)).foregroundColor(.white)
|
||||||
|
}.padding(20)
|
||||||
|
Spacer()
|
||||||
|
}.padding(.leading, -20)
|
||||||
|
Spacer()
|
||||||
|
HStack() {
|
||||||
|
Slider(value: $scrub, onEditingChanged: { bool in
|
||||||
|
let videoPosition = Double(vlcplayer.time.intValue)
|
||||||
|
let videoDuration = Double(vlcplayer.time.intValue + abs(vlcplayer.remainingTime.intValue))
|
||||||
|
if(bool == true) {
|
||||||
|
vlcplayer.pause()
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||||
|
self.processScrubbingState()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Scrub is value from 0..1 - find position in video and add / or remove.
|
||||||
|
let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration);
|
||||||
|
let offset = secondsScrubbedTo - videoPosition;
|
||||||
|
sendProgressReport()
|
||||||
|
vlcplayer.play()
|
||||||
|
if(offset > 0) {
|
||||||
|
vlcplayer.jumpForward(Int32(offset)/1000);
|
||||||
|
} else {
|
||||||
|
vlcplayer.jumpBackward(Int32(abs(offset))/1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.accentColor(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||||
|
Text(timeText).fontWeight(.semibold).frame(width: 80).foregroundColor(.white)
|
||||||
|
}.padding(EdgeInsets(top: -20, leading: 44, bottom: 42, trailing: 40))
|
||||||
|
}.transition(.fade)
|
||||||
|
.padding(EdgeInsets(top: 0, leading: -30, bottom: 0, trailing: -30))
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color(UIColor.black).opacity(0.4))
|
||||||
|
.isHidden(inactivity)
|
||||||
|
}.padding(EdgeInsets(top: 0, leading: 34, bottom: 0, trailing: 34))
|
||||||
|
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||||
|
.onAppear(perform: startStream)
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.statusBar(hidden: true)
|
||||||
|
.introspectTabBarController { (UITabBarController) in
|
||||||
|
UITabBarController.tabBar.isHidden = true
|
||||||
|
}
|
||||||
|
.prefersHomeIndicatorAutoHidden(true)
|
||||||
|
.supportedOrientations(.landscapeRight)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.onTapGesture(perform: resetTimer)
|
||||||
|
.overrideViewPreference(.dark)
|
||||||
|
.popover( isPresented: self.$captionConfiguration, arrowEdge: .bottom) {
|
||||||
|
NavigationView() {
|
||||||
|
Form() {
|
||||||
|
Picker("Closed Captions", selection: $selectedCaptionTrack) {
|
||||||
|
ForEach(subtitles, id: \.id) { caption in
|
||||||
|
Text(caption.name).tag(caption.id)
|
||||||
|
}
|
||||||
|
}.onChange(of: selectedCaptionTrack) { track in
|
||||||
|
vlcplayer.currentVideoSubTitleIndex = track;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitle("Audio & Captions", displayMode: .inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
captionConfiguration = false;
|
||||||
|
playPauseButtonSystemName = "pause";
|
||||||
|
} label: {
|
||||||
|
HStack() {
|
||||||
|
Text("Back").font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.edgesIgnoringSafeArea(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
//
|
||||||
|
// SettingsView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 4/29/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// VideoPlayerView.swift
|
||||||
|
// JellyfinPlayer
|
||||||
|
//
|
||||||
|
// Created by Aiden Vigue on 5/10/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import MobileVLCKit
|
||||||
|
|
||||||
|
extension NSNotification {
|
||||||
|
static let PlayerUpdate = NSNotification.Name.init("PlayerUpdate")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VideoType {
|
||||||
|
case hls;
|
||||||
|
case direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PlaybackItem {
|
||||||
|
var videoType: VideoType;
|
||||||
|
var videoUrl: URL;
|
||||||
|
var subtitles: [Subtitle];
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VLCPlayer: UIViewRepresentable{
|
||||||
|
var url: Binding<PlaybackItem>;
|
||||||
|
var player: Binding<VLCMediaPlayer>;
|
||||||
|
var startTime: Int;
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext<VLCPlayer>) {
|
||||||
|
uiView.url = self.url
|
||||||
|
if(self.url.wrappedValue.videoUrl.absoluteString != "https://example.com") {
|
||||||
|
uiView.videoSetup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> PlayerUIView {
|
||||||
|
return PlayerUIView(frame: .zero, url: url, player: self.player, startTime: self.startTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerUIView: UIView, VLCMediaPlayerDelegate {
|
||||||
|
|
||||||
|
private var mediaPlayer: Binding<VLCMediaPlayer>;
|
||||||
|
var url:Binding<PlaybackItem>
|
||||||
|
var lastUrl: PlaybackItem?
|
||||||
|
var startTime: Int
|
||||||
|
|
||||||
|
init(frame: CGRect, url: Binding<PlaybackItem>, player: Binding<VLCMediaPlayer>, startTime: Int) {
|
||||||
|
self.mediaPlayer = player;
|
||||||
|
self.url = url;
|
||||||
|
self.startTime = startTime;
|
||||||
|
super.init(frame: frame)
|
||||||
|
mediaPlayer.wrappedValue.delegate = self
|
||||||
|
mediaPlayer.wrappedValue.drawable = self
|
||||||
|
}
|
||||||
|
|
||||||
|
func videoSetup() {
|
||||||
|
if(lastUrl == nil || lastUrl?.videoUrl != url.wrappedValue.videoUrl) {
|
||||||
|
lastUrl = url.wrappedValue
|
||||||
|
print("update called")
|
||||||
|
print(self.url.wrappedValue.videoUrl)
|
||||||
|
mediaPlayer.wrappedValue.stop()
|
||||||
|
mediaPlayer.wrappedValue.media = VLCMedia(url: self.url.wrappedValue.videoUrl)
|
||||||
|
self.url.wrappedValue.subtitles.forEach() { sub in
|
||||||
|
if(sub.id != -1) {
|
||||||
|
mediaPlayer.wrappedValue.addPlaybackSlave(sub.url, type: .subtitle, enforce: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||||
|
mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||||
|
mediaPlayer.wrappedValue.play()
|
||||||
|
mediaPlayer.wrappedValue.jumpForward(Int32(startTime/10000000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue