Merge pull request #155 from jellyfin/PangMo5/coordinator-and-deep-link

Apply Coordinator Pattern and Add Deep-Links
This commit is contained in:
Anthony Lavado 2021-10-02 12:09:03 -04:00 committed by GitHub
commit 084f96b7e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1528 additions and 738 deletions

View File

@ -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" */;

View File

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

View File

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

View File

@ -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,9 +31,9 @@ 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)

View File

@ -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,11 +45,9 @@ 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: {
},
label: {
VStack { VStack {
ImageView(src: item.imageURLContsructor(maxWidth: maxWidth), ImageView(src: item.imageURLContsructor(maxWidth: maxWidth),
bh: item.blurHash, bh: item.blurHash,
@ -76,7 +74,7 @@ struct PortraitImageHStackView<TopBarView: View, NavigationView: View, ItemType:
.lineLimit(2) .lineLimit(2)
} }
} }
}) }
} }
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55) Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
} }

View File

@ -1,5 +1,5 @@
// //
/* /*
* 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/.
@ -7,30 +7,26 @@
* 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), bh: item.type != "Episode" ? item.getPrimaryImageBlurHash() : item.getSeriesPrimaryImageBlurHash()) ImageView(src: item.type != "Episode" ? item.getPrimaryImage(maxWidth: 100) : item.getSeriesPrimaryImage(maxWidth: 100),
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 {
.overlay(
ZStack {
if item.userData?.isFavorite ?? false { if item.userData?.isFavorite ?? false {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.foregroundColor(.white) .foregroundColor(.white)
@ -43,8 +39,7 @@ struct PortraitItemView: View {
.padding(.leading, 2) .padding(.leading, 2)
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9) .padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
.opacity(1), alignment: .bottomLeading) .opacity(1), alignment: .bottomLeading)
.overlay( .overlay(ZStack {
ZStack {
if item.userData?.played ?? false { if item.userData?.played ?? false {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
@ -87,5 +82,4 @@ struct PortraitItemView: View {
} }
}.frame(width: 100) }.frame(width: 100)
} }
}
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import 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)
}
}

View File

@ -0,0 +1,38 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
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()
}
}

View File

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

View File

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

View File

@ -0,0 +1,33 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import 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()
}
}

View File

@ -0,0 +1,88 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@ 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")
@ -43,9 +43,12 @@ 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())
@ -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)
}
} }
} }

View File

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

View File

@ -5,18 +5,17 @@
* 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) {
@ -29,12 +28,13 @@ struct ItemNavigationView: View {
} }
} }
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
@ -56,6 +56,7 @@ fileprivate struct ItemView: View {
} }
var body: some View { var body: some View {
Group {
if hSizeClass == .compact && vSizeClass == .regular { if hSizeClass == .compact && vSizeClass == .regular {
ItemPortraitMainView(videoIsLoading: $videoIsLoading) ItemPortraitMainView(videoIsLoading: $videoIsLoading)
.environmentObject(videoPlayerItem) .environmentObject(videoPlayerItem)
@ -66,4 +67,9 @@ fileprivate struct ItemView: View {
.environmentObject(viewModel) .environmentObject(viewModel)
} }
} }
.onReceive(videoPlayerItem.$shouldShowPlayer) { flag in
guard flag else { return }
self.itemRouter.route(to: \.videoPlayer, viewModel.item)
}
}
} }

View File

@ -1,5 +1,5 @@
// //
/* /*
* 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/.
@ -7,23 +7,24 @@
* 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)
}) })
} }
} }

View File

@ -1,5 +1,5 @@
// //
/* /*
* 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/.
@ -7,10 +7,11 @@
* 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
@ -20,10 +21,11 @@ struct ItemLandscapeMainView: View {
} }
// 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())
@ -38,8 +40,8 @@ struct ItemLandscapeMainView: View {
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)
@ -59,14 +61,17 @@ struct ItemLandscapeMainView: View {
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)
@ -77,24 +82,12 @@ 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)

View File

@ -1,5 +1,5 @@
// //
/* /*
* 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/.
@ -7,11 +7,11 @@
* 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
@ -21,6 +21,7 @@ struct ItemPortraitMainView: View {
} }
// 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())
@ -29,40 +30,31 @@ struct ItemPortraitMainView: View {
} }
// 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
itemRouter.route(to: \.item, episode)
}
.padding(.top, 5) .padding(.top, 5)
} else { } else {
ItemViewBody() ItemViewBody()

View File

@ -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,7 +17,7 @@ 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)
} }
@ -27,7 +28,7 @@ extension UIWindow {
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 {
@ -111,12 +111,11 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
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 {

View File

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

View File

@ -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,11 +20,11 @@ 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()
@ -64,7 +66,7 @@ struct LibraryFilterView: View {
Button { Button {
viewModel.resetFilters() viewModel.resetFilters()
self.filters = viewModel.modifiedFilters self.filters = viewModel.modifiedFilters
presentationMode.wrappedValue.dismiss() filterRouter.dismissCoordinator()
} label: { } label: {
Text("Reset") Text("Reset")
} }
@ -74,7 +76,7 @@ struct LibraryFilterView: View {
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) { ToolbarItemGroup(placement: .navigationBarLeading) {
Button { Button {
presentationMode.wrappedValue.dismiss() filterRouter.dismissCoordinator()
} label: { } label: {
Image(systemName: "xmark") Image(systemName: "xmark")
} }
@ -83,12 +85,11 @@ struct LibraryFilterView: View {
Button { Button {
viewModel.updateModifiedFilter() viewModel.updateModifiedFilter()
self.filters = viewModel.modifiedFilters self.filters = viewModel.modifiedFilters
presentationMode.wrappedValue.dismiss() filterRouter.dismissCoordinator()
} label: { } label: {
Text("Apply") Text("Apply")
} }
} }
} }
} }
}
} }

View File

@ -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)
@ -96,9 +101,9 @@ struct LibraryListView: View {
.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")
} }
} }

View File

@ -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,9 +80,13 @@ 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
Button {
searchRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item) 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:

View File

@ -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,9 +35,13 @@ 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" {
Button {
libraryRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item) PortraitItemView(item: item)
} }
} }
}
}.onRotate { _ in }.onRotate { _ in
recalcTracks() recalcTracks()
} }
@ -91,24 +95,17 @@ struct LibraryView: View {
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()
}
)
} }
} }

View File

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

View File

@ -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
Button {
homeRouter.route(to: \.item, item)
} label: {
PortraitItemView(item: item) PortraitItemView(item: item)
}
}.padding(.trailing, 16) }.padding(.trailing, 16)
} }
.padding(.leading, 20) .padding(.leading, 20)

View File

@ -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,29 +26,30 @@ 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 { HStack {
Text("User") Text("User")
Spacer() Spacer()
Text(SessionManager.current.user.username ?? "") Text(SessionManager.current.user?.username ?? "")
.foregroundColor(.jellyfinPurple) .foregroundColor(.jellyfinPurple)
} }
NavigationLink( Button {
destination: ServerDetailView(), settingsRouter.route(to: \.serverDetail)
label: { } label: {
HStack { HStack {
Text("Server") Text("Server")
Spacer() Spacer()
Text(ServerEnvironment.current.server.name ?? "") Text(ServerEnvironment.current.server?.name ?? "")
.foregroundColor(.jellyfinPurple) .foregroundColor(.jellyfinPurple)
Image(systemName: "chevron.right")
}
} }
})
Button { Button {
close = false settingsRouter.dismissCoordinator()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
SessionManager.current.logout() SessionManager.current.logout()
let nc = NotificationCenter.default let nc = NotificationCenter.default
@ -89,24 +91,27 @@ struct SettingsView: View {
SearchablePicker(label: "Preferred subtitle language", SearchablePicker(label: "Preferred subtitle language",
options: viewModel.langs, options: viewModel.langs,
optionToString: { $0.name }, optionToString: { $0.name },
selected: Binding<TrackLanguage>( selected: Binding<TrackLanguage>(get: {
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, viewModel.langs
set: {autoSelectSubtitlesLangcode = $0.isoCode} .first(where: { $0.isoCode == autoSelectSubtitlesLangcode
) }) ??
) .auto
},
set: { autoSelectSubtitlesLangcode = $0.isoCode }))
SearchablePicker(label: "Preferred audio language", SearchablePicker(label: "Preferred audio language",
options: viewModel.langs, options: viewModel.langs,
optionToString: { $0.name }, optionToString: { $0.name },
selected: Binding<TrackLanguage>( selected: Binding<TrackLanguage>(get: {
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, viewModel.langs
set: { autoSelectAudioLangcode = $0.isoCode} .first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
) .auto
) },
set: { autoSelectAudioLangcode = $0.isoCode }))
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) { Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
ForEach(self.viewModel.appearances, id: \.self) { appearance in ForEach(self.viewModel.appearances, id: \.self) { appearance in
Text(appearance.localizedName).tag(appearance.rawValue) Text(appearance.localizedName).tag(appearance.rawValue)
} }
}.onChange(of: appAppearance, perform: { value in }.onChange(of: appAppearance, perform: { _ in
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
}) })
} }
@ -115,12 +120,11 @@ struct SettingsView: View {
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) { ToolbarItemGroup(placement: .navigationBarLeading) {
Button { Button {
close = false settingsRouter.dismissCoordinator()
} label: { } label: {
Image(systemName: "xmark") Image(systemName: "xmark")
} }
} }
} }
} }
}
} }

View File

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

View File

@ -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
if flag {
mainRouter.root(\.mainTab)
} else { } else {
NavigationView { mainRouter.root(\.connectToServer)
ConnectToServerView()
} }
.navigationViewStyle(StackNavigationViewStyle())
} }
} }
} }

View File

@ -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 {
@ -390,8 +402,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
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
} }
} }

View File

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

View File

@ -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,17 +50,16 @@ 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"
@ -67,7 +70,8 @@ final class ConnectToServerViewModel: ViewModel {
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)
} }

View File

@ -24,7 +24,6 @@ final class HomeViewModel: ViewModel {
override init() { override init() {
super.init() super.init()
refresh() refresh()
} }

View File

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

View File

@ -26,7 +26,8 @@ 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,
@ -35,6 +36,7 @@ final class LatestMediaViewModel: ViewModel {
.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

View File

@ -198,6 +198,7 @@ extension NextUpEntryView {
} }
func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View { func smallVideoView(item: (BaseItemDto, UIImage?)) -> some View {
Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: {
VStack(alignment: .leading) { VStack(alignment: .leading) {
if let image = item.1 { if let image = item.1 {
Image(uiImage: image) Image(uiImage: image)
@ -218,9 +219,11 @@ extension NextUpEntryView {
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
} }
})
} }
func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View { 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) { HStack(spacing: 20) {
if let image = item.1 { if let image = item.1 {
Image(uiImage: image) Image(uiImage: image)
@ -243,6 +246,7 @@ extension NextUpEntryView {
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
} }
} }
})
} }
} }
@ -281,6 +285,8 @@ 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] {
Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(firstItem.0.id!)")!,
label: {
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {
ZStack(alignment: .bottomLeading) { ZStack(alignment: .bottomLeading) {
if let image = firstItem.1 { if let image = firstItem.1 {
@ -308,6 +314,7 @@ extension NextUpEntryView {
} }
.clipped() .clipped()
.shadow(radius: 8) .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))