Merge pull request #64 from PangMo5/PangMo5/refactoring
Structural improvements - 1
This commit is contained in:
commit
3b0429deb3
|
@ -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" */;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -9,7 +9,6 @@ import SwiftUI
|
|||
import JellyfinAPI
|
||||
|
||||
struct LibraryFilterView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@Binding var filter: LibraryFilters
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ?? "")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue