Upload all files

This commit is contained in:
Aiden Vigue 2021-05-14 17:13:24 -04:00
parent 7232b8d54b
commit 52cac0ab6f
35 changed files with 3800 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

1
Cartfile Normal file
View File

@ -0,0 +1 @@
binary "https://code.videolan.org/videolan/VLCKit/raw/master/Packaging/MobileVLCKit.json" ~> 3.3.0

1
Cartfile.resolved Normal file
View 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

Binary file not shown.

View 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View 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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -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

Binary file not shown.

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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]
}
}

View 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))
}
}

View 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)
}
}

View 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
View 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>

View 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 :(")
}
}
}

View 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 {
}

View 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
}
}

View 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
})
}
}
}
}

View 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()
}
}

View 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)
}
}
}
}
}
}
}

View 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")
}
}
}
}
}
}

View 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)
}
}
}
}
}

View 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>

View File

@ -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>

View 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)
}
}
}

View 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
)
}
}

View 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))
}
}

View 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)")
}
})
}
}

View 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)
}
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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()
}
}

View 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()
}
}