initial screens Refactoring
remove ContentView add HomeView add MainTabView add SplashView add ConnectToServerViewModel add HomeViewModel add LibraryListViewModel add SplashViewModel add ViewModel
This commit is contained in:
parent
f1138b50f2
commit
94aa3bc4b4
|
@ -42,7 +42,6 @@
|
||||||
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||||
5364F456266CA0DC0026ECBA /* 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 */; };
|
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 */; };
|
5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; };
|
||||||
5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBFB263B596B003A4E83 /* Preview 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 */; };
|
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||||
|
@ -70,6 +69,15 @@
|
||||||
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
|
||||||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.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 */; };
|
||||||
6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
6267B3D42671024A00A7371D /* APIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* APIExtensions.swift */; };
|
||||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
||||||
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
6267B3D726710B9700A7371D /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6267B3D526710B8900A7371D /* CollectionExtensions.swift */; };
|
||||||
|
@ -177,7 +185,6 @@
|
||||||
5364F454266CA0DC0026ECBA /* APIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExtensions.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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
5377CBFD263B596B003A4E83 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -206,6 +213,14 @@
|
||||||
621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
||||||
6267B3D526710B8900A7371D /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; 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>"; };
|
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; };
|
628B95202670CABD0091AF3B /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -242,6 +257,7 @@
|
||||||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||||
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
|
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
|
||||||
621C638026672A30004216EA /* NukeUI in Frameworks */,
|
621C638026672A30004216EA /* NukeUI in Frameworks */,
|
||||||
|
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
|
||||||
53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */,
|
53A431BD266B0FF20016769F /* JellyfinAPI in Frameworks */,
|
||||||
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */,
|
62EC3527267665D8000E9F2D /* MobileVLCKit.xcframework in Frameworks */,
|
||||||
);
|
);
|
||||||
|
@ -266,6 +282,11 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */,
|
5321753A2671BCFC005491E6 /* SettingsViewModel.swift */,
|
||||||
|
625CB5692678B71200530A6E /* SplashViewModel.swift */,
|
||||||
|
625CB5722678C32A00530A6E /* HomeViewModel.swift */,
|
||||||
|
625CB5742678C33500530A6E /* LibraryListViewModel.swift */,
|
||||||
|
625CB5762678C34300530A6E /* ConnectToServerViewModel.swift */,
|
||||||
|
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = ViewModel;
|
path = ViewModel;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -338,10 +359,10 @@
|
||||||
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
625CB56D2678C1C400530A6E /* ViewModels */,
|
||||||
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
|
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
|
||||||
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
||||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||||
5377CBF6263B596A003A4E83 /* ContentView.swift */,
|
|
||||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
||||||
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
|
53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */,
|
||||||
53987CA72657424A00E7EA70 /* EpisodeItemView.swift */,
|
53987CA72657424A00E7EA70 /* EpisodeItemView.swift */,
|
||||||
|
@ -366,6 +387,9 @@
|
||||||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
|
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
|
||||||
53DE4BD1267098F300739748 /* SearchBarView.swift */,
|
53DE4BD1267098F300739748 /* SearchBarView.swift */,
|
||||||
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
531AC8BE26750DE20091C7EB /* ImageView.swift */,
|
||||||
|
625CB5672678B6FB00530A6E /* SplashView.swift */,
|
||||||
|
625CB56B2678C0FD00530A6E /* MainTabView.swift */,
|
||||||
|
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||||
);
|
);
|
||||||
path = JellyfinPlayer;
|
path = JellyfinPlayer;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -406,6 +430,13 @@
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
625CB56D2678C1C400530A6E /* ViewModels */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
path = ViewModels;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
628B95252670CABD0091AF3B /* WidgetExtension */ = {
|
628B95252670CABD0091AF3B /* WidgetExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -485,6 +516,7 @@
|
||||||
53352570265EA0A0006CCA86 /* Introspect */,
|
53352570265EA0A0006CCA86 /* Introspect */,
|
||||||
621C637F26672A30004216EA /* NukeUI */,
|
621C637F26672A30004216EA /* NukeUI */,
|
||||||
53A431BC266B0FF20016769F /* JellyfinAPI */,
|
53A431BC266B0FF20016769F /* JellyfinAPI */,
|
||||||
|
625CB5792678C4A400530A6E /* ActivityIndicator */,
|
||||||
);
|
);
|
||||||
productName = JellyfinPlayer;
|
productName = JellyfinPlayer;
|
||||||
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
||||||
|
@ -549,6 +581,7 @@
|
||||||
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
5335256F265EA0A0006CCA86 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */,
|
621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */,
|
||||||
53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
53A431BB266B0FF20016769F /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
|
||||||
|
625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -626,16 +659,20 @@
|
||||||
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
5364F455266CA0DC0026ECBA /* APIExtensions.swift in Sources */,
|
||||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
||||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||||
|
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||||
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */,
|
53AD124E26702B8A0094A276 /* SeasonItemView.swift in Sources */,
|
||||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||||
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
|
53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */,
|
||||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
||||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||||
|
625CB56F2678C23300530A6E /* HomeView.swift in Sources */,
|
||||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||||
|
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
|
||||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||||
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
5321753B2671BCFC005491E6 /* SettingsViewModel.swift in Sources */,
|
||||||
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
|
532175402671EE4F005491E6 /* LibraryFilterView.swift in Sources */,
|
||||||
|
@ -643,23 +680,26 @@
|
||||||
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */,
|
||||||
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
|
6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */,
|
||||||
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */,
|
||||||
|
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
|
||||||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
||||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||||
|
625CB57C2678CE1000530A6E /* ViewModel.swift in Sources */,
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */,
|
||||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||||
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
||||||
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
|
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
|
||||||
5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */,
|
|
||||||
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
|
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
|
||||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||||
|
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */,
|
||||||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||||
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
|
53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */,
|
||||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||||
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
53892772263C8C6F0035E14B /* LoadingView.swift in Sources */,
|
||||||
|
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -1065,6 +1105,14 @@
|
||||||
version = 0.3.0;
|
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 */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
@ -1108,6 +1156,11 @@
|
||||||
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
||||||
productName = NukeUI;
|
productName = NukeUI;
|
||||||
};
|
};
|
||||||
|
625CB5792678C4A400530A6E /* ActivityIndicator */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
|
||||||
|
productName = ActivityIndicator;
|
||||||
|
};
|
||||||
628B95322670CAEA0091AF3B /* NukeUI */ = {
|
628B95322670CAEA0091AF3B /* NukeUI */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
{
|
{
|
||||||
"object": {
|
"object": {
|
||||||
"pins": [
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "ActivityIndicator",
|
||||||
|
"repositoryURL": "https://github.com/duyquang91/ActivityIndicator",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "0101a02196f6a67cf26f6434b007d3db6bd07fee",
|
||||||
|
"version": "1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "AnyCodable",
|
"package": "AnyCodable",
|
||||||
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
|
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
|
||||||
|
|
|
@ -5,259 +5,46 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import KeychainSwift
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import KeychainSwift
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@StateObject
|
||||||
@EnvironmentObject var jsi: justSignedIn
|
var viewModel = ConnectToServerViewModel()
|
||||||
|
|
||||||
@State private var uri = ""
|
@Binding
|
||||||
@State private var isWorking = false
|
var isLoggedIn: Bool
|
||||||
@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)
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
ZStack {
|
||||||
if !isConnected {
|
Form {
|
||||||
Section(header: Text("Server Information")) {
|
if viewModel.isConnectedServer {
|
||||||
TextField("Jellyfin Server URL", text: $uri)
|
if viewModel.publicUsers.isEmpty {
|
||||||
.disableAutocorrection(true)
|
Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) {
|
||||||
.autocapitalization(.none)
|
TextField("Username", text: $viewModel.username)
|
||||||
Button {
|
.disableAutocorrection(true)
|
||||||
isWorking = true
|
.autocapitalization(.none)
|
||||||
if !uri.contains("http") {
|
SecureField("Password", text: $viewModel.password)
|
||||||
uri = "https://" + uri
|
.disableAutocorrection(true)
|
||||||
}
|
.autocapitalization(.none)
|
||||||
if uri.last == "/" {
|
Button {
|
||||||
uri = String(uri.dropLast())
|
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 {
|
Section {
|
||||||
Button {
|
Button {
|
||||||
serverSkippedAlert = false
|
viewModel.isConnectedServer = false
|
||||||
server_id = ""
|
|
||||||
serverName = ""
|
|
||||||
isConnected = false
|
|
||||||
serverSkipped = false
|
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -269,85 +56,85 @@ struct ConnectToServerView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Section {
|
Section(header: Text("Login to \(ServerEnvironment.current.server.name ?? "")")) {
|
||||||
Button {
|
ForEach(viewModel.publicUsers, id: \.id) { publicUser in
|
||||||
publicUsers = lastPublicUsers
|
|
||||||
usernameDisabled = false
|
|
||||||
} label: {
|
|
||||||
HStack {
|
HStack {
|
||||||
HStack {
|
Button(action: {
|
||||||
Image(systemName: "chevron.left")
|
viewModel.username = publicUser.name ?? ""
|
||||||
Text("Back")
|
viewModel.publicUsers.removeAll()
|
||||||
}
|
if !(publicUser.hasPassword ?? true) {
|
||||||
Spacer()
|
viewModel.password = ""
|
||||||
}
|
viewModel.login()
|
||||||
}
|
}
|
||||||
}
|
}) {
|
||||||
}
|
HStack {
|
||||||
} else {
|
Text(publicUser.name ?? "").font(.subheadline).fontWeight(.semibold)
|
||||||
Section(header: Text("\(serverSkipped ? "Reauthenticate" : "Login") to \(serverName)")) {
|
Spacer()
|
||||||
ForEach(publicUsers, id: \.id) { publicUser in
|
if publicUser.primaryImageTag != nil {
|
||||||
HStack {
|
ImageView(src: URL(string: "\(ServerEnvironment.current.server.baseURI ?? "")/Users/\(publicUser.id ?? "")/Images/Primary?width=200&quality=80&tag=\(publicUser.primaryImageTag!)")!)
|
||||||
Button() {
|
.frame(width: 60, height: 60)
|
||||||
if publicUser.hasPassword ?? true {
|
.cornerRadius(30.0)
|
||||||
lastPublicUsers = publicUsers
|
} else {
|
||||||
username = publicUser.name ?? ""
|
Image(systemName: "person.fill")
|
||||||
usernameDisabled = true
|
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
||||||
publicUsers = []
|
.font(.system(size: 35))
|
||||||
} else {
|
.frame(width: 60, height: 60)
|
||||||
publicUsers = []
|
.background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255))
|
||||||
password = ""
|
.cornerRadius(30.0)
|
||||||
username = publicUser.name ?? ""
|
.shadow(radius: 6)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button() {
|
Button {
|
||||||
lastPublicUsers = publicUsers
|
viewModel.publicUsers.removeAll()
|
||||||
publicUsers = []
|
viewModel.username = ""
|
||||||
username = ""
|
} label: {
|
||||||
} label: {
|
HStack {
|
||||||
HStack {
|
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
||||||
Text("Other User").font(.subheadline).fontWeight(.semibold)
|
Spacer()
|
||||||
Spacer()
|
Image(systemName: "person.fill.questionmark")
|
||||||
Image(systemName: "person.fill.questionmark")
|
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
||||||
.foregroundColor(Color(red: 1, green: 1, blue: 1).opacity(0.8))
|
.font(.system(size: 35))
|
||||||
.font(.system(size: 35))
|
.frame(width: 60, height: 60)
|
||||||
.frame(width: 60, height: 60)
|
.background(Color(red: 98 / 255, green: 121 / 255, blue: 205 / 255))
|
||||||
.background(Color(red: 98/255, green: 121/255, blue: 205/255))
|
.cornerRadius(30.0)
|
||||||
.cornerRadius(30.0)
|
.shadow(radius: 6)
|
||||||
.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,231 +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 jsi: justSignedIn
|
|
||||||
|
|
||||||
@State private var orientation = UIDeviceOrientation.unknown
|
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = ServerEnvironment.current.server.baseURI ?? ""
|
|
||||||
JellyfinAPI.customHeaders = ["X-Emby-Authorization": globalData.authHeader]
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
UserAPI.getCurrentUser()
|
|
||||||
.sink(receiveCompletion: { completion in
|
|
||||||
print(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: SessionManager.current.userID ?? "")
|
|
||||||
.sink(receiveCompletion: { completion in
|
|
||||||
print(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 || SessionManager.current.userID == nil {
|
|
||||||
ProgressView()
|
|
||||||
.onAppear(perform: startup)
|
|
||||||
} else {
|
|
||||||
VStack {
|
|
||||||
TabView(selection: $tabSelection) {
|
|
||||||
NavigationView {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
ScrollView {
|
|
||||||
Spacer().frame(height: 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")))
|
|
||||||
}
|
|
||||||
.onRotate { orientation in
|
|
||||||
self.orientation = orientation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("Please wait...")
|
|
||||||
.onAppear(perform: {
|
|
||||||
DispatchQueue.main.async { [self] in
|
|
||||||
_viewDidLoad.wrappedValue = false
|
|
||||||
sleep(1)
|
|
||||||
self.jsi.did = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -32,21 +32,7 @@ struct ProgressBar: Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContinueWatchingView: View {
|
struct ContinueWatchingView: View {
|
||||||
@State private var items: [BaseItemDto] = []
|
var items: [BaseItemDto]
|
||||||
|
|
||||||
func onAppear() {
|
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
ItemsAPI.getResumeItems(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], mediaTypes: ["Video"], imageTypeLimit: 1, enableImageTypes: [.primary, .backdrop, .thumb])
|
|
||||||
.sink(receiveCompletion: { completion in
|
|
||||||
print(completion)
|
|
||||||
}, receiveValue: { response in
|
|
||||||
items = response.items ?? []
|
|
||||||
})
|
|
||||||
.store(in: &tempCancellables)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
@ -97,6 +83,6 @@ struct ContinueWatchingView: View {
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}.onAppear(perform: onAppear)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,11 @@ import JellyfinAPI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
struct EpisodeItemView: View {
|
struct EpisodeItemView: View {
|
||||||
|
@StateObject
|
||||||
|
var tempViewModel = ViewModel()
|
||||||
@State private var orientation = UIDeviceOrientation.unknown
|
@State private var orientation = UIDeviceOrientation.unknown
|
||||||
|
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||||
|
@Environment(\.verticalSizeClass) var vSizeClass
|
||||||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
||||||
|
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
|
@ -18,7 +22,6 @@ struct EpisodeItemView: View {
|
||||||
@State private var settingState: Bool = true
|
@State private var settingState: Bool = true
|
||||||
@State private var watched: Bool = false {
|
@State private var watched: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
if !settingState {
|
if !settingState {
|
||||||
if watched == true {
|
if watched == true {
|
||||||
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
|
@ -26,14 +29,14 @@ struct EpisodeItemView: View {
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
} else {
|
} else {
|
||||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +45,6 @@ struct EpisodeItemView: View {
|
||||||
@State
|
@State
|
||||||
private var favorite: Bool = false {
|
private var favorite: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
if !settingState {
|
if !settingState {
|
||||||
if favorite == true {
|
if favorite == true {
|
||||||
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
|
@ -50,14 +52,14 @@ struct EpisodeItemView: View {
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
} else {
|
} else {
|
||||||
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +154,7 @@ struct EpisodeItemView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if 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) {
|
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView, overlayAlignment: .bottomLeading, headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds.width * 0.5625) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -141,15 +141,13 @@ extension View {
|
||||||
@main
|
@main
|
||||||
struct JellyfinPlayerApp: App {
|
struct JellyfinPlayerApp: App {
|
||||||
let persistenceController = PersistenceController.shared
|
let persistenceController = PersistenceController.shared
|
||||||
@StateObject private var jsi = justSignedIn()
|
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
SplashView()
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.environmentObject(jsi)
|
|
||||||
.withHostingWindow { window in
|
.withHostingWindow { window in
|
||||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: ContentView().environment(\.managedObjectContext, persistenceController.container.viewContext).environmentObject(jsi))
|
window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Combine
|
||||||
|
|
||||||
struct LatestMediaView: View {
|
struct LatestMediaView: View {
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
var tempViewModel = ViewModel()
|
||||||
@State var items: [BaseItemDto] = []
|
@State var items: [BaseItemDto] = []
|
||||||
private var library_id: String = ""
|
private var library_id: String = ""
|
||||||
@State private var viewDidLoad: Bool = false
|
@State private var viewDidLoad: Bool = false
|
||||||
|
@ -24,8 +26,6 @@ struct LatestMediaView: View {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
viewDidLoad = true
|
viewDidLoad = true
|
||||||
|
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.userID!, 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)
|
||||||
|
@ -34,7 +34,7 @@ struct LatestMediaView: View {
|
||||||
}, receiveValue: { response in
|
}, receiveValue: { response in
|
||||||
items = response
|
items = response
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,51 +9,33 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryListView: View {
|
struct LibraryListView: View {
|
||||||
|
@StateObject
|
||||||
@State var library_ids: [String] = ["favorites", "genres"]
|
var viewModel = LibraryListViewModel()
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List(library_ids, id: \.self) { key in
|
List(viewModel.libraries, id: \.self) { library in
|
||||||
switch key {
|
switch library.id {
|
||||||
case "favorites":
|
case "favorites":
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
LibraryView(usingParentID: "", title: library_names[key] ?? "", usingFilters: withFavorites)
|
LibraryView(usingParentID: "", title: library.name ?? "", usingFilters: viewModel.withFavorites)
|
||||||
}) {
|
}) {
|
||||||
Text(library_names[key] ?? "")
|
Text(library.name ?? "")
|
||||||
}
|
}
|
||||||
case "genres":
|
case "genres":
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}) {
|
}) {
|
||||||
Text(library_names[key] ?? "")
|
Text(library.name ?? "")
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
LibraryView(usingParentID: key, title: library_names[key] ?? "")
|
LibraryView(usingParentID: library.id ?? "", title: library.name ?? "")
|
||||||
}) {
|
}) {
|
||||||
Text(library_names[key] ?? "")
|
Text(library.name ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("All Media")
|
.navigationTitle("All Media")
|
||||||
.onAppear(perform: onAppear)
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
NavigationLink(destination: LazyView {
|
NavigationLink(destination: LazyView {
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Combine
|
||||||
|
|
||||||
struct LibrarySearchView: View {
|
struct LibrarySearchView: View {
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
var tempViewModel = ViewModel()
|
||||||
@State private var items: [BaseItemDto] = []
|
@State private var items: [BaseItemDto] = []
|
||||||
@State private var searchQuery: String = ""
|
@State private var searchQuery: String = ""
|
||||||
@State private var isLoading: Bool = false
|
@State private var isLoading: Bool = false
|
||||||
|
@ -28,7 +30,6 @@ struct LibrarySearchView: View {
|
||||||
|
|
||||||
func requestSearch(query: String) {
|
func requestSearch(query: String) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
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)
|
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
|
.sink(receiveCompletion: { completion in
|
||||||
|
@ -37,7 +38,7 @@ struct LibrarySearchView: View {
|
||||||
items = response.items ?? []
|
items = response.items ?? []
|
||||||
isLoading = false
|
isLoading = false
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import Combine
|
||||||
|
|
||||||
struct LibraryView: View {
|
struct LibraryView: View {
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
var tempViewModel = ViewModel()
|
||||||
@State private var items: [BaseItemDto] = []
|
@State private var items: [BaseItemDto] = []
|
||||||
@State private var isLoading: Bool = false
|
@State private var isLoading: Bool = false
|
||||||
|
|
||||||
|
@ -66,8 +68,6 @@ struct LibraryView: View {
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
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)
|
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)
|
||||||
|
@ -81,7 +81,7 @@ struct LibraryView: View {
|
||||||
isLoading = false
|
isLoading = false
|
||||||
viewDidLoad = true
|
viewDidLoad = true
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,21 +5,29 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import JellyfinAPI
|
|
||||||
import Combine
|
import Combine
|
||||||
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct MovieItemView: View {
|
struct MovieItemView: View {
|
||||||
@State private var orientation = UIDeviceOrientation.unknown
|
@StateObject
|
||||||
@EnvironmentObject private var playbackInfo: VideoPlayerItem
|
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
|
var item: BaseItemDto
|
||||||
|
|
||||||
@State private var settingState: Bool = true
|
@State
|
||||||
@State private var watched: Bool = false {
|
private var settingState: Bool = true
|
||||||
|
@State
|
||||||
|
private var watched: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
if !settingState {
|
if !settingState {
|
||||||
if watched == true {
|
if watched == true {
|
||||||
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
PlaystateAPI.markPlayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
|
@ -27,14 +35,14 @@ struct MovieItemView: View {
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
} else {
|
} else {
|
||||||
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
PlaystateAPI.markUnplayedItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,8 +51,6 @@ struct MovieItemView: View {
|
||||||
@State
|
@State
|
||||||
private var favorite: Bool = false {
|
private var favorite: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
if !settingState {
|
if !settingState {
|
||||||
if favorite == true {
|
if favorite == true {
|
||||||
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
UserLibraryAPI.markFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
|
@ -52,21 +58,24 @@ struct MovieItemView: View {
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
} else {
|
} else {
|
||||||
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
UserLibraryAPI.unmarkFavoriteItem(userId: SessionManager.current.userID!, itemId: item.id!)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var portraitHeaderView: some View {
|
var portraitHeaderView: some View {
|
||||||
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.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)
|
.opacity(0.4)
|
||||||
.blur(radius: 2.0)
|
.blur(radius: 2.0)
|
||||||
}
|
}
|
||||||
|
@ -156,8 +165,11 @@ struct MovieItemView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if 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) {
|
ParallaxHeaderScrollView(header: portraitHeaderView, staticOverlayView: portraitHeaderOverlayView,
|
||||||
|
overlayAlignment: .bottomLeading,
|
||||||
|
headerHeight: UIDevice.current.userInterfaceIdiom == .pad ? 350 : UIScreen.main.bounds
|
||||||
|
.width * 0.5625) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
|
.frame(height: UIDevice.current.userInterfaceIdiom == .pad ? 135 : 40)
|
||||||
|
@ -196,7 +208,9 @@ struct MovieItemView: View {
|
||||||
LibraryView(withPerson: person)
|
LibraryView(withPerson: person)
|
||||||
}) {
|
}) {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.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)
|
.frame(width: 100, height: 100)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
Text(person.name ?? "").font(.footnote).fontWeight(.regular).lineLimit(1)
|
||||||
|
@ -235,7 +249,8 @@ struct MovieItemView: View {
|
||||||
} else {
|
} else {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
ZStack {
|
ZStack {
|
||||||
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200), bh: item.getBackdropImageBlurHash())
|
ImageView(src: item.getBackdropImage(baseURL: ServerEnvironment.current.server.baseURI!, maxWidth: 200),
|
||||||
|
bh: item.getBackdropImageBlurHash())
|
||||||
.opacity(0.3)
|
.opacity(0.3)
|
||||||
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
|
.frame(width: geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing,
|
||||||
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
height: geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom)
|
||||||
|
@ -243,7 +258,8 @@ struct MovieItemView: View {
|
||||||
.blur(radius: 4)
|
.blur(radius: 4)
|
||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(src: item.getPrimaryImage(baseURL: ServerEnvironment.current.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)
|
.frame(width: 120, height: 180)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
Spacer().frame(height: 15)
|
Spacer().frame(height: 15)
|
||||||
|
@ -369,10 +385,14 @@ struct MovieItemView: View {
|
||||||
LibraryView(withPerson: person)
|
LibraryView(withPerson: person)
|
||||||
}) {
|
}) {
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(src: person.getImage(baseURL: ServerEnvironment.current.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)
|
.frame(width: 100, height: 100)
|
||||||
.cornerRadius(10)
|
.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)
|
.frame(width: 100).foregroundColor(Color.primary)
|
||||||
if person.role != "" {
|
if person.role != "" {
|
||||||
Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1)
|
Text(person.role!).font(.caption).fontWeight(.medium).lineLimit(1)
|
||||||
|
|
|
@ -11,27 +11,7 @@ import JellyfinAPI
|
||||||
|
|
||||||
struct NextUpView: View {
|
struct NextUpView: View {
|
||||||
|
|
||||||
@State private var items: [BaseItemDto] = []
|
var items: [BaseItemDto]
|
||||||
@State private var viewDidLoad: Bool = false
|
|
||||||
|
|
||||||
func onAppear() {
|
|
||||||
if viewDidLoad == true {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
viewDidLoad = true
|
|
||||||
|
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
TvShowsAPI.getNextUp(userId: SessionManager.current.userID!, limit: 12, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
|
||||||
.sink(receiveCompletion: { result in
|
|
||||||
print(result)
|
|
||||||
}, receiveValue: { response in
|
|
||||||
items = response.items ?? []
|
|
||||||
})
|
|
||||||
.store(in: &tempCancellables)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -69,7 +49,6 @@ struct NextUpView: View {
|
||||||
.frame(height: 200)
|
.frame(height: 200)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear(perform: onAppear)
|
|
||||||
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
.padding(EdgeInsets(top: 4, leading: 0, bottom: 0, trailing: 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,15 @@ import Combine
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct SeasonItemView: View {
|
struct SeasonItemView: View {
|
||||||
|
@StateObject
|
||||||
|
var tempViewModel = ViewModel()
|
||||||
@State private var orientation = UIDeviceOrientation.unknown
|
@State private var orientation = UIDeviceOrientation.unknown
|
||||||
|
@Environment(\.horizontalSizeClass) var hSizeClass
|
||||||
|
@Environment(\.verticalSizeClass) var vSizeClass
|
||||||
|
|
||||||
var item: BaseItemDto = BaseItemDto()
|
var item: BaseItemDto = BaseItemDto()
|
||||||
@State private var episodes: [BaseItemDto] = []
|
@State private var episodes: [BaseItemDto] = []
|
||||||
|
|
||||||
|
|
||||||
@State private var isLoading: Bool = true
|
@State private var isLoading: Bool = true
|
||||||
@State private var viewDidLoad: Bool = false
|
@State private var viewDidLoad: Bool = false
|
||||||
|
@ -26,7 +31,6 @@ struct SeasonItemView: View {
|
||||||
if viewDidLoad {
|
if viewDidLoad {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
TvShowsAPI.getEpisodes(seriesId: item.seriesId ?? "", userId: SessionManager.current.userID!, 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 ?? "")
|
||||||
|
@ -37,7 +41,7 @@ struct SeasonItemView: View {
|
||||||
viewDidLoad = true
|
viewDidLoad = true
|
||||||
episodes = response.items ?? []
|
episodes = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +80,7 @@ struct SeasonItemView: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var innerBody: some View {
|
var innerBody: some View {
|
||||||
if orientation == .portrait {
|
if hSizeClass == .compact && vSizeClass == .regular {
|
||||||
ParallaxHeaderScrollView(header: portraitHeaderView,
|
ParallaxHeaderScrollView(header: portraitHeaderView,
|
||||||
staticOverlayView: portraitHeaderOverlayView,
|
staticOverlayView: portraitHeaderOverlayView,
|
||||||
overlayAlignment: .bottomLeading,
|
overlayAlignment: .bottomLeading,
|
||||||
|
|
|
@ -10,6 +10,8 @@ import JellyfinAPI
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
struct SeriesItemView: View {
|
struct SeriesItemView: View {
|
||||||
|
@StateObject
|
||||||
|
var tempViewModel = ViewModel()
|
||||||
|
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
@ -25,7 +27,6 @@ struct SeriesItemView: View {
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
var tempCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
TvShowsAPI.getSeasons(seriesId: item.id ?? "", fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
|
||||||
|
@ -36,7 +37,7 @@ struct SeriesItemView: View {
|
||||||
viewDidLoad = true
|
viewDidLoad = true
|
||||||
seasons = response.items ?? []
|
seasons = response.items ?? []
|
||||||
})
|
})
|
||||||
.store(in: &tempCancellables)
|
.store(in: &tempViewModel.cancellables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,18 +10,16 @@ import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
|
||||||
@EnvironmentObject var jsi: justSignedIn
|
|
||||||
|
|
||||||
@ObservedObject var viewModel: SettingsViewModel
|
@ObservedObject var viewModel: SettingsViewModel
|
||||||
|
|
||||||
@Binding var close: Bool
|
@Binding var close: Bool
|
||||||
@State private var inNetworkStreamBitrate: Int = 40_000_000
|
@State private var inNetworkStreamBitrate: Int = 40_000_000
|
||||||
@State private var outOfNetworkStreamBitrate: Int = 40_000_000
|
@State private var outOfNetworkStreamBitrate: Int = 40_000_000
|
||||||
@State private var autoSelectSubtitles: Bool = false
|
@State private var autoSelectSubtitles: Bool = false
|
||||||
@State private var autoSelectSubtitlesLangcode: String = "none"
|
@State private var autoSelectSubtitlesLangcode: String = "none"
|
||||||
@State private var username: String = ""
|
@State private var username: String = ""
|
||||||
|
|
||||||
func onAppear() {
|
func onAppear() {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
username = SessionManager.current.user.username!
|
username = SessionManager.current.user.username!
|
||||||
|
@ -30,7 +28,7 @@ struct SettingsView: View {
|
||||||
autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles")
|
autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles")
|
||||||
autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""
|
autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
Form {
|
Form {
|
||||||
|
@ -43,7 +41,7 @@ struct SettingsView: View {
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth")
|
defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth")
|
||||||
}
|
}
|
||||||
|
|
||||||
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
|
@ -53,7 +51,7 @@ struct SettingsView: View {
|
||||||
defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth")
|
defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Accessibility")) {
|
Section(header: Text("Accessibility")) {
|
||||||
Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in
|
Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in
|
||||||
let defaults = UserDefaults.standard
|
let defaults = UserDefaults.standard
|
||||||
|
@ -61,7 +59,7 @@ struct SettingsView: View {
|
||||||
})
|
})
|
||||||
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {}
|
Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Signed in as \(username)").foregroundColor(.primary)
|
Text("Signed in as \(username)").foregroundColor(.primary)
|
||||||
|
@ -69,27 +67,28 @@ struct SettingsView: View {
|
||||||
Button {
|
Button {
|
||||||
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Server")
|
||||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try viewContext.execute(deleteRequest)
|
try viewContext.execute(deleteRequest)
|
||||||
} catch _ as NSError {
|
} catch _ as NSError {
|
||||||
// TODO: handle the error
|
// TODO: handle the error
|
||||||
}
|
}
|
||||||
|
|
||||||
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
let fetchRequest2: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "SignedInUser")
|
||||||
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try viewContext.execute(deleteRequest2)
|
try viewContext.execute(deleteRequest2)
|
||||||
} catch _ as NSError {
|
} catch _ as NSError {
|
||||||
// TODO: handle the error
|
// TODO: handle the error
|
||||||
}
|
}
|
||||||
|
|
||||||
globalData.server = Server()
|
do {
|
||||||
globalData.user = SignedInUser()
|
try SessionManager.current.logout()
|
||||||
globalData.authToken = ""
|
try ServerEnvironment.current.reset()
|
||||||
globalData.authHeader = ""
|
} catch {
|
||||||
jsi.did = true
|
print(error)
|
||||||
|
}
|
||||||
// TODO: This should redirect to the server selection screen
|
// TODO: This should redirect to the server selection screen
|
||||||
exit(-1)
|
exit(-1)
|
||||||
} label: {
|
} 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@ final class SessionManager {
|
||||||
fileprivate(set) var authToken: String!
|
fileprivate(set) var authToken: String!
|
||||||
fileprivate(set) var deviceID: String
|
fileprivate(set) var deviceID: String
|
||||||
var userID: String? {
|
var userID: String? {
|
||||||
user.user_id
|
user?.user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
|
@ -22,7 +22,3 @@ public enum SortBy: String, Codable, CaseIterable {
|
||||||
case name = "SortName"
|
case name = "SortName"
|
||||||
case dateAdded = "DateCreated"
|
case dateAdded = "DateCreated"
|
||||||
}
|
}
|
||||||
|
|
||||||
class justSignedIn: ObservableObject {
|
|
||||||
@Published var did: Bool = false
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue