Merge pull request #64 from PangMo5/PangMo5/refactoring

Structural improvements - 1
This commit is contained in:
aiden vigue 2021-06-15 12:33:10 -04:00 committed by GitHub
commit 3b0429deb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1070 additions and 953 deletions

View File

@ -18,8 +18,6 @@
53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; };
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; };
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; };
533A8E6626748B4F00719967 /* MobileVLCKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 533A8E6526748B4F00719967 /* MobileVLCKit.framework */; platformFilter = ios; };
533A8E6726748B4F00719967 /* MobileVLCKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 533A8E6526748B4F00719967 /* MobileVLCKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */; };
535870652669D21600D05A09 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870642669D21600D05A09 /* ContentView.swift */; };
535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; };
@ -30,8 +28,6 @@
5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5358708C2669D7A800D05A09 /* KeychainSwift */; };
535870912669D7A800D05A09 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 535870902669D7A800D05A09 /* Introspect */; };
5358709B2669D7A800D05A09 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 5358709A2669D7A800D05A09 /* NukeUI */; };
5358709D2669D82900D05A09 /* TVVLCKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5358709C2669D82900D05A09 /* TVVLCKit.framework */; };
5358709E2669D82900D05A09 /* TVVLCKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5358709C2669D82900D05A09 /* TVVLCKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
@ -44,7 +40,6 @@
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; };
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; };
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview Assets.xcassets */; };
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
@ -59,8 +54,6 @@
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 53A431BE266B0FFE0016769F /* JellyfinAPI */; };
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA526572F0700E7EA70 /* SeriesItemView.swift */; };
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA326572C1300E7EA70 /* SeasonItemView.swift */; };
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; };
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */; };
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */; };
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DE4BD1267098F300739748 /* SearchBarView.swift */; };
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53DF641D263D9C0600A7CD1A /* LibraryView.swift */; };
@ -74,6 +67,17 @@
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; };
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; };
625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; };
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; };
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; };
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */; };
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 625CB5792678C4A400530A6E /* ActivityIndicator */; };
625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB57B2678CE1000530A6E /* ViewModel.swift */; };
625CB57E2678E81E00530A6E /* TVVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */; };
625CB57F2678E81E00530A6E /* TVVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
@ -91,7 +95,15 @@
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
62FA8A522671DE3C004BA2AB /* WidgetEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */; };
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; };
62EC3528267665D8000E9F2D /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; };
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; };
62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; };
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
/* End PBXBuildFile section */
@ -115,24 +127,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
533A8E6826748B4F00719967 /* Embed Frameworks */ = {
625CB5802678E81E00530A6E /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
533A8E6726748B4F00719967 /* MobileVLCKit.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
5358709F2669D82900D05A09 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
5358709E2669D82900D05A09 /* TVVLCKit.framework in Embed Frameworks */,
625CB57F2678E81E00530A6E /* TVVLCKit.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
@ -148,6 +149,17 @@
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
62EC3529267665D8000E9F2D /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
62EC3528267665D8000E9F2D /* MobileVLCKit.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@ -156,7 +168,6 @@
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = "<group>"; };
5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = "<group>"; };
533A8E6526748B4F00719967 /* MobileVLCKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileVLCKit.framework; path = "Carthage/Build/MobileVLCKit.xcframework/ios-arm64_armv7_armv7s/MobileVLCKit.framework"; sourceTree = "<group>"; };
535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
535870622669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayer_tvOSApp.swift; sourceTree = "<group>"; };
535870642669D21600D05A09 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@ -165,14 +176,12 @@
5358706B2669D21700D05A09 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
5358706E2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = JellyfinPlayer_tvOS.xcdatamodel; sourceTree = "<group>"; };
535870702669D21700D05A09 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5358709C2669D82900D05A09 /* TVVLCKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVVLCKit.framework; path = "Carthage/Build/TVVLCKit.xcframework/tvos-arm64/TVVLCKit.framework"; sourceTree = "<group>"; };
535870AC2669D8DD00D05A09 /* Typings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typings.swift; sourceTree = "<group>"; };
535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = "<group>"; };
535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.swift; sourceTree = "<group>"; };
5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "JellyfinPlayer iOS.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>"; };
@ -188,7 +197,6 @@
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>"; };
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = JellyfinPlayer.entitlements; sourceTree = "<group>"; };
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleAPIRequestCompletion.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>"; };
53DE4BD1267098F300739748 /* SearchBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarView.swift; sourceTree = "<group>"; };
@ -202,6 +210,15 @@
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = "<group>"; };
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = "<group>"; };
625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerViewModel.swift; sourceTree = "<group>"; };
625CB57B2678CE1000530A6E /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = "<group>"; };
625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = TVVLCKit.xcframework; path = Carthage/Build/TVVLCKit.xcframework; sourceTree = "<group>"; };
6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = "<group>"; };
6267B3D92671138200A7371D /* ImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageExtensions.swift; sourceTree = "<group>"; };
628B95202670CABD0091AF3B /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@ -212,7 +229,9 @@
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetEnvironment.swift; sourceTree = "<group>"; };
62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = "<group>"; };
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -223,8 +242,8 @@
files = (
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
535870912669D7A800D05A09 /* Introspect in Frameworks */,
625CB57E2678E81E00530A6E /* TVVLCKit.xcframework in Frameworks */,
5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */,
5358709D2669D82900D05A09 /* TVVLCKit.framework in Frameworks */,
5358709B2669D7A800D05A09 /* NukeUI in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -233,11 +252,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
533A8E6626748B4F00719967 /* MobileVLCKit.framework in Frameworks */,
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
621C638026672A30004216EA /* NukeUI in Frameworks */,
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */,
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -260,6 +280,11 @@
isa = PBXGroup;
children = (
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */,
625CB5692678B71200530A6E /* SplashViewModel.swift */,
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
625CB5742678C33500530A6E /* LibraryListViewModel.swift */,
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
625CB57B2678CE1000530A6E /* ViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -289,6 +314,7 @@
535870752669D60C00D05A09 /* Shared */ = {
isa = PBXGroup;
children = (
62EC352A26766657000E9F2D /* Shared */,
532175392671BCED005491E6 /* ViewModel */,
621338912660106C00A81A2A /* Extensions */,
AE8C3157265D6F5E008AA076 /* Resources */,
@ -331,10 +357,10 @@
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
isa = PBXGroup;
children = (
625CB56D2678C1C400530A6E /* ViewModels */,
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
5377CBF6263B596A003A4E83 /* ContentView.swift */,
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
53987CA72657424A00E7EA70 /* EpisodeItemView.swift */,
@ -359,6 +385,9 @@
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
53DE4BD1267098F300739748 /* SearchBarView.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */,
625CB5672678B6FB00530A6E /* SplashView.swift */,
625CB56B2678C0FD00530A6E /* MainTabView.swift */,
625CB56E2678C23300530A6E /* HomeView.swift */,
);
path = JellyfinPlayer;
sourceTree = "<group>";
@ -374,8 +403,7 @@
53D5E3DB264B47EE00BADDC8 /* Frameworks */ = {
isa = PBXGroup;
children = (
533A8E6526748B4F00719967 /* MobileVLCKit.framework */,
5358709C2669D82900D05A09 /* TVVLCKit.framework */,
625CB57D2678E81E00530A6E /* TVVLCKit.xcframework */,
53D5E3DC264B47EE00BADDC8 /* MobileVLCKit.xcframework */,
628B95212670CABD0091AF3B /* WidgetKit.framework */,
628B95232670CABD0091AF3B /* SwiftUI.framework */,
@ -388,17 +416,24 @@
children = (
5364F454266CA0DC0026ECBA /* APIExtensions.swift */,
5389277B263CC3DB0035E14B /* BlurHashDecode.swift */,
53C4404D266C75C70049424C /* HandleAPIRequestCompletion.swift */,
621338B22660A07800A81A2A /* LazyView.swift */,
53E4E648263F725B00F67C6B /* MultiSelectorView.swift */,
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */,
621338922660107500A81A2A /* StringExtensions.swift */,
6267B3D526710B8900A7371D /* CollectionExtensions.swift */,
6267B3D92671138200A7371D /* ImageExtensions.swift */,
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
625CB56D2678C1C400530A6E /* ViewModels */ = {
isa = PBXGroup;
children = (
);
path = ViewModels;
sourceTree = "<group>";
};
628B95252670CABD0091AF3B /* WidgetExtension */ = {
isa = PBXGroup;
children = (
@ -407,11 +442,19 @@
628B95262670CABD0091AF3B /* NextUpWidget.swift */,
628B95282670CABE0091AF3B /* Assets.xcassets */,
628B952A2670CABE0091AF3B /* Info.plist */,
62FA8A512671DE3C004BA2AB /* WidgetEnvironment.swift */,
);
path = WidgetExtension;
sourceTree = "<group>";
};
62EC352A26766657000E9F2D /* Shared */ = {
isa = PBXGroup;
children = (
62EC352B26766675000E9F2D /* ServerEnvironment.swift */,
62EC352E267666A5000E9F2D /* SessionManager.swift */,
);
path = Shared;
sourceTree = "<group>";
};
AE8C3157265D6F5E008AA076 /* Resources */ = {
isa = PBXGroup;
children = (
@ -431,7 +474,7 @@
5358705C2669D21600D05A09 /* Sources */,
5358705D2669D21600D05A09 /* Frameworks */,
5358705E2669D21600D05A09 /* Resources */,
5358709F2669D82900D05A09 /* Embed Frameworks */,
625CB5802678E81E00530A6E /* Embed Frameworks */,
);
buildRules = (
);
@ -457,7 +500,7 @@
5377CBEF263B596A003A4E83 /* Resources */,
5302F8322658B74800647A2E /* CopyFiles */,
628B95312670CABE0091AF3B /* Embed App Extensions */,
533A8E6826748B4F00719967 /* Embed Frameworks */,
62EC3529267665D8000E9F2D /* Embed Frameworks */,
);
buildRules = (
);
@ -470,6 +513,7 @@
53352570265EA0A0006CCA86 /* Introspect */,
621C637F26672A30004216EA /* NukeUI */,
53A431BC266B0FF20016769F /* JellyfinAPI */,
625CB5792678C4A400530A6E /* ActivityIndicator */,
);
productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
@ -534,6 +578,7 @@
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */,
53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
);
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
projectDirPath = "";
@ -584,6 +629,8 @@
buildActionMask = 2147483647;
files = (
6267B3DC2671139500A7371D /* ImageExtensions.swift in Sources */,
62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */,
62EC3530267666A5000E9F2D /* SessionManager.swift in Sources */,
535870A82669D8AE00D05A09 /* StringExtensions.swift in Sources */,
6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */,
535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */,
@ -593,7 +640,6 @@
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
535870652669D21600D05A09 /* ContentView.swift in Sources */,
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
53C4404F266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
5358706F2669D21700D05A09 /* JellyfinPlayer_tvOS.xcdatamodeld in Sources */,
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
@ -609,16 +655,20 @@
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */,
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
53892770263C25230035E14B /* NextUpView.swift in Sources */,
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
@ -626,21 +676,26 @@
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */,
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
53C4404E266C75C70049424C /* HandleAPIRequestCompletion.swift in Sources */,
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */,
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -648,6 +703,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */,
6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */,
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */,
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */,
@ -655,8 +711,8 @@
628B95372670CB800091AF3B /* JellyfinWidget.swift in Sources */,
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */,
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
62FA8A522671DE3C004BA2AB /* WidgetEnvironment.swift in Sources */,
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */,
62EC353226766849000E9F2D /* SessionManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1045,6 +1101,14 @@
version = 0.3.0;
};
};
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/duyquang91/ActivityIndicator";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.0;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -1088,6 +1152,11 @@
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
productName = NukeUI;
};
625CB5792678C4A400530A6E /* ActivityIndicator */ = {
isa = XCSwiftPackageProductDependency;
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
productName = ActivityIndicator;
};
628B95322670CAEA0091AF3B /* NukeUI */ = {
isa = XCSwiftPackageProductDependency;
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;

View File

@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "ActivityIndicator",
"repositoryURL": "https://github.com/duyquang91/ActivityIndicator",
"state": {
"branch": null,
"revision": "0101a02196f6a67cf26f6434b007d3db6bd07fee",
"version": "1.1.0"
}
},
{
"package": "AnyCodable",
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
@ -20,7 +29,7 @@
}
},
{
"package": "jellyfin-sdk-swift",
"package": "JellyfinAPI",
"repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift",
"state": {
"branch": "main",
@ -29,7 +38,7 @@
}
},
{
"package": "keychain-swift",
"package": "KeychainSwift",
"repositoryURL": "https://github.com/evgenyneu/keychain-swift",
"state": {
"branch": null,
@ -56,7 +65,7 @@
}
},
{
"package": "SwiftUI-Introspect",
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
"state": {
"branch": null,

View File

@ -5,260 +5,46 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import CoreData
import KeychainSwift
import JellyfinAPI
import KeychainSwift
import SwiftUI
struct ConnectToServerView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var jsi: justSignedIn
@StateObject
var viewModel = ConnectToServerViewModel()
@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 = ""
@State private var usernameDisabled: Bool = false
@State private var publicUsers: [UserDto] = []
@State private var lastPublicUsers: [UserDto] = []
@State private var username = ""
@State private var password = ""
@State private var server_id = ""
@State private var serverSkipped: Bool = false
@State private var serverSkippedAlert: Bool = false
@State private var skip_server_bool: Bool = false
@State private var skip_server_obj: Server!
@Binding var rootIsActive: Bool
private var reauthDeviceID: String = ""
private let userUUID = UUID()
init(skip_server: Bool, skip_server_prefill: Server, reauth_deviceId: String, isActive: Binding<Bool>) {
_rootIsActive = isActive
skip_server_bool = skip_server
skip_server_obj = skip_server_prefill
reauthDeviceID = reauth_deviceId
}
init(isActive: Binding<Bool>) {
_rootIsActive = isActive
}
func start() {
if skip_server_bool {
uri = skip_server_obj.baseURI!
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure:
skip_server_bool = false
skip_server_obj = Server()
break
}
}, receiveValue: { response in
publicUsers = response
serverSkipped = true
serverSkippedAlert = true
server_id = skip_server_obj.server_id!
serverName = skip_server_obj.name!
isConnected = true
})
.store(in: &globalData.pendingAPIRequests)
}
}
func doLogin() {
isWorking = true
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
let authHeader = "MediaBrowser Client=\"SwiftFin\", Device=\"\(deviceName)\", DeviceId=\"\(serverSkipped ? reauthDeviceID : userUUID.uuidString)\", Version=\"\(appVersion ?? "0.0.1")\""
print(authHeader)
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
let x: AuthenticateUserByName = AuthenticateUserByName(username: username, pw: password, password: nil)
UserAPI.authenticateUserByName(authenticateUserByName: x)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
isWorking = false
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
isSignInErrored = true
case .error:
globalData.networkError = true
}
}
break
}
}, receiveValue: { response in
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try viewContext.execute(deleteRequest)
} catch _ as NSError {
}
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
do {
try viewContext.execute(deleteRequest2)
} catch _ as NSError {
}
let newServer = Server(context: viewContext)
newServer.baseURI = uri
newServer.name = serverName
newServer.server_id = server_id
let newUser = SignedInUser(context: viewContext)
newUser.device_uuid = userUUID.uuidString
newUser.username = username
newUser.user_id = response.user!.id!
let keychain = KeychainSwift()
keychain.set(response.accessToken!, forKey: "AccessToken_\(newUser.user_id!)")
do {
try viewContext.save()
DispatchQueue.main.async { [self] in
globalData.authHeader = authHeader
_rootIsActive.wrappedValue = false
globalData.expiredCredentials = false
globalData.networkError = false
globalData.user = newUser
globalData.server = newServer
jsi.did = true
print("logged in")
}
} catch {
print("Couldn't store objects to CoreData")
}
})
.store(in: &globalData.pendingAPIRequests)
}
@Binding
var isLoggedIn: Bool
var body: some View {
Form {
if !isConnected {
Section(header: Text("Server Information")) {
TextField("Jellyfin Server URL", text: $uri)
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
isWorking = true
if !uri.contains("http") {
uri = "https://" + uri
}
if uri.last == "/" {
uri = String(uri.dropLast())
ZStack {
Form {
if viewModel.isConnectedServer {
if viewModel.publicUsers.isEmpty {
Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) {
TextField("Username", text: $viewModel.username)
.disableAutocorrection(true)
.autocapitalization(.none)
SecureField("Password", text: $viewModel.password)
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
viewModel.login()
} label: {
HStack {
Text("Login")
Spacer()
if viewModel.isLoading {
ProgressView()
}
}
}.disabled(viewModel.isLoading || viewModel.username.isEmpty)
}
JellyfinAPI.basePath = uri
SystemAPI.getPublicSystemInfo()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure:
isErrored = true
isWorking = false
break
}
}, receiveValue: { response in
let server = response
serverName = server.serverName!
server_id = server.id!
if server.startupWizardCompleted ?? true {
isConnected = true
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure:
isErrored = true
isWorking = false
break
}
}, receiveValue: { response in
publicUsers = response
isWorking = false
})
.store(in: &globalData.pendingAPIRequests)
}
})
.store(in: &globalData.pendingAPIRequests)
} label: {
HStack {
Text("Connect")
Spacer()
if isWorking == true {
ProgressView()
}
}
}.disabled(isWorking || uri.isEmpty)
}.alert(isPresented: $isErrored) {
Alert(title: Text("Error"), message: Text("Couldn't connect to server"), dismissButton: .default(Text("Try again")))
}
} else {
if publicUsers.count == 0 {
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
TextField("Username", text: $username)
.disableAutocorrection(true)
.autocapitalization(.none)
.disabled(usernameDisabled)
SecureField("Password", text: $password)
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
doLogin()
} label: {
HStack {
Text("Login")
Spacer()
if isWorking {
ProgressView()
}
}
}.disabled(isWorking || username.isEmpty)
.alert(isPresented: $isSignInErrored) {
Alert(title: Text("Error"), message: Text("Invalid credentials"), dismissButton: .default(Text("Back")))
}
}
if serverSkipped {
Section {
Button {
serverSkippedAlert = false
server_id = ""
serverName = ""
isConnected = false
serverSkipped = false
viewModel.isConnectedServer = false
} label: {
HStack {
HStack {
@ -270,85 +56,85 @@ struct ConnectToServerView: View {
}
}
} else {
Section {
Button {
publicUsers = lastPublicUsers
usernameDisabled = false
} label: {
Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) {
ForEach(viewModel.publicUsers, id: \.id) { publicUser in
HStack {
HStack {
Image(systemName: "chevron.left")
Text("Back")
}
Spacer()
}
}
}
}
} else {
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
ForEach(publicUsers, id: \.id) { publicUser in
HStack {
Button() {
if publicUser.hasPassword ?? true {
lastPublicUsers = publicUsers
username = publicUser.name ?? ""
usernameDisabled = true
publicUsers = []
} else {
publicUsers = []
password = ""
username = publicUser.name ?? ""
doLogin()
}
} label: {
HStack {
Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold)
Spacer()
if publicUser.primaryImageTag != nil {
ImageView(src: URL(string: "\(uri)/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!)
.frame(width: 60, height: 60)
.cornerRadius(30.0)
} else {
Image(systemName: "person.fill")
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
.font(.system(size: 35))
.frame(width: 60, height: 60)
.background(Color(red: 98/255, green: 121/255, blue: 205/255))
.cornerRadius(30.0)
.shadow(radius: 6)
Button(action: {
viewModel.username = publicUser.name ?? ""
viewModel.publicUsers.removeAll()
if !(publicUser.hasPassword ?? true) {
viewModel.password = ""
viewModel.login()
}
}) {
HStack {
Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold)
Spacer()
if publicUser.primaryImageTag != nil {
ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!)
.frame(width: 60, height: 60)
.cornerRadius(30.0)
} else {
Image(systemName: "person.fill")
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
.font(.system(size: 35))
.frame(width: 60, height: 60)
.background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255))
.cornerRadius(30.0)
.shadow(radius: 6)
}
}
}
}
}
}
}
Section {
Button() {
lastPublicUsers = publicUsers
publicUsers = []
username = ""
} label: {
HStack {
Text("Other User").font(.subheadline).fontWeight(.semibold)
Spacer()
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
.font(.system(size: 35))
.frame(width: 60, height: 60)
.background(Color(red: 98/255, green: 121/255, blue: 205/255))
.cornerRadius(30.0)
.shadow(radius: 6)
Section {
Button {
viewModel.publicUsers.removeAll()
viewModel.username = ""
} label: {
HStack {
Text("Other User").font(.subheadline).fontWeight(.semibold)
Spacer()
Image(systemName: "person.fill.questionmark")
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
.font(.system(size: 35))
.frame(width: 60, height: 60)
.background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255))
.cornerRadius(30.0)
.shadow(radius: 6)
}
}
}
}
} else {
Section(header: Text("Server Information")) {
TextField("Jellyfin Server URL", text: $viewModel.uri)
.disableAutocorrection(true)
.autocapitalization(.none)
Button {
viewModel.connectToServer()
} label: {
HStack {
Text("Connect")
Spacer()
}
if viewModel.isLoading {
ProgressView()
}
}
.disabled(viewModel.isLoading || viewModel.uri.isEmpty)
}
}
}
}.navigationTitle("Connect to Server")
.alert(isPresented: $serverSkippedAlert) {
Alert(title: Text("Error"), message: Text("Credentials have expired"), dismissButton: .default(Text("Sign in again")))
}
.onAppear(perform: start)
.alert(item: $viewModel.errorMessage) { _ in
Alert(title: Text("Error"), message: Text("message"), dismissButton: .default(Text("Try again")))
}
.onReceive(viewModel.$isLoggedIn, perform: { flag in
isLoggedIn = flag
})
.navigationTitle("Connect to Server")
}
}

View File

@ -1,236 +0,0 @@
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import KeychainSwift
import Nuke
import JellyfinAPI
import WidgetKit
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var orientationInfo: OrientationInfo
@EnvironmentObject var jsi: justSignedIn
@StateObject private var globalData = GlobalData()
@State private var needsToSelectServer = false
@State private var isLoading = false
@State private var tabSelection: String = "Home"
@State private var libraries: [String] = []
@State private var library_names: [String: String] = [:]
@State private var librariesShowRecentlyAdded: [String] = []
@State private var libraryPrefillID: String = ""
@State private var showSettingsPopover: Bool = false
@State private var viewDidLoad: Bool = false
@State private var loadState: Int = 2
@FetchRequest(entity: Server.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Server.name, ascending: true)])
var servers: FetchedResults<Server>
@FetchRequest(entity: SignedInUser.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \SignedInUser.username, ascending: true)])
var savedUsers: FetchedResults<SignedInUser>
private var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
func startup() {
if viewDidLoad == true {
return
}
let size = UIScreen.main.bounds.size
if size.width < size.height {
orientationInfo.orientation = .portrait
} else {
orientationInfo.orientation = .landscape
}
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
if servers.isEmpty {
isLoading = false
needsToSelectServer = true
} else {
isLoading = true
let savedUser = savedUsers[0]
let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
if keychain.get("AccessToken_\(savedUser.user_id ?? "")") != nil {
globalData.authToken = keychain.get("AccessToken_\(savedUser.user_id ?? "")") ?? ""
globalData.server = servers[0]
globalData.user = savedUser
}
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
var header = "MediaBrowser "
header.append("Client=\"SwiftFin\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(globalData.user.device_uuid ?? "")\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
header.append("Token=\"\(globalData.authToken)\"")
globalData.authHeader = header
JellyfinAPI.basePath = globalData.server.baseURI ?? ""
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
DispatchQueue.global(qos: .userInitiated).async {
UserAPI.getCurrentUser()
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
loadState = loadState - 1
}, receiveValue: { response in
libraries = response.configuration?.orderedViews ?? []
librariesShowRecentlyAdded = libraries.filter { element in
return !(response.configuration?.latestItemsExcludes?.contains(element))!
}
if loadState == 1 {
isLoading = false
viewDidLoad = true
}
})
.store(in: &globalData.pendingAPIRequests)
UserViewsAPI.getUserViews(userId: globalData.user.user_id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
loadState = loadState - 1
}, receiveValue: { response in
response.items?.forEach({ item in
library_names[item.id ?? ""] = item.name
})
if loadState == 1 {
isLoading = false
viewDidLoad = true
}
})
.store(in: &globalData.pendingAPIRequests)
}
let defaults = UserDefaults.standard
if defaults.integer(forKey: "InNetworkBandwidth") == 0 {
defaults.setValue(40_000_000, forKey: "InNetworkBandwidth")
}
if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 {
defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth")
}
}
WidgetCenter.shared.reloadAllTimelines()
}
var body: some View {
if needsToSelectServer == true || globalData.user == nil || globalData.server == nil {
NavigationView {
ConnectToServerView(isActive: $needsToSelectServer)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(globalData)
.onAppear(perform: startup)
} else if globalData.expiredCredentials == true {
NavigationView {
ConnectToServerView(skip_server: true, skip_server_prefill: globalData.server,
reauth_deviceId: globalData.user.device_uuid!, isActive: $globalData.expiredCredentials)
}
.navigationViewStyle(StackNavigationViewStyle())
.environmentObject(globalData)
.onAppear(perform: startup)
} else {
if !jsi.did {
if isLoading || globalData.user == nil || globalData.user.user_id == nil {
ProgressView()
.onAppear(perform: startup)
} else {
VStack {
TabView(selection: $tabSelection) {
NavigationView {
VStack(alignment: .leading) {
ScrollView {
Spacer().frame(height: orientationInfo.orientation == .portrait ? 0 : 16)
ContinueWatchingView()
NextUpView()
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: LazyView {
LibraryView(usingParentID: library_id,
title: library_names[library_id] ?? "", usingFilters: recentFilterSet)
}) {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
}
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
LatestMediaView(usingParentID: library_id)
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
}
.navigationTitle("Home")
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
showSettingsPopover = true
} label: {
Image(systemName: "gear")
}
}
}
.fullScreenCover(isPresented: $showSettingsPopover) {
SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover)
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Text("Home")
Image(systemName: "house")
}
.tag("Home")
NavigationView {
LibraryListView(libraries: library_names)
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Text("All Media")
Image(systemName: "folder")
}
.tag("All Media")
}
}
.environmentObject(globalData)
.onAppear(perform: startup)
.alert(isPresented: $globalData.networkError) {
Alert(title: Text("Network Error"), message: Text("An error occured while performing a network request"), dismissButton: .default(Text("Ok")))
}
}
} else {
Text("Please wait...")
.onAppear(perform: {
DispatchQueue.main.async { [self] in
_viewDidLoad.wrappedValue = false
sleep(1)
self.jsi.did = false
}
})
}
}
}
}

View File

@ -8,6 +8,7 @@
import SwiftUI
import JellyfinAPI
import Combine
struct ProgressBar: Shape {
func path(in rect: CGRect) -> Path {
@ -31,21 +32,7 @@ struct ProgressBar: Shape {
}
struct ContinueWatchingView: View {
@EnvironmentObject var globalData: GlobalData
@State private var items: [BaseItemDto] = []
func onAppear() {
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getResumeItems(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
items = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
}
}
var items: [BaseItemDto]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
@ -56,7 +43,7 @@ struct ContinueWatchingView: View {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height: 10)
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 320), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 320), bh: item.getBackdropImageBlurHash())
.frame(width: 320, height: 180)
.cornerRadius(10)
.overlay(
@ -96,6 +83,6 @@ struct ContinueWatchingView: View {
} else {
EmptyView()
}
}.onAppear(perform: onAppear)
}
}
}

View File

@ -7,10 +7,14 @@
import SwiftUI
import JellyfinAPI
import Combine
struct EpisodeItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
@StateObject
var tempViewModel = ViewModel()
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
@EnvironmentObject private var playbackInfo: VideoPlayerItem
var item: BaseItemDto
@ -20,19 +24,19 @@ struct EpisodeItemView: View {
didSet {
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
}
@ -43,26 +47,26 @@ struct EpisodeItemView: View {
didSet {
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -70,7 +74,7 @@ struct EpisodeItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
@ -150,7 +154,7 @@ struct EpisodeItemView: View {
var body: some View {
VStack(alignment: .leading) {
if orientationInfo.orientation == .portrait {
if hSizeClass == .compact && vSizeClass == .regular {
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) {
VStack(alignment: .leading) {
Spacer()
@ -190,7 +194,7 @@ struct EpisodeItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
@ -229,7 +233,7 @@ struct EpisodeItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -237,7 +241,7 @@ struct EpisodeItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
@ -361,7 +365,7 @@ struct EpisodeItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
@ -410,6 +414,9 @@ struct EpisodeItemView: View {
watched = item.userData?.played ?? false
settingState = false
})
.onRotate(perform: { orientation in
self.orientation = orientation
})
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(item.seriesName ?? "") - S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
.supportedOrientations(.allButUpsideDown)

View File

@ -0,0 +1,85 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import SwiftUI
struct HomeView: View {
@StateObject
var viewModel = HomeViewModel()
@State
private var orientation = UIDevice.current.orientation
@Environment(\.horizontalSizeClass)
var hSizeClass
@Environment(\.verticalSizeClass)
var vSizeClass
@State
var showingSettings = false
var body: some View {
ZStack {
ScrollView {
LazyVStack(alignment: .leading) {
Spacer().frame(height: hSizeClass == .compact && vSizeClass == .regular ? 0 : 16)
if !viewModel.resumeItems.isEmpty {
ContinueWatchingView(items: viewModel.resumeItems)
}
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)
}
if !viewModel.librariesShowRecentlyAddedIDs.isEmpty {
ForEach(viewModel.librariesShowRecentlyAddedIDs, id: \.self) { libraryID in
VStack(alignment: .leading) {
let library = viewModel.libraries.first(where: { $0.id == libraryID })
HStack {
Text("Latest \(library?.name ?? "")")
.font(.title2)
.fontWeight(.bold)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 16))
Spacer()
NavigationLink(destination: LazyView {
LibraryView(usingParentID: libraryID,
title: library?.name ?? "", usingFilters: viewModel.recentFilterSet)
}) {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
Image(systemName: "chevron.right").font(Font.subheadline.bold())
}
}
}.padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
LatestMediaView(usingParentID: libraryID)
}.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
}
Spacer().frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 20 : 30)
}
}
if viewModel.isLoading {
ProgressView()
}
}
.onRotate {
orientation = $0
}
.navigationTitle(MainTabView.Tab.home.localized)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
showingSettings = true
} label: {
Image(systemName: "gear")
}
}
}
.fullScreenCover(isPresented: $showingSettings) {
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
}
}
}

View File

@ -15,7 +15,6 @@ class VideoPlayerItem: ObservableObject {
}
struct ItemView: View {
@EnvironmentObject private var globalData: GlobalData
private var item: BaseItemDto
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()

View File

@ -14,36 +14,6 @@ extension UIDevice {
}
}
class OrientationInfo: ObservableObject {
enum Orientation {
case portrait
case landscape
}
@Published var orientation: Orientation = .portrait
private var _observer: NSObjectProtocol?
init() {
_observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [weak self] note in
guard let device = note.object as? UIDevice else {
return
}
if device.orientation.isPortrait {
self?.orientation = .portrait
} else if device.orientation.isLandscape {
self?.orientation = .landscape
}
}
}
deinit {
if let observer = _observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
extension View {
func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
@ -171,16 +141,13 @@ extension View {
@main
struct JellyfinPlayerApp: App {
let persistenceController = PersistenceController.shared
@StateObject private var jsi = justSignedIn()
var body: some Scene {
WindowGroup {
ContentView()
SplashView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(OrientationInfo())
.environmentObject(jsi)
.withHostingWindow { window in
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(OrientationInfo()).environmentObject(jsi))
window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext))
}
}
}

View File

@ -7,10 +7,12 @@
import SwiftUI
import JellyfinAPI
import Combine
struct LatestMediaView: View {
@EnvironmentObject var globalData: GlobalData
@StateObject
var tempViewModel = ViewModel()
@State var items: [BaseItemDto] = []
private var library_id: String = ""
@State private var viewDidLoad: Bool = false
@ -26,13 +28,13 @@ struct LatestMediaView: View {
viewDidLoad = true
DispatchQueue.global(qos: .userInitiated).async {
UserLibraryAPI.getLatestMedia(userId: globalData.user.user_id!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.userID!, parentId: library_id, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], enableUserData: true, limit: 12)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
items = response
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
@ -45,7 +47,7 @@ struct LatestMediaView: View {
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
Spacer().frame(height: 10)
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height: 5)

View File

@ -9,7 +9,6 @@ import SwiftUI
import JellyfinAPI
struct LibraryFilterView: View {
@EnvironmentObject var globalData: GlobalData
@Binding var filter: LibraryFilters
var body: some View {

View File

@ -9,52 +9,33 @@ import Foundation
import SwiftUI
struct LibraryListView: View {
@EnvironmentObject var globalData: GlobalData
@State var library_ids: [String] = ["favorites", "genres"]
@State var library_names: [String: String] = ["favorites": "Favorites", "genres": "Genres"]
var libraries: [String: String] = [:] // input libraries
var withFavorites: LibraryFilters = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
init(libraries: [String: String]) {
self.libraries = libraries
}
func onAppear() {
if library_ids.count == 2 {
libraries.forEach { k, v in
print("\(k): \(v)")
_library_ids.wrappedValue.append(k)
_library_names.wrappedValue[k] = v
}
}
}
@StateObject
var viewModel = LibraryListViewModel()
var body: some View {
List(library_ids, id: \.self) { key in
switch key {
case "favorites":
NavigationLink(destination: LazyView {
LibraryView(usingParentID: "", title: library_names[key] ?? "", usingFilters: withFavorites)
}) {
Text(library_names[key] ?? "")
}
case "genres":
NavigationLink(destination: LazyView {
EmptyView()
}) {
Text(library_names[key] ?? "")
}
default:
NavigationLink(destination: LazyView {
LibraryView(usingParentID: key, title: library_names[key] ?? "")
}) {
Text(library_names[key] ?? "")
}
List(viewModel.libraries, id: \.self) { library in
switch library.id {
case "favorites":
NavigationLink(destination: LazyView {
LibraryView(usingParentID: "", title: library.name ?? "", usingFilters: viewModel.withFavorites)
}) {
Text(library.name ?? "")
}
case "genres":
NavigationLink(destination: LazyView {
EmptyView()
}) {
Text(library.name ?? "")
}
default:
NavigationLink(destination: LazyView {
LibraryView(usingParentID: library.id ?? "", title: library.name ?? "")
}) {
Text(library.name ?? "")
}
}
}
.navigationTitle("All Media")
.onAppear(perform: onAppear)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
NavigationLink(destination: LazyView {

View File

@ -7,11 +7,12 @@
import SwiftUI
import JellyfinAPI
import Combine
struct LibrarySearchView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@StateObject
var tempViewModel = ViewModel()
@State private var items: [BaseItemDto] = []
@State private var searchQuery: String = ""
@State private var isLoading: Bool = false
@ -29,16 +30,15 @@ struct LibrarySearchView: View {
func requestSearch(query: String) {
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, limit: 60, recursive: true, searchTerm: query, sortOrder: [.ascending], parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], sortBy: ["SortName"], enableUserData: true, enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
items = response.items ?? []
isLoading = false
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
@ -68,7 +68,7 @@ struct LibrarySearchView: View {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.name ?? "")
@ -89,7 +89,7 @@ struct LibrarySearchView: View {
}
}
Spacer().frame(height: 16)
.onChange(of: orientationInfo.orientation) { _ in
.onRotate { _ in
recalcTracks()
}
}

View File

@ -9,11 +9,12 @@
import SwiftUI
import NukeUI
import JellyfinAPI
import Combine
struct LibraryView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@StateObject
var tempViewModel = ViewModel()
@State private var items: [BaseItemDto] = []
@State private var isLoading: Bool = false
@ -69,9 +70,9 @@ struct LibraryView: View {
items = []
DispatchQueue.global(qos: .userInitiated).async {
ItemsAPI.getItemsByUserId(userId: globalData.user.user_id!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
ItemsAPI.getItemsByUserId(userId: SessionManager.current.userID!, startIndex: currentPage * 100, limit: 100, recursive: true, searchTerm: nil, sortOrder: filters.sortOrder, parentId: (usingParentID != "" ? usingParentID : nil), fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], includeItemTypes: ["Movie", "Series"], filters: filters.filters, sortBy: filters.sortBy, enableUserData: true, personIds: (personId == "" ? nil : [personId]), studioIds: (studio == "" ? nil : [studio]), genreIds: (genre == "" ? nil : [genre]), enableImages: true)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
isLoading = false
}, receiveValue: { response in
let x = ceil(Double(response.totalRecordCount!) / 100.0)
@ -80,7 +81,7 @@ struct LibraryView: View {
isLoading = false
viewDidLoad = true
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
@ -107,7 +108,7 @@ struct LibraryView: View {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Text(item.name ?? "")
@ -126,7 +127,7 @@ struct LibraryView: View {
}.frame(width: 100)
}
}
}.onChange(of: orientationInfo.orientation) { _ in
}.onRotate { _ in
recalcTracks()
}
if totalPages > 1 {

View File

@ -0,0 +1,55 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import SwiftUI
struct MainTabView: View {
@State private var tabSelection: Tab = .home
var body: some View {
TabView(selection: $tabSelection) {
NavigationView {
HomeView()
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Text(Tab.home.localized)
Image(systemName: "house")
}
.tag(Tab.home)
NavigationView {
LibraryListView()
}
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Text(Tab.allMedia.localized)
Image(systemName: "folder")
}
.tag(Tab.allMedia)
}
}
}
extension MainTabView {
enum Tab: String {
case home
case allMedia
var localized: String {
switch self {
case .home:
return "Home"
case .allMedia:
return "All Media"
}
}
}
}

View File

@ -5,34 +5,44 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import Combine
import JellyfinAPI
import SwiftUI
struct MovieItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
@EnvironmentObject private var playbackInfo: VideoPlayerItem
@StateObject
var tempViewModel = ViewModel()
@State
private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass)
var hSizeClass
@Environment(\.verticalSizeClass)
var vSizeClass
@EnvironmentObject
private var playbackInfo: VideoPlayerItem
var item: BaseItemDto
@State private var settingState: Bool = true
@State private var watched: Bool = false {
@State
private var settingState: Bool = true
@State
private var watched: Bool = false {
didSet {
if !settingState {
if watched == true {
PlaystateAPI.markPlayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
} else {
PlaystateAPI.markUnplayedItem(userId: globalData.user.user_id!, itemId: item.id!)
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
}
@ -43,26 +53,29 @@ struct MovieItemView: View {
didSet {
if !settingState {
if favorite == true {
UserLibraryAPI.markFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
} else {
UserLibraryAPI.unmarkFavoriteItem(userId: globalData.user.user_id!, itemId: item.id!)
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { _ in
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
}
}
var portraitHeaderView: some View {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getBackdropImageBlurHash())
ImageView(src: item
.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!,
maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)),
bh: item.getBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -70,7 +83,7 @@ struct MovieItemView: View {
var portraitHeaderOverlayView: some View {
VStack(alignment: .leading) {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120))
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120))
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
@ -152,8 +165,11 @@ struct MovieItemView: View {
var body: some View {
VStack(alignment: .leading) {
if orientationInfo.orientation == .portrait {
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) {
if hSizeClass == .compact && vSizeClass == .regular {
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView,
overlayAlignment: .bottomLeading,
headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds
.width * 0.5625) {
VStack(alignment: .leading) {
Spacer()
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
@ -192,7 +208,9 @@ struct MovieItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person
.getImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100),
bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
@ -231,7 +249,8 @@ struct MovieItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200),
bh: item.getBackdropImageBlurHash())
.opacity(0.3)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -239,7 +258,8 @@ struct MovieItemView: View {
.blur(radius: 4)
HStack {
VStack {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120),
bh: item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 15)
@ -365,10 +385,14 @@ struct MovieItemView: View {
LibraryView(withPerson: person)
}) {
VStack {
ImageView(src: person.getImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: person.getBlurHash())
ImageView(src: person
.getImage(baseURL: ServerEnvironment.current.server.baseURI!,
maxWidth: 100),
bh: person.getBlurHash())
.frame(width: 100, height: 100)
.cornerRadius(10)
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
Text(person.name ?? "").font(.footnote).fontWeight(.regular)
.lineLimit(1)
.frame(width: 100).foregroundColor(Color.primary)
if person.role != "" {
Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1)
@ -414,6 +438,9 @@ struct MovieItemView: View {
watched = item.userData?.played ?? false
settingState = false
})
.onRotate {
orientation = $0
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(item.name ?? "")
.supportedOrientations(.allButUpsideDown)

View File

@ -6,30 +6,12 @@
*/
import SwiftUI
import Combine
import JellyfinAPI
struct NextUpView: View {
@EnvironmentObject var globalData: GlobalData
@State private var items: [BaseItemDto] = []
@State private var viewDidLoad: Bool = false
func onAppear() {
if viewDidLoad == true {
return
}
viewDidLoad = true
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getNextUp(userId: globalData.user.user_id!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
}, receiveValue: { response in
items = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
}
}
var items: [BaseItemDto]
var body: some View {
VStack(alignment: .leading) {
@ -44,7 +26,7 @@ struct NextUpView: View {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(item: item)) {
VStack(alignment: .leading) {
ImageView(src: item.getSeriesPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash())
ImageView(src: item.getSeriesPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: item.getSeriesPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
Spacer().frame(height: 5)
@ -67,7 +49,6 @@ struct NextUpView: View {
.frame(height: 200)
}
}
.onAppear(perform: onAppear)
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
}
}

View File

@ -6,14 +6,19 @@
*/
import SwiftUI
import Combine
import JellyfinAPI
struct SeasonItemView: View {
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var orientationInfo: OrientationInfo
@StateObject
var tempViewModel = ViewModel()
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@Environment(\.verticalSizeClass) var vSizeClass
var item: BaseItemDto = BaseItemDto()
@State private var episodes: [BaseItemDto] = []
@State private var isLoading: Bool = true
@State private var viewDidLoad: Bool = false
@ -28,15 +33,15 @@ struct SeasonItemView: View {
}
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: globalData.user.user_id!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.userID!, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], seasonId: item.id ?? "")
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
isLoading = false
}, receiveValue: { response in
viewDidLoad = true
episodes = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
@ -45,7 +50,7 @@ struct SeasonItemView: View {
if isLoading {
EmptyView()
} else {
ImageView(src: item.getSeriesBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash())
ImageView(src: item.getSeriesBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: UIDevice.current.userInterfaceIdiom == .pad ? 622 : Int(UIScreen.main.bounds.width)), bh: item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.blur(radius: 2.0)
}
@ -53,7 +58,7 @@ struct SeasonItemView: View {
var portraitHeaderOverlayView: some View {
HStack(alignment: .bottom, spacing: 12) {
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
VStack(alignment: .leading) {
@ -75,7 +80,7 @@ struct SeasonItemView: View {
@ViewBuilder
var innerBody: some View {
if orientationInfo.orientation == .portrait {
if hSizeClass == .compact && vSizeClass == .regular {
ParallaxHeaderScrollView(header: portraitHeaderView,
staticOverlayView: portraitHeaderOverlayView,
overlayAlignment: .bottomLeading,
@ -92,7 +97,7 @@ struct SeasonItemView: View {
ForEach(episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
HStack {
ImageView(src: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
ImageView(src: episode.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
.shadow(radius: 5)
.frame(width: 150, height: 90)
.cornerRadius(10)
@ -151,7 +156,7 @@ struct SeasonItemView: View {
} else {
GeometryReader { geometry in
ZStack {
ImageView(src: item.getSeriesBackdropImage(baseURL: globalData.server.baseURI!, maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash())
ImageView(src: item.getSeriesBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getSeriesBackdropImageBlurHash())
.opacity(0.4)
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
@ -160,7 +165,7 @@ struct SeasonItemView: View {
HStack {
VStack(alignment: .leading) {
Spacer().frame(height: 16)
ImageView(src: item.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 120), bh: item.getPrimaryImageBlurHash())
.frame(width: 120, height: 180)
.cornerRadius(10)
Spacer().frame(height: 4)
@ -185,7 +190,7 @@ struct SeasonItemView: View {
ForEach(episodes, id: \.id) { episode in
NavigationLink(destination: ItemView(item: episode)) {
HStack {
ImageView(src: episode.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
ImageView(src: episode.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
.shadow(radius: 5)
.frame(width: 150, height: 90)
.cornerRadius(10)
@ -244,15 +249,18 @@ struct SeasonItemView: View {
}
}
}
var body: some View {
if isLoading {
ProgressView()
.onAppear(perform: onAppear)
.onAppear(perform: onAppear)
} else {
innerBody
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(item.name ?? "") - \(item.seriesName ?? "")")
.onRotate {
orientation = $0
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("\(item.name ?? "") - \(item.seriesName ?? "")")
}
}
}

View File

@ -7,10 +7,11 @@
import SwiftUI
import JellyfinAPI
import Combine
struct SeriesItemView: View {
@EnvironmentObject private var globalData: GlobalData
@EnvironmentObject private var orientationInfo: OrientationInfo
@StateObject
var tempViewModel = ViewModel()
var item: BaseItemDto
@ -25,17 +26,18 @@ struct SeriesItemView: View {
}
isLoading = true
DispatchQueue.global(qos: .userInitiated).async {
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: globalData, completion: completion)
print(completion)
}, receiveValue: { response in
isLoading = false
viewDidLoad = true
seasons = response.items ?? []
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &tempViewModel.cancellables)
}
}
@ -60,7 +62,7 @@ struct SeriesItemView: View {
ForEach(seasons, id: \.id) { season in
NavigationLink(destination: ItemView(item: season)) {
VStack(alignment: .leading) {
ImageView(src: season.getPrimaryImage(baseURL: globalData.server.baseURI!, maxWidth: 100), bh: season.getPrimaryImageBlurHash())
ImageView(src: season.getPrimaryImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 100), bh: season.getPrimaryImageBlurHash())
.frame(width: 100, height: 150)
.cornerRadius(10)
.shadow(radius: 5)
@ -79,7 +81,7 @@ struct SeriesItemView: View {
}
}
Spacer().frame(height: 2)
}.onChange(of: orientationInfo.orientation) { _ in
}.onRotate { _ in
recalcTracks()
}
}

View File

@ -10,28 +10,25 @@ import SwiftUI
struct SettingsView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var globalData: GlobalData
@EnvironmentObject var jsi: justSignedIn
@ObservedObject var viewModel: SettingsViewModel
@Binding var close: Bool
@State private var inNetworkStreamBitrate: Int = 40_000_000
@State private var outOfNetworkStreamBitrate: Int = 40_000_000
@State private var autoSelectSubtitles: Bool = false
@State private var autoSelectSubtitlesLangcode: String = "none"
@State private var username: String = ""
func onAppear() {
let defaults = UserDefaults.standard
username = globalData.user.username!
username = SessionManager.current.user.username!
inNetworkStreamBitrate = defaults.integer(forKey: "InNetworkBandwidth")
outOfNetworkStreamBitrate = defaults.integer(forKey: "OutOfNetworkBandwidth")
autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles")
autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""
}
var body: some View {
NavigationView {
Form {
@ -44,7 +41,7 @@ struct SettingsView: View {
let defaults = UserDefaults.standard
defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth")
}
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
@ -54,7 +51,7 @@ struct SettingsView: View {
defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth")
}
}
Section(header: Text("Accessibility")) {
Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in
let defaults = UserDefaults.standard
@ -62,7 +59,7 @@ struct SettingsView: View {
})
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {}
}
Section {
HStack {
Text("Signed in as \(username)").foregroundColor(.primary)
@ -70,27 +67,28 @@ struct SettingsView: View {
Button {
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
}
globalData.server = Server()
globalData.user = SignedInUser()
globalData.authToken = ""
globalData.authHeader = ""
jsi.did = true
do {
try SessionManager.current.logout()
try ServerEnvironment.current.reset()
} catch {
print(error)
}
// TODO: This should redirect to the server selection screen
exit(-1)
} label: {

View File

@ -0,0 +1,26 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
struct SplashView: View {
@StateObject
var viewModel = SplashViewModel()
var body: some View {
if viewModel.isLoggedIn {
MainTabView()
} else {
NavigationView {
ConnectToServerView(isLoggedIn: $viewModel.isLoggedIn)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

View File

@ -9,6 +9,7 @@ import SwiftUI
import MobileVLCKit
import JellyfinAPI
import MediaPlayer
import Combine
struct Subtitle {
var name: String
@ -38,8 +39,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
weak var delegate: PlayerViewControllerDelegate?
var cancellables = Set<AnyCancellable>()
var mediaPlayer = VLCMediaPlayer()
var globalData = GlobalData()
@IBOutlet weak var timeText: UILabel!
@IBOutlet weak var videoContentView: UIView!
@ -281,20 +282,21 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Fetch max bitrate from UserDefaults depending on current connection mode
let defaults = UserDefaults.standard
let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
// globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth")
let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth")
// Build a device profile
let builder = DeviceProfileBuilder()
builder.setMaxBitrate(bitrate: maxBitrate)
let profile = builder.buildProfile()
let playbackInfo = PlaybackInfoDto(userId: globalData.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.userID!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
DispatchQueue.global(qos: .userInitiated).async { [self] in
delegate?.showLoadingView(self)
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: globalData.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.userID!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { [self] response in
playSessionId = response.playSessionId ?? ""
@ -306,7 +308,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let mediaSource = response.mediaSources!.first.self!
if mediaSource.transcodingUrl != nil {
// Item is being transcoded by request of server
let streamURL = URL(string: "\(globalData.server.baseURI!)\(mediaSource.transcodingUrl!)")
let streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(mediaSource.transcodingUrl!)")
let item = PlaybackItem()
item.videoType = .transcode
item.videoUrl = streamURL!
@ -319,7 +321,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
if stream.type == .subtitle {
var deliveryUrl: URL?
if stream.deliveryMethod == .external {
deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")!
} else {
deliveryUrl = nil
}
@ -346,7 +348,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
playbackItem = item
} else {
// Item will be directly played by the client.
let streamURL: URL = URL(string: "\(globalData.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(globalData.user.device_uuid!)&api_key=\(globalData.authToken)&Tag=\(mediaSource.eTag!)")!
let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.authToken)&Tag=\(mediaSource.eTag!)")!
let item = PlaybackItem()
item.videoUrl = streamURL
@ -360,7 +362,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
if stream.type == .subtitle {
var deliveryUrl: URL?
if stream.deliveryMethod == .external {
deliveryUrl = URL(string: "\(globalData.server.baseURI!)\(stream.deliveryUrl!)")!
deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")!
} else {
deliveryUrl = nil
}
@ -416,7 +418,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
mediaPlayer.pause()
mediaPlayer.play()
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
}
@ -521,12 +523,12 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { _ in
print("Playback progress report sent!")
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
}
@ -534,12 +536,12 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { _ in
print("Playback stop report sent!")
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
func sendPlayReport() {
@ -548,19 +550,18 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
.sink(receiveCompletion: { completion in
HandleAPIRequestCompletion(globalData: self.globalData, completion: completion)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { _ in
print("Playback start report sent!")
})
.store(in: &globalData.pendingAPIRequests)
.store(in: &cancellables)
}
}
struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: BaseItemDto
@Environment(\.presentationMode) var presentationMode
@EnvironmentObject private var globalData: GlobalData
var loadBinding: Binding<Bool>
var pBinding: Binding<Bool>
@ -597,7 +598,6 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
customViewController.manifest = item
customViewController.delegate = context.coordinator
customViewController.globalData = globalData
return customViewController
}

View File

@ -0,0 +1,33 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
// https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation
import Foundation
import SwiftUI
// Our custom view modifier to track rotation and
// call our action
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
// A View wrapper to make the modifier easier to use
extension View {
func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}

View File

@ -1,27 +0,0 @@
/* JellyfinPlayer/Swiftfin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import Combine
import JellyfinAPI
func HandleAPIRequestCompletion(globalData: GlobalData, completion: Subscribers.Completion<Error>) {
switch completion {
case .finished:
break
case .failure(let error):
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
globalData.expiredCredentials = true
case .error:
globalData.networkError = true
}
}
break
}
}

View File

@ -0,0 +1,59 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Combine
import CoreData
import Foundation
import JellyfinAPI
final class ServerEnvironment {
static let current = ServerEnvironment()
fileprivate(set) var server: Server!
init() {
let serverRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Server")
let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server]
server = servers?.first
guard let baseURI = server?.baseURI else { return }
JellyfinAPI.basePath = baseURI
}
func setUp(with uri: String) -> AnyPublisher<Server, Error> {
var uri = uri
if !uri.contains("http") {
uri = "https://" + uri
}
if uri.last == "/" {
uri = String(uri.dropLast())
}
JellyfinAPI.basePath = uri
return SystemAPI.getPublicSystemInfo()
.map { response in
let server = Server(context: PersistenceController.shared.container.viewContext)
server.baseURI = uri
server.name = response.serverName
server.server_id = response.id
return server
}
.handleEvents(receiveOutput: { [unowned self] response in
server = response
_ = try? PersistenceController.shared.container.viewContext.save()
}).eraseToAnyPublisher()
}
func reset() throws {
JellyfinAPI.basePath = ""
server = nil
let serverRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: serverRequest)
try PersistenceController.shared.container.viewContext.execute(deleteRequest)
}
}

View File

@ -0,0 +1,108 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Combine
import CoreData
import Foundation
import JellyfinAPI
import KeychainSwift
import UIKit
final class SessionManager {
static let current = SessionManager()
fileprivate(set) var user: SignedInUser!
fileprivate(set) var authHeader: String!
fileprivate(set) var authToken: String!
fileprivate(set) var deviceID: String
var userID: String? {
user?.user_id
}
init() {
let savedUserRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "SignedInUser")
let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) as? [SignedInUser]
user = savedUsers?.first
let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
if let deviceID = keychain.get("DeviceID") {
self.deviceID = deviceID
} else {
self.deviceID = UUID().uuidString
keychain.set(deviceID, forKey: "DeviceID")
}
guard let authToken = keychain.get("AccessToken_\(user?.user_id ?? "")") else {
return
}
updateHeader(with: authToken)
}
fileprivate func updateHeader(with authToken: String?) {
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
var header = "MediaBrowser "
header.append("Client=\"SwiftFin\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(deviceID)\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
if let token = authToken {
self.authToken = token
header.append("Token=\"\(token)\"")
}
authHeader = header
JellyfinAPI.customHeaders["X-Emby-Authorization"] = authHeader
}
func login(username: String, password: String) -> AnyPublisher<SignedInUser, Error> {
updateHeader(with: nil)
return UserAPI.authenticateUserByName(authenticateUserByName: AuthenticateUserByName(username: username, pw: password))
.map { [unowned self] response -> (SignedInUser, String?) in
let user = SignedInUser(context: PersistenceController.shared.container.viewContext)
user.device_uuid = deviceID
user.username = response.user?.name
user.user_id = response.user?.id
return (user, response.accessToken)
}
.handleEvents(receiveOutput: { [unowned self] response, accessToken in
user = response
_ = try? PersistenceController.shared.container.viewContext.save()
if let userID = user.user_id,
let token = accessToken
{
let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
keychain.set(token, forKey: "AccessToken_\(userID)")
}
updateHeader(with: accessToken)
})
.map(\.0)
.eraseToAnyPublisher()
}
func logout() throws {
let keychain = KeychainSwift()
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
keychain.delete("AccessToken_\(user.user_id ?? "")")
JellyfinAPI.customHeaders["X-Emby-Authorization"] = nil
user = nil
authHeader = nil
let userRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: userRequest)
try PersistenceController.shared.container.viewContext.execute(deleteRequest)
}
}

View File

@ -22,28 +22,3 @@ public enum SortBy: String, Codable, CaseIterable {
case name = "SortName"
case dateAdded = "DateCreated"
}
class justSignedIn: ObservableObject {
@Published var did: Bool = false
}
class GlobalData: ObservableObject {
@Published var user: SignedInUser!
@Published var authToken: String = ""
@Published var server: Server!
@Published var authHeader: String = ""
@Published var isInNetwork: Bool = true
@Published var networkError: Bool = false
@Published var expiredCredentials: Bool = false
var pendingAPIRequests = Set<AnyCancellable>()
}
extension GlobalData: Equatable {
static func == (lhs: GlobalData, rhs: GlobalData) -> Bool {
lhs.user == rhs.user
&& lhs.authToken == rhs.authToken
&& lhs.server == rhs.server
&& lhs.authHeader == rhs.authHeader
}
}

View File

@ -0,0 +1,88 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Combine
import Foundation
import JellyfinAPI
final class ConnectToServerViewModel: ViewModel {
@Published
var publicUsers = [UserDto]()
@Published
var isConnectedServer = false
@Published
var isLoggedIn = false
@Published
var uri = ""
@Published
var username = ""
@Published
var password = ""
override init() {
super.init()
refresh()
}
func refresh() {
if ServerEnvironment.current.server != nil {
UserAPI.getPublicUsers()
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure:
self.isConnectedServer = false
}
}, receiveValue: { response in
self.publicUsers = response
self.isConnectedServer = true
})
.store(in: &cancellables)
}
}
func connectToServer() {
ServerEnvironment.current.setUp(with: uri)
.sink(receiveCompletion: { result in
switch result {
case let .failure(error):
self.errorMessage = error.localizedDescription
default:
break
}
}, receiveValue: { response in
guard response.server_id != nil else {
return
}
self.isConnectedServer = true
})
.store(in: &cancellables)
}
func login() {
SessionManager.current.login(username: username, password: password)
.sink(receiveCompletion: { result in
switch result {
case let .failure(error):
self.errorMessage = error.localizedDescription
default:
break
}
}, receiveValue: { response in
guard response.user_id != nil else {
return
}
self.isLoggedIn = true
})
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,78 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import ActivityIndicator
import Combine
import Foundation
import JellyfinAPI
final class HomeViewModel: ViewModel {
@Published
var librariesShowRecentlyAddedIDs = [String]()
@Published
var libraries = [BaseItemDto]()
@Published
var resumeItems = [BaseItemDto]()
@Published
var nextUpItems = [BaseItemDto]()
// temp
var recentFilterSet: LibraryFilters = LibraryFilters(filters: [], sortOrder: [.descending], sortBy: ["DateCreated"])
override init() {
super.init()
refresh()
}
func refresh() {
UserAPI.getCurrentUser()
.trackActivity(loading)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
let libraries = response.configuration?.orderedViews ?? []
self.librariesShowRecentlyAddedIDs = libraries.filter { element in
!(response.configuration?.latestItemsExcludes?.contains(element))!
}
})
.store(in: &cancellables)
UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "")
.trackActivity(loading)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
self.libraries = response.items ?? []
})
.store(in: &cancellables)
ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],
mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
.trackActivity(loading)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
self.resumeItems = response.items ?? []
})
.store(in: &cancellables)
TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { response in
self.nextUpItems = response.items ?? []
})
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,38 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
final class LibraryListViewModel: ViewModel {
@Published
var libraries = [BaseItemDto]()
// temp
var withFavorites = LibraryFilters(filters: [.isFavorite], sortOrder: [], withGenres: [], sortBy: [])
override init() {
super.init()
libraries.append(.init(name: "Favorites", id: "favorites"))
libraries.append(.init(name: "Genres", id: "genres"))
refresh()
}
func refresh() {
UserViewsAPI.getUserViews(userId: SessionManager.current.userID ?? "")
.trackActivity(loading)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { response in
self.libraries.append(contentsOf: response.items ?? [])
})
.store(in: &cancellables)
}
}

View File

@ -0,0 +1,37 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import Combine
import Nuke
import WidgetKit
final class SplashViewModel: ViewModel {
@Published
var isLoggedIn: Bool
override init() {
isLoggedIn = ServerEnvironment.current.server != nil && SessionManager.current.user != nil
super.init()
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
WidgetCenter.shared.reloadAllTimelines()
let defaults = UserDefaults.standard
if defaults.integer(forKey: "InNetworkBandwidth") == 0 {
defaults.setValue(40_000_000, forKey: "InNetworkBandwidth")
}
if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 {
defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth")
}
}
}

View File

@ -0,0 +1,33 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Combine
import Foundation
import ActivityIndicator
typealias ErrorMessage = String
extension ErrorMessage: Identifiable {
public var id: String {
self
}
}
class ViewModel: ObservableObject {
var cancellables = Set<AnyCancellable>()
@Published
var isLoading = true
let loading = ActivityIndicator()
@Published
var errorMessage: ErrorMessage?
init() {
loading.loading.assign(to: \.isLoading, on: self).store(in: &cancellables)
}
}

View File

@ -25,18 +25,17 @@ struct NextUpWidgetProvider: TimelineProvider {
func getSnapshot(in context: Context, completion: @escaping (NextUpEntry) -> Void) {
let currentDate = Date()
WidgetEnvironment.shared.update()
guard let server = WidgetEnvironment.shared.server else { return
guard let server = ServerEnvironment.current.server else { return
DispatchQueue.main.async {
completion(NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyServer))
}
}
guard let savedUser = WidgetEnvironment.shared.user else { return
guard let savedUser = SessionManager.current.user else { return
DispatchQueue.main.async {
completion(NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyUser))
}
}
guard let header = WidgetEnvironment.shared.header else { return
guard let header = SessionManager.current.authHeader else { return
DispatchQueue.main.async {
completion(NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyHeader))
}
@ -81,20 +80,19 @@ struct NextUpWidgetProvider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
WidgetEnvironment.shared.update()
guard let server = WidgetEnvironment.shared.server else { return
guard let server = ServerEnvironment.current.server else { return
DispatchQueue.main.async {
completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyServer)],
policy: .after(entryDate)))
}
}
guard let savedUser = WidgetEnvironment.shared.user else { return
guard let savedUser = SessionManager.current.user else { return
DispatchQueue.main.async {
completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyUser)],
policy: .after(entryDate)))
}
}
guard let header = WidgetEnvironment.shared.header else { return
guard let header = SessionManager.current.authHeader else { return
DispatchQueue.main.async {
completion(Timeline(entries: [NextUpEntry(date: currentDate, items: [], error: WidgetError.emptyHeader)],
policy: .after(entryDate)))

View File

@ -1,56 +0,0 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import CoreData
import KeychainSwift
import UIKit
final class WidgetEnvironment {
static let shared = WidgetEnvironment()
var server: Server?
var user: SignedInUser?
var header: String?
init() {
update()
}
func update() {
let serverRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Server")
let servers = try? PersistenceController.shared.container.viewContext.fetch(serverRequest) as? [Server]
let savedUserRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "SignedInUser")
let savedUsers = try? PersistenceController.shared.container.viewContext.fetch(savedUserRequest) as? [SignedInUser]
server = servers?.first
user = savedUsers?.first
let keychain = KeychainSwift()
// need prefix
keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain"
guard let authToken = keychain.get("AccessToken_\(user?.user_id ?? "")") else {
return
}
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
var deviceName = UIDevice.current.name
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
deviceName = deviceName.removeRegexMatches(pattern: "[^\\w\\s]")
var header = "MediaBrowser "
header.append("Client=\"SwiftFin\", ")
header.append("Device=\"\(deviceName)\", ")
header.append("DeviceId=\"\(user?.device_uuid ?? "")\", ")
header.append("Version=\"\(appVersion ?? "0.0.1")\", ")
header.append("Token=\"\(authToken)\"")
self.header = header
}
}