Merge pull request #155 from jellyfin/PangMo5/coordinator-and-deep-link
Apply Coordinator Pattern and Add Deep-Links
This commit is contained in:
commit
084f96b7e6
|
@ -171,12 +171,25 @@
|
||||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
||||||
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; };
|
||||||
|
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; };
|
||||||
|
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; };
|
||||||
|
6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */; };
|
||||||
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */; };
|
||||||
|
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */; };
|
||||||
|
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */; };
|
||||||
|
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0B926D6092100B8E046 /* FilterCoordinator.swift */; };
|
||||||
|
6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; };
|
||||||
|
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; };
|
||||||
|
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; };
|
||||||
|
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; };
|
||||||
|
6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; };
|
||||||
|
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 6220D0C826D63F3700B8E046 /* Stinsen */; };
|
||||||
|
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0CB26D640C400B8E046 /* AppURLHandler.swift */; };
|
||||||
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 */; };
|
||||||
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
|
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
|
||||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
|
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
|
||||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.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 */; };
|
625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; };
|
||||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; };
|
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; };
|
||||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; };
|
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; };
|
||||||
|
@ -200,10 +213,17 @@
|
||||||
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
||||||
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
|
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
|
||||||
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||||
|
62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */ = {isa = PBXBuildFile; productRef = 62C29E9B26D0FE4200C1D2E7 /* Stinsen */; };
|
||||||
|
62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; };
|
||||||
|
62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */; };
|
||||||
|
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; };
|
||||||
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; };
|
||||||
|
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; };
|
||||||
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; };
|
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; };
|
||||||
62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; };
|
62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; };
|
||||||
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; };
|
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; };
|
||||||
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; };
|
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; };
|
||||||
|
62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */; };
|
||||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
|
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; };
|
||||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
||||||
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; };
|
||||||
|
@ -229,6 +249,7 @@
|
||||||
62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
|
62EC353126766848000E9F2D /* ServerEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352B26766675000E9F2D /* ServerEnvironment.swift */; };
|
||||||
62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; };
|
62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.swift */; };
|
||||||
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
|
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */; };
|
||||||
|
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; };
|
||||||
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
|
AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; };
|
||||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; };
|
||||||
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; };
|
||||||
|
@ -244,7 +265,7 @@
|
||||||
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; };
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; };
|
||||||
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; };
|
E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */; };
|
||||||
E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* CardVStackView.swift */; };
|
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */; };
|
||||||
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
||||||
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.swift */; };
|
||||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; };
|
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */; };
|
||||||
|
@ -413,11 +434,19 @@
|
||||||
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
|
6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewExtensions.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
6220D0CB26D640C400B8E046 /* AppURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLHandler.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>"; };
|
||||||
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
|
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
|
||||||
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -434,6 +463,11 @@
|
||||||
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
|
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
|
||||||
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||||
|
62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = "<group>"; };
|
||||||
|
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = "<group>"; };
|
||||||
62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = "<group>"; };
|
62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = "<group>"; };
|
||||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
||||||
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -447,6 +481,7 @@
|
||||||
62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = "<group>"; };
|
62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = "<group>"; };
|
||||||
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
|
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
|
||||||
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
|
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
|
||||||
|
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = "<group>"; };
|
||||||
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
|
AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = "<group>"; };
|
||||||
BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = "<group>"; };
|
BEEC50E7EFD4848C0E320941 /* Pods-JellyfinPlayer iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; };
|
D79953919FED0C4DF72BA578 /* Pods-JellyfinPlayer tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer tvOS.release.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer tvOS/Pods-JellyfinPlayer tvOS.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
@ -461,7 +496,7 @@
|
||||||
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = "<group>"; };
|
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+Stackable.swift"; sourceTree = "<group>"; };
|
||||||
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = "<group>"; };
|
E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewBody.swift; sourceTree = "<group>"; };
|
||||||
E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = "<group>"; };
|
E18845FF26DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemLandscapeTopBarView.swift; sourceTree = "<group>"; };
|
||||||
E188460326DEF04800B0C5B7 /* CardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardVStackView.swift; sourceTree = "<group>"; };
|
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpisodeCardVStackView.swift; sourceTree = "<group>"; };
|
||||||
E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = "<group>"; };
|
E1AD104926D94822003E4A08 /* DetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailItem.swift; sourceTree = "<group>"; };
|
||||||
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = "<group>"; };
|
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemDtoExtensions.swift; sourceTree = "<group>"; };
|
||||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = "<group>"; };
|
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitHStackView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -481,6 +516,7 @@
|
||||||
files = (
|
files = (
|
||||||
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
|
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
|
||||||
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
||||||
|
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */,
|
||||||
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
|
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
|
||||||
535870912669D7A800D05A09 /* Introspect in Frameworks */,
|
535870912669D7A800D05A09 /* Introspect in Frameworks */,
|
||||||
6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */,
|
6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */,
|
||||||
|
@ -499,6 +535,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */,
|
53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */,
|
||||||
|
62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */,
|
||||||
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */,
|
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */,
|
||||||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||||
53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */,
|
53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */,
|
||||||
|
@ -573,6 +610,7 @@
|
||||||
625CB5692678B71200530A6E /* SplashViewModel.swift */,
|
625CB5692678B71200530A6E /* SplashViewModel.swift */,
|
||||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */,
|
09389CC626819B4500AE350E /* VideoPlayerModel.swift */,
|
||||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||||
|
6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */,
|
||||||
);
|
);
|
||||||
path = ViewModels;
|
path = ViewModels;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -730,7 +768,9 @@
|
||||||
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
62C29E9D26D0FE5900C1D2E7 /* Coordinators */,
|
||||||
53F866422687A45400DCD1D7 /* Components */,
|
53F866422687A45400DCD1D7 /* Components */,
|
||||||
|
62ECA01926FA6D6900E8EBB7 /* Singleton */,
|
||||||
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
|
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
|
||||||
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
||||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||||
|
@ -755,8 +795,8 @@
|
||||||
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
|
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
|
||||||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
|
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
|
||||||
625CB5672678B6FB00530A6E /* SplashView.swift */,
|
625CB5672678B6FB00530A6E /* SplashView.swift */,
|
||||||
625CB56B2678C0FD00530A6E /* MainTabView.swift */,
|
|
||||||
625CB56E2678C23300530A6E /* HomeView.swift */,
|
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||||
|
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */,
|
||||||
);
|
);
|
||||||
path = JellyfinPlayer;
|
path = JellyfinPlayer;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -923,7 +963,7 @@
|
||||||
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
||||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
||||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
||||||
E188460326DEF04800B0C5B7 /* CardVStackView.swift */,
|
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -938,6 +978,7 @@
|
||||||
6267B3D92671138200A7371D /* ImageExtensions.swift */,
|
6267B3D92671138200A7371D /* ImageExtensions.swift */,
|
||||||
E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */,
|
E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */,
|
||||||
621338922660107500A81A2A /* StringExtensions.swift */,
|
621338922660107500A81A2A /* StringExtensions.swift */,
|
||||||
|
6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -954,6 +995,24 @@
|
||||||
path = WidgetExtension;
|
path = WidgetExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
62C29E9D26D0FE5900C1D2E7 /* Coordinators */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */,
|
||||||
|
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
|
||||||
|
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */,
|
||||||
|
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
|
||||||
|
6220D0B326D5ED8000B8E046 /* LibraryCoordinator.swift */,
|
||||||
|
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */,
|
||||||
|
62C29E9E26D1016600C1D2E7 /* MainCoordinator.swift */,
|
||||||
|
62C29EA026D102A500C1D2E7 /* MainTabCoordinator.swift */,
|
||||||
|
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
|
||||||
|
6220D0B026D5EC9900B8E046 /* SettingsCoordinator.swift */,
|
||||||
|
6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */,
|
||||||
|
);
|
||||||
|
path = Coordinators;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
62EC352A26766657000E9F2D /* Singleton */ = {
|
62EC352A26766657000E9F2D /* Singleton */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -965,6 +1024,14 @@
|
||||||
path = Singleton;
|
path = Singleton;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
62ECA01926FA6D6900E8EBB7 /* Singleton */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
6220D0CB26D640C400B8E046 /* AppURLHandler.swift */,
|
||||||
|
);
|
||||||
|
path = Singleton;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
AE8C3157265D6F5E008AA076 /* Resources */ = {
|
AE8C3157265D6F5E008AA076 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1077,6 +1144,7 @@
|
||||||
53272534268BF9710035FBF1 /* SwiftUIFocusGuide */,
|
53272534268BF9710035FBF1 /* SwiftUIFocusGuide */,
|
||||||
53649AAE269CFAF600A2D8B7 /* Puppy */,
|
53649AAE269CFAF600A2D8B7 /* Puppy */,
|
||||||
6261A0DF26A0AB710072EF1C /* CombineExt */,
|
6261A0DF26A0AB710072EF1C /* CombineExt */,
|
||||||
|
6220D0C826D63F3700B8E046 /* Stinsen */,
|
||||||
);
|
);
|
||||||
productName = "JellyfinPlayer tvOS";
|
productName = "JellyfinPlayer tvOS";
|
||||||
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
||||||
|
@ -1111,6 +1179,7 @@
|
||||||
62CB3F452685BAF7003D0A6F /* Defaults */,
|
62CB3F452685BAF7003D0A6F /* Defaults */,
|
||||||
53649AAC269CFAEA00A2D8B7 /* Puppy */,
|
53649AAC269CFAEA00A2D8B7 /* Puppy */,
|
||||||
6260FFF826A09754003FA968 /* CombineExt */,
|
6260FFF826A09754003FA968 /* CombineExt */,
|
||||||
|
62C29E9B26D0FE4200C1D2E7 /* Stinsen */,
|
||||||
);
|
);
|
||||||
productName = JellyfinPlayer;
|
productName = JellyfinPlayer;
|
||||||
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
||||||
|
@ -1199,6 +1268,7 @@
|
||||||
53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */,
|
53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */,
|
||||||
53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */,
|
53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */,
|
||||||
6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */,
|
6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */,
|
||||||
|
62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
|
@ -1447,6 +1517,7 @@
|
||||||
5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */,
|
5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */,
|
||||||
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
|
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
|
||||||
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
||||||
|
62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */,
|
||||||
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||||
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
|
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
|
||||||
53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */,
|
53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */,
|
||||||
|
@ -1454,9 +1525,11 @@
|
||||||
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
|
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
|
||||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
||||||
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */,
|
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */,
|
||||||
|
6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
|
||||||
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
||||||
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
||||||
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
|
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
|
||||||
|
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
||||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||||
|
@ -1466,6 +1539,8 @@
|
||||||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||||
53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */,
|
53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */,
|
||||||
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
||||||
|
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
||||||
|
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
|
||||||
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
||||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||||
E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||||
|
@ -1480,27 +1555,36 @@
|
||||||
files = (
|
files = (
|
||||||
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
||||||
E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */,
|
E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */,
|
||||||
|
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
||||||
|
6220D0B426D5ED8000B8E046 /* LibraryCoordinator.swift in Sources */,
|
||||||
|
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
||||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
||||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||||
|
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
||||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||||
|
6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
|
||||||
|
62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */,
|
||||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */,
|
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */,
|
||||||
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||||
|
62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */,
|
||||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||||
|
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||||
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
||||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||||
E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */,
|
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.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 */,
|
||||||
|
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
|
||||||
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */,
|
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */,
|
||||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||||
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
||||||
|
@ -1508,6 +1592,7 @@
|
||||||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
|
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
|
||||||
|
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||||
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||||
|
@ -1525,11 +1610,16 @@
|
||||||
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
||||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
|
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
|
||||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
|
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
||||||
|
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
||||||
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
||||||
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
||||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||||
|
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
|
||||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||||
|
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||||
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||||
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */,
|
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */,
|
||||||
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
||||||
|
@ -1542,14 +1632,15 @@
|
||||||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||||
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
|
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
|
||||||
|
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
|
||||||
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
||||||
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
|
6267B3DA2671138200A7371D /* ImageExtensions.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 */,
|
||||||
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||||
|
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
|
||||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||||
|
@ -1573,6 +1664,7 @@
|
||||||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */,
|
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */,
|
||||||
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
||||||
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
||||||
|
6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */,
|
||||||
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */,
|
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */,
|
||||||
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */,
|
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */,
|
||||||
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||||
|
@ -2152,6 +2244,14 @@
|
||||||
kind = branch;
|
kind = branch;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/rundfunk47/stinsen";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = {
|
62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/acvigue/Defaults";
|
repositoryURL = "https://github.com/acvigue/Defaults";
|
||||||
|
@ -2248,6 +2348,11 @@
|
||||||
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
||||||
productName = NukeUI;
|
productName = NukeUI;
|
||||||
};
|
};
|
||||||
|
6220D0C826D63F3700B8E046 /* Stinsen */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||||
|
productName = Stinsen;
|
||||||
|
};
|
||||||
625CB5792678C4A400530A6E /* ActivityIndicator */ = {
|
625CB5792678C4A400530A6E /* ActivityIndicator */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
|
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
|
||||||
|
@ -2278,6 +2383,11 @@
|
||||||
package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||||
productName = KeychainSwift;
|
productName = KeychainSwift;
|
||||||
};
|
};
|
||||||
|
62C29E9B26D0FE4200C1D2E7 /* Stinsen */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||||
|
productName = Stinsen;
|
||||||
|
};
|
||||||
62CB3F452685BAF7003D0A6F /* Defaults */ = {
|
62CB3F452685BAF7003D0A6F /* Defaults */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */;
|
package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||||
|
|
|
@ -109,6 +109,15 @@
|
||||||
"version": "0.3.1"
|
"version": "0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"package": "Stinsen",
|
||||||
|
"repositoryURL": "https://github.com/rundfunk47/stinsen",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "3d06c7603c70f8af1bd49f8d49f17e98f25b2d6a",
|
||||||
|
"version": "2.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"package": "swift-log",
|
"package": "swift-log",
|
||||||
"repositoryURL": "https://github.com/apple/swift-log.git",
|
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||||
|
|
|
@ -10,9 +10,10 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
struct CardVStackView: View {
|
struct EpisodeCardVStackView: View {
|
||||||
|
|
||||||
let items: [BaseItemDto]
|
let items: [BaseItemDto]
|
||||||
|
let selectedAction: (BaseItemDto) -> Void
|
||||||
|
|
||||||
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
@ -45,8 +46,9 @@ struct CardVStackView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
NavigationLink(destination: ItemNavigationView(item: item)) {
|
Button {
|
||||||
|
selectedAction(item)
|
||||||
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
// MARK: Image
|
// MARK: Image
|
|
@ -13,11 +13,12 @@ protocol PillStackable {
|
||||||
var title: String { get }
|
var title: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PillHStackView<NavigationView: View, ItemType: PillStackable>: View {
|
struct PillHStackView<ItemType: PillStackable>: View {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let items: [ItemType]
|
let items: [ItemType]
|
||||||
let navigationView: (ItemType) -> NavigationView
|
// let navigationView: (ItemType) -> NavigationView
|
||||||
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
@ -30,14 +31,14 @@ struct PillHStackView<NavigationView: View, ItemType: PillStackable>: View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(items, id: \.title) { item in
|
ForEach(items, id: \.title) { item in
|
||||||
NavigationLink(destination: LazyView {
|
Button {
|
||||||
navigationView(item)
|
selectedAction(item)
|
||||||
}) {
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color(UIColor.systemFill)
|
Color(UIColor.systemFill)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
|
|
|
@ -17,20 +17,20 @@ public protocol PortraitImageStackable {
|
||||||
var failureInitials: String { get }
|
var failureInitials: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PortraitImageHStackView<TopBarView: View, NavigationView: View, ItemType: PortraitImageStackable>: View {
|
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||||
|
|
||||||
let items: [ItemType]
|
let items: [ItemType]
|
||||||
let maxWidth: Int
|
let maxWidth: Int
|
||||||
let horizontalAlignment: HorizontalAlignment
|
let horizontalAlignment: HorizontalAlignment
|
||||||
let topBarView: () -> TopBarView
|
let topBarView: () -> TopBarView
|
||||||
let navigationView: (ItemType) -> NavigationView
|
let selectedAction: (ItemType) -> Void
|
||||||
|
|
||||||
init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, navigationView: @escaping (ItemType) -> NavigationView) {
|
init(items: [ItemType], maxWidth: Int, horizontalAlignment: HorizontalAlignment = .leading, topBarView: @escaping () -> TopBarView, selectedAction: @escaping (ItemType) -> Void) {
|
||||||
self.items = items
|
self.items = items
|
||||||
self.maxWidth = maxWidth
|
self.maxWidth = maxWidth
|
||||||
self.horizontalAlignment = horizontalAlignment
|
self.horizontalAlignment = horizontalAlignment
|
||||||
self.topBarView = topBarView
|
self.topBarView = topBarView
|
||||||
self.navigationView = navigationView
|
self.selectedAction = selectedAction
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
@ -45,38 +45,36 @@ struct PortraitImageHStackView<TopBarView: View, NavigationView: View, ItemType:
|
||||||
Spacer().frame(width: 16)
|
Spacer().frame(width: 16)
|
||||||
|
|
||||||
ForEach(items, id: \.title) { item in
|
ForEach(items, id: \.title) { item in
|
||||||
NavigationLink(
|
Button {
|
||||||
destination: LazyView {
|
selectedAction(item)
|
||||||
navigationView(item)
|
} label: {
|
||||||
},
|
VStack {
|
||||||
label: {
|
ImageView(src: item.imageURLContsructor(maxWidth: maxWidth),
|
||||||
VStack {
|
bh: item.blurHash,
|
||||||
ImageView(src: item.imageURLContsructor(maxWidth: maxWidth),
|
failureInitials: item.failureInitials)
|
||||||
bh: item.blurHash,
|
.frame(width: 100, height: CGFloat(maxWidth))
|
||||||
failureInitials: item.failureInitials)
|
.cornerRadius(10)
|
||||||
.frame(width: 100, height: CGFloat(maxWidth))
|
.shadow(radius: 4, y: 2)
|
||||||
.cornerRadius(10)
|
|
||||||
.shadow(radius: 4, y: 2)
|
Text(item.title)
|
||||||
|
.font(.footnote)
|
||||||
Text(item.title)
|
.fontWeight(.regular)
|
||||||
.font(.footnote)
|
.frame(width: 100)
|
||||||
.fontWeight(.regular)
|
.foregroundColor(.primary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
if let description = item.description {
|
||||||
|
Text(description)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
.frame(width: 100)
|
.frame(width: 100)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
if let description = item.description {
|
|
||||||
Text(description)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.frame(width: 100)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineLimit(2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,91 +1,85 @@
|
||||||
//
|
//
|
||||||
/*
|
/*
|
||||||
* SwiftFin is subject to the terms of the Mozilla Public
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
* 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/.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*
|
*
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct PortraitItemView: View {
|
struct PortraitItemView: View {
|
||||||
|
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) {
|
VStack(alignment: .leading) {
|
||||||
VStack(alignment: .leading) {
|
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100),
|
||||||
ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
|
bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash())
|
||||||
.frame(width: 100, height: 150)
|
.frame(width: 100, height: 150)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.overlay(
|
.overlay(Rectangle()
|
||||||
Rectangle()
|
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
.mask(ProgressBar())
|
||||||
.mask(ProgressBar())
|
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
||||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
.padding(0), alignment: .bottomLeading)
|
||||||
.padding(0), alignment: .bottomLeading
|
.overlay(ZStack {
|
||||||
)
|
if item.userData?.isFavorite ?? false {
|
||||||
.overlay(
|
Image(systemName: "circle.fill")
|
||||||
ZStack {
|
.foregroundColor(.white)
|
||||||
if item.userData?.isFavorite ?? false {
|
.opacity(0.6)
|
||||||
Image(systemName: "circle.fill")
|
Image(systemName: "heart.fill")
|
||||||
.foregroundColor(.white)
|
.foregroundColor(Color(.systemRed))
|
||||||
.opacity(0.6)
|
.font(.system(size: 10))
|
||||||
Image(systemName: "heart.fill")
|
}
|
||||||
.foregroundColor(Color(.systemRed))
|
|
||||||
.font(.system(size: 10))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.leading, 2)
|
|
||||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
|
||||||
.opacity(1), alignment: .bottomLeading)
|
|
||||||
.overlay(
|
|
||||||
ZStack {
|
|
||||||
if item.userData?.played ?? false {
|
|
||||||
Image(systemName: "checkmark.circle.fill")
|
|
||||||
.foregroundColor(.accentColor)
|
|
||||||
.background(Color(.white))
|
|
||||||
.clipShape(Circle().scale(0.8))
|
|
||||||
} else {
|
|
||||||
if item.userData?.unplayedItemCount != nil {
|
|
||||||
Capsule()
|
|
||||||
.fill(Color.accentColor)
|
|
||||||
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
|
|
||||||
Text(String(item.userData!.unplayedItemCount ?? 0))
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.font(.caption2)
|
|
||||||
.padding(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.padding(2)
|
|
||||||
.fixedSize()
|
|
||||||
.opacity(1), alignment: .topTrailing).opacity(1)
|
|
||||||
Text(item.seriesName ?? item.name ?? "")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(1)
|
|
||||||
if item.type == "Movie" || item.type == "Series" {
|
|
||||||
Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
} else if item.type == "Season" {
|
|
||||||
Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
} else {
|
|
||||||
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
}
|
}
|
||||||
}.frame(width: 100)
|
.padding(.leading, 2)
|
||||||
}
|
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||||
|
.opacity(1), alignment: .bottomLeading)
|
||||||
|
.overlay(ZStack {
|
||||||
|
if item.userData?.played ?? false {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.background(Color(.white))
|
||||||
|
.clipShape(Circle().scale(0.8))
|
||||||
|
} else {
|
||||||
|
if item.userData?.unplayedItemCount != nil {
|
||||||
|
Capsule()
|
||||||
|
.fill(Color.accentColor)
|
||||||
|
.frame(minWidth: 20, minHeight: 20, maxHeight: 20)
|
||||||
|
Text(String(item.userData!.unplayedItemCount ?? 0))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.font(.caption2)
|
||||||
|
.padding(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding(2)
|
||||||
|
.fixedSize()
|
||||||
|
.opacity(1), alignment: .topTrailing).opacity(1)
|
||||||
|
Text(item.seriesName ?? item.name ?? "")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
if item.type == "Movie" || item.type == "Series" {
|
||||||
|
Text("\(String(item.productionYear ?? 0)) • \(item.officialRating ?? "N/A")")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
} else if item.type == "Season" {
|
||||||
|
Text("\(item.name ?? "") • \(String(item.productionYear ?? 0))")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
} else {
|
||||||
|
Text("S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0))")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
|
}.frame(width: 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Stinsen
|
||||||
|
|
||||||
struct ConnectToServerView: View {
|
struct ConnectToServerView: View {
|
||||||
|
@EnvironmentObject var mainRouter: MainCoordinator.Router
|
||||||
@StateObject var viewModel = ConnectToServerViewModel()
|
@StateObject var viewModel = ConnectToServerViewModel()
|
||||||
@State var username = ""
|
@State var username = ""
|
||||||
@State var password = ""
|
@State var password = ""
|
||||||
|
@ -59,6 +61,7 @@ struct ConnectToServerView: View {
|
||||||
if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) {
|
if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) {
|
||||||
let user = SessionManager.current.getSavedSession(userID: publicUser.id!)
|
let user = SessionManager.current.getSavedSession(userID: publicUser.id!)
|
||||||
SessionManager.current.loginWithSavedSession(user: user)
|
SessionManager.current.loginWithSavedSession(user: user)
|
||||||
|
mainRouter.root(\.mainTab)
|
||||||
} else {
|
} else {
|
||||||
username = publicUser.name ?? ""
|
username = publicUser.name ?? ""
|
||||||
viewModel.selectedPublicUser = publicUser
|
viewModel.selectedPublicUser = publicUser
|
||||||
|
@ -174,5 +177,8 @@ struct ConnectToServerView: View {
|
||||||
dismissButton: .cancel())
|
dismissButton: .cancel())
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("Connect to Server", comment: ""))
|
.navigationTitle(NSLocalizedString("Connect to Server", comment: ""))
|
||||||
|
.onAppear {
|
||||||
|
AppURLHandler.shared.appURLState = .allowedInLogin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ProgressBar: Shape {
|
struct ProgressBar: Shape {
|
||||||
func path(in rect: CGRect) -> Path {
|
func path(in rect: CGRect) -> Path {
|
||||||
|
@ -31,26 +31,27 @@ struct ProgressBar: Shape {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContinueWatchingView: View {
|
struct ContinueWatchingView: View {
|
||||||
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
var items: [BaseItemDto]
|
var items: [BaseItemDto]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack {
|
LazyHStack {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) {
|
Button {
|
||||||
|
homeRouter.route(to: \.item, item)
|
||||||
|
} label: {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
||||||
.frame(width: 320, height: 180)
|
.frame(width: 320, height: 180)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.shadow(radius: 4, y: 2)
|
.shadow(radius: 4, y: 2)
|
||||||
.overlay(
|
.overlay(Rectangle()
|
||||||
Rectangle()
|
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
.mask(ProgressBar())
|
||||||
.mask(ProgressBar())
|
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
|
||||||
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
|
.padding(0), alignment: .bottomLeading)
|
||||||
.padding(0), alignment: .bottomLeading
|
|
||||||
)
|
|
||||||
HStack {
|
HStack {
|
||||||
Text("\(item.seriesName ?? item.name ?? "")")
|
Text("\(item.seriesName ?? item.name ?? "")")
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
|
@ -68,11 +69,11 @@ struct ContinueWatchingView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}.frame(width: 320, alignment: .leading)
|
}.frame(width: 320, alignment: .leading)
|
||||||
}.padding(.top, 10)
|
}.padding(.top, 10)
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
}
|
}
|
||||||
}.padding(.trailing, 16)
|
}.padding(.trailing, 16)
|
||||||
}.frame(height: 215)
|
}.frame(height: 215)
|
||||||
.padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
|
.padding(EdgeInsets(top: 8, leading: 20, bottom: 10, trailing: 2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class ConnectToServerCoodinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
ConnectToServerView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 Foundation
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
||||||
|
|
||||||
|
final class FilterCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \FilterCoordinator.start)
|
||||||
|
@Root var start = makeStart
|
||||||
|
|
||||||
|
@Binding var filters: LibraryFilters
|
||||||
|
var enabledFilterType: [FilterType]
|
||||||
|
var parentId: String = ""
|
||||||
|
|
||||||
|
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
|
||||||
|
_filters = filters
|
||||||
|
self.enabledFilterType = enabledFilterType
|
||||||
|
self.parentId = parentId
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class HomeCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \HomeCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.modal) var settings = makeSettings
|
||||||
|
@Route(.push) var library = makeLibrary
|
||||||
|
@Route(.push) var item = makeItem
|
||||||
|
|
||||||
|
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
|
||||||
|
NavigationViewCoordinator(SettingsCoordinator())
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||||
|
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
|
ItemCoordinator(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
HomeView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class ItemCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \ItemCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.push) var item = makeItem
|
||||||
|
@Route(.push) var library = makeLibrary
|
||||||
|
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
|
||||||
|
|
||||||
|
let itemDto: BaseItemDto
|
||||||
|
|
||||||
|
init(item: BaseItemDto) {
|
||||||
|
self.itemDto = item
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||||
|
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
|
ItemCoordinator(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
|
||||||
|
NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
ItemNavigationView(item: itemDto)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
|
||||||
|
|
||||||
|
final class LibraryCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \LibraryCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.push) var search = makeSearch
|
||||||
|
@Route(.modal) var filter = makeFilter
|
||||||
|
@Route(.push) var item = makeItem
|
||||||
|
|
||||||
|
var viewModel: LibraryViewModel
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
init(viewModel: LibraryViewModel, title: String) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
LibraryView(viewModel: self.viewModel, title: title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
|
||||||
|
SearchCoordinator(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
|
||||||
|
NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
|
||||||
|
enabledFilterType: params.enabledFilterType,
|
||||||
|
parentId: params.parentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
|
ItemCoordinator(item: item)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 Foundation
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class LibraryListCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.push) var search = makeSearch
|
||||||
|
@Route(.push) var library = makeLibrary
|
||||||
|
|
||||||
|
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||||
|
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
|
||||||
|
SearchCoordinator(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeStart() -> some View {
|
||||||
|
LibraryListView()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 Foundation
|
||||||
|
import Nuke
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
#if !os(tvOS)
|
||||||
|
import WidgetKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
final class MainCoordinator: NavigationCoordinatable {
|
||||||
|
var stack: NavigationStack<MainCoordinator>
|
||||||
|
|
||||||
|
@Root var mainTab = makeMainTab
|
||||||
|
@Root var connectToServer = makeConnectToServer
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if ServerEnvironment.current.server != nil, SessionManager.current.user != nil {
|
||||||
|
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
|
||||||
|
} else {
|
||||||
|
self.stack = NavigationStack(initial: \MainCoordinator.connectToServer)
|
||||||
|
}
|
||||||
|
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||||
|
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
UIScrollView.appearance().keyboardDismissMode = .onDrag
|
||||||
|
#endif
|
||||||
|
|
||||||
|
let nc = NotificationCenter.default
|
||||||
|
nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil)
|
||||||
|
nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil)
|
||||||
|
nc.addObserver(self, selector: #selector(processDeepLink), name: Notification.Name("processDeepLink"), object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didLogIn() {
|
||||||
|
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
|
||||||
|
root(\.mainTab)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func didLogOut() {
|
||||||
|
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
|
||||||
|
root(\.connectToServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func processDeepLink(_ notification: Notification) {
|
||||||
|
guard let deepLink = notification.object as? DeepLink else { return }
|
||||||
|
if let coordinator = hasRoot(\.mainTab) {
|
||||||
|
switch deepLink {
|
||||||
|
case let .item(item):
|
||||||
|
coordinator.focusFirst(\.home)
|
||||||
|
.child
|
||||||
|
.popToRoot()
|
||||||
|
.route(to: \.item, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMainTab() -> MainTabCoordinator {
|
||||||
|
MainTabCoordinator()
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeConnectToServer() -> NavigationViewCoordinator<ConnectToServerCoodinator> {
|
||||||
|
NavigationViewCoordinator(ConnectToServerCoodinator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#elseif os(tvOS)
|
||||||
|
// temp for fixing build error
|
||||||
|
final class MainCoordinator: NavigationCoordinatable {
|
||||||
|
var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
|
||||||
|
|
||||||
|
@Root var mainTab = makeEmpty
|
||||||
|
|
||||||
|
@ViewBuilder func makeEmpty() -> some View {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
|
@ -0,0 +1,50 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import Stinsen
|
||||||
|
|
||||||
|
final class MainTabCoordinator: TabCoordinatable {
|
||||||
|
var child = TabChild(startingItems: [
|
||||||
|
\MainTabCoordinator.home,
|
||||||
|
\MainTabCoordinator.allMedia,
|
||||||
|
])
|
||||||
|
|
||||||
|
@Route(tabItem: makeHomeTab) var home = makeHome
|
||||||
|
@Route(tabItem: makeTodosTab) var allMedia = makeTodos
|
||||||
|
|
||||||
|
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
|
||||||
|
return NavigationViewCoordinator(HomeCoordinator())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||||
|
Image(systemName: "house")
|
||||||
|
Text("Home")
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeTodos() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||||
|
return NavigationViewCoordinator(LibraryListCoordinator())
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeTodosTab(isActive: Bool) -> some View {
|
||||||
|
Image(systemName: "folder")
|
||||||
|
Text("All Media")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func customize(_ view: AnyView) -> some View {
|
||||||
|
view.onAppear {
|
||||||
|
AppURLHandler.shared.appURLState = .allowed
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||||
|
AppURLHandler.shared.processLaunchedURLIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
import JellyfinAPI
|
||||||
|
|
||||||
|
final class SearchCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \SearchCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.push) var item = makeItem
|
||||||
|
|
||||||
|
var viewModel: LibrarySearchViewModel
|
||||||
|
|
||||||
|
init(viewModel: LibrarySearchViewModel) {
|
||||||
|
self.viewModel = viewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||||
|
ItemCoordinator(item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
LibrarySearchView(viewModel: self.viewModel)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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 Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
@Route(.push) var serverDetail = makeServerDetail
|
||||||
|
|
||||||
|
@ViewBuilder func makeServerDetail() -> some View {
|
||||||
|
ServerDetailView()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
SettingsView(viewModel: .init())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||||
|
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
|
||||||
|
|
||||||
|
@Root var start = makeStart
|
||||||
|
var item: BaseItemDto
|
||||||
|
|
||||||
|
init(item: BaseItemDto) {
|
||||||
|
self.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder func makeStart() -> some View {
|
||||||
|
VideoPlayerView(item: item)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
enum DeepLink {
|
||||||
|
case item(BaseItemDto)
|
||||||
|
}
|
|
@ -11,9 +11,9 @@ import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
@StateObject var viewModel = HomeViewModel()
|
@StateObject var viewModel = HomeViewModel()
|
||||||
@State var showingSettings = false
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
|
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
|
||||||
let barAppearance = UINavigationBar.appearance()
|
let barAppearance = UINavigationBar.appearance()
|
||||||
|
@ -43,16 +43,19 @@ struct HomeView: View {
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
Spacer()
|
Spacer()
|
||||||
NavigationLink(destination: LazyView {
|
Button {
|
||||||
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
|
homeRouter
|
||||||
}) {
|
.route(to: \.library, (viewModel: .init(parentID: libraryID,
|
||||||
|
filters: viewModel.recentFilterSet),
|
||||||
|
title: library?.name ?? ""))
|
||||||
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.padding(.leading, 16)
|
}.padding(.leading, 16)
|
||||||
.padding(.trailing, 16)
|
.padding(.trailing, 16)
|
||||||
LatestMediaView(viewModel: .init(libraryID: libraryID))
|
LatestMediaView(viewModel: .init(libraryID: libraryID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,14 +71,11 @@ struct HomeView: View {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
showingSettings = true
|
homeRouter.route(to: \.settings)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "gear")
|
Image(systemName: "gear")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $showingSettings) {
|
|
||||||
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,17 @@
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>jellyfin</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|
|
@ -5,41 +5,41 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Introspect
|
import Introspect
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
class VideoPlayerItem: ObservableObject {
|
class VideoPlayerItem: ObservableObject {
|
||||||
@Published var shouldShowPlayer: Bool = false
|
@Published var shouldShowPlayer: Bool = false
|
||||||
@Published var itemToPlay: BaseItemDto = BaseItemDto()
|
@Published var itemToPlay = BaseItemDto()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intermediary view for ItemView to set navigation bar settings
|
// Intermediary view for ItemView to set navigation bar settings
|
||||||
struct ItemNavigationView: View {
|
struct ItemNavigationView: View {
|
||||||
|
|
||||||
private let item: BaseItemDto
|
private let item: BaseItemDto
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
self.item = item
|
self.item = item
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ItemView(item: item)
|
ItemView(item: item)
|
||||||
.navigationBarTitle("", displayMode: .inline)
|
.navigationBarTitle("", displayMode: .inline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate struct ItemView: View {
|
private struct ItemView: View {
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
|
|
||||||
@State private var videoIsLoading: Bool = false; // This variable is only changed by the underlying VLC view.
|
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
|
||||||
@State private var viewDidLoad: Bool = false
|
@State private var viewDidLoad: Bool = false
|
||||||
@State private var orientation: UIDeviceOrientation = .unknown
|
@State private var orientation: UIDeviceOrientation = .unknown
|
||||||
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
|
@StateObject private var videoPlayerItem = VideoPlayerItem()
|
||||||
@Environment(\.horizontalSizeClass) private var hSizeClass
|
@Environment(\.horizontalSizeClass) private var hSizeClass
|
||||||
@Environment(\.verticalSizeClass) private var vSizeClass
|
@Environment(\.verticalSizeClass) private var vSizeClass
|
||||||
|
|
||||||
private let viewModel: ItemViewModel
|
private let viewModel: ItemViewModel
|
||||||
|
|
||||||
init(item: BaseItemDto) {
|
init(item: BaseItemDto) {
|
||||||
switch item.itemType {
|
switch item.itemType {
|
||||||
case .movie:
|
case .movie:
|
||||||
|
@ -56,14 +56,20 @@ fileprivate struct ItemView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if hSizeClass == .compact && vSizeClass == .regular {
|
Group {
|
||||||
ItemPortraitMainView(videoIsLoading: $videoIsLoading)
|
if hSizeClass == .compact && vSizeClass == .regular {
|
||||||
.environmentObject(videoPlayerItem)
|
ItemPortraitMainView(videoIsLoading: $videoIsLoading)
|
||||||
.environmentObject(viewModel)
|
.environmentObject(videoPlayerItem)
|
||||||
} else {
|
.environmentObject(viewModel)
|
||||||
ItemLandscapeMainView(videoIsLoading: $videoIsLoading)
|
} else {
|
||||||
.environmentObject(videoPlayerItem)
|
ItemLandscapeMainView(videoIsLoading: $videoIsLoading)
|
||||||
.environmentObject(viewModel)
|
.environmentObject(videoPlayerItem)
|
||||||
|
.environmentObject(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(videoPlayerItem.$shouldShowPlayer) { flag in
|
||||||
|
guard flag else { return }
|
||||||
|
self.itemRouter.route(to: \.videoPlayer, viewModel.item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
//
|
//
|
||||||
/*
|
/*
|
||||||
* SwiftFin is subject to the terms of the Mozilla Public
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
* 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/.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*
|
*
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ItemViewBody: View {
|
struct ItemViewBody: View {
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@EnvironmentObject private var viewModel: ItemViewModel
|
@EnvironmentObject private var viewModel: ItemViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
// MARK: Overview
|
// MARK: Overview
|
||||||
|
|
||||||
Text(viewModel.item.overview ?? "")
|
Text(viewModel.item.overview ?? "")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 3)
|
.padding(.vertical, 3)
|
||||||
|
|
||||||
// MARK: Seasons
|
// MARK: Seasons
|
||||||
|
|
||||||
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
||||||
PortraitImageHStackView(items: seriesViewModel.seasons,
|
PortraitImageHStackView(items: seriesViewModel.seasons,
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
|
@ -33,28 +34,32 @@ struct ItemViewBody: View {
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.padding(.top, 3)
|
.padding(.top, 3)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
}, navigationView: { season in
|
}, selectedAction: { season in
|
||||||
ItemNavigationView(item: season)
|
itemRouter.route(to: \.item, season)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Genres
|
// MARK: Genres
|
||||||
|
|
||||||
PillHStackView(title: "Genres",
|
PillHStackView(title: "Genres",
|
||||||
items: viewModel.item.genreItems ?? []) { genre in
|
items: viewModel.item.genreItems ?? [],
|
||||||
LibraryView(viewModel: .init(genre: genre), title: genre.title)
|
selectedAction: { genre in
|
||||||
}
|
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||||
|
})
|
||||||
|
|
||||||
// MARK: Studios
|
// MARK: Studios
|
||||||
|
|
||||||
if let studios = viewModel.item.studios {
|
if let studios = viewModel.item.studios {
|
||||||
PillHStackView(title: "Studios",
|
PillHStackView(title: "Studios",
|
||||||
items: studios) { studio in
|
items: studios) { studio in
|
||||||
LibraryView(viewModel: .init(studio: studio), title: studio.name ?? "")
|
itemRouter.route(to: \.library, (viewModel: .init(studio: studio), title: studio.name ?? ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Cast & Crew
|
// MARK: Cast & Crew
|
||||||
|
|
||||||
if let castAndCrew = viewModel.item.people {
|
if let castAndCrew = viewModel.item.people {
|
||||||
PortraitImageHStackView(items: castAndCrew.filter({ BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") }),
|
PortraitImageHStackView(items: castAndCrew.filter { BaseItemPerson.DisplayedType.allCasesRaw.contains($0.type ?? "") },
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
topBarView: {
|
topBarView: {
|
||||||
Text("Cast & Crew")
|
Text("Cast & Crew")
|
||||||
|
@ -63,12 +68,13 @@ struct ItemViewBody: View {
|
||||||
.padding(.top, 3)
|
.padding(.top, 3)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
},
|
},
|
||||||
navigationView: { person in
|
selectedAction: { person in
|
||||||
LibraryView(viewModel: .init(person: person), title: person.title)
|
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: More Like This
|
// MARK: More Like This
|
||||||
|
|
||||||
if !viewModel.similarItems.isEmpty {
|
if !viewModel.similarItems.isEmpty {
|
||||||
PortraitImageHStackView(items: viewModel.similarItems,
|
PortraitImageHStackView(items: viewModel.similarItems,
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
|
@ -79,8 +85,8 @@ struct ItemViewBody: View {
|
||||||
.padding(.top, 3)
|
.padding(.top, 3)
|
||||||
.padding(.leading, 16)
|
.padding(.leading, 16)
|
||||||
},
|
},
|
||||||
navigationView: { item in
|
selectedAction: { item in
|
||||||
ItemNavigationView(item: item)
|
itemRouter.route(to: \.item, item)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,47 @@
|
||||||
//
|
//
|
||||||
/*
|
/*
|
||||||
* SwiftFin is subject to the terms of the Mozilla Public
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
* 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/.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*
|
*
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ItemLandscapeMainView: View {
|
struct ItemLandscapeMainView: View {
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@Binding private var videoIsLoading: Bool
|
@Binding private var videoIsLoading: Bool
|
||||||
@EnvironmentObject private var viewModel: ItemViewModel
|
@EnvironmentObject private var viewModel: ItemViewModel
|
||||||
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
||||||
|
|
||||||
init(videoIsLoading: Binding<Bool>) {
|
init(videoIsLoading: Binding<Bool>) {
|
||||||
self._videoIsLoading = videoIsLoading
|
self._videoIsLoading = videoIsLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: innerBody
|
// MARK: innerBody
|
||||||
|
|
||||||
private var innerBody: some View {
|
private var innerBody: some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
||||||
// MARK: Sidebar Image
|
// MARK: Sidebar Image
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130),
|
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130),
|
||||||
bh: viewModel.item.getPrimaryImageBlurHash())
|
bh: viewModel.item.getPrimaryImageBlurHash())
|
||||||
.frame(width: 130, height: 195)
|
.frame(width: 130, height: 195)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
|
|
||||||
Spacer().frame(height: 15)
|
Spacer().frame(height: 15)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
if let playButtonItem = viewModel.playButtonItem {
|
if let playButtonItem = viewModel.playButtonItem {
|
||||||
self.videoPlayerItem.itemToPlay = playButtonItem
|
self.videoPlayerItem.itemToPlay = playButtonItem
|
||||||
self.videoPlayerItem.shouldShowPlayer = true
|
self.videoPlayerItem.shouldShowPlayer = true
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
||||||
// MARK: Play
|
// MARK: Play
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "play.fill")
|
Image(systemName: "play.fill")
|
||||||
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white)
|
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white)
|
||||||
|
@ -53,20 +55,23 @@ struct ItemLandscapeMainView: View {
|
||||||
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
}.disabled(viewModel.playButtonItem == nil)
|
}.disabled(viewModel.playButtonItem == nil)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
||||||
// MARK: ItemLandscapeTopBarView
|
// MARK: ItemLandscapeTopBarView
|
||||||
|
|
||||||
ItemLandscapeTopBarView()
|
ItemLandscapeTopBarView()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
|
|
||||||
// MARK: ItemViewBody
|
// MARK: ItemViewBody
|
||||||
|
|
||||||
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
||||||
CardVStackView(items: episodeViewModel.episodes)
|
EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in
|
||||||
|
itemRouter.route(to: \.item, episode)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ItemViewBody()
|
ItemViewBody()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
|
@ -75,32 +80,20 @@ struct ItemLandscapeMainView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: body
|
// MARK: body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) {
|
|
||||||
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay,
|
|
||||||
loadBinding: $videoIsLoading,
|
|
||||||
pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
.statusBar(hidden: true)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.prefersHomeIndicatorAutoHidden(true)
|
|
||||||
}, isActive: $videoPlayerItem.shouldShowPlayer) {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
||||||
// MARK: Backdrop
|
// MARK: Backdrop
|
||||||
|
|
||||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
|
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
|
||||||
bh: viewModel.item.getBackdropImageBlurHash())
|
bh: viewModel.item.getBackdropImageBlurHash())
|
||||||
.opacity(0.3)
|
.opacity(0.3)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
.blur(radius: 4)
|
.blur(radius: 4)
|
||||||
|
|
||||||
// iPadOS is making the view go all the way to the edge.
|
// iPadOS is making the view go all the way to the edge.
|
||||||
// We have to accomodate this here
|
// We have to accomodate this here
|
||||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||||
|
|
|
@ -1,69 +1,61 @@
|
||||||
//
|
//
|
||||||
/*
|
/*
|
||||||
* SwiftFin is subject to the terms of the Mozilla Public
|
* SwiftFin is subject to the terms of the Mozilla Public
|
||||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
* 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/.
|
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*
|
*
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct ItemPortraitMainView: View {
|
struct ItemPortraitMainView: View {
|
||||||
|
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||||
@Binding private var videoIsLoading: Bool
|
@Binding private var videoIsLoading: Bool
|
||||||
@EnvironmentObject private var viewModel: ItemViewModel
|
@EnvironmentObject private var viewModel: ItemViewModel
|
||||||
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
||||||
|
|
||||||
init(videoIsLoading: Binding<Bool>) {
|
init(videoIsLoading: Binding<Bool>) {
|
||||||
self._videoIsLoading = videoIsLoading
|
self._videoIsLoading = videoIsLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: portraitHeaderView
|
// MARK: portraitHeaderView
|
||||||
|
|
||||||
var portraitHeaderView: some View {
|
var portraitHeaderView: some View {
|
||||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
|
ImageView(src: viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
|
||||||
bh: viewModel.item.getBackdropImageBlurHash())
|
bh: viewModel.item.getBackdropImageBlurHash())
|
||||||
.opacity(0.4)
|
.opacity(0.4)
|
||||||
.blur(radius: 2.0)
|
.blur(radius: 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: portraitStaticOverlayView
|
// MARK: portraitStaticOverlayView
|
||||||
|
|
||||||
var portraitStaticOverlayView: some View {
|
var portraitStaticOverlayView: some View {
|
||||||
PortraitHeaderOverlayView()
|
PortraitHeaderOverlayView()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: body
|
// MARK: body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) {
|
|
||||||
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay,
|
|
||||||
loadBinding: $videoIsLoading,
|
|
||||||
pBinding: _videoPlayerItem.projectedValue.shouldShowPlayer)
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
.navigationBarBackButtonHidden(true)
|
|
||||||
.statusBar(hidden: true)
|
|
||||||
.edgesIgnoringSafeArea(.all)
|
|
||||||
.prefersHomeIndicatorAutoHidden(true)
|
|
||||||
}, isActive: $videoPlayerItem.shouldShowPlayer) {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: ParallaxScrollView
|
// MARK: ParallaxScrollView
|
||||||
|
|
||||||
ParallaxHeaderScrollView(header: portraitHeaderView,
|
ParallaxHeaderScrollView(header: portraitHeaderView,
|
||||||
staticOverlayView: portraitStaticOverlayView,
|
staticOverlayView: portraitStaticOverlayView,
|
||||||
overlayAlignment: .bottomLeading,
|
overlayAlignment: .bottomLeading,
|
||||||
headerHeight: UIScreen.main.bounds.width * 0.5625) {
|
headerHeight: UIScreen.main.bounds.width * 0.5625) {
|
||||||
|
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 70)
|
.frame(height: 70)
|
||||||
|
|
||||||
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
||||||
Spacer()
|
Spacer()
|
||||||
CardVStackView(items: episodeViewModel.episodes)
|
EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in
|
||||||
.padding(.top, 5)
|
itemRouter.route(to: \.item, episode)
|
||||||
|
}
|
||||||
|
.padding(.top, 5)
|
||||||
} else {
|
} else {
|
||||||
ItemViewBody()
|
ItemViewBody()
|
||||||
.environmentObject(viewModel)
|
.environmentObject(viewModel)
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import MessageUI
|
|
||||||
import Defaults
|
import Defaults
|
||||||
|
import MessageUI
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
// The notification we'll send when a shake gesture happens.
|
// The notification we'll send when a shake gesture happens.
|
||||||
extension UIDevice {
|
extension UIDevice {
|
||||||
|
@ -16,18 +17,18 @@ extension UIDevice {
|
||||||
|
|
||||||
// Override the default behavior of shake gestures to send our notification instead.
|
// Override the default behavior of shake gestures to send our notification instead.
|
||||||
extension UIWindow {
|
extension UIWindow {
|
||||||
open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
override open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
|
||||||
if motion == .motionShake {
|
if motion == .motionShake {
|
||||||
NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil)
|
NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A view modifier that detects shaking and calls a function of our choosing.
|
// A view modifier that detects shaking and calls a function of our choosing.
|
||||||
struct DeviceShakeViewModifier: ViewModifier {
|
struct DeviceShakeViewModifier: ViewModifier {
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Self.Content) -> some View {
|
||||||
content
|
content
|
||||||
.onAppear()
|
.onAppear()
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in
|
||||||
|
@ -39,20 +40,20 @@ struct DeviceShakeViewModifier: ViewModifier {
|
||||||
// A View extension to make the modifier easier to use.
|
// A View extension to make the modifier easier to use.
|
||||||
extension View {
|
extension View {
|
||||||
func onShake(perform action: @escaping () -> Void) -> some View {
|
func onShake(perform action: @escaping () -> Void) -> some View {
|
||||||
self.modifier(DeviceShakeViewModifier(action: action))
|
modifier(DeviceShakeViewModifier(action: action))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UIDevice {
|
extension UIDevice {
|
||||||
var hasNotch: Bool {
|
var hasNotch: Bool {
|
||||||
let bottom = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.safeAreaInsets.bottom ?? 0
|
let bottom = UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.safeAreaInsets.bottom ?? 0
|
||||||
return bottom > 0
|
return bottom > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
|
func withHostingWindow(_ callback: @escaping (UIWindow?) -> Void) -> some View {
|
||||||
self.background(HostingWindowFinder(callback: callback))
|
background(HostingWindowFinder(callback: callback))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +68,7 @@ struct HostingWindowFinder: UIViewRepresentable {
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIView, context: Context) {
|
func updateUIView(_ uiView: UIView, context: Context) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
|
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
|
||||||
|
@ -105,18 +105,17 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
init<V: View>(wrappedView: V) {
|
init<V: View>(wrappedView: V) {
|
||||||
let box = Box()
|
let box = Box()
|
||||||
super.init(rootView: AnyView(wrappedView
|
super.init(rootView: AnyView(wrappedView
|
||||||
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
|
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
|
||||||
box.value?._prefersHomeIndicatorAutoHidden = $0
|
box.value?._prefersHomeIndicatorAutoHidden = $0
|
||||||
}.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
|
}.onPreferenceChange(SupportedOrientationsPreferenceKey.self) {
|
||||||
box.value?._orientations = $0
|
box.value?._orientations = $0
|
||||||
}.onPreferenceChange(ViewPreferenceKey.self) {
|
}.onPreferenceChange(ViewPreferenceKey.self) {
|
||||||
box.value?._viewPreference = $0
|
box.value?._viewPreference = $0
|
||||||
}
|
}))
|
||||||
))
|
|
||||||
box.value = self
|
box.value = self
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc required dynamic init?(coder aDecoder: NSCoder) {
|
@objc dynamic required init?(coder aDecoder: NSCoder) {
|
||||||
super.init(coder: aDecoder)
|
super.init(coder: aDecoder)
|
||||||
super.modalPresentationStyle = .fullScreen
|
super.modalPresentationStyle = .fullScreen
|
||||||
}
|
}
|
||||||
|
@ -131,6 +130,7 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
public var _prefersHomeIndicatorAutoHidden = false {
|
public var _prefersHomeIndicatorAutoHidden = false {
|
||||||
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
|
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||||
_prefersHomeIndicatorAutoHidden
|
_prefersHomeIndicatorAutoHidden
|
||||||
}
|
}
|
||||||
|
@ -146,6 +146,7 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||||
_orientations
|
_orientations
|
||||||
}
|
}
|
||||||
|
@ -176,7 +177,7 @@ extension View {
|
||||||
|
|
||||||
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||||
public static let shared = EmailHelper()
|
public static let shared = EmailHelper()
|
||||||
private override init() {
|
override private init() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +193,9 @@ class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||||
let data = fileManager.contents(atPath: logURL.path)
|
let data = fileManager.contents(atPath: logURL.path)
|
||||||
|
|
||||||
picker.setSubject("[DEV-BUG] SwiftFin")
|
picker.setSubject("[DEV-BUG] SwiftFin")
|
||||||
picker.setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n", isHTML: false)
|
picker
|
||||||
|
.setMessageBody("Please don't edit this email.\n Please don't change the subject. \nUDID: \(UIDevice.current.identifierForVendor?.uuidString ?? "NIL")\n",
|
||||||
|
isHTML: false)
|
||||||
picker.setToRecipients(["SwiftFin Bug Reports <swiftfin-bugs@jellyfin.org>"])
|
picker.setToRecipients(["SwiftFin Bug Reports <swiftfin-bugs@jellyfin.org>"])
|
||||||
picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent)
|
picker.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent)
|
||||||
picker.mailComposeDelegate = self
|
picker.mailComposeDelegate = self
|
||||||
|
@ -218,17 +221,22 @@ struct JellyfinPlayerApp: App {
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
SplashView()
|
EmptyView()
|
||||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||||
.onAppear(perform: {
|
.onAppear(perform: {
|
||||||
setupAppearance()
|
setupAppearance()
|
||||||
})
|
})
|
||||||
.withHostingWindow { window in
|
.withHostingWindow { window in
|
||||||
window?.rootViewController = PreferenceUIHostingController(wrappedView: SplashView().environment(\.managedObjectContext, persistenceController.container.viewContext))
|
window?
|
||||||
|
.rootViewController = PreferenceUIHostingController(wrappedView: MainCoordinator().view()
|
||||||
|
.environment(\.managedObjectContext, persistenceController.container.viewContext))
|
||||||
}
|
}
|
||||||
.onShake {
|
.onShake {
|
||||||
EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL())
|
EmailHelper.shared.sendLogs(logURL: LogManager.shared.logFileURL())
|
||||||
}
|
}
|
||||||
|
.onOpenURL { url in
|
||||||
|
AppURLHandler.shared.processDeepLink(url: url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +246,6 @@ struct JellyfinPlayerApp: App {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
|
|
||||||
static var orientationLock = UIInterfaceOrientationMask.all
|
static var orientationLock = UIInterfaceOrientationMask.all
|
||||||
|
|
||||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||||
|
|
|
@ -5,16 +5,20 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LatestMediaView: View {
|
struct LatestMediaView: View {
|
||||||
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
@StateObject var viewModel: LatestMediaViewModel
|
@StateObject var viewModel: LatestMediaViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack {
|
LazyHStack {
|
||||||
ForEach(viewModel.items, id: \.id) { item in
|
ForEach(viewModel.items, id: \.id) { item in
|
||||||
if item.type == "Series" || item.type == "Movie" {
|
Button {
|
||||||
|
homeRouter.route(to: \.item, item)
|
||||||
|
} label: {
|
||||||
PortraitItemView(item: item)
|
PortraitItemView(item: item)
|
||||||
}
|
}
|
||||||
}.padding(.trailing, 16)
|
}.padding(.trailing, 16)
|
||||||
|
|
|
@ -6,9 +6,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryFilterView: View {
|
struct LibraryFilterView: View {
|
||||||
|
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@Environment(\.presentationMode) var presentationMode
|
||||||
@Binding var filters: LibraryFilters
|
@Binding var filters: LibraryFilters
|
||||||
var parentId: String = ""
|
var parentId: String = ""
|
||||||
|
@ -18,75 +20,74 @@ struct LibraryFilterView: View {
|
||||||
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
|
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
|
||||||
_filters = filters
|
_filters = filters
|
||||||
self.parentId = parentId
|
self.parentId = parentId
|
||||||
_viewModel = StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId))
|
_viewModel =
|
||||||
|
StateObject(wrappedValue: .init(filters: filters.wrappedValue, enabledFilterType: enabledFilterType, parentId: parentId))
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
VStack {
|
||||||
VStack {
|
if viewModel.isLoading {
|
||||||
if viewModel.isLoading {
|
ProgressView()
|
||||||
ProgressView()
|
} else {
|
||||||
} else {
|
Form {
|
||||||
Form {
|
if viewModel.enabledFilterType.contains(.genre) {
|
||||||
if viewModel.enabledFilterType.contains(.genre) {
|
MultiSelector(label: NSLocalizedString("Genres", comment: ""),
|
||||||
MultiSelector(label: NSLocalizedString("Genres", comment: ""),
|
options: viewModel.possibleGenres,
|
||||||
options: viewModel.possibleGenres,
|
optionToString: { $0.name ?? "" },
|
||||||
optionToString: { $0.name ?? "" },
|
selected: $viewModel.modifiedFilters.withGenres)
|
||||||
selected: $viewModel.modifiedFilters.withGenres)
|
}
|
||||||
}
|
if viewModel.enabledFilterType.contains(.filter) {
|
||||||
if viewModel.enabledFilterType.contains(.filter) {
|
MultiSelector(label: NSLocalizedString("Filters", comment: ""),
|
||||||
MultiSelector(label: NSLocalizedString("Filters", comment: ""),
|
options: viewModel.possibleItemFilters,
|
||||||
options: viewModel.possibleItemFilters,
|
optionToString: { $0.localized },
|
||||||
optionToString: { $0.localized },
|
selected: $viewModel.modifiedFilters.filters)
|
||||||
selected: $viewModel.modifiedFilters.filters)
|
}
|
||||||
}
|
if viewModel.enabledFilterType.contains(.tag) {
|
||||||
if viewModel.enabledFilterType.contains(.tag) {
|
MultiSelector(label: NSLocalizedString("Tags", comment: ""),
|
||||||
MultiSelector(label: NSLocalizedString("Tags", comment: ""),
|
options: viewModel.possibleTags,
|
||||||
options: viewModel.possibleTags,
|
optionToString: { $0 },
|
||||||
optionToString: { $0 },
|
selected: $viewModel.modifiedFilters.tags)
|
||||||
selected: $viewModel.modifiedFilters.tags)
|
}
|
||||||
}
|
if viewModel.enabledFilterType.contains(.sortBy) {
|
||||||
if viewModel.enabledFilterType.contains(.sortBy) {
|
Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) {
|
||||||
Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) {
|
ForEach(viewModel.possibleSortBys, id: \.self) { so in
|
||||||
ForEach(viewModel.possibleSortBys, id: \.self) { so in
|
Text(so.localized).tag(so)
|
||||||
Text(so.localized).tag(so)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if viewModel.enabledFilterType.contains(.sortOrder) {
|
|
||||||
Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) {
|
|
||||||
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
|
||||||
Text(so.rawValue).tag(so)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Button {
|
if viewModel.enabledFilterType.contains(.sortOrder) {
|
||||||
viewModel.resetFilters()
|
Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) {
|
||||||
self.filters = viewModel.modifiedFilters
|
ForEach(viewModel.possibleSortOrders, id: \.self) { so in
|
||||||
presentationMode.wrappedValue.dismiss()
|
Text(so.rawValue).tag(so)
|
||||||
} label: {
|
}
|
||||||
Text("Reset")
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Button {
|
||||||
|
viewModel.resetFilters()
|
||||||
|
self.filters = viewModel.modifiedFilters
|
||||||
|
filterRouter.dismissCoordinator()
|
||||||
|
} label: {
|
||||||
|
Text("Reset")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle(NSLocalizedString("Filter Results", comment: ""), displayMode: .inline)
|
}
|
||||||
.toolbar {
|
.navigationBarTitle(NSLocalizedString("Filter Results", comment: ""), displayMode: .inline)
|
||||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
.toolbar {
|
||||||
Button {
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
presentationMode.wrappedValue.dismiss()
|
Button {
|
||||||
} label: {
|
filterRouter.dismissCoordinator()
|
||||||
Image(systemName: "xmark")
|
} label: {
|
||||||
}
|
Image(systemName: "xmark")
|
||||||
}
|
}
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
}
|
||||||
Button {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
viewModel.updateModifiedFilter()
|
Button {
|
||||||
self.filters = viewModel.modifiedFilters
|
viewModel.updateModifiedFilter()
|
||||||
presentationMode.wrappedValue.dismiss()
|
self.filters = viewModel.modifiedFilters
|
||||||
} label: {
|
filterRouter.dismissCoordinator()
|
||||||
Text("Apply")
|
} label: {
|
||||||
}
|
Text("Apply")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryListView: View {
|
struct LibraryListView: View {
|
||||||
|
@EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router
|
||||||
@StateObject var viewModel = LibraryListViewModel()
|
@StateObject var viewModel = LibraryListViewModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
NavigationLink(destination: LazyView {
|
Button {
|
||||||
LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")
|
libraryListRouter.route(to: \.library,
|
||||||
}) {
|
(viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: "Favorites"))
|
||||||
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
@ -59,9 +62,11 @@ struct LibraryListView: View {
|
||||||
if !viewModel.isLoading {
|
if !viewModel.isLoading {
|
||||||
ForEach(viewModel.libraries, id: \.id) { library in
|
ForEach(viewModel.libraries, id: \.id) { library in
|
||||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
|
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
|
||||||
NavigationLink(destination: LazyView {
|
Button {
|
||||||
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
|
libraryListRouter.route(to: \.library,
|
||||||
}) {
|
(viewModel: LibraryViewModel(parentID: library.id),
|
||||||
|
title: library.name ?? ""))
|
||||||
|
} label: {
|
||||||
ZStack {
|
ZStack {
|
||||||
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
|
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
|
||||||
.opacity(0.4)
|
.opacity(0.4)
|
||||||
|
@ -76,8 +81,8 @@ struct LibraryListView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}.padding(32)
|
}.padding(32)
|
||||||
}.background(Color.black)
|
}.background(Color.black)
|
||||||
.frame(minWidth: 100, maxWidth: .infinity)
|
.frame(minWidth: 100, maxWidth: .infinity)
|
||||||
.frame(height: 100)
|
.frame(height: 100)
|
||||||
}
|
}
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.shadow(radius: 5)
|
.shadow(radius: 5)
|
||||||
|
@ -90,15 +95,15 @@ struct LibraryListView: View {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
}.padding(.leading, 16)
|
}.padding(.leading, 16)
|
||||||
.padding(.trailing, 16)
|
.padding(.trailing, 16)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("All Media", comment: ""))
|
.navigationTitle(NSLocalizedString("All Media", comment: ""))
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||||
NavigationLink(destination: LazyView {
|
Button {
|
||||||
LibrarySearchView(viewModel: .init(parentID: nil))
|
libraryListRouter.route(to: \.search, LibrarySearchViewModel(parentID: nil))
|
||||||
}) {
|
} label: {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,13 @@
|
||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibrarySearchView: View {
|
struct LibrarySearchView: View {
|
||||||
|
@EnvironmentObject var searchRouter: SearchCoordinator.Router
|
||||||
@StateObject var viewModel: LibrarySearchViewModel
|
@StateObject var viewModel: LibrarySearchViewModel
|
||||||
@State var searchQuery = ""
|
@State private var searchQuery = ""
|
||||||
|
|
||||||
@State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
@State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||||
|
|
||||||
|
@ -78,7 +80,11 @@ struct LibrarySearchView: View {
|
||||||
if !items.isEmpty {
|
if !items.isEmpty {
|
||||||
LazyVGrid(columns: tracks) {
|
LazyVGrid(columns: tracks) {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
PortraitItemView(item: item)
|
Button {
|
||||||
|
searchRouter.route(to: \.item, item)
|
||||||
|
} label: {
|
||||||
|
PortraitItemView(item: item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
|
@ -106,7 +112,6 @@ struct LibrarySearchView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension ItemType {
|
private extension ItemType {
|
||||||
|
|
||||||
var localized: String {
|
var localized: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .episode:
|
case .episode:
|
||||||
|
|
|
@ -6,17 +6,17 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct LibraryView: View {
|
struct LibraryView: View {
|
||||||
|
@EnvironmentObject var libraryRouter: LibraryCoordinator.Router
|
||||||
@StateObject var viewModel: LibraryViewModel
|
@StateObject var viewModel: LibraryViewModel
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
// MARK: tracks for grid
|
// MARK: tracks for grid
|
||||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
|
||||||
|
|
||||||
@State var isShowingSearchView = false
|
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||||
@State var isShowingFilterView = false
|
|
||||||
|
|
||||||
@State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
@State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125)
|
||||||
|
|
||||||
|
@ -35,7 +35,11 @@ struct LibraryView: View {
|
||||||
LazyVGrid(columns: tracks) {
|
LazyVGrid(columns: tracks) {
|
||||||
ForEach(viewModel.items, id: \.id) { item in
|
ForEach(viewModel.items, id: \.id) { item in
|
||||||
if item.type != "Folder" {
|
if item.type != "Folder" {
|
||||||
PortraitItemView(item: item)
|
Button {
|
||||||
|
libraryRouter.route(to: \.item, item)
|
||||||
|
} label: {
|
||||||
|
PortraitItemView(item: item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onRotate { _ in
|
}.onRotate { _ in
|
||||||
|
@ -89,26 +93,19 @@ struct LibraryView: View {
|
||||||
}.disabled(viewModel.isLoading)
|
}.disabled(viewModel.isLoading)
|
||||||
}
|
}
|
||||||
Label("Icon One", systemImage: "line.horizontal.3.decrease.circle")
|
Label("Icon One", systemImage: "line.horizontal.3.decrease.circle")
|
||||||
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
isShowingFilterView = true
|
libraryRouter
|
||||||
}
|
.route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
|
||||||
|
parentId: viewModel.parentID ?? ""))
|
||||||
|
}
|
||||||
Button {
|
Button {
|
||||||
isShowingSearchView = true
|
libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "magnifyingglass")
|
Image(systemName: "magnifyingglass")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingFilterView) {
|
|
||||||
LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "")
|
|
||||||
}
|
|
||||||
.background(
|
|
||||||
NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)),
|
|
||||||
isActive: $isShowingSearchView) {
|
|
||||||
EmptyView()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
//
|
|
||||||
/*
|
|
||||||
* SwiftFin is subject to the terms of the Mozilla Public
|
|
||||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
|
||||||
*
|
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct MainTabView: View {
|
|
||||||
@State private var tabSelection: Tab = .home
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
TabView(selection: $tabSelection) {
|
|
||||||
NavigationView {
|
|
||||||
HomeView()
|
|
||||||
}
|
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.tabItem {
|
|
||||||
Text("Home")
|
|
||||||
Image(systemName: "house")
|
|
||||||
}
|
|
||||||
.tag(Tab.home)
|
|
||||||
NavigationView {
|
|
||||||
LibraryListView()
|
|
||||||
}
|
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
.tabItem {
|
|
||||||
Text("All Media")
|
|
||||||
Image(systemName: "folder")
|
|
||||||
}
|
|
||||||
.tag(Tab.allMedia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MainTabView {
|
|
||||||
|
|
||||||
enum Tab: String {
|
|
||||||
case home
|
|
||||||
case allMedia
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,11 +5,13 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
import Combine
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct NextUpView: View {
|
struct NextUpView: View {
|
||||||
|
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||||
|
|
||||||
var items: [BaseItemDto]
|
var items: [BaseItemDto]
|
||||||
|
|
||||||
|
@ -22,7 +24,11 @@ struct NextUpView: View {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
LazyHStack {
|
LazyHStack {
|
||||||
ForEach(items, id: \.id) { item in
|
ForEach(items, id: \.id) { item in
|
||||||
PortraitItemView(item: item)
|
Button {
|
||||||
|
homeRouter.route(to: \.item, item)
|
||||||
|
} label: {
|
||||||
|
PortraitItemView(item: item)
|
||||||
|
}
|
||||||
}.padding(.trailing, 16)
|
}.padding(.trailing, 16)
|
||||||
}
|
}
|
||||||
.padding(.leading, 20)
|
.padding(.leading, 20)
|
||||||
|
|
|
@ -6,15 +6,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import CoreData
|
import CoreData
|
||||||
import SwiftUI
|
|
||||||
import Defaults
|
import Defaults
|
||||||
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||||
@Environment(\.managedObjectContext) private var viewContext
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
|
||||||
@ObservedObject var viewModel: SettingsViewModel
|
@ObservedObject var viewModel: SettingsViewModel
|
||||||
|
|
||||||
@Binding var close: Bool
|
|
||||||
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
||||||
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
|
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
|
||||||
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
||||||
|
@ -25,101 +26,104 @@ struct SettingsView: View {
|
||||||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Form {
|
||||||
Form {
|
Section(header: EmptyView()) {
|
||||||
Section(header: EmptyView()) {
|
HStack {
|
||||||
|
Text("User")
|
||||||
|
Spacer()
|
||||||
|
Text(SessionManager.current.user?.username ?? "")
|
||||||
|
.foregroundColor(.jellyfinPurple)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
settingsRouter.route(to: \.serverDetail)
|
||||||
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("User")
|
Text("Server")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(SessionManager.current.user.username ?? "")
|
Text(ServerEnvironment.current.server?.name ?? "")
|
||||||
.foregroundColor(.jellyfinPurple)
|
.foregroundColor(.jellyfinPurple)
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink(
|
Image(systemName: "chevron.right")
|
||||||
destination: ServerDetailView(),
|
|
||||||
label: {
|
|
||||||
HStack {
|
|
||||||
Text("Server")
|
|
||||||
Spacer()
|
|
||||||
Text(ServerEnvironment.current.server.name ?? "")
|
|
||||||
.foregroundColor(.jellyfinPurple)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
Button {
|
|
||||||
close = false
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
SessionManager.current.logout()
|
|
||||||
let nc = NotificationCenter.default
|
|
||||||
nc.post(name: Notification.Name("didSignOut"), object: nil)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("Sign out")
|
|
||||||
.font(.callout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Section(header: Text("Playback")) {
|
|
||||||
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
|
||||||
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
|
||||||
Text(bitrate.name).tag(bitrate.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
|
||||||
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
|
|
||||||
Text(length.label).tag(length.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
|
||||||
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
|
|
||||||
Text(length.label).tag(length.rawValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Accessibility")) {
|
Button {
|
||||||
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
settingsRouter.dismissCoordinator()
|
||||||
SearchablePicker(label: "Preferred subtitle language",
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
options: viewModel.langs,
|
SessionManager.current.logout()
|
||||||
optionToString: { $0.name },
|
let nc = NotificationCenter.default
|
||||||
selected: Binding<TrackLanguage>(
|
nc.post(name: Notification.Name("didSignOut"), object: nil)
|
||||||
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto },
|
}
|
||||||
set: {autoSelectSubtitlesLangcode = $0.isoCode}
|
} label: {
|
||||||
)
|
Text("Sign out")
|
||||||
)
|
.font(.callout)
|
||||||
SearchablePicker(label: "Preferred audio language",
|
|
||||||
options: viewModel.langs,
|
|
||||||
optionToString: { $0.name },
|
|
||||||
selected: Binding<TrackLanguage>(
|
|
||||||
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto },
|
|
||||||
set: { autoSelectAudioLangcode = $0.isoCode}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
|
||||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
|
||||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
|
||||||
}
|
|
||||||
}.onChange(of: appAppearance, perform: { value in
|
|
||||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitle("Settings", displayMode: .inline)
|
Section(header: Text("Playback")) {
|
||||||
.toolbar {
|
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
|
||||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
Button {
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
close = false
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
|
||||||
|
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
|
||||||
|
Text(bitrate.name).tag(bitrate.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker("Jump Forward Length", selection: $jumpForwardLength) {
|
||||||
|
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
|
||||||
|
Text(length.label).tag(length.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker("Jump Backward Length", selection: $jumpBackwardLength) {
|
||||||
|
ForEach(self.viewModel.videoPlayerJumpLengths, id: \.self) { length in
|
||||||
|
Text(length.label).tag(length.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Accessibility")) {
|
||||||
|
Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles)
|
||||||
|
SearchablePicker(label: "Preferred subtitle language",
|
||||||
|
options: viewModel.langs,
|
||||||
|
optionToString: { $0.name },
|
||||||
|
selected: Binding<TrackLanguage>(get: {
|
||||||
|
viewModel.langs
|
||||||
|
.first(where: { $0.isoCode == autoSelectSubtitlesLangcode
|
||||||
|
}) ??
|
||||||
|
.auto
|
||||||
|
},
|
||||||
|
set: { autoSelectSubtitlesLangcode = $0.isoCode }))
|
||||||
|
SearchablePicker(label: "Preferred audio language",
|
||||||
|
options: viewModel.langs,
|
||||||
|
optionToString: { $0.name },
|
||||||
|
selected: Binding<TrackLanguage>(get: {
|
||||||
|
viewModel.langs
|
||||||
|
.first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
|
||||||
|
.auto
|
||||||
|
},
|
||||||
|
set: { autoSelectAudioLangcode = $0.isoCode }))
|
||||||
|
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
||||||
|
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
||||||
|
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||||
|
}
|
||||||
|
}.onChange(of: appAppearance, perform: { _ in
|
||||||
|
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitle("Settings", displayMode: .inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
settingsRouter.dismissCoordinator()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import Stinsen
|
||||||
|
|
||||||
|
final class AppURLHandler {
|
||||||
|
static let deepLinkScheme = "jellyfin"
|
||||||
|
|
||||||
|
enum AppURLState {
|
||||||
|
case launched
|
||||||
|
case allowedInLogin
|
||||||
|
case allowed
|
||||||
|
|
||||||
|
func allowedScheme(with url: URL) -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .launched:
|
||||||
|
return false
|
||||||
|
case .allowed:
|
||||||
|
return true
|
||||||
|
case .allowedInLogin:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static let shared = AppURLHandler()
|
||||||
|
|
||||||
|
var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
var appURLState: AppURLState = .launched
|
||||||
|
var launchURL: URL?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppURLHandler {
|
||||||
|
@discardableResult
|
||||||
|
func processDeepLink(url: URL) -> Bool {
|
||||||
|
guard url.scheme == Self.deepLinkScheme || url.scheme == "widget-extension" else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if AppURLHandler.shared.appURLState.allowedScheme(with: url) {
|
||||||
|
return processURL(url)
|
||||||
|
} else {
|
||||||
|
launchURL = url
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func processLaunchedURLIfNeeded() {
|
||||||
|
guard let launchURL = launchURL,
|
||||||
|
!launchURL.absoluteString.isEmpty else { return }
|
||||||
|
if processDeepLink(url: launchURL) {
|
||||||
|
self.launchURL = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processURL(_ url: URL) -> Bool {
|
||||||
|
if processURLForUser(url: url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processURLForUser(url: URL) -> Bool {
|
||||||
|
guard url.host?.lowercased() == "users",
|
||||||
|
url.pathComponents[safe: 1]?.isEmpty == false else { return false }
|
||||||
|
|
||||||
|
// /Users/{UserID}/Items/{ItemID}
|
||||||
|
if url.pathComponents[safe: 2]?.lowercased() == "items",
|
||||||
|
let userID = url.pathComponents[safe: 1],
|
||||||
|
let itemID = url.pathComponents[safe: 3]
|
||||||
|
{
|
||||||
|
// It would be nice if the ItemViewModel could be initialized to id later.
|
||||||
|
getItem(userID: userID, itemID: itemID) { item in
|
||||||
|
guard let item = item else { return }
|
||||||
|
NotificationCenter.default.post(name: Notification.Name("processDeepLink"), object: DeepLink.item(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension AppURLHandler {
|
||||||
|
func getItem(userID: String, itemID: String, completion: @escaping (BaseItemDto?) -> Void) {
|
||||||
|
UserLibraryAPI.getItem(userId: userID, itemId: itemID)
|
||||||
|
.sink(receiveCompletion: { innerCompletion in
|
||||||
|
switch innerCompletion {
|
||||||
|
case .failure:
|
||||||
|
completion(nil)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, receiveValue: { item in
|
||||||
|
completion(item)
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,19 +7,21 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Stinsen
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SplashView: View {
|
struct SplashView: View {
|
||||||
|
@EnvironmentObject var mainRouter: MainCoordinator.Router
|
||||||
@StateObject var viewModel = SplashViewModel()
|
@StateObject var viewModel = SplashViewModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if viewModel.isLoggedIn {
|
ProgressView()
|
||||||
MainTabView()
|
.onReceive(viewModel.$isLoggedIn) { flag in
|
||||||
} else {
|
if flag {
|
||||||
NavigationView {
|
mainRouter.root(\.mainTab)
|
||||||
ConnectToServerView()
|
} else {
|
||||||
|
mainRouter.root(\.connectToServer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationViewStyle(StackNavigationViewStyle())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,15 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SwiftUI
|
import Combine
|
||||||
import MobileVLCKit
|
import Defaults
|
||||||
|
import GoogleCast
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import Combine
|
import MobileVLCKit
|
||||||
import GoogleCast
|
import Stinsen
|
||||||
|
import SwiftUI
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
import Defaults
|
|
||||||
|
|
||||||
enum PlayerDestination {
|
enum PlayerDestination {
|
||||||
case remote
|
case remote
|
||||||
|
@ -26,6 +27,8 @@ protocol PlayerViewControllerDelegate: AnyObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener {
|
class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener {
|
||||||
|
@RouterObject
|
||||||
|
var main: MainCoordinator.Router?
|
||||||
|
|
||||||
weak var delegate: PlayerViewControllerDelegate?
|
weak var delegate: PlayerViewControllerDelegate?
|
||||||
|
|
||||||
|
@ -64,9 +67,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
private var castDiscoveryManager: GCKDiscoveryManager {
|
private var castDiscoveryManager: GCKDiscoveryManager {
|
||||||
return GCKCastContext.sharedInstance().discoveryManager
|
return GCKCastContext.sharedInstance().discoveryManager
|
||||||
}
|
}
|
||||||
|
|
||||||
private var castSessionManager: GCKSessionManager {
|
private var castSessionManager: GCKSessionManager {
|
||||||
return GCKCastContext.sharedInstance().sessionManager
|
return GCKCastContext.sharedInstance().sessionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasSentRemoteSeek: Bool = false
|
var hasSentRemoteSeek: Bool = false
|
||||||
|
|
||||||
var selectedPlaybackSpeedIndex: Int = 3
|
var selectedPlaybackSpeedIndex: Int = 3
|
||||||
|
@ -80,17 +85,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
var jumpForwardLength: VideoPlayerJumpLength {
|
var jumpForwardLength: VideoPlayerJumpLength {
|
||||||
return Defaults[.videoPlayerJumpForward]
|
return Defaults[.videoPlayerJumpForward]
|
||||||
}
|
}
|
||||||
|
|
||||||
var jumpBackwardLength: VideoPlayerJumpLength {
|
var jumpBackwardLength: VideoPlayerJumpLength {
|
||||||
return Defaults[.videoPlayerJumpBackward]
|
return Defaults[.videoPlayerJumpBackward]
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifest: BaseItemDto = BaseItemDto()
|
var manifest = BaseItemDto()
|
||||||
var playbackItem = PlaybackItem()
|
var playbackItem = PlaybackItem()
|
||||||
var remoteTimeUpdateTimer: Timer?
|
var remoteTimeUpdateTimer: Timer?
|
||||||
var upNextViewModel: UpNextViewModel = UpNextViewModel()
|
var upNextViewModel = UpNextViewModel()
|
||||||
var lastOri: UIInterfaceOrientation?
|
var lastOri: UIInterfaceOrientation?
|
||||||
|
|
||||||
// MARK: IBActions
|
// MARK: IBActions
|
||||||
|
|
||||||
@IBAction func seekSliderStart(_ sender: Any) {
|
@IBAction func seekSliderStart(_ sender: Any) {
|
||||||
if playerDestination == .local {
|
if playerDestination == .local {
|
||||||
sendProgressReport(eventName: "pause")
|
sendProgressReport(eventName: "pause")
|
||||||
|
@ -101,7 +108,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func seekSliderValueChanged(_ sender: Any) {
|
@IBAction func seekSliderValueChanged(_ sender: Any) {
|
||||||
let videoDuration: Double = Double(manifest.runTimeTicks! / Int64(10_000_000))
|
let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000))
|
||||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||||
let secondsScrubbedRemaining = videoDuration - secondsScrubbedTo
|
let secondsScrubbedRemaining = videoDuration - secondsScrubbedTo
|
||||||
|
|
||||||
|
@ -111,15 +118,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
private func calculateTimeText(from duration: Double) -> String {
|
private func calculateTimeText(from duration: Double) -> String {
|
||||||
let hours = floor(duration / 3600)
|
let hours = floor(duration / 3600)
|
||||||
let minutes = (duration.truncatingRemainder(dividingBy: 3600)) / 60
|
let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60
|
||||||
let seconds = (duration.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60)
|
let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60)
|
||||||
|
|
||||||
let timeText: String
|
let timeText: String
|
||||||
|
|
||||||
if hours != 0 {
|
if hours != 0 {
|
||||||
timeText = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
timeText =
|
||||||
|
"\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||||
} else {
|
} else {
|
||||||
timeText = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
timeText =
|
||||||
|
"\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
|
||||||
}
|
}
|
||||||
|
|
||||||
return timeText
|
return timeText
|
||||||
|
@ -127,7 +136,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
@IBAction func seekSliderEnd(_ sender: Any) {
|
@IBAction func seekSliderEnd(_ sender: Any) {
|
||||||
isSeeking = false
|
isSeeking = false
|
||||||
let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : Double(remotePositionTicks / Int(10_000_000))
|
let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) :
|
||||||
|
Double(remotePositionTicks / Int(10_000_000))
|
||||||
let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000))
|
let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000))
|
||||||
// Scrub is value from 0..1 - find position in video and add / or remove.
|
// Scrub is value from 0..1 - find position in video and add / or remove.
|
||||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||||
|
@ -143,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
sendProgressReport(eventName: "unpause")
|
sendProgressReport(eventName: "unpause")
|
||||||
} else {
|
} else {
|
||||||
sendJellyfinCommand(command: "Seek", options: [
|
sendJellyfinCommand(command: "Seek", options: [
|
||||||
"position": Int(secondsScrubbedTo)
|
"position": Int(secondsScrubbedTo),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,7 +190,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
if playerDestination == .local {
|
if playerDestination == .local {
|
||||||
mediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
|
mediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
|
||||||
} else {
|
} else {
|
||||||
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000) - Int(jumpBackwardLength.rawValue)])
|
sendJellyfinCommand(command: "Seek",
|
||||||
|
options: ["position": (remotePositionTicks / 10_000_000) - Int(jumpBackwardLength.rawValue)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +201,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
if playerDestination == .local {
|
if playerDestination == .local {
|
||||||
mediaPlayer.jumpForward(jumpForwardLength.rawValue)
|
mediaPlayer.jumpForward(jumpForwardLength.rawValue)
|
||||||
} else {
|
} else {
|
||||||
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000) + Int(jumpForwardLength.rawValue)])
|
sendJellyfinCommand(command: "Seek",
|
||||||
|
options: ["position": (remotePositionTicks / 10_000_000) + Int(jumpForwardLength.rawValue)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,7 +240,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
|
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
|
||||||
|
|
||||||
// Present the view controller (in a popover).
|
// Present the view controller (in a popover).
|
||||||
self.present(optionsVC!, animated: true) {
|
present(optionsVC!, animated: true) {
|
||||||
print("popover visible, pause playback")
|
print("popover visible, pause playback")
|
||||||
self.mediaPlayer.pause()
|
self.mediaPlayer.pause()
|
||||||
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
||||||
|
@ -236,6 +248,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Cast methods
|
// MARK: Cast methods
|
||||||
|
|
||||||
@IBAction func castButtonPressed(_ sender: Any) {
|
@IBAction func castButtonPressed(_ sender: Any) {
|
||||||
if selectedCastDevice == nil {
|
if selectedCastDevice == nil {
|
||||||
LogManager.shared.log.debug("Presenting Cast modal")
|
LogManager.shared.log.debug("Presenting Cast modal")
|
||||||
|
@ -246,7 +259,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
castDeviceVC?.popoverPresentationController?.sourceView = castButton
|
castDeviceVC?.popoverPresentationController?.sourceView = castButton
|
||||||
|
|
||||||
// Present the view controller (in a popover).
|
// Present the view controller (in a popover).
|
||||||
self.present(castDeviceVC!, animated: true) {
|
present(castDeviceVC!, animated: true) {
|
||||||
self.mediaPlayer.pause()
|
self.mediaPlayer.pause()
|
||||||
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
||||||
}
|
}
|
||||||
|
@ -254,8 +267,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
LogManager.shared.log.info("Stopping casting session: button was pressed.")
|
LogManager.shared.log.info("Stopping casting session: button was pressed.")
|
||||||
castSessionManager.endSessionAndStopCasting(true)
|
castSessionManager.endSessionAndStopCasting(true)
|
||||||
selectedCastDevice = nil
|
selectedCastDevice = nil
|
||||||
self.castButton.isEnabled = true
|
castButton.isEnabled = true
|
||||||
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
|
castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
|
||||||
playerDestination = .local
|
playerDestination = .local
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -264,9 +277,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
LogManager.shared.log.debug("Cast modal dismissed")
|
LogManager.shared.log.debug("Cast modal dismissed")
|
||||||
castDeviceVC?.dismiss(animated: true, completion: nil)
|
castDeviceVC?.dismiss(animated: true, completion: nil)
|
||||||
if playerDestination == .local {
|
if playerDestination == .local {
|
||||||
self.mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
}
|
}
|
||||||
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func castDeviceChanged() {
|
func castDeviceChanged() {
|
||||||
|
@ -280,11 +293,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Cast End
|
// MARK: Cast End
|
||||||
|
|
||||||
func settingsPopoverDismissed() {
|
func settingsPopoverDismissed() {
|
||||||
optionsVC?.dismiss(animated: true, completion: nil)
|
optionsVC?.dismiss(animated: true, completion: nil)
|
||||||
if playerDestination == .local {
|
if playerDestination == .local {
|
||||||
self.mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,7 +340,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
self.mediaPlayer.jumpForward(30)
|
self.mediaPlayer.jumpForward(30)
|
||||||
self.sendProgressReport(eventName: "timeupdate")
|
self.sendProgressReport(eventName: "timeupdate")
|
||||||
} else {
|
} else {
|
||||||
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)+30])
|
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks / 10_000_000) + 30])
|
||||||
}
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
@ -337,14 +351,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
self.mediaPlayer.jumpBackward(15)
|
self.mediaPlayer.jumpBackward(15)
|
||||||
self.sendProgressReport(eventName: "timeupdate")
|
self.sendProgressReport(eventName: "timeupdate")
|
||||||
} else {
|
} else {
|
||||||
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)-15])
|
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks / 10_000_000) - 15])
|
||||||
}
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scrubber
|
// Scrubber
|
||||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
|
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] (remoteEvent) -> MPRemoteCommandHandlerStatus in
|
||||||
guard let self = self else {return .commandFailed}
|
guard let self = self else { return .commandFailed }
|
||||||
|
|
||||||
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
|
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
|
||||||
let targetSeconds = event.positionTime
|
let targetSeconds = event.positionTime
|
||||||
|
@ -354,14 +368,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
if self.playerDestination == .local {
|
if self.playerDestination == .local {
|
||||||
if offset > 0 {
|
if offset > 0 {
|
||||||
self.mediaPlayer.jumpForward(Int32(offset)/1000)
|
self.mediaPlayer.jumpForward(Int32(offset) / 1000)
|
||||||
} else {
|
} else {
|
||||||
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
|
self.mediaPlayer.jumpBackward(Int32(abs(offset)) / 1000)
|
||||||
}
|
}
|
||||||
self.sendProgressReport(eventName: "unpause")
|
self.sendProgressReport(eventName: "unpause")
|
||||||
} else {
|
} else {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return .success
|
return .success
|
||||||
} else {
|
} else {
|
||||||
|
@ -383,17 +395,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
|
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
|
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
|
||||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
|
||||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks
|
||||||
|
|
||||||
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
|
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
|
||||||
if let artworkImage = UIImage(data: imageData as Data) {
|
if let artworkImage = UIImage(data: imageData as Data) {
|
||||||
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
|
let artwork = MPMediaItemArtwork(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
|
||||||
return artworkImage
|
artworkImage
|
||||||
})
|
})
|
||||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,6 +415,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: viewDidLoad
|
// MARK: viewDidLoad
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
if manifest.type == "Movie" {
|
if manifest.type == "Movie" {
|
||||||
|
@ -426,7 +439,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation),
|
||||||
|
name: UIDevice.orientationDidChangeNotification, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func didChangedOrientation() {
|
@objc func didChangedOrientation() {
|
||||||
|
@ -447,7 +461,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
let totalDevices = castDiscoveryManager.deviceCount
|
let totalDevices = castDiscoveryManager.deviceCount
|
||||||
discoveredCastDevices = []
|
discoveredCastDevices = []
|
||||||
if totalDevices > 0 {
|
if totalDevices > 0 {
|
||||||
for i in 0...totalDevices-1 {
|
for i in 0 ... totalDevices - 1 {
|
||||||
let device = castDiscoveryManager.device(at: i)
|
let device = castDiscoveryManager.device(at: i)
|
||||||
discoveredCastDevices.append(device)
|
discoveredCastDevices.append(device)
|
||||||
}
|
}
|
||||||
|
@ -466,8 +480,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
self.tabBarController?.tabBar.isHidden = false
|
tabBarController?.tabBar.isHidden = false
|
||||||
self.navigationController?.isNavigationBarHidden = false
|
navigationController?.isNavigationBarHidden = false
|
||||||
overrideUserInterfaceStyle = .unspecified
|
overrideUserInterfaceStyle = .unspecified
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.lastOri != nil {
|
if self.lastOri != nil {
|
||||||
|
@ -479,11 +493,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: viewDidAppear
|
// MARK: viewDidAppear
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
super.viewDidAppear(animated)
|
super.viewDidAppear(animated)
|
||||||
overrideUserInterfaceStyle = .dark
|
overrideUserInterfaceStyle = .dark
|
||||||
self.tabBarController?.tabBar.isHidden = true
|
tabBarController?.tabBar.isHidden = true
|
||||||
self.navigationController?.isNavigationBarHidden = true
|
navigationController?.isNavigationBarHidden = true
|
||||||
|
|
||||||
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||||
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||||
|
@ -496,7 +511,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupMediaPlayer() {
|
func setupMediaPlayer() {
|
||||||
|
|
||||||
// Fetch max bitrate from UserDefaults depending on current connection mode
|
// Fetch max bitrate from UserDefaults depending on current connection mode
|
||||||
let maxBitrate = Defaults[.inNetworkBandwidth]
|
let maxBitrate = Defaults[.inNetworkBandwidth]
|
||||||
print(maxBitrate)
|
print(maxBitrate)
|
||||||
|
@ -504,26 +518,31 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
let builder = DeviceProfileBuilder()
|
let builder = DeviceProfileBuilder()
|
||||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||||
let profile = builder.buildProfile()
|
let profile = builder.buildProfile()
|
||||||
let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
|
let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate),
|
||||||
|
startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile,
|
||||||
|
autoOpenLiveStream: true)
|
||||||
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||||
delegate?.showLoadingView(self)
|
delegate?.showLoadingView(self)
|
||||||
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
|
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!,
|
||||||
|
maxStreamingBitrate: Int(maxBitrate),
|
||||||
|
startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true,
|
||||||
|
playbackInfoDto: playbackInfo)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .finished:
|
case .finished:
|
||||||
break
|
break
|
||||||
case .failure(let error):
|
case let .failure(error):
|
||||||
if let err = error as? ErrorResponse {
|
if let err = error as? ErrorResponse {
|
||||||
switch err {
|
switch err {
|
||||||
case .error(401, _, _, _):
|
case .error(401, _, _, _):
|
||||||
self.delegate?.exitPlayer(self)
|
self.delegate?.exitPlayer(self)
|
||||||
SessionManager.current.logout()
|
SessionManager.current.logout()
|
||||||
|
main?.root(\.connectToServer)
|
||||||
case .error:
|
case .error:
|
||||||
self.delegate?.exitPlayer(self)
|
self.delegate?.exitPlayer(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}, receiveValue: { [self] response in
|
}, receiveValue: { [self] response in
|
||||||
dump(response)
|
dump(response)
|
||||||
|
@ -536,7 +555,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
item.videoType = .transcode
|
item.videoType = .transcode
|
||||||
item.videoUrl = streamURL!
|
item.videoUrl = streamURL!
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "",
|
||||||
|
languageCode: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack)
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
// Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
|
@ -548,7 +568,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
} else {
|
} else {
|
||||||
deliveryUrl = nil
|
deliveryUrl = nil
|
||||||
}
|
}
|
||||||
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "")
|
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl,
|
||||||
|
delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt",
|
||||||
|
languageCode: stream.language ?? "")
|
||||||
|
|
||||||
if subtitle.delivery != .encode {
|
if subtitle.delivery != .encode {
|
||||||
subtitleTrackArray.append(subtitle)
|
subtitleTrackArray.append(subtitle)
|
||||||
|
@ -556,7 +578,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.type == .audio {
|
if stream.type == .audio {
|
||||||
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
|
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "",
|
||||||
|
id: Int32(stream.index!))
|
||||||
if stream.isDefault! == true {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!)
|
selectedAudioTrack = Int32(stream.index!)
|
||||||
}
|
}
|
||||||
|
@ -565,7 +588,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedAudioTrack == -1 {
|
if selectedAudioTrack == -1 {
|
||||||
if audioTrackArray.count > 0 {
|
if !audioTrackArray.isEmpty {
|
||||||
selectedAudioTrack = audioTrackArray[0].id
|
selectedAudioTrack = audioTrackArray[0].id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -574,13 +597,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
playbackItem = item
|
playbackItem = item
|
||||||
} else {
|
} else {
|
||||||
// Item will be directly played by the client.
|
// Item will be directly played by the client.
|
||||||
let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")!
|
let streamURL =
|
||||||
|
URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")!
|
||||||
|
|
||||||
let item = PlaybackItem()
|
let item = PlaybackItem()
|
||||||
item.videoUrl = streamURL
|
item.videoUrl = streamURL
|
||||||
item.videoType = .directPlay
|
item.videoType = .directPlay
|
||||||
|
|
||||||
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
|
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "",
|
||||||
|
languageCode: "")
|
||||||
subtitleTrackArray.append(disableSubtitleTrack)
|
subtitleTrackArray.append(disableSubtitleTrack)
|
||||||
|
|
||||||
// Loop through media streams and add to array
|
// Loop through media streams and add to array
|
||||||
|
@ -592,7 +617,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
} else {
|
} else {
|
||||||
deliveryUrl = nil
|
deliveryUrl = nil
|
||||||
}
|
}
|
||||||
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "")
|
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl,
|
||||||
|
delivery: stream.deliveryMethod!, codec: stream.codec!,
|
||||||
|
languageCode: stream.language ?? "")
|
||||||
|
|
||||||
if subtitle.delivery != .encode {
|
if subtitle.delivery != .encode {
|
||||||
subtitleTrackArray.append(subtitle)
|
subtitleTrackArray.append(subtitle)
|
||||||
|
@ -600,7 +627,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if stream.type == .audio {
|
if stream.type == .audio {
|
||||||
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
|
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "",
|
||||||
|
id: Int32(stream.index!))
|
||||||
if stream.isDefault! == true {
|
if stream.isDefault! == true {
|
||||||
selectedAudioTrack = Int32(stream.index!)
|
selectedAudioTrack = Int32(stream.index!)
|
||||||
}
|
}
|
||||||
|
@ -609,7 +637,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
if selectedAudioTrack == -1 {
|
if selectedAudioTrack == -1 {
|
||||||
if audioTrackArray.count > 0 {
|
if !audioTrackArray.isEmpty {
|
||||||
selectedAudioTrack = audioTrackArray[0].id
|
selectedAudioTrack = audioTrackArray[0].id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -636,7 +664,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
subtitleTrackArray.forEach { subtitle in
|
subtitleTrackArray.forEach { subtitle in
|
||||||
if Defaults[.isAutoSelectSubtitles] {
|
if Defaults[.isAutoSelectSubtitles] {
|
||||||
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
||||||
subtitle.languageCode.contains(Locale.current.languageCode ?? "") {
|
subtitle.languageCode.contains(Locale.current.languageCode ?? "")
|
||||||
|
{
|
||||||
selectedCaptionTrack = subtitle.id
|
selectedCaptionTrack = subtitle.id
|
||||||
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
||||||
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
||||||
|
@ -683,21 +712,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
subtitleTrackArray.forEach { sub in
|
subtitleTrackArray.forEach { sub in
|
||||||
// stupid fxcking jeff decides to re-encode these when added.
|
// stupid fxcking jeff decides to re-encode these when added.
|
||||||
// only add playback streams when codec not supported by VLC.
|
// only add playback streams when codec not supported by VLC.
|
||||||
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
|
if sub.id != -1, sub.delivery == .external, sub.codec != "subrip" {
|
||||||
mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false)
|
mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mediaHasStartedPlaying()
|
mediaHasStartedPlaying()
|
||||||
delegate?.hideLoadingView(self)
|
delegate?.hideLoadingView(self)
|
||||||
|
|
||||||
videoContentView.setNeedsLayout()
|
videoContentView.setNeedsLayout()
|
||||||
videoContentView.setNeedsDisplay()
|
videoContentView.setNeedsDisplay()
|
||||||
self.view.setNeedsLayout()
|
view.setNeedsLayout()
|
||||||
self.view.setNeedsDisplay()
|
view.setNeedsDisplay()
|
||||||
self.videoControlsView.setNeedsLayout()
|
videoControlsView.setNeedsLayout()
|
||||||
self.videoControlsView.setNeedsDisplay()
|
videoControlsView.setNeedsDisplay()
|
||||||
|
|
||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
mediaPlayer.play()
|
mediaPlayer.play()
|
||||||
|
@ -705,6 +734,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: VideoPlayerSettings Delegate
|
// MARK: VideoPlayerSettings Delegate
|
||||||
|
|
||||||
func subtitleTrackChanged(newTrackID: Int32) {
|
func subtitleTrackChanged(newTrackID: Int32) {
|
||||||
selectedCaptionTrack = newTrackID
|
selectedCaptionTrack = newTrackID
|
||||||
mediaPlayer.currentVideoSubTitleIndex = newTrackID
|
mediaPlayer.currentVideoSubTitleIndex = newTrackID
|
||||||
|
@ -731,7 +761,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
|
|
||||||
// Create the swiftUI view
|
// Create the swiftUI view
|
||||||
let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel))
|
let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel))
|
||||||
self.upNextView.addSubview(contentView.view)
|
upNextView.addSubview(contentView.view)
|
||||||
contentView.view.backgroundColor = .clear
|
contentView.view.backgroundColor = .clear
|
||||||
contentView.view.translatesAutoresizingMaskIntoConstraints = false
|
contentView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true
|
contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true
|
||||||
|
@ -741,7 +771,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
}
|
}
|
||||||
|
|
||||||
func getNextEpisode() {
|
func getNextEpisode() {
|
||||||
TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, limit: 2)
|
TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id,
|
||||||
|
limit: 2)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
print(completion)
|
print(completion)
|
||||||
}, receiveValue: { [self] response in
|
}, receiveValue: { [self] response in
|
||||||
|
@ -790,20 +821,20 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
||||||
setupMediaPlayer()
|
setupMediaPlayer()
|
||||||
getNextEpisode()
|
getNextEpisode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - GCKGenericChannelDelegate
|
// MARK: - GCKGenericChannelDelegate
|
||||||
|
|
||||||
extension PlayerViewController: GCKGenericChannelDelegate {
|
extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
@objc func updateRemoteTime() {
|
@objc func updateRemoteTime() {
|
||||||
castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
|
castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
|
||||||
if !paused {
|
if !paused {
|
||||||
remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt.
|
remotePositionTicks = remotePositionTicks + 2_000_000 // add 0.2 secs every timer evt.
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSeeking == false {
|
if isSeeking == false {
|
||||||
let positiveSeconds = Double(remotePositionTicks/10_000_000)
|
let positiveSeconds = Double(remotePositionTicks / 10_000_000)
|
||||||
let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000)
|
let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks)) / 10_000_000)
|
||||||
|
|
||||||
timeText.text = calculateTimeText(from: positiveSeconds)
|
timeText.text = calculateTimeText(from: positiveSeconds)
|
||||||
timeLeftText.text = calculateTimeText(from: remainingSeconds)
|
timeLeftText.text = calculateTimeText(from: remainingSeconds)
|
||||||
|
@ -823,14 +854,15 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
if hasSentRemoteSeek == false {
|
if hasSentRemoteSeek == false {
|
||||||
hasSentRemoteSeek = true
|
hasSentRemoteSeek = true
|
||||||
sendJellyfinCommand(command: "Seek", options: [
|
sendJellyfinCommand(command: "Seek", options: [
|
||||||
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position)
|
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
paused = json["data"]["PlayState"]["IsPaused"].boolValue
|
paused = json["data"]["PlayState"]["IsPaused"].boolValue
|
||||||
self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
|
remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
|
||||||
if remoteTimeUpdateTimer == nil {
|
if remoteTimeUpdateTimer == nil {
|
||||||
remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true)
|
remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime),
|
||||||
|
userInfo: nil, repeats: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -848,7 +880,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
"serverId": ServerEnvironment.current.server.server_id!,
|
"serverId": ServerEnvironment.current.server.server_id!,
|
||||||
"serverVersion": "10.8.0",
|
"serverVersion": "10.8.0",
|
||||||
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
|
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
|
||||||
"subtitleBurnIn": false
|
"subtitleBurnIn": false,
|
||||||
]
|
]
|
||||||
let jsonData = JSON(payload)
|
let jsonData = JSON(payload)
|
||||||
|
|
||||||
|
@ -857,7 +889,13 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
if command == "Seek" {
|
if command == "Seek" {
|
||||||
remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000)
|
remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000)
|
||||||
// Send playback report as Jellyfin Chromecast isn't smarter than a rock.
|
// Send playback report as Jellyfin Chromecast isn't smarter than a rock.
|
||||||
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId,
|
||||||
|
mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack),
|
||||||
|
subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false,
|
||||||
|
positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime),
|
||||||
|
volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType,
|
||||||
|
liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone,
|
||||||
|
nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
||||||
.sink(receiveCompletion: { result in
|
.sink(receiveCompletion: { result in
|
||||||
|
@ -871,9 +909,10 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - GCKSessionManagerListener
|
// MARK: - GCKSessionManagerListener
|
||||||
|
|
||||||
extension PlayerViewController: GCKSessionManagerListener {
|
extension PlayerViewController: GCKSessionManagerListener {
|
||||||
func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) {
|
func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) {
|
||||||
self.sendStopReport()
|
sendStopReport()
|
||||||
mediaPlayer.stop()
|
mediaPlayer.stop()
|
||||||
|
|
||||||
playerDestination = .remote
|
playerDestination = .remote
|
||||||
|
@ -891,25 +930,25 @@ extension PlayerViewController: GCKSessionManagerListener {
|
||||||
|
|
||||||
let playNowOptions: [String: Any] = [
|
let playNowOptions: [String: Any] = [
|
||||||
"items": [[
|
"items": [[
|
||||||
"Id": self.manifest.id!,
|
"Id": manifest.id!,
|
||||||
"ServerId": ServerEnvironment.current.server.server_id!,
|
"ServerId": ServerEnvironment.current.server.server_id!,
|
||||||
"Name": self.manifest.name!,
|
"Name": manifest.name!,
|
||||||
"Type": self.manifest.type!,
|
"Type": manifest.type!,
|
||||||
"MediaType": self.manifest.mediaType!,
|
"MediaType": manifest.mediaType!,
|
||||||
"IsFolder": self.manifest.isFolder!
|
"IsFolder": manifest.isFolder!,
|
||||||
]]
|
]],
|
||||||
]
|
]
|
||||||
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
|
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) {
|
func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) {
|
||||||
self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
||||||
self.sessionDidStart(manager: sessionManager, didStart: session)
|
sessionDidStart(manager: sessionManager, didStart: session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) {
|
func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) {
|
||||||
self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
||||||
self.sessionDidStart(manager: sessionManager, didStart: session)
|
sessionDidStart(manager: sessionManager, didStart: session)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) {
|
func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) {
|
||||||
|
@ -938,30 +977,29 @@ extension PlayerViewController: GCKSessionManagerListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - VLCMediaPlayer Delegates
|
// MARK: - VLCMediaPlayer Delegates
|
||||||
|
|
||||||
extension PlayerViewController: VLCMediaPlayerDelegate {
|
extension PlayerViewController: VLCMediaPlayerDelegate {
|
||||||
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||||
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
||||||
switch currentState {
|
switch currentState {
|
||||||
case .stopped :
|
case .stopped:
|
||||||
LogManager.shared.log.debug("Player state changed: STOPPED")
|
LogManager.shared.log.debug("Player state changed: STOPPED")
|
||||||
break
|
case .ended:
|
||||||
case .ended :
|
|
||||||
LogManager.shared.log.debug("Player state changed: ENDED")
|
LogManager.shared.log.debug("Player state changed: ENDED")
|
||||||
break
|
case .playing:
|
||||||
case .playing :
|
|
||||||
LogManager.shared.log.debug("Player state changed: PLAYING")
|
LogManager.shared.log.debug("Player state changed: PLAYING")
|
||||||
sendProgressReport(eventName: "unpause")
|
sendProgressReport(eventName: "unpause")
|
||||||
delegate?.hideLoadingView(self)
|
delegate?.hideLoadingView(self)
|
||||||
paused = false
|
paused = false
|
||||||
case .paused :
|
case .paused:
|
||||||
LogManager.shared.log.debug("Player state changed: PAUSED")
|
LogManager.shared.log.debug("Player state changed: PAUSED")
|
||||||
paused = true
|
paused = true
|
||||||
case .opening :
|
case .opening:
|
||||||
LogManager.shared.log.debug("Player state changed: OPENING")
|
LogManager.shared.log.debug("Player state changed: OPENING")
|
||||||
case .buffering :
|
case .buffering:
|
||||||
LogManager.shared.log.debug("Player state changed: BUFFERING")
|
LogManager.shared.log.debug("Player state changed: BUFFERING")
|
||||||
delegate?.showLoadingView(self)
|
delegate?.showLoadingView(self)
|
||||||
case .error :
|
case .error:
|
||||||
LogManager.shared.log.error("Video had error.")
|
LogManager.shared.log.error("Video had error.")
|
||||||
sendStopReport()
|
sendStopReport()
|
||||||
case .esAdded:
|
case .esAdded:
|
||||||
|
@ -973,19 +1011,19 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
|
||||||
|
|
||||||
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||||
let time = mediaPlayer.position
|
let time = mediaPlayer.position
|
||||||
if abs(time-lastTime) > 0.00005 {
|
if abs(time - lastTime) > 0.00005 {
|
||||||
paused = false
|
paused = false
|
||||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||||
seekSlider.setValue(mediaPlayer.position, animated: true)
|
seekSlider.setValue(mediaPlayer.position, animated: true)
|
||||||
delegate?.hideLoadingView(self)
|
delegate?.hideLoadingView(self)
|
||||||
|
|
||||||
if manifest.type == "Episode" && upNextViewModel.item != nil {
|
if manifest.type == "Episode", upNextViewModel.item != nil {
|
||||||
if time > 0.96 {
|
if time > 0.96 {
|
||||||
upNextView.isHidden = false
|
upNextView.isHidden = false
|
||||||
self.jumpForwardButton.isHidden = true
|
jumpForwardButton.isHidden = true
|
||||||
} else {
|
} else {
|
||||||
upNextView.isHidden = true
|
upNextView.isHidden = true
|
||||||
self.jumpForwardButton.isHidden = false
|
jumpForwardButton.isHidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -993,7 +1031,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
|
||||||
timeLeftText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst())
|
timeLeftText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst())
|
||||||
|
|
||||||
if CACurrentMediaTime() - controlsAppearTime > 5 {
|
if CACurrentMediaTime() - controlsAppearTime > 5 {
|
||||||
self.smallNextUpView()
|
smallNextUpView()
|
||||||
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
|
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
|
||||||
self.videoControlsView.alpha = 0.0
|
self.videoControlsView.alpha = 0.0
|
||||||
}, completion: { (_: Bool) in
|
}, completion: { (_: Bool) in
|
||||||
|
@ -1013,42 +1051,61 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct VideoPlayerView: View {
|
||||||
|
var item: BaseItemDto
|
||||||
|
@State private var isLoading = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
// Loading UI needs to be moved into ViewController later
|
||||||
|
LoadingViewNoBlur(isShowing: $isLoading) {
|
||||||
|
VLCPlayerWithControls(item: item, loadBinding: $isLoading)
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.statusBar(hidden: true)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.prefersHomeIndicatorAutoHidden(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: End VideoPlayerVC
|
// MARK: End VideoPlayerVC
|
||||||
|
|
||||||
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||||
var item: BaseItemDto
|
var item: BaseItemDto
|
||||||
@Environment(\.presentationMode) var presentationMode
|
@RouterObject var playerRouter: VideoPlayerCoordinator.Router?
|
||||||
|
|
||||||
var loadBinding: Binding<Bool>
|
let loadBinding: Binding<Bool>
|
||||||
var pBinding: Binding<Bool>
|
|
||||||
|
|
||||||
class Coordinator: NSObject, PlayerViewControllerDelegate {
|
class Coordinator: NSObject, PlayerViewControllerDelegate {
|
||||||
|
var parent: VLCPlayerWithControls
|
||||||
let loadBinding: Binding<Bool>
|
let loadBinding: Binding<Bool>
|
||||||
let pBinding: Binding<Bool>
|
|
||||||
|
|
||||||
init(loadBinding: Binding<Bool>, pBinding: Binding<Bool>) {
|
init(parent: VLCPlayerWithControls, loadBinding: Binding<Bool>) {
|
||||||
|
self.parent = parent
|
||||||
self.loadBinding = loadBinding
|
self.loadBinding = loadBinding
|
||||||
self.pBinding = pBinding
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func hideLoadingView(_ viewController: PlayerViewController) {
|
func hideLoadingView(_ viewController: PlayerViewController) {
|
||||||
self.loadBinding.wrappedValue = false
|
loadBinding.wrappedValue = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func showLoadingView(_ viewController: PlayerViewController) {
|
func showLoadingView(_ viewController: PlayerViewController) {
|
||||||
self.loadBinding.wrappedValue = true
|
loadBinding.wrappedValue = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitPlayer(_ viewController: PlayerViewController) {
|
func exitPlayer(_ viewController: PlayerViewController) {
|
||||||
self.pBinding.wrappedValue = false
|
parent.playerRouter?.dismissCoordinator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeCoordinator() -> Coordinator {
|
func makeCoordinator() -> Coordinator {
|
||||||
Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding)
|
Coordinator(parent: self, loadBinding: loadBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias UIViewControllerType = PlayerViewController
|
typealias UIViewControllerType = PlayerViewController
|
||||||
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls.UIViewControllerType {
|
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls
|
||||||
|
.UIViewControllerType
|
||||||
|
{
|
||||||
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
|
||||||
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
||||||
customViewController.manifest = item
|
customViewController.manifest = item
|
||||||
|
@ -1056,20 +1113,27 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||||
return customViewController
|
return customViewController
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) {
|
func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType,
|
||||||
}
|
context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Play State Update Methods
|
// MARK: - Play State Update Methods
|
||||||
|
|
||||||
extension PlayerViewController {
|
extension PlayerViewController {
|
||||||
func sendProgressReport(eventName: String) {
|
func sendProgressReport(eventName: String) {
|
||||||
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {
|
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {
|
||||||
var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!))
|
var ticks = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!))
|
||||||
if ticks == 0 {
|
if ticks == 0 {
|
||||||
ticks = manifest.userData?.playbackPositionTicks ?? 0
|
ticks = manifest.userData?.playbackPositionTicks ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId,
|
||||||
|
mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack),
|
||||||
|
subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: mediaPlayer.state == .paused,
|
||||||
|
isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime),
|
||||||
|
volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType,
|
||||||
|
liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone,
|
||||||
|
nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
|
||||||
.sink(receiveCompletion: { result in
|
.sink(receiveCompletion: { result in
|
||||||
|
@ -1082,7 +1146,10 @@ extension PlayerViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendStopReport() {
|
func sendStopReport() {
|
||||||
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
|
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id,
|
||||||
|
positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil,
|
||||||
|
playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0",
|
||||||
|
nowPlayingQueue: [])
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
|
||||||
.sink(receiveCompletion: { result in
|
.sink(receiveCompletion: { result in
|
||||||
|
@ -1098,7 +1165,13 @@ extension PlayerViewController {
|
||||||
|
|
||||||
print("sending play report!")
|
print("sending play report!")
|
||||||
|
|
||||||
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
|
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId,
|
||||||
|
mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack),
|
||||||
|
subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false,
|
||||||
|
positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime),
|
||||||
|
volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType,
|
||||||
|
liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [],
|
||||||
|
playlistItemId: "playlistItem0")
|
||||||
|
|
||||||
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
|
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
|
||||||
.sink(receiveCompletion: { result in
|
.sink(receiveCompletion: { result in
|
||||||
|
@ -1111,7 +1184,7 @@ extension PlayerViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UINavigationController {
|
extension UINavigationController {
|
||||||
open override var childForHomeIndicatorAutoHidden: UIViewController? {
|
override open var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func eraseToAnyView() -> AnyView {
|
||||||
|
return AnyView(self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,8 +10,11 @@
|
||||||
import Combine
|
import Combine
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
import Stinsen
|
||||||
|
|
||||||
final class ConnectToServerViewModel: ViewModel {
|
final class ConnectToServerViewModel: ViewModel {
|
||||||
|
@RouterObject
|
||||||
|
var main: MainCoordinator.Router?
|
||||||
|
|
||||||
@Published var isConnectedServer = false
|
@Published var isConnectedServer = false
|
||||||
|
|
||||||
|
@ -23,13 +26,14 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
@Published var publicUsers = [UserDto]()
|
@Published var publicUsers = [UserDto]()
|
||||||
@Published var selectedPublicUser = UserDto()
|
@Published var selectedPublicUser = UserDto()
|
||||||
|
|
||||||
private let discovery: ServerDiscovery = ServerDiscovery()
|
private let discovery = ServerDiscovery()
|
||||||
@Published var servers: [ServerDiscovery.ServerLookupResponse] = []
|
@Published var servers: [ServerDiscovery.ServerLookupResponse] = []
|
||||||
@Published var searching = false
|
@Published var searching = false
|
||||||
|
|
||||||
func getPublicUsers() {
|
func getPublicUsers() {
|
||||||
if ServerEnvironment.current.server != nil {
|
if ServerEnvironment.current.server != nil {
|
||||||
LogManager.shared.log.debug("Attempting to read public users from \(ServerEnvironment.current.server.baseURI!)", tag: "getPublicUsers")
|
LogManager.shared.log.debug("Attempting to read public users from \(ServerEnvironment.current.server.baseURI!)",
|
||||||
|
tag: "getPublicUsers")
|
||||||
UserAPI.getPublicUsers()
|
UserAPI.getPublicUsers()
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
|
@ -46,28 +50,28 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func hidePublicUsers() {
|
func hidePublicUsers() {
|
||||||
self.lastPublicUsers = publicUsers
|
lastPublicUsers = publicUsers
|
||||||
publicUsers = []
|
publicUsers = []
|
||||||
}
|
}
|
||||||
|
|
||||||
func showPublicUsers() {
|
func showPublicUsers() {
|
||||||
self.publicUsers = lastPublicUsers
|
publicUsers = lastPublicUsers
|
||||||
lastPublicUsers = []
|
lastPublicUsers = []
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectToServer() {
|
func connectToServer() {
|
||||||
|
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
if uriSubject.value == "localhost" {
|
if uriSubject.value == "localhost" {
|
||||||
uriSubject.value = "http://localhost:8096"
|
uriSubject.value = "http://localhost:8096"
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer")
|
LogManager.shared.log.debug("Attempting to connect to server at \"\(uriSubject.value)\"", tag: "connectToServer")
|
||||||
ServerEnvironment.current.create(with: uriSubject.value)
|
ServerEnvironment.current.create(with: uriSubject.value)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer", completion: completion)
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "connectToServer",
|
||||||
|
completion: completion)
|
||||||
}, receiveValue: { _ in
|
}, receiveValue: { _ in
|
||||||
LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer")
|
LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer")
|
||||||
self.getPublicUsers()
|
self.getPublicUsers()
|
||||||
|
@ -77,7 +81,7 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
|
|
||||||
func connectToServer(at url: URL) {
|
func connectToServer(at url: URL) {
|
||||||
uriSubject.send(url.absoluteString)
|
uriSubject.send(url.absoluteString)
|
||||||
self.connectToServer()
|
connectToServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
func discoverServers() {
|
func discoverServers() {
|
||||||
|
@ -88,7 +92,7 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
self.searching = false
|
self.searching = false
|
||||||
}
|
}
|
||||||
|
|
||||||
discovery.locateServer { [self] (server) in
|
discovery.locateServer { [self] server in
|
||||||
if let server = server, !servers.contains(server) {
|
if let server = server, !servers.contains(server) {
|
||||||
servers.append(server)
|
servers.append(server)
|
||||||
}
|
}
|
||||||
|
@ -98,13 +102,16 @@ final class ConnectToServerViewModel: ViewModel {
|
||||||
|
|
||||||
func login() {
|
func login() {
|
||||||
LogManager.shared.log.debug("Attempting to login to server at \"\(uriSubject.value)\"", tag: "login")
|
LogManager.shared.log.debug("Attempting to login to server at \"\(uriSubject.value)\"", tag: "login")
|
||||||
LogManager.shared.log.debug("username == \"\": \(usernameSubject.value.isEmpty), password == \"\": \(passwordSubject.value.isEmpty)", tag: "login")
|
LogManager.shared.log
|
||||||
|
.debug("username == \"\": \(usernameSubject.value.isEmpty), password == \"\": \(passwordSubject.value.isEmpty)",
|
||||||
|
tag: "login")
|
||||||
SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value)
|
SessionManager.current.login(username: usernameSubject.value, password: passwordSubject.value)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { completion in
|
.sink(receiveCompletion: { completion in
|
||||||
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion)
|
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
|
||||||
}, receiveValue: { _ in
|
completion: completion)
|
||||||
|
}, receiveValue: { [weak self] _ in
|
||||||
|
self?.main?.root(\.mainTab)
|
||||||
})
|
})
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ final class HomeViewModel: ViewModel {
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import JellyfinAPI
|
import JellyfinAPI
|
||||||
|
|
||||||
|
|
|
@ -26,15 +26,17 @@ final class LatestMediaViewModel: ViewModel {
|
||||||
|
|
||||||
func requestLatestMedia() {
|
func requestLatestMedia() {
|
||||||
LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.current.user.user_id ?? "NIL")")
|
LogManager.shared.log.debug("Requesting latest media for user id \(SessionManager.current.user.user_id ?? "NIL")")
|
||||||
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!, parentId: libraryID,
|
UserLibraryAPI.getLatestMedia(userId: SessionManager.current.user.user_id!,
|
||||||
|
parentId: libraryID,
|
||||||
fields: [
|
fields: [
|
||||||
.primaryImageAspectRatio,
|
.primaryImageAspectRatio,
|
||||||
.seriesPrimaryImage,
|
.seriesPrimaryImage,
|
||||||
.seasonUserData,
|
.seasonUserData,
|
||||||
.overview,
|
.overview,
|
||||||
.genres,
|
.genres,
|
||||||
.people
|
.people
|
||||||
],
|
],
|
||||||
|
includeItemTypes: ["Series", "Movie"],
|
||||||
enableUserData: true, limit: 12)
|
enableUserData: true, limit: 12)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink(receiveCompletion: { [weak self] completion in
|
.sink(receiveCompletion: { [weak self] completion in
|
||||||
|
|
|
@ -142,14 +142,14 @@ struct NextUpEntryView: View {
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||||
} else {
|
} else {
|
||||||
switch family {
|
switch family {
|
||||||
case .systemSmall:
|
case .systemSmall:
|
||||||
small(item: entry.items.first)
|
small(item: entry.items.first)
|
||||||
case .systemMedium:
|
case .systemMedium:
|
||||||
medium(items: entry.items)
|
medium(items: entry.items)
|
||||||
case .systemLarge:
|
case .systemLarge:
|
||||||
large(items: entry.items)
|
large(items: entry.items)
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,51 +198,55 @@ extension NextUpEntryView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View {
|
func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View {
|
||||||
VStack(alignment: .leading) {
|
Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: {
|
||||||
if let image = item.1 {
|
VStack(alignment: .leading) {
|
||||||
Image(uiImage: image)
|
if let image = item.1 {
|
||||||
.resizable()
|
Image(uiImage: image)
|
||||||
.aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill)
|
.resizable()
|
||||||
.clipped()
|
.aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill)
|
||||||
.cornerRadius(8)
|
.clipped()
|
||||||
.shadow(radius: 8)
|
.cornerRadius(8)
|
||||||
}
|
.shadow(radius: 8)
|
||||||
Text(item.0.seriesName ?? "")
|
}
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(1)
|
|
||||||
Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)")
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View {
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
if let image = item.1 {
|
|
||||||
Image(uiImage: image)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill)
|
|
||||||
.clipped()
|
|
||||||
.cornerRadius(8)
|
|
||||||
.shadow(radius: 8)
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text(item.0.seriesName ?? "")
|
Text(item.0.seriesName ?? "")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
.lineLimit(1)
|
||||||
Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)")
|
Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.fontWeight(.semibold)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View {
|
||||||
|
Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
if let image = item.1 {
|
||||||
|
Image(uiImage: image)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(.init(width: 1, height: 0.5625), contentMode: .fill)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(8)
|
||||||
|
.shadow(radius: 8)
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(item.0.seriesName ?? "")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
Text("\(item.0.name ?? "") · S\(item.0.parentIndexNumber ?? 0):E\(item.0.indexNumber ?? 0)")
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,33 +285,36 @@ extension NextUpEntryView {
|
||||||
func large(items: [(BaseItemDto, UIImage?)]) -> some View {
|
func large(items: [(BaseItemDto, UIImage?)]) -> some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if let firstItem = items[safe: 0] {
|
if let firstItem = items[safe: 0] {
|
||||||
ZStack(alignment: .topTrailing) {
|
Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(firstItem.0.id!)")!,
|
||||||
ZStack(alignment: .bottomLeading) {
|
label: {
|
||||||
if let image = firstItem.1 {
|
ZStack(alignment: .topTrailing) {
|
||||||
Image(uiImage: image)
|
ZStack(alignment: .bottomLeading) {
|
||||||
.centerCropped()
|
if let image = firstItem.1 {
|
||||||
.innerShadow(color: Color.black.opacity(0.5), radius: 0.5)
|
Image(uiImage: image)
|
||||||
}
|
.centerCropped()
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
.innerShadow(color: Color.black.opacity(0.5), radius: 0.5)
|
||||||
Text(firstItem.0.seriesName ?? "")
|
}
|
||||||
.font(.caption)
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
.fontWeight(.semibold)
|
Text(firstItem.0.seriesName ?? "")
|
||||||
.foregroundColor(.white)
|
.font(.caption)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
.fontWeight(.semibold)
|
||||||
Text("\(firstItem.0.name ?? "") · S\(firstItem.0.parentIndexNumber ?? 0):E\(firstItem.0.indexNumber ?? 0)")
|
.foregroundColor(.white)
|
||||||
.font(.caption)
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
.fontWeight(.semibold)
|
Text("\(firstItem.0.name ?? "") · S\(firstItem.0.parentIndexNumber ?? 0):E\(firstItem.0.indexNumber ?? 0)")
|
||||||
.foregroundColor(.gray)
|
.font(.caption)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
.fontWeight(.semibold)
|
||||||
}
|
.foregroundColor(.gray)
|
||||||
.shadow(radius: 8)
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(12)
|
}
|
||||||
}
|
.shadow(radius: 8)
|
||||||
headerSymbol
|
.padding(12)
|
||||||
.padding(12)
|
}
|
||||||
}
|
headerSymbol
|
||||||
.clipped()
|
.padding(12)
|
||||||
.shadow(radius: 8)
|
}
|
||||||
|
.clipped()
|
||||||
|
.shadow(radius: 8)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
if let secondItem = items[safe: 1] {
|
if let secondItem = items[safe: 1] {
|
||||||
|
@ -354,7 +361,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol"))
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -365,7 +372,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||||
UIImage(named: "WidgetHeaderSymbol"))
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
@ -380,7 +387,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol"))
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -392,7 +399,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||||
UIImage(named: "WidgetHeaderSymbol"))
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
@ -405,7 +412,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
NextUpEntryView(entry: .init(date: Date(),
|
NextUpEntryView(entry: .init(date: Date(),
|
||||||
items: [
|
items: [
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol"))
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||||
|
@ -415,7 +422,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
||||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||||
UIImage(named: "WidgetHeaderSymbol")),
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||||
UIImage(named: "WidgetHeaderSymbol"))
|
UIImage(named: "WidgetHeaderSymbol")),
|
||||||
],
|
],
|
||||||
error: nil))
|
error: nil))
|
||||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||||
|
|
Loading…
Reference in New Issue