mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 05:59:51 +00:00
Upload all files
This commit is contained in:
parent
7232b8d54b
commit
52cac0ab6f
1
Cartfile
Normal file
1
Cartfile
Normal file
@ -0,0 +1 @@
|
||||
binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.3.0
|
1
Cartfile.resolved
Normal file
1
Cartfile.resolved
Normal file
@ -0,0 +1 @@
|
||||
binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" "3.3.16"
|
BIN
Carthage/.DS_Store
vendored
Normal file
BIN
Carthage/.DS_Store
vendored
Normal file
Binary file not shown.
613
JellyfinPlayer.xcodeproj/project.pbxproj
Normal file
613
JellyfinPlayer.xcodeproj/project.pbxproj
Normal file
@ -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 */;
|
||||
}
|
7
JellyfinPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
JellyfinPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -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
|
||||
}
|
BIN
JellyfinPlayer/.DS_Store
vendored
Normal file
BIN
JellyfinPlayer/.DS_Store
vendored
Normal file
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
|
||||
}
|
||||
}
|
6
JellyfinPlayer/Assets.xcassets/Contents.json
Normal file
6
JellyfinPlayer/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
146
JellyfinPlayer/BlurHashDecode.swift
Normal file
146
JellyfinPlayer/BlurHashDecode.swift
Normal file
@ -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]
|
||||
}
|
||||
}
|
197
JellyfinPlayer/ConnectToServerView.swift
Normal file
197
JellyfinPlayer/ConnectToServerView.swift
Normal file
@ -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))
|
||||
}
|
||||
}
|
337
JellyfinPlayer/ContentView.swift
Normal file
337
JellyfinPlayer/ContentView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
155
JellyfinPlayer/ContinueWatchingView.swift
Normal file
155
JellyfinPlayer/ContinueWatchingView.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
54
JellyfinPlayer/Info.plist
Normal file
54
JellyfinPlayer/Info.plist
Normal file
@ -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>
|
30
JellyfinPlayer/ItemView.swift
Normal file
30
JellyfinPlayer/ItemView.swift
Normal file
@ -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 :(")
|
||||
}
|
||||
}
|
||||
}
|
76
JellyfinPlayer/JellyApiTypings.swift
Normal file
76
JellyfinPlayer/JellyApiTypings.swift
Normal file
@ -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 {
|
||||
|
||||
}
|
148
JellyfinPlayer/JellyfinHLSResourceLoaderDelegate.swift
Normal file
148
JellyfinPlayer/JellyfinHLSResourceLoaderDelegate.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
39
JellyfinPlayer/JellyfinPlayerApp.swift
Normal file
39
JellyfinPlayer/JellyfinPlayerApp.swift
Normal file
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
147
JellyfinPlayer/LatestMediaView.swift
Normal file
147
JellyfinPlayer/LatestMediaView.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
185
JellyfinPlayer/LibraryFilterView.swift
Normal file
185
JellyfinPlayer/LibraryFilterView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
269
JellyfinPlayer/LibraryView.swift
Normal file
269
JellyfinPlayer/LibraryView.swift
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
40
JellyfinPlayer/LoadingView.swift
Normal file
40
JellyfinPlayer/LoadingView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
JellyfinPlayer/Model.xcdatamodeld/.xccurrentversion
Normal file
8
JellyfinPlayer/Model.xcdatamodeld/.xccurrentversion
Normal file
@ -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>
|
266
JellyfinPlayer/MovieItemView.swift
Normal file
266
JellyfinPlayer/MovieItemView.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
73
JellyfinPlayer/MultiSelector.swift
Normal file
73
JellyfinPlayer/MultiSelector.swift
Normal file
@ -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
|
||||
)
|
||||
}
|
||||
}
|
107
JellyfinPlayer/NextUpView.swift
Normal file
107
JellyfinPlayer/NextUpView.swift
Normal file
@ -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))
|
||||
}
|
||||
}
|
53
JellyfinPlayer/PersistenceController.swift
Normal file
53
JellyfinPlayer/PersistenceController.swift
Normal file
@ -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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
436
JellyfinPlayer/PlayerDemo.swift
Normal file
436
JellyfinPlayer/PlayerDemo.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
20
JellyfinPlayer/SettingsView.swift
Normal file
20
JellyfinPlayer/SettingsView.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
86
JellyfinPlayer/VLCPlayer.swift
Normal file
86
JellyfinPlayer/VLCPlayer.swift
Normal file
@ -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
Block a user