Merge pull request #155 from jellyfin/PangMo5/coordinator-and-deep-link
Apply Coordinator Pattern and Add Deep-Links
This commit is contained in:
commit
084f96b7e6
|
@ -171,12 +171,25 @@
|
|||
621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338B22660A07800A81A2A /* LazyView.swift */; };
|
||||
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 */; };
|
||||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
|
||||
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
|
||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; };
|
||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; };
|
||||
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; };
|
||||
625CB56F2678C23300530A6E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56E2678C23300530A6E /* HomeView.swift */; };
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5722678C32A00530A6E /* HomeViewModel.swift */; };
|
||||
625CB5752678C33500530A6E /* LibraryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5742678C33500530A6E /* LibraryListViewModel.swift */; };
|
||||
|
@ -200,10 +213,17 @@
|
|||
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; };
|
||||
628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; };
|
||||
628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; };
|
||||
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 */; };
|
||||
62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; };
|
||||
62CB3F4B2685BB77003D0A6F /* 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 */; };
|
||||
62E632DC267D2E130063E547 /* 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 */; };
|
||||
62EC353226766849000E9F2D /* SessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62EC352E267666A5000E9F2D /* SessionManager.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 */; };
|
||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.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 */; };
|
||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.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 */; };
|
||||
E1AD104B26D94822003E4A08 /* DetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD104926D94822003E4A08 /* DetailItem.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
|
||||
625CB56E2678C23300530A6E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
625CB5722678C32A00530A6E /* HomeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewModel.swift; sourceTree = "<group>"; };
|
||||
625CB5742678C33500530A6E /* LibraryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -434,6 +463,11 @@
|
|||
628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = "<group>"; };
|
||||
628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -447,6 +481,7 @@
|
|||
62EC352B26766675000E9F2D /* ServerEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEnvironment.swift; sourceTree = "<group>"; };
|
||||
62EC352E267666A5000E9F2D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = "<group>"; };
|
||||
62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -461,7 +496,7 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -481,6 +516,7 @@
|
|||
files = (
|
||||
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
|
||||
53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */,
|
||||
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */,
|
||||
53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */,
|
||||
535870912669D7A800D05A09 /* Introspect in Frameworks */,
|
||||
6261A0E026A0AB710072EF1C /* CombineExt in Frameworks */,
|
||||
|
@ -499,6 +535,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */,
|
||||
62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */,
|
||||
62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */,
|
||||
5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */,
|
||||
53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */,
|
||||
|
@ -573,6 +610,7 @@
|
|||
625CB5692678B71200530A6E /* SplashViewModel.swift */,
|
||||
09389CC626819B4500AE350E /* VideoPlayerModel.swift */,
|
||||
625CB57B2678CE1000530A6E /* ViewModel.swift */,
|
||||
6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
|
@ -730,7 +768,9 @@
|
|||
5377CBF3263B596A003A4E83 /* JellyfinPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
62C29E9D26D0FE5900C1D2E7 /* Coordinators */,
|
||||
53F866422687A45400DCD1D7 /* Components */,
|
||||
62ECA01926FA6D6900E8EBB7 /* Singleton */,
|
||||
53AD124C2670278D0094A276 /* JellyfinPlayer.entitlements */,
|
||||
5377CBF8263B596B003A4E83 /* Assets.xcassets */,
|
||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||
|
@ -755,8 +795,8 @@
|
|||
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
|
||||
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
|
||||
625CB5672678B6FB00530A6E /* SplashView.swift */,
|
||||
625CB56B2678C0FD00530A6E /* MainTabView.swift */,
|
||||
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||
62ECA01726FA685A00E8EBB7 /* DeepLink.swift */,
|
||||
);
|
||||
path = JellyfinPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -923,7 +963,7 @@
|
|||
E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */,
|
||||
E1AD105526D981CE003E4A08 /* PortraitHStackView.swift */,
|
||||
53F866432687A45F00DCD1D7 /* PortraitItemView.swift */,
|
||||
E188460326DEF04800B0C5B7 /* CardVStackView.swift */,
|
||||
E188460326DEF04800B0C5B7 /* EpisodeCardVStackView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
|
@ -938,6 +978,7 @@
|
|||
6267B3D92671138200A7371D /* ImageExtensions.swift */,
|
||||
E1AD105226D96D5F003E4A08 /* JellyfinAPIExtensions */,
|
||||
621338922660107500A81A2A /* StringExtensions.swift */,
|
||||
6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -954,6 +995,24 @@
|
|||
path = WidgetExtension;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -965,6 +1024,14 @@
|
|||
path = Singleton;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
62ECA01926FA6D6900E8EBB7 /* Singleton */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6220D0CB26D640C400B8E046 /* AppURLHandler.swift */,
|
||||
);
|
||||
path = Singleton;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
AE8C3157265D6F5E008AA076 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1077,6 +1144,7 @@
|
|||
53272534268BF9710035FBF1 /* SwiftUIFocusGuide */,
|
||||
53649AAE269CFAF600A2D8B7 /* Puppy */,
|
||||
6261A0DF26A0AB710072EF1C /* CombineExt */,
|
||||
6220D0C826D63F3700B8E046 /* Stinsen */,
|
||||
);
|
||||
productName = "JellyfinPlayer tvOS";
|
||||
productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */;
|
||||
|
@ -1111,6 +1179,7 @@
|
|||
62CB3F452685BAF7003D0A6F /* Defaults */,
|
||||
53649AAC269CFAEA00A2D8B7 /* Puppy */,
|
||||
6260FFF826A09754003FA968 /* CombineExt */,
|
||||
62C29E9B26D0FE4200C1D2E7 /* Stinsen */,
|
||||
);
|
||||
productName = JellyfinPlayer;
|
||||
productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */;
|
||||
|
@ -1199,6 +1268,7 @@
|
|||
53272533268BF9710035FBF1 /* XCRemoteSwiftPackageReference "SwiftUIFocusGuide" */,
|
||||
53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */,
|
||||
6260FFF726A09754003FA968 /* XCRemoteSwiftPackageReference "CombineExt" */,
|
||||
62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */,
|
||||
);
|
||||
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1447,6 +1517,7 @@
|
|||
5398514626B64DBB00101B49 /* SearchablePickerView.swift in Sources */,
|
||||
53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */,
|
||||
62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
||||
62D8535B26FC631300FDFC59 /* MainCoordinator.swift in Sources */,
|
||||
62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */,
|
||||
5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */,
|
||||
53649AB2269D019100A2D8B7 /* LogManager.swift in Sources */,
|
||||
|
@ -1454,9 +1525,11 @@
|
|||
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
|
||||
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
|
||||
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */,
|
||||
6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
|
||||
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
|
||||
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
|
||||
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
|
||||
6220D0AE26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
5321753E2671DE9C005491E6 /* Typings.swift in Sources */,
|
||||
E1F0204F26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */,
|
||||
53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */,
|
||||
|
@ -1466,6 +1539,8 @@
|
|||
535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */,
|
||||
53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */,
|
||||
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
||||
5364F456266CA0DC0026ECBA /* BaseItemPersonExtensions.swift in Sources */,
|
||||
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
|
||||
531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */,
|
||||
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||
E1AD105726D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||
|
@ -1480,27 +1555,36 @@
|
|||
files = (
|
||||
5364F455266CA0DC0026ECBA /* BaseItemPersonExtensions.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 */,
|
||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
|
||||
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
|
||||
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */,
|
||||
62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */,
|
||||
5377CBFE263B596B003A4E83 /* PersistenceController.swift in Sources */,
|
||||
6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */,
|
||||
62C29E9F26D1016600C1D2E7 /* MainCoordinator.swift in Sources */,
|
||||
5389276E263C25100035E14B /* ContinueWatchingView.swift in Sources */,
|
||||
53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */,
|
||||
E1AD105626D981CE003E4A08 /* PortraitHStackView.swift in Sources */,
|
||||
62C29EA126D102A500C1D2E7 /* MainTabCoordinator.swift in Sources */,
|
||||
535BAE9F2649E569005FA86D /* ItemView.swift in Sources */,
|
||||
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */,
|
||||
6220D0AD26D5EABB00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */,
|
||||
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
|
||||
62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */,
|
||||
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
|
||||
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
|
||||
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,
|
||||
E188460426DEF04800B0C5B7 /* CardVStackView.swift in Sources */,
|
||||
E188460426DEF04800B0C5B7 /* EpisodeCardVStackView.swift in Sources */,
|
||||
53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */,
|
||||
53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */,
|
||||
62133890265F83A900A81A2A /* LibraryListView.swift in Sources */,
|
||||
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */,
|
||||
0959A5FD2686D29800C7C9A9 /* VideoUpNextView.swift in Sources */,
|
||||
62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */,
|
||||
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */,
|
||||
|
@ -1508,6 +1592,7 @@
|
|||
E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */,
|
||||
53892770263C25230035E14B /* NextUpView.swift in Sources */,
|
||||
625CB5682678B6FB00530A6E /* SplashView.swift in Sources */,
|
||||
62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */,
|
||||
535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */,
|
||||
62E632E6267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */,
|
||||
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||
|
@ -1525,11 +1610,16 @@
|
|||
62E632E9267D3FF50063E547 /* SeasonItemViewModel.swift in Sources */,
|
||||
625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */,
|
||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||
6220D0CC26D640C400B8E046 /* AppURLHandler.swift in Sources */,
|
||||
62E632F3267D54030063E547 /* ItemViewModel.swift in Sources */,
|
||||
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
|
||||
E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */,
|
||||
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */,
|
||||
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
|
||||
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
|
||||
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
|
||||
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
|
||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,
|
||||
531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */,
|
||||
E1AD104A26D94822003E4A08 /* DetailItem.swift in Sources */,
|
||||
62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */,
|
||||
|
@ -1542,14 +1632,15 @@
|
|||
E1AD104D26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */,
|
||||
535870AD2669D8DD00D05A09 /* Typings.swift in Sources */,
|
||||
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */,
|
||||
6220D0BA26D6092100B8E046 /* FilterCoordinator.swift in Sources */,
|
||||
62EC352C26766675000E9F2D /* ServerEnvironment.swift in Sources */,
|
||||
6267B3DA2671138200A7371D /* ImageExtensions.swift in Sources */,
|
||||
62EC353426766B03000E9F2D /* DeviceRotationViewModifier.swift in Sources */,
|
||||
5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */,
|
||||
625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */,
|
||||
539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */,
|
||||
5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */,
|
||||
09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */,
|
||||
6220D0B726D5EE1100B8E046 /* SearchCoordinator.swift in Sources */,
|
||||
5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */,
|
||||
E1FCD09626C47118007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */,
|
||||
|
@ -1573,6 +1664,7 @@
|
|||
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */,
|
||||
E1FCD09A26C4F35A007C8DCF /* ErrorMessage.swift in Sources */,
|
||||
628B95272670CABD0091AF3B /* NextUpWidget.swift in Sources */,
|
||||
6220D0AF26D5EABE00B8E046 /* ViewExtensions.swift in Sources */,
|
||||
628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */,
|
||||
E1FCD09926C4F358007C8DCF /* NetworkError.swift in Sources */,
|
||||
E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */,
|
||||
|
@ -2152,6 +2244,14 @@
|
|||
kind = branch;
|
||||
};
|
||||
};
|
||||
62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/rundfunk47/stinsen";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.0.2;
|
||||
};
|
||||
};
|
||||
62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/acvigue/Defaults";
|
||||
|
@ -2248,6 +2348,11 @@
|
|||
package = 621C637E26672A30004216EA /* XCRemoteSwiftPackageReference "NukeUI" */;
|
||||
productName = NukeUI;
|
||||
};
|
||||
6220D0C826D63F3700B8E046 /* Stinsen */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||
productName = Stinsen;
|
||||
};
|
||||
625CB5792678C4A400530A6E /* ActivityIndicator */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */;
|
||||
|
@ -2278,6 +2383,11 @@
|
|||
package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||
productName = KeychainSwift;
|
||||
};
|
||||
62C29E9B26D0FE4200C1D2E7 /* Stinsen */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */;
|
||||
productName = Stinsen;
|
||||
};
|
||||
62CB3F452685BAF7003D0A6F /* Defaults */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */;
|
||||
|
|
|
@ -109,6 +109,15 @@
|
|||
"version": "0.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Stinsen",
|
||||
"repositoryURL": "https://github.com/rundfunk47/stinsen",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3d06c7603c70f8af1bd49f8d49f17e98f25b2d6a",
|
||||
"version": "2.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-log",
|
||||
"repositoryURL": "https://github.com/apple/swift-log.git",
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
|
||||
struct CardVStackView: View {
|
||||
struct EpisodeCardVStackView: View {
|
||||
|
||||
let items: [BaseItemDto]
|
||||
let selectedAction: (BaseItemDto) -> Void
|
||||
|
||||
private func buildCardOverlayView(item: BaseItemDto) -> some View {
|
||||
HStack {
|
||||
|
@ -45,8 +46,9 @@ struct CardVStackView: View {
|
|||
var body: some View {
|
||||
VStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: ItemNavigationView(item: item)) {
|
||||
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
HStack {
|
||||
|
||||
// MARK: Image
|
|
@ -13,11 +13,12 @@ protocol PillStackable {
|
|||
var title: String { get }
|
||||
}
|
||||
|
||||
struct PillHStackView<NavigationView: View, ItemType: PillStackable>: View {
|
||||
struct PillHStackView<ItemType: PillStackable>: View {
|
||||
|
||||
let title: String
|
||||
let items: [ItemType]
|
||||
let navigationView: (ItemType) -> NavigationView
|
||||
// let navigationView: (ItemType) -> NavigationView
|
||||
let selectedAction: (ItemType) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -30,9 +31,9 @@ struct PillHStackView<NavigationView: View, ItemType: PillStackable>: View {
|
|||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(items, id: \.title) { item in
|
||||
NavigationLink(destination: LazyView {
|
||||
navigationView(item)
|
||||
}) {
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
ZStack {
|
||||
Color(UIColor.systemFill)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
|
|
@ -17,20 +17,20 @@ public protocol PortraitImageStackable {
|
|||
var failureInitials: String { get }
|
||||
}
|
||||
|
||||
struct PortraitImageHStackView<TopBarView: View, NavigationView: View, ItemType: PortraitImageStackable>: View {
|
||||
struct PortraitImageHStackView<TopBarView: View, ItemType: PortraitImageStackable>: View {
|
||||
|
||||
let items: [ItemType]
|
||||
let maxWidth: Int
|
||||
let horizontalAlignment: HorizontalAlignment
|
||||
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.maxWidth = maxWidth
|
||||
self.horizontalAlignment = horizontalAlignment
|
||||
self.topBarView = topBarView
|
||||
self.navigationView = navigationView
|
||||
self.selectedAction = selectedAction
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -45,11 +45,9 @@ struct PortraitImageHStackView<TopBarView: View, NavigationView: View, ItemType:
|
|||
Spacer().frame(width: 16)
|
||||
|
||||
ForEach(items, id: \.title) { item in
|
||||
NavigationLink(
|
||||
destination: LazyView {
|
||||
navigationView(item)
|
||||
},
|
||||
label: {
|
||||
Button {
|
||||
selectedAction(item)
|
||||
} label: {
|
||||
VStack {
|
||||
ImageView(src: item.imageURLContsructor(maxWidth: maxWidth),
|
||||
bh: item.blurHash,
|
||||
|
@ -76,7 +74,7 @@ struct PortraitImageHStackView<TopBarView: View, NavigationView: View, ItemType:
|
|||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Spacer().frame(width: UIDevice.current.userInterfaceIdiom == .pad ? 16 : 55)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
/*
|
||||
/*
|
||||
* 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/.
|
||||
|
@ -7,30 +7,26 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct PortraitItemView: View {
|
||||
|
||||
var item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) {
|
||||
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)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.overlay(Rectangle()
|
||||
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat(item.userData?.playedPercentage ?? 0), height: 7)
|
||||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
.overlay(
|
||||
ZStack {
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
.overlay(ZStack {
|
||||
if item.userData?.isFavorite ?? false {
|
||||
Image(systemName: "circle.fill")
|
||||
.foregroundColor(.white)
|
||||
|
@ -43,8 +39,7 @@ struct PortraitItemView: View {
|
|||
.padding(.leading, 2)
|
||||
.padding(.bottom, item.userData?.playedPercentage == nil ? 2 : 9)
|
||||
.opacity(1), alignment: .bottomLeading)
|
||||
.overlay(
|
||||
ZStack {
|
||||
.overlay(ZStack {
|
||||
if item.userData?.played ?? false {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
|
@ -87,5 +82,4 @@ struct PortraitItemView: View {
|
|||
}
|
||||
}.frame(width: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Stinsen
|
||||
|
||||
struct ConnectToServerView: View {
|
||||
@EnvironmentObject var mainRouter: MainCoordinator.Router
|
||||
@StateObject var viewModel = ConnectToServerViewModel()
|
||||
@State var username = ""
|
||||
@State var password = ""
|
||||
|
@ -59,6 +61,7 @@ struct ConnectToServerView: View {
|
|||
if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) {
|
||||
let user = SessionManager.current.getSavedSession(userID: publicUser.id!)
|
||||
SessionManager.current.loginWithSavedSession(user: user)
|
||||
mainRouter.root(\.mainTab)
|
||||
} else {
|
||||
username = publicUser.name ?? ""
|
||||
viewModel.selectedPublicUser = publicUser
|
||||
|
@ -174,5 +177,8 @@ struct ConnectToServerView: View {
|
|||
dismissButton: .cancel())
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Connect to Server", comment: ""))
|
||||
.onAppear {
|
||||
AppURLHandler.shared.appURLState = .allowedInLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ProgressBar: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
|
@ -31,26 +31,27 @@ struct ProgressBar: Shape {
|
|||
}
|
||||
|
||||
struct ContinueWatchingView: View {
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
var items: [BaseItemDto]
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
NavigationLink(destination: LazyView { ItemNavigationView(item: item) }) {
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())
|
||||
.frame(width: 320, height: 180)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.shadow(radius: 4, y: 2)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.fill(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.overlay(Rectangle()
|
||||
.fill(Color(red: 172 / 255, green: 92 / 255, blue: 195 / 255))
|
||||
.mask(ProgressBar())
|
||||
.frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7)
|
||||
.padding(0), alignment: .bottomLeading
|
||||
)
|
||||
.padding(0), alignment: .bottomLeading)
|
||||
HStack {
|
||||
Text("\(item.seriesName ?? item.name ?? "")")
|
||||
.font(.callout)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class ConnectToServerCoodinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \ConnectToServerCoodinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
ConnectToServerView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
typealias FilterCoordinatorParams = (filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String)
|
||||
|
||||
final class FilterCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \FilterCoordinator.start)
|
||||
@Root var start = makeStart
|
||||
|
||||
@Binding var filters: LibraryFilters
|
||||
var enabledFilterType: [FilterType]
|
||||
var parentId: String = ""
|
||||
|
||||
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
|
||||
_filters = filters
|
||||
self.enabledFilterType = enabledFilterType
|
||||
self.parentId = parentId
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
LibraryFilterView(filters: $filters, enabledFilterType: enabledFilterType, parentId: parentId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class HomeCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \HomeCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.modal) var settings = makeSettings
|
||||
@Route(.push) var library = makeLibrary
|
||||
@Route(.push) var item = makeItem
|
||||
|
||||
func makeSettings() -> NavigationViewCoordinator<SettingsCoordinator> {
|
||||
NavigationViewCoordinator(SettingsCoordinator())
|
||||
}
|
||||
|
||||
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||
}
|
||||
|
||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
HomeView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class ItemCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \ItemCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var item = makeItem
|
||||
@Route(.push) var library = makeLibrary
|
||||
@Route(.fullScreen) var videoPlayer = makeVideoPlayer
|
||||
|
||||
let itemDto: BaseItemDto
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.itemDto = item
|
||||
}
|
||||
|
||||
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||
}
|
||||
|
||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
|
||||
func makeVideoPlayer(item: BaseItemDto) -> NavigationViewCoordinator<VideoPlayerCoordinator> {
|
||||
NavigationViewCoordinator(VideoPlayerCoordinator(item: item))
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
ItemNavigationView(item: itemDto)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
typealias LibraryCoordinatorParams = (viewModel: LibraryViewModel, title: String)
|
||||
|
||||
final class LibraryCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \LibraryCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var search = makeSearch
|
||||
@Route(.modal) var filter = makeFilter
|
||||
@Route(.push) var item = makeItem
|
||||
|
||||
var viewModel: LibraryViewModel
|
||||
var title: String
|
||||
|
||||
init(viewModel: LibraryViewModel, title: String) {
|
||||
self.viewModel = viewModel
|
||||
self.title = title
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
LibraryView(viewModel: self.viewModel, title: title)
|
||||
}
|
||||
|
||||
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
|
||||
SearchCoordinator(viewModel: viewModel)
|
||||
}
|
||||
|
||||
func makeFilter(params: FilterCoordinatorParams) -> NavigationViewCoordinator<FilterCoordinator> {
|
||||
NavigationViewCoordinator(FilterCoordinator(filters: params.filters,
|
||||
enabledFilterType: params.enabledFilterType,
|
||||
parentId: params.parentId))
|
||||
}
|
||||
|
||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class LibraryListCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \LibraryListCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var search = makeSearch
|
||||
@Route(.push) var library = makeLibrary
|
||||
|
||||
func makeLibrary(params: LibraryCoordinatorParams) -> LibraryCoordinator {
|
||||
LibraryCoordinator(viewModel: params.viewModel, title: params.title)
|
||||
}
|
||||
|
||||
func makeSearch(viewModel: LibrarySearchViewModel) -> SearchCoordinator {
|
||||
SearchCoordinator(viewModel: viewModel)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func makeStart() -> some View {
|
||||
LibraryListView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Nuke
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
#if !os(tvOS)
|
||||
import WidgetKit
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
final class MainCoordinator: NavigationCoordinatable {
|
||||
var stack: NavigationStack<MainCoordinator>
|
||||
|
||||
@Root var mainTab = makeMainTab
|
||||
@Root var connectToServer = makeConnectToServer
|
||||
|
||||
init() {
|
||||
if ServerEnvironment.current.server != nil, SessionManager.current.user != nil {
|
||||
self.stack = NavigationStack(initial: \MainCoordinator.mainTab)
|
||||
} else {
|
||||
self.stack = NavigationStack(initial: \MainCoordinator.connectToServer)
|
||||
}
|
||||
ImageCache.shared.costLimit = 125 * 1024 * 1024 // 125MB memory
|
||||
DataLoader.sharedUrlCache.diskCapacity = 1000 * 1024 * 1024 // 1000MB disk
|
||||
|
||||
#if !os(tvOS)
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
UIScrollView.appearance().keyboardDismissMode = .onDrag
|
||||
#endif
|
||||
|
||||
let nc = NotificationCenter.default
|
||||
nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil)
|
||||
nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil)
|
||||
nc.addObserver(self, selector: #selector(processDeepLink), name: Notification.Name("processDeepLink"), object: nil)
|
||||
}
|
||||
|
||||
@objc func didLogIn() {
|
||||
LogManager.shared.log.info("Received `didSignIn` from NSNotificationCenter.")
|
||||
root(\.mainTab)
|
||||
}
|
||||
|
||||
@objc func didLogOut() {
|
||||
LogManager.shared.log.info("Received `didSignOut` from NSNotificationCenter.")
|
||||
root(\.connectToServer)
|
||||
}
|
||||
|
||||
@objc func processDeepLink(_ notification: Notification) {
|
||||
guard let deepLink = notification.object as? DeepLink else { return }
|
||||
if let coordinator = hasRoot(\.mainTab) {
|
||||
switch deepLink {
|
||||
case let .item(item):
|
||||
coordinator.focusFirst(\.home)
|
||||
.child
|
||||
.popToRoot()
|
||||
.route(to: \.item, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeMainTab() -> MainTabCoordinator {
|
||||
MainTabCoordinator()
|
||||
}
|
||||
|
||||
func makeConnectToServer() -> NavigationViewCoordinator<ConnectToServerCoodinator> {
|
||||
NavigationViewCoordinator(ConnectToServerCoodinator())
|
||||
}
|
||||
}
|
||||
|
||||
#elseif os(tvOS)
|
||||
// temp for fixing build error
|
||||
final class MainCoordinator: NavigationCoordinatable {
|
||||
var stack = NavigationStack<MainCoordinator>(initial: \MainCoordinator.mainTab)
|
||||
|
||||
@Root var mainTab = makeEmpty
|
||||
|
||||
@ViewBuilder func makeEmpty() -> some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
import Stinsen
|
||||
|
||||
final class MainTabCoordinator: TabCoordinatable {
|
||||
var child = TabChild(startingItems: [
|
||||
\MainTabCoordinator.home,
|
||||
\MainTabCoordinator.allMedia,
|
||||
])
|
||||
|
||||
@Route(tabItem: makeHomeTab) var home = makeHome
|
||||
@Route(tabItem: makeTodosTab) var allMedia = makeTodos
|
||||
|
||||
func makeHome() -> NavigationViewCoordinator<HomeCoordinator> {
|
||||
return NavigationViewCoordinator(HomeCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder func makeHomeTab(isActive: Bool) -> some View {
|
||||
Image(systemName: "house")
|
||||
Text("Home")
|
||||
}
|
||||
|
||||
func makeTodos() -> NavigationViewCoordinator<LibraryListCoordinator> {
|
||||
return NavigationViewCoordinator(LibraryListCoordinator())
|
||||
}
|
||||
|
||||
@ViewBuilder func makeTodosTab(isActive: Bool) -> some View {
|
||||
Image(systemName: "folder")
|
||||
Text("All Media")
|
||||
}
|
||||
|
||||
@ViewBuilder func customize(_ view: AnyView) -> some View {
|
||||
view.onAppear {
|
||||
AppURLHandler.shared.appURLState = .allowed
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
||||
AppURLHandler.shared.processLaunchedURLIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
|
||||
final class SearchCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \SearchCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var item = makeItem
|
||||
|
||||
var viewModel: LibrarySearchViewModel
|
||||
|
||||
init(viewModel: LibrarySearchViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
func makeItem(item: BaseItemDto) -> ItemCoordinator {
|
||||
ItemCoordinator(item: item)
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
LibrarySearchView(viewModel: self.viewModel)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class SettingsCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
@Route(.push) var serverDetail = makeServerDetail
|
||||
|
||||
@ViewBuilder func makeServerDetail() -> some View {
|
||||
ServerDetailView()
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
SettingsView(viewModel: .init())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
final class VideoPlayerCoordinator: NavigationCoordinatable {
|
||||
let stack = NavigationStack(initial: \VideoPlayerCoordinator.start)
|
||||
|
||||
@Root var start = makeStart
|
||||
var item: BaseItemDto
|
||||
|
||||
init(item: BaseItemDto) {
|
||||
self.item = item
|
||||
}
|
||||
|
||||
@ViewBuilder func makeStart() -> some View {
|
||||
VideoPlayerView(item: item)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
enum DeepLink {
|
||||
case item(BaseItemDto)
|
||||
}
|
|
@ -11,8 +11,8 @@ import Foundation
|
|||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
@StateObject var viewModel = HomeViewModel()
|
||||
@State var showingSettings = false
|
||||
|
||||
init() {
|
||||
let backButtonBackgroundImage = UIImage(systemName: "chevron.backward.circle.fill")
|
||||
|
@ -43,9 +43,12 @@ struct HomeView: View {
|
|||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
homeRouter
|
||||
.route(to: \.library, (viewModel: .init(parentID: libraryID,
|
||||
filters: viewModel.recentFilterSet),
|
||||
title: library?.name ?? ""))
|
||||
} label: {
|
||||
HStack {
|
||||
Text("See All").font(.subheadline).fontWeight(.bold)
|
||||
Image(systemName: "chevron.right").font(Font.subheadline.bold())
|
||||
|
@ -68,14 +71,11 @@ struct HomeView: View {
|
|||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showingSettings = true
|
||||
homeRouter.route(to: \.settings)
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showingSettings) {
|
||||
SettingsView(viewModel: SettingsViewModel(), close: $showingSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,17 @@
|
|||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<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>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
|
|
@ -5,18 +5,17 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Introspect
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
class VideoPlayerItem: ObservableObject {
|
||||
@Published var shouldShowPlayer: Bool = false
|
||||
@Published var itemToPlay: BaseItemDto = BaseItemDto()
|
||||
@Published var itemToPlay = BaseItemDto()
|
||||
}
|
||||
|
||||
// Intermediary view for ItemView to set navigation bar settings
|
||||
struct ItemNavigationView: View {
|
||||
|
||||
private let 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 orientation: UIDeviceOrientation = .unknown
|
||||
@StateObject private var videoPlayerItem: VideoPlayerItem = VideoPlayerItem()
|
||||
@StateObject private var videoPlayerItem = VideoPlayerItem()
|
||||
@Environment(\.horizontalSizeClass) private var hSizeClass
|
||||
@Environment(\.verticalSizeClass) private var vSizeClass
|
||||
|
||||
|
@ -56,6 +56,7 @@ fileprivate struct ItemView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if hSizeClass == .compact && vSizeClass == .regular {
|
||||
ItemPortraitMainView(videoIsLoading: $videoIsLoading)
|
||||
.environmentObject(videoPlayerItem)
|
||||
|
@ -66,4 +67,9 @@ fileprivate struct ItemView: View {
|
|||
.environmentObject(viewModel)
|
||||
}
|
||||
}
|
||||
.onReceive(videoPlayerItem.$shouldShowPlayer) { flag in
|
||||
guard flag else { return }
|
||||
self.itemRouter.route(to: \.videoPlayer, viewModel.item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
/*
|
||||
/*
|
||||
* 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/.
|
||||
|
@ -7,23 +7,24 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ItemViewBody: View {
|
||||
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
// MARK: Overview
|
||||
|
||||
Text(viewModel.item.overview ?? "")
|
||||
.font(.footnote)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 3)
|
||||
|
||||
// MARK: Seasons
|
||||
|
||||
if let seriesViewModel = viewModel as? SeriesItemViewModel {
|
||||
PortraitImageHStackView(items: seriesViewModel.seasons,
|
||||
maxWidth: 150,
|
||||
|
@ -33,28 +34,32 @@ struct ItemViewBody: View {
|
|||
.fontWeight(.semibold)
|
||||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
}, navigationView: { season in
|
||||
ItemNavigationView(item: season)
|
||||
}, selectedAction: { season in
|
||||
itemRouter.route(to: \.item, season)
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: Genres
|
||||
|
||||
PillHStackView(title: "Genres",
|
||||
items: viewModel.item.genreItems ?? []) { genre in
|
||||
LibraryView(viewModel: .init(genre: genre), title: genre.title)
|
||||
}
|
||||
items: viewModel.item.genreItems ?? [],
|
||||
selectedAction: { genre in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(genre: genre), title: genre.title))
|
||||
})
|
||||
|
||||
// MARK: Studios
|
||||
|
||||
if let studios = viewModel.item.studios {
|
||||
PillHStackView(title: "Studios",
|
||||
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
|
||||
|
||||
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,
|
||||
topBarView: {
|
||||
Text("Cast & Crew")
|
||||
|
@ -63,12 +68,13 @@ struct ItemViewBody: View {
|
|||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
},
|
||||
navigationView: { person in
|
||||
LibraryView(viewModel: .init(person: person), title: person.title)
|
||||
selectedAction: { person in
|
||||
itemRouter.route(to: \.library, (viewModel: .init(person: person), title: person.title))
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: More Like This
|
||||
|
||||
if !viewModel.similarItems.isEmpty {
|
||||
PortraitImageHStackView(items: viewModel.similarItems,
|
||||
maxWidth: 150,
|
||||
|
@ -79,8 +85,8 @@ struct ItemViewBody: View {
|
|||
.padding(.top, 3)
|
||||
.padding(.leading, 16)
|
||||
},
|
||||
navigationView: { item in
|
||||
ItemNavigationView(item: item)
|
||||
selectedAction: { item in
|
||||
itemRouter.route(to: \.item, item)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
/*
|
||||
/*
|
||||
* 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/.
|
||||
|
@ -7,10 +7,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct ItemLandscapeMainView: View {
|
||||
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
@Binding private var videoIsLoading: Bool
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
||||
|
@ -20,10 +21,11 @@ struct ItemLandscapeMainView: View {
|
|||
}
|
||||
|
||||
// MARK: innerBody
|
||||
|
||||
private var innerBody: some View {
|
||||
HStack {
|
||||
|
||||
// MARK: Sidebar Image
|
||||
|
||||
VStack {
|
||||
ImageView(src: viewModel.item.getPrimaryImage(maxWidth: 130),
|
||||
bh: viewModel.item.getPrimaryImageBlurHash())
|
||||
|
@ -38,8 +40,8 @@ struct ItemLandscapeMainView: View {
|
|||
self.videoPlayerItem.shouldShowPlayer = true
|
||||
}
|
||||
} label: {
|
||||
|
||||
// MARK: Play
|
||||
|
||||
HStack {
|
||||
Image(systemName: "play.fill")
|
||||
.foregroundColor(viewModel.playButtonItem == nil ? Color(UIColor.secondaryLabel) : Color.white)
|
||||
|
@ -59,14 +61,17 @@ struct ItemLandscapeMainView: View {
|
|||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
// MARK: ItemLandscapeTopBarView
|
||||
|
||||
ItemLandscapeTopBarView()
|
||||
.environmentObject(viewModel)
|
||||
|
||||
// MARK: ItemViewBody
|
||||
|
||||
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
||||
CardVStackView(items: episodeViewModel.episodes)
|
||||
EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in
|
||||
itemRouter.route(to: \.item, episode)
|
||||
}
|
||||
} else {
|
||||
ItemViewBody()
|
||||
.environmentObject(viewModel)
|
||||
|
@ -77,24 +82,12 @@ struct ItemLandscapeMainView: View {
|
|||
}
|
||||
|
||||
// MARK: body
|
||||
|
||||
var body: some View {
|
||||
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 {
|
||||
|
||||
// MARK: Backdrop
|
||||
|
||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 200),
|
||||
bh: viewModel.item.getBackdropImageBlurHash())
|
||||
.opacity(0.3)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
/*
|
||||
/*
|
||||
* 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/.
|
||||
|
@ -7,11 +7,11 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import JellyfinAPI
|
||||
import SwiftUI
|
||||
|
||||
struct ItemPortraitMainView: View {
|
||||
|
||||
@EnvironmentObject var itemRouter: ItemCoordinator.Router
|
||||
@Binding private var videoIsLoading: Bool
|
||||
@EnvironmentObject private var viewModel: ItemViewModel
|
||||
@EnvironmentObject private var videoPlayerItem: VideoPlayerItem
|
||||
|
@ -21,6 +21,7 @@ struct ItemPortraitMainView: View {
|
|||
}
|
||||
|
||||
// MARK: portraitHeaderView
|
||||
|
||||
var portraitHeaderView: some View {
|
||||
ImageView(src: viewModel.item.getBackdropImage(maxWidth: Int(UIScreen.main.bounds.width)),
|
||||
bh: viewModel.item.getBackdropImageBlurHash())
|
||||
|
@ -29,40 +30,31 @@ struct ItemPortraitMainView: View {
|
|||
}
|
||||
|
||||
// MARK: portraitStaticOverlayView
|
||||
|
||||
var portraitStaticOverlayView: some View {
|
||||
PortraitHeaderOverlayView()
|
||||
.environmentObject(viewModel)
|
||||
}
|
||||
|
||||
// MARK: body
|
||||
|
||||
var body: some View {
|
||||
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
|
||||
|
||||
ParallaxHeaderScrollView(header: portraitHeaderView,
|
||||
staticOverlayView: portraitStaticOverlayView,
|
||||
overlayAlignment: .bottomLeading,
|
||||
headerHeight: UIScreen.main.bounds.width * 0.5625) {
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
.frame(height: 70)
|
||||
|
||||
if let episodeViewModel = viewModel as? SeasonItemViewModel {
|
||||
Spacer()
|
||||
CardVStackView(items: episodeViewModel.episodes)
|
||||
EpisodeCardVStackView(items: episodeViewModel.episodes) { episode in
|
||||
itemRouter.route(to: \.item, episode)
|
||||
}
|
||||
.padding(.top, 5)
|
||||
} else {
|
||||
ItemViewBody()
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MessageUI
|
||||
import Defaults
|
||||
import MessageUI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
// The notification we'll send when a shake gesture happens.
|
||||
extension UIDevice {
|
||||
|
@ -16,7 +17,7 @@ extension UIDevice {
|
|||
|
||||
// Override the default behavior of shake gestures to send our notification instead.
|
||||
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 {
|
||||
NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil)
|
||||
}
|
||||
|
@ -27,7 +28,7 @@ extension UIWindow {
|
|||
struct DeviceShakeViewModifier: ViewModifier {
|
||||
let action: () -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
func body(content: Self.Content) -> some View {
|
||||
content
|
||||
.onAppear()
|
||||
.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.
|
||||
extension View {
|
||||
func onShake(perform action: @escaping () -> Void) -> some View {
|
||||
self.modifier(DeviceShakeViewModifier(action: action))
|
||||
modifier(DeviceShakeViewModifier(action: action))
|
||||
}
|
||||
}
|
||||
|
||||
extension UIDevice {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
extension 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
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: Context) {
|
||||
}
|
||||
func updateUIView(_ uiView: UIView, context: Context) {}
|
||||
}
|
||||
|
||||
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
|
||||
|
@ -111,12 +111,11 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
|||
box.value?._orientations = $0
|
||||
}.onPreferenceChange(ViewPreferenceKey.self) {
|
||||
box.value?._viewPreference = $0
|
||||
}
|
||||
))
|
||||
}))
|
||||
box.value = self
|
||||
}
|
||||
|
||||
@objc required dynamic init?(coder aDecoder: NSCoder) {
|
||||
@objc dynamic required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
super.modalPresentationStyle = .fullScreen
|
||||
}
|
||||
|
@ -131,6 +130,7 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
|||
public var _prefersHomeIndicatorAutoHidden = false {
|
||||
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
_prefersHomeIndicatorAutoHidden
|
||||
}
|
||||
|
@ -146,6 +146,7 @@ class PreferenceUIHostingController: UIHostingController<AnyView> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
_orientations
|
||||
}
|
||||
|
@ -176,7 +177,7 @@ extension View {
|
|||
|
||||
class EmailHelper: NSObject, MFMailComposeViewControllerDelegate {
|
||||
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)
|
||||
|
||||
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.addAttachmentData(data!, mimeType: "text/plain", fileName: logURL.lastPathComponent)
|
||||
picker.mailComposeDelegate = self
|
||||
|
@ -218,17 +221,22 @@ struct JellyfinPlayerApp: App {
|
|||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
SplashView()
|
||||
EmptyView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
.onAppear(perform: {
|
||||
setupAppearance()
|
||||
})
|
||||
.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 {
|
||||
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 {
|
||||
|
||||
static var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
|
|
|
@ -5,16 +5,20 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LatestMediaView: View {
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
@StateObject var viewModel: LatestMediaViewModel
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
if item.type == "Series" || item.type == "Movie" {
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
*/
|
||||
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryFilterView: View {
|
||||
@EnvironmentObject var filterRouter: FilterCoordinator.Router
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Binding var filters: LibraryFilters
|
||||
var parentId: String = ""
|
||||
|
@ -18,11 +20,11 @@ struct LibraryFilterView: View {
|
|||
init(filters: Binding<LibraryFilters>, enabledFilterType: [FilterType], parentId: String) {
|
||||
_filters = filters
|
||||
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 {
|
||||
NavigationView {
|
||||
VStack {
|
||||
if viewModel.isLoading {
|
||||
ProgressView()
|
||||
|
@ -64,7 +66,7 @@ struct LibraryFilterView: View {
|
|||
Button {
|
||||
viewModel.resetFilters()
|
||||
self.filters = viewModel.modifiedFilters
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Reset")
|
||||
}
|
||||
|
@ -74,7 +76,7 @@ struct LibraryFilterView: View {
|
|||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
|
@ -83,12 +85,11 @@ struct LibraryFilterView: View {
|
|||
Button {
|
||||
viewModel.updateModifiedFilter()
|
||||
self.filters = viewModel.modifiedFilters
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
filterRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Text("Apply")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,17 +6,20 @@
|
|||
*/
|
||||
|
||||
import Foundation
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryListView: View {
|
||||
@EnvironmentObject var libraryListRouter: LibraryListCoordinator.Router
|
||||
@StateObject var viewModel = LibraryListViewModel()
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites")
|
||||
}) {
|
||||
Button {
|
||||
libraryListRouter.route(to: \.library,
|
||||
(viewModel: LibraryViewModel(filters: viewModel.withFavorites), title: "Favorites"))
|
||||
} label: {
|
||||
ZStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
@ -59,9 +62,11 @@ struct LibraryListView: View {
|
|||
if !viewModel.isLoading {
|
||||
ForEach(viewModel.libraries, id: \.id) { library in
|
||||
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "")
|
||||
}) {
|
||||
Button {
|
||||
libraryListRouter.route(to: \.library,
|
||||
(viewModel: LibraryViewModel(parentID: library.id),
|
||||
title: library.name ?? ""))
|
||||
} label: {
|
||||
ZStack {
|
||||
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
|
||||
.opacity(0.4)
|
||||
|
@ -96,9 +101,9 @@ struct LibraryListView: View {
|
|||
.navigationTitle(NSLocalizedString("All Media", comment: ""))
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
NavigationLink(destination: LazyView {
|
||||
LibrarySearchView(viewModel: .init(parentID: nil))
|
||||
}) {
|
||||
Button {
|
||||
libraryListRouter.route(to: \.search, LibrarySearchViewModel(parentID: nil))
|
||||
} label: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,13 @@
|
|||
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibrarySearchView: View {
|
||||
@EnvironmentObject var searchRouter: SearchCoordinator.Router
|
||||
@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)
|
||||
|
||||
|
@ -78,9 +80,13 @@ struct LibrarySearchView: View {
|
|||
if !items.isEmpty {
|
||||
LazyVGrid(columns: tracks) {
|
||||
ForEach(items, id: \.id) { item in
|
||||
Button {
|
||||
searchRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +112,6 @@ struct LibrarySearchView: View {
|
|||
}
|
||||
|
||||
private extension ItemType {
|
||||
|
||||
var localized: String {
|
||||
switch self {
|
||||
case .episode:
|
||||
|
|
|
@ -6,17 +6,17 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryView: View {
|
||||
@EnvironmentObject var libraryRouter: LibraryCoordinator.Router
|
||||
@StateObject var viewModel: LibraryViewModel
|
||||
var title: String
|
||||
|
||||
// MARK: tracks for grid
|
||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||
|
||||
@State var isShowingSearchView = false
|
||||
@State var isShowingFilterView = false
|
||||
var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name])
|
||||
|
||||
@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) {
|
||||
ForEach(viewModel.items, id: \.id) { item in
|
||||
if item.type != "Folder" {
|
||||
Button {
|
||||
libraryRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onRotate { _ in
|
||||
recalcTracks()
|
||||
}
|
||||
|
@ -91,24 +95,17 @@ struct LibraryView: View {
|
|||
Label("Icon One", systemImage: "line.horizontal.3.decrease.circle")
|
||||
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
|
||||
.onTapGesture {
|
||||
isShowingFilterView = true
|
||||
libraryRouter
|
||||
.route(to: \.filter, (filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
|
||||
parentId: viewModel.parentID ?? ""))
|
||||
}
|
||||
Button {
|
||||
isShowingSearchView = true
|
||||
libraryRouter.route(to: \.search, .init(parentID: viewModel.parentID))
|
||||
} label: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isShowingFilterView) {
|
||||
LibraryFilterView(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType, parentId: viewModel.parentID ?? "")
|
||||
}
|
||||
.background(
|
||||
NavigationLink(destination: LibrarySearchView(viewModel: .init(parentID: viewModel.parentID)),
|
||||
isActive: $isShowingSearchView) {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@State private var tabSelection: Tab = .home
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $tabSelection) {
|
||||
NavigationView {
|
||||
HomeView()
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Text("Home")
|
||||
Image(systemName: "house")
|
||||
}
|
||||
.tag(Tab.home)
|
||||
NavigationView {
|
||||
LibraryListView()
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Text("All Media")
|
||||
Image(systemName: "folder")
|
||||
}
|
||||
.tag(Tab.allMedia)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MainTabView {
|
||||
|
||||
enum Tab: String {
|
||||
case home
|
||||
case allMedia
|
||||
}
|
||||
}
|
|
@ -5,11 +5,13 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct NextUpView: View {
|
||||
@EnvironmentObject var homeRouter: HomeCoordinator.Router
|
||||
|
||||
var items: [BaseItemDto]
|
||||
|
||||
|
@ -22,7 +24,11 @@ struct NextUpView: View {
|
|||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack {
|
||||
ForEach(items, id: \.id) { item in
|
||||
Button {
|
||||
homeRouter.route(to: \.item, item)
|
||||
} label: {
|
||||
PortraitItemView(item: item)
|
||||
}
|
||||
}.padding(.trailing, 16)
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
|
|
|
@ -6,15 +6,16 @@
|
|||
*/
|
||||
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
import Defaults
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
||||
@ObservedObject var viewModel: SettingsViewModel
|
||||
|
||||
@Binding var close: Bool
|
||||
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate
|
||||
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate
|
||||
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles
|
||||
|
@ -25,29 +26,30 @@ struct SettingsView: View {
|
|||
@Default(.videoPlayerJumpBackward) var jumpBackwardLength
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: EmptyView()) {
|
||||
HStack {
|
||||
Text("User")
|
||||
Spacer()
|
||||
Text(SessionManager.current.user.username ?? "")
|
||||
Text(SessionManager.current.user?.username ?? "")
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
}
|
||||
|
||||
NavigationLink(
|
||||
destination: ServerDetailView(),
|
||||
label: {
|
||||
Button {
|
||||
settingsRouter.route(to: \.serverDetail)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Server")
|
||||
Spacer()
|
||||
Text(ServerEnvironment.current.server.name ?? "")
|
||||
Text(ServerEnvironment.current.server?.name ?? "")
|
||||
.foregroundColor(.jellyfinPurple)
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Button {
|
||||
close = false
|
||||
settingsRouter.dismissCoordinator()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
SessionManager.current.logout()
|
||||
let nc = NotificationCenter.default
|
||||
|
@ -89,24 +91,27 @@ struct SettingsView: View {
|
|||
SearchablePicker(label: "Preferred subtitle language",
|
||||
options: viewModel.langs,
|
||||
optionToString: { $0.name },
|
||||
selected: Binding<TrackLanguage>(
|
||||
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto },
|
||||
set: {autoSelectSubtitlesLangcode = $0.isoCode}
|
||||
)
|
||||
)
|
||||
selected: Binding<TrackLanguage>(get: {
|
||||
viewModel.langs
|
||||
.first(where: { $0.isoCode == autoSelectSubtitlesLangcode
|
||||
}) ??
|
||||
.auto
|
||||
},
|
||||
set: { autoSelectSubtitlesLangcode = $0.isoCode }))
|
||||
SearchablePicker(label: "Preferred audio language",
|
||||
options: viewModel.langs,
|
||||
optionToString: { $0.name },
|
||||
selected: Binding<TrackLanguage>(
|
||||
get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto },
|
||||
set: { autoSelectAudioLangcode = $0.isoCode}
|
||||
)
|
||||
)
|
||||
selected: Binding<TrackLanguage>(get: {
|
||||
viewModel.langs
|
||||
.first(where: { $0.isoCode == autoSelectAudioLangcode }) ??
|
||||
.auto
|
||||
},
|
||||
set: { autoSelectAudioLangcode = $0.isoCode }))
|
||||
Picker(NSLocalizedString("Appearance", comment: ""), selection: $appAppearance) {
|
||||
ForEach(self.viewModel.appearances, id: \.self) { appearance in
|
||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||
}
|
||||
}.onChange(of: appAppearance, perform: { value in
|
||||
}.onChange(of: appAppearance, perform: { _ in
|
||||
UIApplication.shared.windows.first?.overrideUserInterfaceStyle = appAppearance.style
|
||||
})
|
||||
}
|
||||
|
@ -115,12 +120,11 @@ struct SettingsView: View {
|
|||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
close = false
|
||||
settingsRouter.dismissCoordinator()
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
|
||||
final class AppURLHandler {
|
||||
static let deepLinkScheme = "jellyfin"
|
||||
|
||||
enum AppURLState {
|
||||
case launched
|
||||
case allowedInLogin
|
||||
case allowed
|
||||
|
||||
func allowedScheme(with url: URL) -> Bool {
|
||||
switch self {
|
||||
case .launched:
|
||||
return false
|
||||
case .allowed:
|
||||
return true
|
||||
case .allowedInLogin:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static let shared = AppURLHandler()
|
||||
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var appURLState: AppURLState = .launched
|
||||
var launchURL: URL?
|
||||
}
|
||||
|
||||
extension AppURLHandler {
|
||||
@discardableResult
|
||||
func processDeepLink(url: URL) -> Bool {
|
||||
guard url.scheme == Self.deepLinkScheme || url.scheme == "widget-extension" else {
|
||||
return false
|
||||
}
|
||||
if AppURLHandler.shared.appURLState.allowedScheme(with: url) {
|
||||
return processURL(url)
|
||||
} else {
|
||||
launchURL = url
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func processLaunchedURLIfNeeded() {
|
||||
guard let launchURL = launchURL,
|
||||
!launchURL.absoluteString.isEmpty else { return }
|
||||
if processDeepLink(url: launchURL) {
|
||||
self.launchURL = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func processURL(_ url: URL) -> Bool {
|
||||
if processURLForUser(url: url) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func processURLForUser(url: URL) -> Bool {
|
||||
guard url.host?.lowercased() == "users",
|
||||
url.pathComponents[safe: 1]?.isEmpty == false else { return false }
|
||||
|
||||
// /Users/{UserID}/Items/{ItemID}
|
||||
if url.pathComponents[safe: 2]?.lowercased() == "items",
|
||||
let userID = url.pathComponents[safe: 1],
|
||||
let itemID = url.pathComponents[safe: 3]
|
||||
{
|
||||
// It would be nice if the ItemViewModel could be initialized to id later.
|
||||
getItem(userID: userID, itemID: itemID) { item in
|
||||
guard let item = item else { return }
|
||||
NotificationCenter.default.post(name: Notification.Name("processDeepLink"), object: DeepLink.item(item))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
extension AppURLHandler {
|
||||
func getItem(userID: String, itemID: String, completion: @escaping (BaseItemDto?) -> Void) {
|
||||
UserLibraryAPI.getItem(userId: userID, itemId: itemID)
|
||||
.sink(receiveCompletion: { innerCompletion in
|
||||
switch innerCompletion {
|
||||
case .failure:
|
||||
completion(nil)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}, receiveValue: { item in
|
||||
completion(item)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
|
@ -7,19 +7,21 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
|
||||
struct SplashView: View {
|
||||
@EnvironmentObject var mainRouter: MainCoordinator.Router
|
||||
@StateObject var viewModel = SplashViewModel()
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isLoggedIn {
|
||||
MainTabView()
|
||||
ProgressView()
|
||||
.onReceive(viewModel.$isLoggedIn) { flag in
|
||||
if flag {
|
||||
mainRouter.root(\.mainTab)
|
||||
} else {
|
||||
NavigationView {
|
||||
ConnectToServerView()
|
||||
mainRouter.root(\.connectToServer)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import MobileVLCKit
|
||||
import Combine
|
||||
import Defaults
|
||||
import GoogleCast
|
||||
import JellyfinAPI
|
||||
import MediaPlayer
|
||||
import Combine
|
||||
import GoogleCast
|
||||
import MobileVLCKit
|
||||
import Stinsen
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
import Defaults
|
||||
|
||||
enum PlayerDestination {
|
||||
case remote
|
||||
|
@ -26,6 +27,8 @@ protocol PlayerViewControllerDelegate: AnyObject {
|
|||
}
|
||||
|
||||
class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener {
|
||||
@RouterObject
|
||||
var main: MainCoordinator.Router?
|
||||
|
||||
weak var delegate: PlayerViewControllerDelegate?
|
||||
|
||||
|
@ -64,9 +67,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
private var castDiscoveryManager: GCKDiscoveryManager {
|
||||
return GCKCastContext.sharedInstance().discoveryManager
|
||||
}
|
||||
|
||||
private var castSessionManager: GCKSessionManager {
|
||||
return GCKCastContext.sharedInstance().sessionManager
|
||||
}
|
||||
|
||||
var hasSentRemoteSeek: Bool = false
|
||||
|
||||
var selectedPlaybackSpeedIndex: Int = 3
|
||||
|
@ -80,17 +85,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
var jumpForwardLength: VideoPlayerJumpLength {
|
||||
return Defaults[.videoPlayerJumpForward]
|
||||
}
|
||||
|
||||
var jumpBackwardLength: VideoPlayerJumpLength {
|
||||
return Defaults[.videoPlayerJumpBackward]
|
||||
}
|
||||
|
||||
var manifest: BaseItemDto = BaseItemDto()
|
||||
var manifest = BaseItemDto()
|
||||
var playbackItem = PlaybackItem()
|
||||
var remoteTimeUpdateTimer: Timer?
|
||||
var upNextViewModel: UpNextViewModel = UpNextViewModel()
|
||||
var upNextViewModel = UpNextViewModel()
|
||||
var lastOri: UIInterfaceOrientation?
|
||||
|
||||
// MARK: IBActions
|
||||
|
||||
@IBAction func seekSliderStart(_ sender: Any) {
|
||||
if playerDestination == .local {
|
||||
sendProgressReport(eventName: "pause")
|
||||
|
@ -101,7 +108,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
@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 secondsScrubbedRemaining = videoDuration - secondsScrubbedTo
|
||||
|
||||
|
@ -111,15 +118,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
|
||||
private func calculateTimeText(from duration: Double) -> String {
|
||||
let hours = floor(duration / 3600)
|
||||
let minutes = (duration.truncatingRemainder(dividingBy: 3600)) / 60
|
||||
let seconds = (duration.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60)
|
||||
let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60
|
||||
let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60)
|
||||
|
||||
let timeText: String
|
||||
|
||||
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 {
|
||||
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
|
||||
|
@ -127,7 +136,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
|
||||
@IBAction func seekSliderEnd(_ sender: Any) {
|
||||
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))
|
||||
// Scrub is value from 0..1 - find position in video and add / or remove.
|
||||
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
|
||||
|
@ -143,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
sendProgressReport(eventName: "unpause")
|
||||
} else {
|
||||
sendJellyfinCommand(command: "Seek", options: [
|
||||
"position": Int(secondsScrubbedTo)
|
||||
"position": Int(secondsScrubbedTo),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -180,7 +190,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
if playerDestination == .local {
|
||||
mediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
|
||||
} 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 {
|
||||
mediaPlayer.jumpForward(jumpForwardLength.rawValue)
|
||||
} 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
|
||||
|
||||
// Present the view controller (in a popover).
|
||||
self.present(optionsVC!, animated: true) {
|
||||
present(optionsVC!, animated: true) {
|
||||
print("popover visible, pause playback")
|
||||
self.mediaPlayer.pause()
|
||||
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
|
||||
|
@ -236,6 +248,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
// MARK: Cast methods
|
||||
|
||||
@IBAction func castButtonPressed(_ sender: Any) {
|
||||
if selectedCastDevice == nil {
|
||||
LogManager.shared.log.debug("Presenting Cast modal")
|
||||
|
@ -246,7 +259,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
castDeviceVC?.popoverPresentationController?.sourceView = castButton
|
||||
|
||||
// Present the view controller (in a popover).
|
||||
self.present(castDeviceVC!, animated: true) {
|
||||
present(castDeviceVC!, animated: true) {
|
||||
self.mediaPlayer.pause()
|
||||
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.")
|
||||
castSessionManager.endSessionAndStopCasting(true)
|
||||
selectedCastDevice = nil
|
||||
self.castButton.isEnabled = true
|
||||
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
|
||||
castButton.isEnabled = true
|
||||
castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
|
||||
playerDestination = .local
|
||||
}
|
||||
}
|
||||
|
@ -264,9 +277,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
LogManager.shared.log.debug("Cast modal dismissed")
|
||||
castDeviceVC?.dismiss(animated: true, completion: nil)
|
||||
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() {
|
||||
|
@ -280,11 +293,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
// MARK: Cast End
|
||||
|
||||
func settingsPopoverDismissed() {
|
||||
optionsVC?.dismiss(animated: true, completion: nil)
|
||||
if playerDestination == .local {
|
||||
self.mediaPlayer.play()
|
||||
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||
mediaPlayer.play()
|
||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,7 +340,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
self.mediaPlayer.jumpForward(30)
|
||||
self.sendProgressReport(eventName: "timeupdate")
|
||||
} 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
|
||||
}
|
||||
|
@ -337,14 +351,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
self.mediaPlayer.jumpBackward(15)
|
||||
self.sendProgressReport(eventName: "timeupdate")
|
||||
} 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
|
||||
}
|
||||
|
||||
// Scrubber
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
|
||||
guard let self = self else {return .commandFailed}
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] (remoteEvent) -> MPRemoteCommandHandlerStatus in
|
||||
guard let self = self else { return .commandFailed }
|
||||
|
||||
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
|
||||
let targetSeconds = event.positionTime
|
||||
|
@ -354,14 +368,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
|
||||
if self.playerDestination == .local {
|
||||
if offset > 0 {
|
||||
self.mediaPlayer.jumpForward(Int32(offset)/1000)
|
||||
self.mediaPlayer.jumpForward(Int32(offset) / 1000)
|
||||
} else {
|
||||
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
|
||||
self.mediaPlayer.jumpBackward(Int32(abs(offset)) / 1000)
|
||||
}
|
||||
self.sendProgressReport(eventName: "unpause")
|
||||
} else {
|
||||
|
||||
}
|
||||
} else {}
|
||||
|
||||
return .success
|
||||
} else {
|
||||
|
@ -390,8 +402,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
|
||||
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
|
||||
if let artworkImage = UIImage(data: imageData as Data) {
|
||||
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
|
||||
return artworkImage
|
||||
let artwork = MPMediaItemArtwork(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
|
||||
artworkImage
|
||||
})
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
||||
}
|
||||
|
@ -403,6 +415,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
// MARK: viewDidLoad
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
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() {
|
||||
|
@ -447,7 +461,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
let totalDevices = castDiscoveryManager.deviceCount
|
||||
discoveredCastDevices = []
|
||||
if totalDevices > 0 {
|
||||
for i in 0...totalDevices-1 {
|
||||
for i in 0 ... totalDevices - 1 {
|
||||
let device = castDiscoveryManager.device(at: i)
|
||||
discoveredCastDevices.append(device)
|
||||
}
|
||||
|
@ -466,8 +480,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
self.tabBarController?.tabBar.isHidden = false
|
||||
self.navigationController?.isNavigationBarHidden = false
|
||||
tabBarController?.tabBar.isHidden = false
|
||||
navigationController?.isNavigationBarHidden = false
|
||||
overrideUserInterfaceStyle = .unspecified
|
||||
DispatchQueue.main.async {
|
||||
if self.lastOri != nil {
|
||||
|
@ -479,11 +493,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
// MARK: viewDidAppear
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
overrideUserInterfaceStyle = .dark
|
||||
self.tabBarController?.tabBar.isHidden = true
|
||||
self.navigationController?.isNavigationBarHidden = true
|
||||
tabBarController?.tabBar.isHidden = true
|
||||
navigationController?.isNavigationBarHidden = true
|
||||
|
||||
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
|
||||
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
|
||||
|
@ -496,7 +511,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
func setupMediaPlayer() {
|
||||
|
||||
// Fetch max bitrate from UserDefaults depending on current connection mode
|
||||
let maxBitrate = Defaults[.inNetworkBandwidth]
|
||||
print(maxBitrate)
|
||||
|
@ -504,26 +518,31 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
let builder = DeviceProfileBuilder()
|
||||
builder.setMaxBitrate(bitrate: maxBitrate)
|
||||
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
|
||||
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
|
||||
switch completion {
|
||||
case .finished:
|
||||
break
|
||||
case .failure(let error):
|
||||
case let .failure(error):
|
||||
if let err = error as? ErrorResponse {
|
||||
switch err {
|
||||
case .error(401, _, _, _):
|
||||
self.delegate?.exitPlayer(self)
|
||||
SessionManager.current.logout()
|
||||
main?.root(\.connectToServer)
|
||||
case .error:
|
||||
self.delegate?.exitPlayer(self)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}, receiveValue: { [self] response in
|
||||
dump(response)
|
||||
|
@ -536,7 +555,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
item.videoType = .transcode
|
||||
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)
|
||||
|
||||
// Loop through media streams and add to array
|
||||
|
@ -548,7 +568,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
} else {
|
||||
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 {
|
||||
subtitleTrackArray.append(subtitle)
|
||||
|
@ -556,7 +578,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
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 {
|
||||
selectedAudioTrack = Int32(stream.index!)
|
||||
}
|
||||
|
@ -565,7 +588,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
if selectedAudioTrack == -1 {
|
||||
if audioTrackArray.count > 0 {
|
||||
if !audioTrackArray.isEmpty {
|
||||
selectedAudioTrack = audioTrackArray[0].id
|
||||
}
|
||||
}
|
||||
|
@ -574,13 +597,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
playbackItem = item
|
||||
} else {
|
||||
// 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()
|
||||
item.videoUrl = streamURL
|
||||
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)
|
||||
|
||||
// Loop through media streams and add to array
|
||||
|
@ -592,7 +617,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
} else {
|
||||
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 {
|
||||
subtitleTrackArray.append(subtitle)
|
||||
|
@ -600,7 +627,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
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 {
|
||||
selectedAudioTrack = Int32(stream.index!)
|
||||
}
|
||||
|
@ -609,7 +637,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
if selectedAudioTrack == -1 {
|
||||
if audioTrackArray.count > 0 {
|
||||
if !audioTrackArray.isEmpty {
|
||||
selectedAudioTrack = audioTrackArray[0].id
|
||||
}
|
||||
}
|
||||
|
@ -636,7 +664,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
subtitleTrackArray.forEach { subtitle in
|
||||
if Defaults[.isAutoSelectSubtitles] {
|
||||
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
|
||||
subtitle.languageCode.contains(Locale.current.languageCode ?? "") {
|
||||
subtitle.languageCode.contains(Locale.current.languageCode ?? "")
|
||||
{
|
||||
selectedCaptionTrack = subtitle.id
|
||||
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
|
||||
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
|
||||
|
@ -683,21 +712,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
subtitleTrackArray.forEach { sub in
|
||||
// stupid fxcking jeff decides to re-encode these when added.
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.mediaHasStartedPlaying()
|
||||
mediaHasStartedPlaying()
|
||||
delegate?.hideLoadingView(self)
|
||||
|
||||
videoContentView.setNeedsLayout()
|
||||
videoContentView.setNeedsDisplay()
|
||||
self.view.setNeedsLayout()
|
||||
self.view.setNeedsDisplay()
|
||||
self.videoControlsView.setNeedsLayout()
|
||||
self.videoControlsView.setNeedsDisplay()
|
||||
view.setNeedsLayout()
|
||||
view.setNeedsDisplay()
|
||||
videoControlsView.setNeedsLayout()
|
||||
videoControlsView.setNeedsDisplay()
|
||||
|
||||
mediaPlayer.pause()
|
||||
mediaPlayer.play()
|
||||
|
@ -705,6 +734,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
// MARK: VideoPlayerSettings Delegate
|
||||
|
||||
func subtitleTrackChanged(newTrackID: Int32) {
|
||||
selectedCaptionTrack = newTrackID
|
||||
mediaPlayer.currentVideoSubTitleIndex = newTrackID
|
||||
|
@ -731,7 +761,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
|
||||
// Create the swiftUI view
|
||||
let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel))
|
||||
self.upNextView.addSubview(contentView.view)
|
||||
upNextView.addSubview(contentView.view)
|
||||
contentView.view.backgroundColor = .clear
|
||||
contentView.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true
|
||||
|
@ -741,7 +771,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
}
|
||||
|
||||
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
|
||||
print(completion)
|
||||
}, receiveValue: { [self] response in
|
||||
|
@ -790,20 +821,20 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
|
|||
setupMediaPlayer()
|
||||
getNextEpisode()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - GCKGenericChannelDelegate
|
||||
|
||||
extension PlayerViewController: GCKGenericChannelDelegate {
|
||||
@objc func updateRemoteTime() {
|
||||
castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
|
||||
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 {
|
||||
let positiveSeconds = Double(remotePositionTicks/10_000_000)
|
||||
let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000)
|
||||
let positiveSeconds = Double(remotePositionTicks / 10_000_000)
|
||||
let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks)) / 10_000_000)
|
||||
|
||||
timeText.text = calculateTimeText(from: positiveSeconds)
|
||||
timeLeftText.text = calculateTimeText(from: remainingSeconds)
|
||||
|
@ -823,14 +854,15 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
|||
if hasSentRemoteSeek == false {
|
||||
hasSentRemoteSeek = true
|
||||
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
|
||||
self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
|
||||
remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
|
||||
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!,
|
||||
"serverVersion": "10.8.0",
|
||||
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
|
||||
"subtitleBurnIn": false
|
||||
"subtitleBurnIn": false,
|
||||
]
|
||||
let jsonData = JSON(payload)
|
||||
|
||||
|
@ -857,7 +889,13 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
|||
if command == "Seek" {
|
||||
remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000)
|
||||
// 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)
|
||||
.sink(receiveCompletion: { result in
|
||||
|
@ -871,9 +909,10 @@ extension PlayerViewController: GCKGenericChannelDelegate {
|
|||
}
|
||||
|
||||
// MARK: - GCKSessionManagerListener
|
||||
|
||||
extension PlayerViewController: GCKSessionManagerListener {
|
||||
func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) {
|
||||
self.sendStopReport()
|
||||
sendStopReport()
|
||||
mediaPlayer.stop()
|
||||
|
||||
playerDestination = .remote
|
||||
|
@ -891,25 +930,25 @@ extension PlayerViewController: GCKSessionManagerListener {
|
|||
|
||||
let playNowOptions: [String: Any] = [
|
||||
"items": [[
|
||||
"Id": self.manifest.id!,
|
||||
"Id": manifest.id!,
|
||||
"ServerId": ServerEnvironment.current.server.server_id!,
|
||||
"Name": self.manifest.name!,
|
||||
"Type": self.manifest.type!,
|
||||
"MediaType": self.manifest.mediaType!,
|
||||
"IsFolder": self.manifest.isFolder!
|
||||
]]
|
||||
"Name": manifest.name!,
|
||||
"Type": manifest.type!,
|
||||
"MediaType": manifest.mediaType!,
|
||||
"IsFolder": manifest.isFolder!,
|
||||
]],
|
||||
]
|
||||
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
|
||||
}
|
||||
|
||||
func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) {
|
||||
self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
||||
self.sessionDidStart(manager: sessionManager, didStart: session)
|
||||
jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
||||
sessionDidStart(manager: sessionManager, didStart: session)
|
||||
}
|
||||
|
||||
func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) {
|
||||
self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
||||
self.sessionDidStart(manager: sessionManager, didStart: session)
|
||||
jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
|
||||
sessionDidStart(manager: sessionManager, didStart: session)
|
||||
}
|
||||
|
||||
func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) {
|
||||
|
@ -938,30 +977,29 @@ extension PlayerViewController: GCKSessionManagerListener {
|
|||
}
|
||||
|
||||
// MARK: - VLCMediaPlayer Delegates
|
||||
|
||||
extension PlayerViewController: VLCMediaPlayerDelegate {
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification!) {
|
||||
let currentState: VLCMediaPlayerState = mediaPlayer.state
|
||||
switch currentState {
|
||||
case .stopped :
|
||||
case .stopped:
|
||||
LogManager.shared.log.debug("Player state changed: STOPPED")
|
||||
break
|
||||
case .ended :
|
||||
case .ended:
|
||||
LogManager.shared.log.debug("Player state changed: ENDED")
|
||||
break
|
||||
case .playing :
|
||||
case .playing:
|
||||
LogManager.shared.log.debug("Player state changed: PLAYING")
|
||||
sendProgressReport(eventName: "unpause")
|
||||
delegate?.hideLoadingView(self)
|
||||
paused = false
|
||||
case .paused :
|
||||
case .paused:
|
||||
LogManager.shared.log.debug("Player state changed: PAUSED")
|
||||
paused = true
|
||||
case .opening :
|
||||
case .opening:
|
||||
LogManager.shared.log.debug("Player state changed: OPENING")
|
||||
case .buffering :
|
||||
case .buffering:
|
||||
LogManager.shared.log.debug("Player state changed: BUFFERING")
|
||||
delegate?.showLoadingView(self)
|
||||
case .error :
|
||||
case .error:
|
||||
LogManager.shared.log.error("Video had error.")
|
||||
sendStopReport()
|
||||
case .esAdded:
|
||||
|
@ -973,19 +1011,19 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
|
|||
|
||||
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
|
||||
let time = mediaPlayer.position
|
||||
if abs(time-lastTime) > 0.00005 {
|
||||
if abs(time - lastTime) > 0.00005 {
|
||||
paused = false
|
||||
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
|
||||
seekSlider.setValue(mediaPlayer.position, animated: true)
|
||||
delegate?.hideLoadingView(self)
|
||||
|
||||
if manifest.type == "Episode" && upNextViewModel.item != nil {
|
||||
if manifest.type == "Episode", upNextViewModel.item != nil {
|
||||
if time > 0.96 {
|
||||
upNextView.isHidden = false
|
||||
self.jumpForwardButton.isHidden = true
|
||||
jumpForwardButton.isHidden = true
|
||||
} else {
|
||||
upNextView.isHidden = true
|
||||
self.jumpForwardButton.isHidden = false
|
||||
jumpForwardButton.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -993,7 +1031,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
|
|||
timeLeftText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst())
|
||||
|
||||
if CACurrentMediaTime() - controlsAppearTime > 5 {
|
||||
self.smallNextUpView()
|
||||
smallNextUpView()
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
|
||||
self.videoControlsView.alpha = 0.0
|
||||
}, 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
|
||||
|
||||
struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
||||
var item: BaseItemDto
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@RouterObject var playerRouter: VideoPlayerCoordinator.Router?
|
||||
|
||||
var loadBinding: Binding<Bool>
|
||||
var pBinding: Binding<Bool>
|
||||
let loadBinding: Binding<Bool>
|
||||
|
||||
class Coordinator: NSObject, PlayerViewControllerDelegate {
|
||||
var parent: VLCPlayerWithControls
|
||||
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.pBinding = pBinding
|
||||
}
|
||||
|
||||
func hideLoadingView(_ viewController: PlayerViewController) {
|
||||
self.loadBinding.wrappedValue = false
|
||||
loadBinding.wrappedValue = false
|
||||
}
|
||||
|
||||
func showLoadingView(_ viewController: PlayerViewController) {
|
||||
self.loadBinding.wrappedValue = true
|
||||
loadBinding.wrappedValue = true
|
||||
}
|
||||
|
||||
func exitPlayer(_ viewController: PlayerViewController) {
|
||||
self.pBinding.wrappedValue = false
|
||||
parent.playerRouter?.dismissCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding)
|
||||
Coordinator(parent: self, loadBinding: loadBinding)
|
||||
}
|
||||
|
||||
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 customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
|
||||
customViewController.manifest = item
|
||||
|
@ -1056,20 +1113,27 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
|
|||
return customViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) {
|
||||
}
|
||||
func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType,
|
||||
context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) {}
|
||||
}
|
||||
|
||||
// MARK: - Play State Update Methods
|
||||
|
||||
extension PlayerViewController {
|
||||
func sendProgressReport(eventName: String) {
|
||||
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 {
|
||||
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)
|
||||
.sink(receiveCompletion: { result in
|
||||
|
@ -1082,7 +1146,10 @@ extension PlayerViewController {
|
|||
}
|
||||
|
||||
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)
|
||||
.sink(receiveCompletion: { result in
|
||||
|
@ -1098,7 +1165,13 @@ extension PlayerViewController {
|
|||
|
||||
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)
|
||||
.sink(receiveCompletion: { result in
|
||||
|
@ -1111,7 +1184,7 @@ extension PlayerViewController {
|
|||
}
|
||||
|
||||
extension UINavigationController {
|
||||
open override var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||
override open var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
/*
|
||||
* SwiftFin is subject to the terms of the Mozilla Public
|
||||
* License, v2.0. If a copy of the MPL was not distributed with this
|
||||
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func eraseToAnyView() -> AnyView {
|
||||
return AnyView(self)
|
||||
}
|
||||
}
|
|
@ -10,8 +10,11 @@
|
|||
import Combine
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
import Stinsen
|
||||
|
||||
final class ConnectToServerViewModel: ViewModel {
|
||||
@RouterObject
|
||||
var main: MainCoordinator.Router?
|
||||
|
||||
@Published var isConnectedServer = false
|
||||
|
||||
|
@ -23,13 +26,14 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
@Published var publicUsers = [UserDto]()
|
||||
@Published var selectedPublicUser = UserDto()
|
||||
|
||||
private let discovery: ServerDiscovery = ServerDiscovery()
|
||||
private let discovery = ServerDiscovery()
|
||||
@Published var servers: [ServerDiscovery.ServerLookupResponse] = []
|
||||
@Published var searching = false
|
||||
|
||||
func getPublicUsers() {
|
||||
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()
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
|
@ -46,17 +50,16 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
}
|
||||
|
||||
func hidePublicUsers() {
|
||||
self.lastPublicUsers = publicUsers
|
||||
lastPublicUsers = publicUsers
|
||||
publicUsers = []
|
||||
}
|
||||
|
||||
func showPublicUsers() {
|
||||
self.publicUsers = lastPublicUsers
|
||||
publicUsers = lastPublicUsers
|
||||
lastPublicUsers = []
|
||||
}
|
||||
|
||||
func connectToServer() {
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
if uriSubject.value == "localhost" {
|
||||
uriSubject.value = "http://localhost:8096"
|
||||
|
@ -67,7 +70,8 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
ServerEnvironment.current.create(with: uriSubject.value)
|
||||
.trackActivity(loading)
|
||||
.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
|
||||
LogManager.shared.log.debug("Connected to server at \"\(self.uriSubject.value)\"", tag: "connectToServer")
|
||||
self.getPublicUsers()
|
||||
|
@ -77,7 +81,7 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
|
||||
func connectToServer(at url: URL) {
|
||||
uriSubject.send(url.absoluteString)
|
||||
self.connectToServer()
|
||||
connectToServer()
|
||||
}
|
||||
|
||||
func discoverServers() {
|
||||
|
@ -88,7 +92,7 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
self.searching = false
|
||||
}
|
||||
|
||||
discovery.locateServer { [self] (server) in
|
||||
discovery.locateServer { [self] server in
|
||||
if let server = server, !servers.contains(server) {
|
||||
servers.append(server)
|
||||
}
|
||||
|
@ -98,13 +102,16 @@ final class ConnectToServerViewModel: ViewModel {
|
|||
|
||||
func 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)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { completion in
|
||||
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login", completion: completion)
|
||||
}, receiveValue: { _ in
|
||||
|
||||
self.handleAPIRequestError(displayMessage: "Unable to connect to server.", logLevel: .critical, tag: "login",
|
||||
completion: completion)
|
||||
}, receiveValue: { [weak self] _ in
|
||||
self?.main?.root(\.mainTab)
|
||||
})
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ final class HomeViewModel: ViewModel {
|
|||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Foundation
|
||||
import JellyfinAPI
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ final class LatestMediaViewModel: ViewModel {
|
|||
|
||||
func requestLatestMedia() {
|
||||
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: [
|
||||
.primaryImageAspectRatio,
|
||||
.seriesPrimaryImage,
|
||||
|
@ -35,6 +36,7 @@ final class LatestMediaViewModel: ViewModel {
|
|||
.genres,
|
||||
.people
|
||||
],
|
||||
includeItemTypes: ["Series", "Movie"],
|
||||
enableUserData: true, limit: 12)
|
||||
.trackActivity(loading)
|
||||
.sink(receiveCompletion: { [weak self] completion in
|
||||
|
|
|
@ -198,6 +198,7 @@ extension NextUpEntryView {
|
|||
}
|
||||
|
||||
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) {
|
||||
if let image = item.1 {
|
||||
Image(uiImage: image)
|
||||
|
@ -218,9 +219,11 @@ extension NextUpEntryView {
|
|||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func largeVideoView(item: (BaseItemDto, UIImage?)) -> some View {
|
||||
Link(destination: URL(string: "widget-extension://Users/\(SessionManager.current.user.user_id!)/Items/\(item.0.id!)")!, label: {
|
||||
HStack(spacing: 20) {
|
||||
if let image = item.1 {
|
||||
Image(uiImage: image)
|
||||
|
@ -243,6 +246,7 @@ extension NextUpEntryView {
|
|||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,6 +285,8 @@ extension NextUpEntryView {
|
|||
func large(items: [(BaseItemDto, UIImage?)]) -> some View {
|
||||
VStack(spacing: 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: .bottomLeading) {
|
||||
if let image = firstItem.1 {
|
||||
|
@ -308,6 +314,7 @@ extension NextUpEntryView {
|
|||
}
|
||||
.clipped()
|
||||
.shadow(radius: 8)
|
||||
})
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
if let secondItem = items[safe: 1] {
|
||||
|
@ -354,7 +361,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
|
@ -365,7 +372,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
|
@ -380,7 +387,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
|
@ -392,7 +399,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name2", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series2"),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
|
@ -405,7 +412,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
NextUpEntryView(entry: .init(date: Date(),
|
||||
items: [
|
||||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemMedium))
|
||||
|
@ -415,7 +422,7 @@ struct NextUpWidget_Previews: PreviewProvider {
|
|||
(.init(name: "Name0", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series0"),
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
(.init(name: "Name1", indexNumber: 10, parentIndexNumber: 0, seriesName: "Series1"),
|
||||
UIImage(named: "WidgetHeaderSymbol"))
|
||||
UIImage(named: "WidgetHeaderSymbol")),
|
||||
],
|
||||
error: nil))
|
||||
.previewContext(WidgetPreviewContext(family: .systemLarge))
|
||||
|
|
Loading…
Reference in New Issue